diff --git a/.asf.yaml b/.asf.yaml
index ead2581149..13bd59c224 100644
--- a/.asf.yaml
+++ b/.asf.yaml
@@ -29,12 +29,14 @@ github:
rebase: false
squash: true
features:
+ discussions: true
issues: true
protected_branches:
main:
required_linear_history: true
notifications:
commits: commits@arrow.apache.org
+ discussions: user@arrow.apache.org
issues_status: issues@arrow.apache.org
issues_comment: github@arrow.apache.org
pullrequests: github@arrow.apache.org
diff --git a/.env b/.env
index d3e1c1d63a..ef0c5fb101 100644
--- a/.env
+++ b/.env
@@ -40,18 +40,17 @@ ARCH_SHORT=amd64
# Default repository to pull and push images from
REPO=ghcr.io/apache/arrow-java-dev
-ARROW_REPO=apache/arrow-dev
+ARROW_REPO=ghcr.io/apache/arrow-dev
# The setup attempts to generate coredumps by default, in order to disable the
# coredump generation set it to 0
ULIMIT_CORE=-1
# Default versions for various dependencies
-JDK=11
+JDK=17
MAVEN=3.9.9
# Versions for various dependencies used to build artifacts
# Keep in sync with apache/arrow
ARROW_REPO_ROOT=./arrow
-PYTHON=3.9
-VCPKG="f7423ee180c4b7f40d43402c2feb3859161ef625" # 2024.06.15 Release
+VCPKG="9b965a116838c6cdcd36bca60d1b81b030c8ab8d" # 2026.05.27 (not release, upstream commit)
diff --git a/.github/workflows/comment_bot.yml b/.github/workflows/comment_bot.yml
index 5fbc858cc6..507d6a969c 100644
--- a/.github/workflows/comment_bot.yml
+++ b/.github/workflows/comment_bot.yml
@@ -30,7 +30,7 @@ jobs:
if: github.event.comment.body == 'take'
runs-on: ubuntu-latest
steps:
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ - uses: actions/github-script@v9
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |-
diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml
index 37320898a6..f4dc9ad6f1 100644
--- a/.github/workflows/dev.yml
+++ b/.github/workflows/dev.yml
@@ -19,7 +19,9 @@ name: Dev
on:
pull_request: {}
- push: {}
+ push:
+ branches-ignore:
+ - dependabot/**
concurrency:
group: ${{ github.repository }}-${{ github.ref }}-${{ github.workflow }}
@@ -33,16 +35,16 @@ jobs:
name: "pre-commit"
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- - uses: actions/setup-python@v5
+ - uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: pre-commit (cache)
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
diff --git a/.github/workflows/dev_pr.yml b/.github/workflows/dev_pr.yml
index 34b3363c50..ad000df88e 100644
--- a/.github/workflows/dev_pr.yml
+++ b/.github/workflows/dev_pr.yml
@@ -35,6 +35,7 @@ concurrency:
permissions:
contents: read
+ issues: write
pull-requests: write
jobs:
@@ -42,35 +43,35 @@ jobs:
name: "Ensure PR format"
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- name: Ensure PR title format
id: title-format
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ uses: actions/github-script@v9
with:
script: |
const scripts = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/dev_pr.js`);
return scripts.check_title_format({core, github, context});
- name: Label PR
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ uses: actions/github-script@v9
with:
script: |
const scripts = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/dev_pr.js`);
await scripts.apply_labels({core, github, context});
- name: Ensure PR is labeled
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ uses: actions/github-script@v9
with:
script: |
const scripts = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/dev_pr.js`);
await scripts.check_labels({core, github, context});
- name: Ensure PR is linked to an issue
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+ uses: actions/github-script@v9
with:
script: |
const scripts = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/dev_pr.js`);
diff --git a/.github/workflows/dev_pr_milestone.sh b/.github/workflows/dev_pr_milestone.sh
index b6876b4b08..4a77eb1f73 100755
--- a/.github/workflows/dev_pr_milestone.sh
+++ b/.github/workflows/dev_pr_milestone.sh
@@ -37,8 +37,8 @@ main() {
local -r milestone=$(
gh api "/repos/${repo}/milestones" |
jq --raw-output '.[] | .title' |
- grep -E '^[0-9]+\.[0-9]+\.[0-9]+$'
- head -n1
+ grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' |
+ head -n1
)
echo "Assigning milestone: ${milestone}"
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
new file mode 100644
index 0000000000..3872d03d2f
--- /dev/null
+++ b/.github/workflows/integration.yml
@@ -0,0 +1,127 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: Integration
+
+on:
+ push:
+ branches:
+ - '**'
+ - '!dependabot/**'
+ tags:
+ - '**'
+ paths:
+ - '.github/workflows/integration.yml'
+ - '**/pom.xml'
+ - 'c/**'
+ - 'ci/scripts/**'
+ - 'compose.yaml'
+ - 'flight/**'
+ - 'format/**'
+ - 'testing/data/**'
+ - 'vector/**'
+ pull_request:
+ paths:
+ - '.github/workflows/integration.yml'
+ - '**/pom.xml'
+ - 'c/**'
+ - 'ci/scripts/**'
+ - 'compose.yaml'
+ - 'flight/**'
+ - 'format/**'
+ - 'testing/data/**'
+ - 'vector/**'
+
+concurrency:
+ group: ${{ github.repository }}-${{ github.head_ref || github.sha }}-${{ github.workflow }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+env:
+ DOCKER_VOLUME_PREFIX: ".docker/"
+
+jobs:
+ integration:
+ name: AMD64 integration
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ steps:
+ - name: Checkout Arrow
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ repository: apache/arrow
+ submodules: recursive
+ - name: Checkout Arrow Rust
+ uses: actions/checkout@v7
+ with:
+ repository: apache/arrow-rs
+ path: rust
+ - name: Checkout Arrow nanoarrow
+ uses: actions/checkout@v7
+ with:
+ repository: apache/arrow-nanoarrow
+ path: nanoarrow
+ - name: Checkout Arrow .NET
+ uses: actions/checkout@v7
+ with:
+ repository: apache/arrow-dotnet
+ path: dotnet
+ - name: Checkout Arrow Go
+ uses: actions/checkout@v7
+ with:
+ repository: apache/arrow-go
+ path: go
+ - name: Checkout Arrow Java
+ uses: actions/checkout@v7
+ with:
+ path: java
+ - name: Checkout Arrow JavaScript
+ uses: actions/checkout@v7
+ with:
+ repository: apache/arrow-js
+ path: js
+ - name: Free up disk space
+ run: |
+ ci/scripts/util_free_space.sh
+ - name: Cache Docker Volumes
+ uses: actions/cache@v6
+ with:
+ path: .docker
+ key: integration-conda-${{ hashFiles('cpp/**') }}
+ restore-keys: integration-conda-
+ - name: Setup Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: 3.12
+ - name: Setup Archery
+ run: pip install -e dev/archery[docker]
+ - name: Execute Docker Build
+ run: |
+ source ci/scripts/util_enable_core_dumps.sh
+ archery docker run \
+ -e ARCHERY_DEFAULT_BRANCH=main \
+ -e ARCHERY_INTEGRATION_TARGET_IMPLEMENTATIONS=java \
+ -e ARCHERY_INTEGRATION_WITH_DOTNET=1 \
+ -e ARCHERY_INTEGRATION_WITH_GO=1 \
+ -e ARCHERY_INTEGRATION_WITH_JAVA=1 \
+ -e ARCHERY_INTEGRATION_WITH_JS=1 \
+ -e ARCHERY_INTEGRATION_WITH_NANOARROW=1 \
+ -e ARCHERY_INTEGRATION_WITH_RUST=1 \
+ conda-integration
diff --git a/.github/workflows/rc.yml b/.github/workflows/rc.yml
index 5b78cc9395..18a721ac02 100644
--- a/.github/workflows/rc.yml
+++ b/.github/workflows/rc.yml
@@ -38,7 +38,7 @@ jobs:
timeout-minutes: 5
steps:
- name: Checkout
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@v7
with:
submodules: recursive
- name: Prepare for tag
@@ -71,7 +71,7 @@ jobs:
run: |
dev/release/run_rat.sh "${TAR_GZ}"
- name: Upload source archive
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-source
path: |
@@ -101,45 +101,39 @@ jobs:
packages: write
steps:
- name: Download source archive
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-source
- name: Extract source archive
run: |
tar -xf apache-arrow-java-*.tar.gz --strip-components=1
- # We always use the main branch for apache/arrow for now.
- # Because we want to use
- # https://github.com/apache/arrow/pull/45114 in
- # apache/arrow-java. We can revert this workaround once Apache
- # Arrow 20.0.0 that includes the change released.
- #
- # - name: Download the latest Apache Arrow C++
- # if: github.event_name != 'schedule'
- # run: |
- # ci/scripts/download_cpp.sh
+ - name: Download the latest Apache Arrow C++
+ if: github.event_name != 'schedule'
+ run: |
+ ci/scripts/download_cpp.sh
- name: Checkout Apache Arrow C++
- # if: github.event_name == 'schedule'
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ if: github.event_name == 'schedule'
+ uses: actions/checkout@v7
with:
repository: apache/arrow
path: arrow
- name: Checkout apache/arrow-testing
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@v7
with:
repository: apache/arrow-testing
path: arrow/testing
- name: Checkout apache/parquet-testing
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@v7
with:
repository: apache/parquet-testing
path: arrow/cpp/submodules/parquet-testing
- - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
+ - uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Cache
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .docker
key: jni-linux-${{ matrix.platform.arch }}-${{ hashFiles('arrow/cpp/**') }}
@@ -154,54 +148,54 @@ jobs:
- name: Compress into single artifact to keep directory structure
run: tar -cvzf jni-linux-${{ matrix.platform.arch }}.tar.gz jni/
- name: Upload artifacts
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: jni-linux-${{ matrix.platform.arch }}
path: jni-linux-${{ matrix.platform.arch }}.tar.gz
jni-macos:
name: JNI ${{ matrix.platform.runs_on }} ${{ matrix.platform.arch }}
runs-on: ${{ matrix.platform.runs_on }}
- timeout-minutes: 45
+ timeout-minutes: 60
needs:
- source
strategy:
fail-fast: false
matrix:
platform:
- - { runs_on: macos-13, arch: "x86_64"}
+ - { runs_on: macos-15-intel, arch: "x86_64"}
- { runs_on: macos-14, arch: "aarch_64" }
env:
MACOSX_DEPLOYMENT_TARGET: "14.0"
steps:
- name: Download source archive
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-source
- name: Extract source archive
run: |
tar -xf apache-arrow-java-*.tar.gz --strip-components=1
- # - name: Download the latest Apache Arrow C++
- # if: github.event_name != 'schedule'
- # run: |
- # ci/scripts/download_cpp.sh
+ - name: Download the latest Apache Arrow C++
+ if: github.event_name != 'schedule'
+ run: |
+ ci/scripts/download_cpp.sh
- name: Checkout Apache Arrow C++
- # if: github.event_name == 'schedule'
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ if: github.event_name == 'schedule'
+ uses: actions/checkout@v7
with:
repository: apache/arrow
path: arrow
- name: Checkout apache/arrow-testing
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@v7
with:
repository: apache/arrow-testing
path: arrow/testing
- name: Checkout apache/parquet-testing
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@v7
with:
repository: apache/parquet-testing
path: arrow/cpp/submodules/parquet-testing
- name: Set up Python
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
+ uses: actions/setup-python@v7
with:
cache: 'pip'
python-version: 3.12
@@ -221,6 +215,10 @@ jobs:
# llvm@14 because llvm is newer than llvm@14.
brew uninstall llvm || :
+ # We can remove this when we drop support for
+ # macos-15-intel. because macos-14 or later with arm64 uses /opt/homebrew/
+ # not /usr/local/.
+ #
# Ensure updating python@XXX with the "--overwrite" option.
# If python@XXX is updated without "--overwrite", it causes
# a conflict error. Because Python 3 installed not by
@@ -229,10 +227,10 @@ jobs:
# tries to replace /usr/local/bin/2to3 and so on and causes
# a conflict error.
brew update
- for python_package in $(brew list | grep python@); do
+ for python_package in $(brew list | grep python@ | sort -r); do
brew install --overwrite ${python_package}
done
- brew install --overwrite python
+ brew install --overwrite python3
if [ "$(uname -m)" = "arm64" ]; then
# pkg-config formula is deprecated but it's still installed
@@ -266,7 +264,7 @@ jobs:
run: |
echo "CCACHE_DIR=${PWD}/ccache" >> ${GITHUB_ENV}
- name: Cache ccache
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ccache
key: jni-macos-${{ matrix.platform.arch }}-${{ hashFiles('arrow/cpp/**') }}
@@ -275,12 +273,12 @@ jobs:
run: |
set -e
# make brew Java available to CMake
- export JAVA_HOME=$(brew --prefix openjdk@11)/libexec/openjdk.jdk/Contents/Home
+ export JAVA_HOME=$(brew --prefix openjdk@17)/libexec/openjdk.jdk/Contents/Home
ci/scripts/jni_macos_build.sh . arrow build jni
- name: Compress into single artifact to keep directory structure
run: tar -cvzf jni-macos-${{ matrix.platform.arch }}.tar.gz jni/
- name: Upload artifacts
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: jni-macos-${{ matrix.platform.arch }}
path: jni-macos-${{ matrix.platform.arch }}.tar.gz
@@ -294,11 +292,11 @@ jobs:
fail-fast: false
matrix:
platform:
- - runs_on: windows-2019
+ - runs_on: windows-2022
arch: "x86_64"
steps:
- name: Download source archive
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-source
- name: Extract source archive
@@ -312,14 +310,14 @@ jobs:
ci/scripts/download_cpp.sh
- name: Checkout Apache Arrow C++
if: github.event_name == 'schedule'
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@v7
with:
repository: apache/arrow
path: arrow
- name: Set up Java
- uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
+ uses: actions/setup-java@v5
with:
- java-version: '11'
+ java-version: '17'
distribution: 'temurin'
- name: Download Timezone Database
shell: bash
@@ -342,7 +340,7 @@ jobs:
run: |
echo "CCACHE_DIR=${PWD}/ccache" >> ${GITHUB_ENV}
- name: Cache ccache
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ccache
key: jni-windows-${{ matrix.platform.arch }}-${{ hashFiles('arrow/cpp/**') }}
@@ -350,7 +348,7 @@ jobs:
- name: Build
shell: cmd
run: |
- call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64
+ call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64
REM For ORC
set TZDIR=/c/msys64/usr/share/zoneinfo
bash -c "ci/scripts/jni_windows_build.sh . arrow build jni"
@@ -358,7 +356,7 @@ jobs:
shell: bash
run: tar -cvzf jni-windows-${{ matrix.platform.arch }}.tar.gz jni/
- name: Upload artifacts
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: jni-windows-${{ matrix.platform.arch }}
path: jni-windows-${{ matrix.platform.arch }}.tar.gz
@@ -371,7 +369,7 @@ jobs:
- jni-windows
steps:
- name: Download artifacts
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: artifacts
- name: Decompress artifacts
@@ -411,12 +409,12 @@ jobs:
test -f jni/arrow_dataset_jni/x86_64/arrow_dataset_jni.dll
test -f jni/arrow_orc_jni/x86_64/arrow_orc_jni.dll
- name: Checkout apache/arrow-testing
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@v7
with:
repository: apache/arrow-testing
path: testing
- name: Cache ~/.m2
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.m2
key: binaries-build-${{ hashFiles('**/*.java', '**/pom.xml') }}
@@ -430,12 +428,12 @@ jobs:
cp -a target/site/apidocs reference
tar -cvzf reference.tar.gz reference
- name: Upload binaries
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-binaries
path: binaries/*
- name: Upload docs
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: reference
path: reference.tar.gz
@@ -448,15 +446,15 @@ jobs:
contents: read
packages: write
steps:
- - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
+ - uses: actions/setup-python@v7
with:
cache: 'pip'
- name: Download source archive
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-source
- name: Download Javadocs
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: reference
- name: Extract source archive
@@ -473,7 +471,7 @@ jobs:
- name: Compress into single artifact to keep directory structure
run: tar -cvzf html.tar.gz -C docs/build html
- name: Upload artifacts
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-html
path: html.tar.gz
@@ -499,7 +497,7 @@ jobs:
contents: write
steps:
- name: Checkout
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@v7
with:
path: site
- name: Prepare branch
@@ -521,7 +519,7 @@ jobs:
cp ../.asf.yaml ./
git add .nojekyll .asf.yaml
- name: Download
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-html
- name: Extract
@@ -557,7 +555,7 @@ jobs:
- ubuntu-latest
steps:
- name: Download release artifacts
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: release-*
- name: Verify
@@ -591,7 +589,7 @@ jobs:
contents: write
steps:
- name: Download release artifacts
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: release-*
path: artifacts
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a955964cd8..7692eb6cbe 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -32,7 +32,7 @@ jobs:
publish:
name: Publish
runs-on: ubuntu-latest
- timeout-minutes: 5
+ timeout-minutes: 30
steps:
- name: Download RC contents
run: |
@@ -65,7 +65,7 @@ jobs:
$artifact
done
- name: Checkout for publishing docs
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@v7
with:
path: site
- name: Publish docs
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 5db5c988eb..653b16fa32 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -45,7 +45,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- jdk: [11, 17, 21, 23]
+ jdk: [17, 21, 23]
maven: [3.9.9]
image: [ubuntu, conda-jni-cdata]
include:
@@ -58,15 +58,15 @@ jobs:
MAVEN: ${{ matrix.maven }}
steps:
- name: Checkout Arrow
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
+ uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
- name: Cache Docker Volumes
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+ uses: actions/cache@v6
with:
path: .docker
- key: maven-${{ matrix.jdk }}-${{ matrix.maven }}-${{ hashFiles('compose.yaml', '**/pom.xml', '**/*.java') }}
+ key: maven-${{ matrix.jdk }}-${{ matrix.maven }}-${{ hashFiles('compose.yaml', '**/pom.xml') }}
restore-keys: maven-${{ matrix.jdk }}-${{ matrix.maven }}-
- name: Execute Docker Build
env:
@@ -88,22 +88,23 @@ jobs:
matrix:
include:
- arch: AMD64
- jdk: 11
- macos: 13
+ jdk: 17
+ macos: 15-intel
- arch: AArch64
- jdk: 11
+ jdk: 17
macos: latest
steps:
- - name: Set up Java
- uses: actions/setup-java@v4
- with:
- distribution: 'temurin'
- java-version: ${{ matrix.jdk }}
- name: Checkout Arrow
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
+ - name: Set up Java
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'temurin'
+ java-version: ${{ matrix.jdk }}
+ cache: 'maven'
- name: Build
shell: bash
env:
@@ -123,18 +124,19 @@ jobs:
strategy:
fail-fast: false
matrix:
- jdk: [11]
+ jdk: [17]
steps:
- - name: Set up Java
- uses: actions/setup-java@v4
- with:
- java-version: ${{ matrix.jdk }}
- distribution: 'temurin'
- name: Checkout Arrow
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
+ - name: Set up Java
+ uses: actions/setup-java@v5
+ with:
+ java-version: ${{ matrix.jdk }}
+ distribution: 'temurin'
+ cache: 'maven'
- name: Build
shell: bash
env:
@@ -145,60 +147,3 @@ jobs:
env:
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
run: ci/scripts/test.sh . build jni
-
- integration:
- name: AMD64 integration
- runs-on: ubuntu-latest
- timeout-minutes: 60
- steps:
- - name: Checkout Arrow
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- with:
- fetch-depth: 0
- repository: apache/arrow
- submodules: recursive
- - name: Checkout Arrow Rust
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- with:
- repository: apache/arrow-rs
- path: rust
- - name: Checkout Arrow nanoarrow
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- with:
- repository: apache/arrow-nanoarrow
- path: nanoarrow
- - name: Checkout Arrow Go
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- with:
- repository: apache/arrow-go
- path: go
- - name: Checkout Arrow Java
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- with:
- path: java
- - name: Free up disk space
- run: |
- ci/scripts/util_free_space.sh
- - name: Cache Docker Volumes
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
- with:
- path: .docker
- key: integration-conda-${{ hashFiles('cpp/**') }}
- restore-keys: integration-conda-
- - name: Setup Python
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
- with:
- python-version: 3.12
- - name: Setup Archery
- run: pip install -e dev/archery[docker]
- - name: Execute Docker Build
- run: |
- source ci/scripts/util_enable_core_dumps.sh
- archery docker run \
- -e ARCHERY_DEFAULT_BRANCH=main \
- -e ARCHERY_INTEGRATION_TARGET_IMPLEMENTATIONS=java \
- -e ARCHERY_INTEGRATION_WITH_GO=1 \
- -e ARCHERY_INTEGRATION_WITH_JAVA=1 \
- -e ARCHERY_INTEGRATION_WITH_NANOARROW=1 \
- -e ARCHERY_INTEGRATION_WITH_RUST=1 \
- conda-integration
diff --git a/.gitignore b/.gitignore
index b57597af47..17d1d43ae1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,10 +7,12 @@
.buildpath
.checkstyle
.classpath
+.cursor/
.factorypath
.idea/
.project
.settings/
+.vscode/
/*-build/
/.mvn/.develocity/
/apache-arrow-java-*
diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml
index 1dd32d62d6..74482cb2c4 100644
--- a/.mvn/extensions.xml
+++ b/.mvn/extensions.xml
@@ -23,11 +23,11 @@
com.gradledevelocity-maven-extension
- 1.23.1
+ 2.5.0com.gradlecommon-custom-user-data-maven-extension
- 2.0.1
+ 2.3.0
diff --git a/Brewfile b/Brewfile
index af6bd65615..2c47a38af5 100644
--- a/Brewfile
+++ b/Brewfile
@@ -15,5 +15,5 @@
# specific language governing permissions and limitations
# under the License.
-brew "openjdk@11"
+brew "openjdk@17"
brew "sccache"
diff --git a/README.md b/README.md
index c46b61c49e..0196536514 100644
--- a/README.md
+++ b/README.md
@@ -23,11 +23,11 @@
The following guides explain the fundamental data structures used in the Java implementation of Apache Arrow.
-- [ValueVector](https://arrow.apache.org/docs/java/vector.html) is an abstraction that is used to store a sequence of values having the same type in an individual column.
-- [VectorSchemaRoot](https://arrow.apache.org/docs/java/vector_schema_root.html) is a container that can hold multiple vectors based on a schema.
-- The [Reading/Writing IPC formats](https://arrow.apache.org/docs/java/ipc.html) guide explains how to stream record batches as well as serializing record batches to files.
+- [ValueVector](https://arrow.apache.org/java/current/vector.html) is an abstraction that is used to store a sequence of values having the same type in an individual column.
+- [VectorSchemaRoot](https://arrow.apache.org/java/current/vector_schema_root.html#vectorschemaroot) is a container that can hold multiple vectors based on a schema.
+- The [Reading/Writing IPC formats](https://arrow.apache.org/java/current/ipc.html) guide explains how to stream record batches as well as serializing record batches to files.
-Generated javadoc documentation is available [here](https://arrow.apache.org/docs/java/).
+Generated javadoc documentation is available [here](https://arrow.apache.org/java/current/).
## Building from source
@@ -50,7 +50,7 @@ a version of your choosing.
$ flatc --version
flatc version 25.1.24
-$ grep "dep.fbs.version" java/pom.xml
+$ grep "dep.fbs.version" pom.xml
25.1.24
```
@@ -60,10 +60,10 @@ $ grep "dep.fbs.version" java/pom.xml
cd $ARROW_HOME
# remove the existing files
-rm -rf java/format/src
+rm -rf format/src
# regenerate from the .fbs files
-flatc --java -o java/format/src/main/java format/*.fbs
+flatc --java -o format/src/main/java arrow-format/*.fbs
# prepend license header
mvn spotless:apply -pl :arrow-format
@@ -93,7 +93,7 @@ conflicting or duplicate fields set this JVM flag or use the correct static cons
## Java Code Style Guide
-Arrow Java follows the Google style guide [here][3] with the following
+Arrow Java follows the [Google Java Style Guide](http://google.github.io/styleguide/javaguide.html) with the following
differences:
* Imports are grouped, from top to bottom, in this order: static imports,
@@ -119,12 +119,12 @@ following command run in the project root directory:
mvn -Dlogback.configurationFile=file:
```
-See [Logback Configuration][1] for more details.
+See [Logback Configuration](https://logback.qos.ch/manual/configuration.html) for more details.
## Integration Tests
Integration tests which require more time or more memory can be run by activating
-the `integration-tests` profile. This activates the [maven failsafe][4] plugin
+the `integration-tests` profile. This activates the [Maven Failsafe](https://maven.apache.org/surefire/maven-failsafe-plugin/) plugin
and any class prefixed with `IT` will be run during the testing phase. The integration
tests currently require a larger amount of memory (>4GB) and time to complete. To activate
the profile:
@@ -132,8 +132,3 @@ the profile:
```bash
mvn -Pintegration-tests
```
-
-[1]: https://logback.qos.ch/manual/configuration.html
-[2]: https://github.com/apache/arrow/blob/main/cpp/README.md
-[3]: http://google.github.io/styleguide/javaguide.html
-[4]: https://maven.apache.org/surefire/maven-failsafe-plugin/
diff --git a/adapter/avro/pom.xml b/adapter/avro/pom.xml
index 827d19f2a2..4f7f90d7a9 100644
--- a/adapter/avro/pom.xml
+++ b/adapter/avro/pom.xml
@@ -23,7 +23,7 @@ under the License.
org.apache.arrowarrow-java-root
- 19.0.0-SNAPSHOT
+ 20.0.0-SNAPSHOT../../pom.xml
diff --git a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/ArrowToAvroUtils.java b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/ArrowToAvroUtils.java
index 6f0cb5cffc..e09b99f670 100644
--- a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/ArrowToAvroUtils.java
+++ b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/ArrowToAvroUtils.java
@@ -17,10 +17,14 @@
package org.apache.arrow.adapter.avro;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
import org.apache.arrow.adapter.avro.producers.AvroBigIntProducer;
import org.apache.arrow.adapter.avro.producers.AvroBooleanProducer;
import org.apache.arrow.adapter.avro.producers.AvroBytesProducer;
+import org.apache.arrow.adapter.avro.producers.AvroEnumProducer;
import org.apache.arrow.adapter.avro.producers.AvroFixedSizeBinaryProducer;
import org.apache.arrow.adapter.avro.producers.AvroFixedSizeListProducer;
import org.apache.arrow.adapter.avro.producers.AvroFloat2Producer;
@@ -41,6 +45,7 @@
import org.apache.arrow.adapter.avro.producers.AvroUint8Producer;
import org.apache.arrow.adapter.avro.producers.BaseAvroProducer;
import org.apache.arrow.adapter.avro.producers.CompositeAvroProducer;
+import org.apache.arrow.adapter.avro.producers.DictionaryDecodingProducer;
import org.apache.arrow.adapter.avro.producers.Producer;
import org.apache.arrow.adapter.avro.producers.logical.AvroDateDayProducer;
import org.apache.arrow.adapter.avro.producers.logical.AvroDateMilliProducer;
@@ -59,6 +64,7 @@
import org.apache.arrow.adapter.avro.producers.logical.AvroTimestampSecProducer;
import org.apache.arrow.adapter.avro.producers.logical.AvroTimestampSecTzProducer;
import org.apache.arrow.util.Preconditions;
+import org.apache.arrow.vector.BaseIntVector;
import org.apache.arrow.vector.BigIntVector;
import org.apache.arrow.vector.BitVector;
import org.apache.arrow.vector.DateDayVector;
@@ -96,11 +102,14 @@
import org.apache.arrow.vector.complex.ListVector;
import org.apache.arrow.vector.complex.MapVector;
import org.apache.arrow.vector.complex.StructVector;
+import org.apache.arrow.vector.dictionary.Dictionary;
+import org.apache.arrow.vector.dictionary.DictionaryProvider;
import org.apache.arrow.vector.types.FloatingPointPrecision;
import org.apache.arrow.vector.types.TimeUnit;
import org.apache.arrow.vector.types.Types;
import org.apache.arrow.vector.types.pojo.ArrowType;
import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.util.Text;
import org.apache.avro.LogicalType;
import org.apache.avro.LogicalTypes;
import org.apache.avro.Schema;
@@ -162,17 +171,29 @@ public class ArrowToAvroUtils {
* may be nullable. Record types must contain at least one child field and cannot contain multiple
* fields with the same name
*
+ *
String fields that are dictionary-encoded will be represented as an Avro enum, so long as
+ * all the values meet the restrictions on Avro enums (non-null, valid identifiers). Other data
+ * types that are dictionary encoded, or string fields that do not meet the avro requirements,
+ * will be output as their decoded type.
+ *
* @param arrowFields The arrow fields used to generate the Avro schema
* @param typeName Name of the top level Avro record type
* @param namespace Namespace of the top level Avro record type
+ * @param dictionaries A dictionary provider is required if any fields use dictionary encoding
* @return An Avro record schema for the given list of fields, with the specified name and
* namespace
*/
public static Schema createAvroSchema(
- List arrowFields, String typeName, String namespace) {
+ List arrowFields, String typeName, String namespace, DictionaryProvider dictionaries) {
SchemaBuilder.RecordBuilder assembler =
SchemaBuilder.record(typeName).namespace(namespace);
- return buildRecordSchema(assembler, arrowFields, namespace);
+ return buildRecordSchema(assembler, arrowFields, namespace, dictionaries);
+ }
+
+ /** Overload provided for convenience, sets dictionaries = null. */
+ public static Schema createAvroSchema(
+ List arrowFields, String typeName, String namespace) {
+ return createAvroSchema(arrowFields, typeName, namespace, null);
}
/** Overload provided for convenience, sets namespace = null. */
@@ -185,61 +206,83 @@ public static Schema createAvroSchema(List arrowFields) {
return createAvroSchema(arrowFields, GENERIC_RECORD_TYPE_NAME);
}
+ /**
+ * Overload provided for convenience, sets name = GENERIC_RECORD_TYPE_NAME and namespace = null.
+ */
+ public static Schema createAvroSchema(List arrowFields, DictionaryProvider dictionaries) {
+ return createAvroSchema(arrowFields, GENERIC_RECORD_TYPE_NAME, null, dictionaries);
+ }
+
private static T buildRecordSchema(
- SchemaBuilder.RecordBuilder builder, List fields, String namespace) {
+ SchemaBuilder.RecordBuilder builder,
+ List fields,
+ String namespace,
+ DictionaryProvider dictionaries) {
if (fields.isEmpty()) {
throw new IllegalArgumentException("Record field must have at least one child field");
}
SchemaBuilder.FieldAssembler assembler = builder.namespace(namespace).fields();
for (Field field : fields) {
- assembler = buildFieldSchema(assembler, field, namespace);
+ assembler = buildFieldSchema(assembler, field, namespace, dictionaries);
}
return assembler.endRecord();
}
private static SchemaBuilder.FieldAssembler buildFieldSchema(
- SchemaBuilder.FieldAssembler assembler, Field field, String namespace) {
+ SchemaBuilder.FieldAssembler assembler,
+ Field field,
+ String namespace,
+ DictionaryProvider dictionaries) {
return assembler
.name(field.getName())
- .type(buildTypeSchema(SchemaBuilder.builder(), field, namespace))
+ .type(buildTypeSchema(SchemaBuilder.builder(), field, namespace, dictionaries))
.noDefault();
}
private static T buildTypeSchema(
- SchemaBuilder.TypeBuilder builder, Field field, String namespace) {
+ SchemaBuilder.TypeBuilder builder,
+ Field field,
+ String namespace,
+ DictionaryProvider dictionaries) {
// Nullable unions need special handling, since union types cannot be directly nested
if (field.getType().getTypeID() == ArrowType.ArrowTypeID.Union) {
boolean unionNullable = field.getChildren().stream().anyMatch(Field::isNullable);
if (unionNullable) {
SchemaBuilder.UnionAccumulator union = builder.unionOf().nullType();
- return addTypesToUnion(union, field.getChildren(), namespace);
+ return addTypesToUnion(union, field.getChildren(), namespace, dictionaries);
} else {
Field headType = field.getChildren().get(0);
List tailTypes = field.getChildren().subList(1, field.getChildren().size());
SchemaBuilder.UnionAccumulator union =
- buildBaseTypeSchema(builder.unionOf(), headType, namespace);
- return addTypesToUnion(union, tailTypes, namespace);
+ buildBaseTypeSchema(builder.unionOf(), headType, namespace, dictionaries);
+ return addTypesToUnion(union, tailTypes, namespace, dictionaries);
}
} else if (field.isNullable()) {
- return buildBaseTypeSchema(builder.nullable(), field, namespace);
+ return buildBaseTypeSchema(builder.nullable(), field, namespace, dictionaries);
} else {
- return buildBaseTypeSchema(builder, field, namespace);
+ return buildBaseTypeSchema(builder, field, namespace, dictionaries);
}
}
private static T buildArraySchema(
- SchemaBuilder.ArrayBuilder builder, Field listField, String namespace) {
+ SchemaBuilder.ArrayBuilder builder,
+ Field listField,
+ String namespace,
+ DictionaryProvider dictionaries) {
if (listField.getChildren().size() != 1) {
throw new IllegalArgumentException("List field must have exactly one child field");
}
Field itemField = listField.getChildren().get(0);
- return buildTypeSchema(builder.items(), itemField, namespace);
+ return buildTypeSchema(builder.items(), itemField, namespace, dictionaries);
}
private static T buildMapSchema(
- SchemaBuilder.MapBuilder builder, Field mapField, String namespace) {
+ SchemaBuilder.MapBuilder builder,
+ Field mapField,
+ String namespace,
+ DictionaryProvider dictionaries) {
if (mapField.getChildren().size() != 1) {
throw new IllegalArgumentException("Map field must have exactly one child field");
}
@@ -253,11 +296,14 @@ private static T buildMapSchema(
throw new IllegalArgumentException(
"Map keys must be of type string and cannot be nullable for conversion to Avro");
}
- return buildTypeSchema(builder.values(), valueField, namespace);
+ return buildTypeSchema(builder.values(), valueField, namespace, dictionaries);
}
private static T buildBaseTypeSchema(
- SchemaBuilder.BaseTypeBuilder builder, Field field, String namespace) {
+ SchemaBuilder.BaseTypeBuilder builder,
+ Field field,
+ String namespace,
+ DictionaryProvider dictionaries) {
ArrowType.ArrowTypeID typeID = field.getType().getTypeID();
@@ -269,6 +315,33 @@ private static T buildBaseTypeSchema(
return builder.booleanType();
case Int:
+ if (field.getDictionary() != null) {
+ if (dictionaries == null) {
+ throw new IllegalArgumentException(
+ "Field references a dictionary but no dictionaries were provided: "
+ + field.getName());
+ }
+ Dictionary dictionary = dictionaries.lookup(field.getDictionary().getId());
+ if (dictionary == null) {
+ throw new IllegalArgumentException(
+ "Field references a dictionary that does not exist: "
+ + field.getName()
+ + ", dictionary ID = "
+ + field.getDictionary().getId());
+ }
+ if (dictionaryIsValidEnum(dictionary)) {
+ String[] symbols = dictionarySymbols(dictionary);
+ return builder.enumeration(field.getName()).symbols(symbols);
+ } else {
+ Field decodedField =
+ new Field(
+ field.getName(),
+ dictionary.getVector().getField().getFieldType(),
+ dictionary.getVector().getField().getChildren());
+ return buildBaseTypeSchema(builder, decodedField, namespace, dictionaries);
+ }
+ }
+
ArrowType.Int intType = (ArrowType.Int) field.getType();
if (intType.getBitWidth() > 32 || (intType.getBitWidth() == 32 && !intType.getIsSigned())) {
return builder.longType();
@@ -328,14 +401,24 @@ private static T buildBaseTypeSchema(
String childNamespace =
namespace == null ? field.getName() : namespace + "." + field.getName();
return buildRecordSchema(
- builder.record(field.getName()), field.getChildren(), childNamespace);
+ builder.record(field.getName()), field.getChildren(), childNamespace, dictionaries);
case List:
case FixedSizeList:
- return buildArraySchema(builder.array(), field, namespace);
+ // Arrow uses "$data$" as the field name for list items, that is not a valid Avro name
+ Field itemField = field.getChildren().get(0);
+ if (ListVector.DATA_VECTOR_NAME.equals(itemField.getName())) {
+ Field safeItemField =
+ new Field("item", itemField.getFieldType(), itemField.getChildren());
+ Field safeListField =
+ new Field(field.getName(), field.getFieldType(), List.of(safeItemField));
+ return buildArraySchema(builder.array(), safeListField, namespace, dictionaries);
+ } else {
+ return buildArraySchema(builder.array(), field, namespace, dictionaries);
+ }
case Map:
- return buildMapSchema(builder.map(), field, namespace);
+ return buildMapSchema(builder.map(), field, namespace, dictionaries);
default:
throw new IllegalArgumentException(
@@ -344,9 +427,12 @@ private static T buildBaseTypeSchema(
}
private static T addTypesToUnion(
- SchemaBuilder.UnionAccumulator accumulator, List unionFields, String namespace) {
+ SchemaBuilder.UnionAccumulator accumulator,
+ List unionFields,
+ String namespace,
+ DictionaryProvider dictionaries) {
for (var field : unionFields) {
- accumulator = buildBaseTypeSchema(accumulator.and(), field, namespace);
+ accumulator = buildBaseTypeSchema(accumulator.and(), field, namespace, dictionaries);
}
return accumulator.endUnion();
}
@@ -363,30 +449,88 @@ private static LogicalType timestampLogicalType(ArrowType.Timestamp timestampTyp
}
}
+ private static boolean dictionaryIsValidEnum(Dictionary dictionary) {
+
+ if (dictionary.getVectorType().getTypeID() != ArrowType.ArrowTypeID.Utf8) {
+ return false;
+ }
+
+ VarCharVector vector = (VarCharVector) dictionary.getVector();
+ Set symbols = new HashSet<>();
+
+ for (int i = 0; i < vector.getValueCount(); i++) {
+ if (vector.isNull(i)) {
+ return false;
+ }
+ Text text = vector.getObject(i);
+ if (text == null) {
+ return false;
+ }
+ String symbol = text.toString();
+ if (!ENUM_REGEX.matcher(symbol).matches()) {
+ return false;
+ }
+ if (symbols.contains(symbol)) {
+ return false;
+ }
+ symbols.add(symbol);
+ }
+
+ return true;
+ }
+
+ private static String[] dictionarySymbols(Dictionary dictionary) {
+
+ VarCharVector vector = (VarCharVector) dictionary.getVector();
+ String[] symbols = new String[vector.getValueCount()];
+
+ for (int i = 0; i < vector.getValueCount(); i++) {
+ Text text = vector.getObject(i);
+ // This should never happen if dictionaryIsValidEnum() succeeded
+ if (text == null) {
+ throw new IllegalArgumentException("Illegal null value in enum");
+ }
+ symbols[i] = text.toString();
+ }
+
+ return symbols;
+ }
+
+ private static final Pattern ENUM_REGEX = Pattern.compile("^[A-Za-z_][A-Za-z0-9_]*$");
+
/**
* Create a composite Avro producer for a set of field vectors (typically the root set of a VSR).
*
* @param vectors The vectors that will be used to produce Avro data
* @return The resulting composite Avro producer
*/
- public static CompositeAvroProducer createCompositeProducer(List vectors) {
+ public static CompositeAvroProducer createCompositeProducer(
+ List vectors, DictionaryProvider dictionaries) {
List> producers = new ArrayList<>(vectors.size());
for (FieldVector vector : vectors) {
- BaseAvroProducer extends FieldVector> producer = createProducer(vector);
+ BaseAvroProducer extends FieldVector> producer = createProducer(vector, dictionaries);
producers.add(producer);
}
return new CompositeAvroProducer(producers);
}
- private static BaseAvroProducer> createProducer(FieldVector vector) {
+ /** Overload provided for convenience, sets dictionaries = null. */
+ public static CompositeAvroProducer createCompositeProducer(List vectors) {
+
+ return createCompositeProducer(vectors, null);
+ }
+
+ private static BaseAvroProducer> createProducer(
+ FieldVector vector, DictionaryProvider dictionaries) {
boolean nullable = vector.getField().isNullable();
- return createProducer(vector, nullable);
+ return createProducer(vector, nullable, dictionaries);
}
- private static BaseAvroProducer> createProducer(FieldVector vector, boolean nullable) {
+ private static BaseAvroProducer> createProducer(
+ FieldVector vector, boolean nullable, DictionaryProvider dictionaries) {
Preconditions.checkNotNull(vector, "Arrow vector object can't be null");
@@ -395,10 +539,34 @@ private static BaseAvroProducer> createProducer(FieldVector vector, boolean nu
// Avro understands nullable types as a union of type | null
// Most nullable fields in a VSR will not be unions, so provide a special wrapper
if (nullable && minorType != Types.MinorType.UNION) {
- final BaseAvroProducer> innerProducer = createProducer(vector, false);
+ final BaseAvroProducer> innerProducer = createProducer(vector, false, dictionaries);
return new AvroNullableProducer<>(innerProducer);
}
+ if (vector.getField().getDictionary() != null) {
+ if (dictionaries == null) {
+ throw new IllegalArgumentException(
+ "Field references a dictionary but no dictionaries were provided: "
+ + vector.getField().getName());
+ }
+ Dictionary dictionary = dictionaries.lookup(vector.getField().getDictionary().getId());
+ if (dictionary == null) {
+ throw new IllegalArgumentException(
+ "Field references a dictionary that does not exist: "
+ + vector.getField().getName()
+ + ", dictionary ID = "
+ + vector.getField().getDictionary().getId());
+ }
+ // If a field is dictionary-encoded but cannot be represented as an Avro enum,
+ // then decode it before writing
+ if (dictionaryIsValidEnum(dictionary)) {
+ return new AvroEnumProducer((BaseIntVector) vector);
+ } else {
+ BaseAvroProducer> dictProducer = createProducer(dictionary.getVector(), false, null);
+ return new DictionaryDecodingProducer<>((BaseIntVector) vector, dictProducer);
+ }
+ }
+
switch (minorType) {
case NULL:
return new AvroNullProducer((NullVector) vector);
@@ -476,21 +644,23 @@ private static BaseAvroProducer> createProducer(FieldVector vector, boolean nu
Producer>[] childProducers = new Producer>[childVectors.size()];
for (int i = 0; i < childVectors.size(); i++) {
FieldVector childVector = childVectors.get(i);
- childProducers[i] = createProducer(childVector, childVector.getField().isNullable());
+ childProducers[i] =
+ createProducer(childVector, childVector.getField().isNullable(), dictionaries);
}
return new AvroStructProducer(structVector, childProducers);
case LIST:
ListVector listVector = (ListVector) vector;
FieldVector itemVector = listVector.getDataVector();
- Producer> itemProducer = createProducer(itemVector, itemVector.getField().isNullable());
+ Producer> itemProducer =
+ createProducer(itemVector, itemVector.getField().isNullable(), dictionaries);
return new AvroListProducer(listVector, itemProducer);
case FIXED_SIZE_LIST:
FixedSizeListVector fixedListVector = (FixedSizeListVector) vector;
FieldVector fixedItemVector = fixedListVector.getDataVector();
Producer> fixedItemProducer =
- createProducer(fixedItemVector, fixedItemVector.getField().isNullable());
+ createProducer(fixedItemVector, fixedItemVector.getField().isNullable(), dictionaries);
return new AvroFixedSizeListProducer(fixedListVector, fixedItemProducer);
case MAP:
@@ -504,7 +674,7 @@ private static BaseAvroProducer> createProducer(FieldVector vector, boolean nu
FieldVector valueVector = entryVector.getChildrenFromFields().get(1);
Producer> keyProducer = new AvroStringProducer(keyVector);
Producer> valueProducer =
- createProducer(valueVector, valueVector.getField().isNullable());
+ createProducer(valueVector, valueVector.getField().isNullable(), dictionaries);
Producer> entryProducer =
new AvroStructProducer(entryVector, new Producer>[] {keyProducer, valueProducer});
return new AvroMapProducer(mapVector, entryProducer);
diff --git a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/AvroToArrow.java b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/AvroToArrow.java
index 2392c36f94..2a28ad393b 100644
--- a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/AvroToArrow.java
+++ b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/AvroToArrow.java
@@ -59,4 +59,23 @@ public static AvroToArrowVectorIterator avroToArrowIterator(
return AvroToArrowVectorIterator.create(decoder, schema, config);
}
+
+ /**
+ * Convert an Avro schema to its Arrow equivalent.
+ *
+ *
The resulting set of Arrow fields matches what would be set in the VSR after calling
+ * avroToArrow() or avroToArrowIterator(), respecting the configuration in the config parameter.
+ *
+ * @param schema The Avro schema to convert
+ * @param config Configuration options for conversion
+ * @return The equivalent Arrow schema
+ */
+ public static org.apache.arrow.vector.types.pojo.Schema avroToAvroSchema(
+ Schema schema, AvroToArrowConfig config) {
+
+ Preconditions.checkNotNull(schema, "Avro schema object cannot be null");
+ Preconditions.checkNotNull(config, "config cannot be null");
+
+ return AvroToArrowUtils.createArrowSchema(schema, config);
+ }
}
diff --git a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/AvroToArrowConfig.java b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/AvroToArrowConfig.java
index bd70c2b8ba..5596138586 100644
--- a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/AvroToArrowConfig.java
+++ b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/AvroToArrowConfig.java
@@ -41,6 +41,12 @@ public class AvroToArrowConfig {
/** The field names which to skip when reading decoder values. */
private final Set skipFieldNames;
+ /**
+ * Use legacy-mode to keep compatibility with old behavior (pre-2025), enabled by default. This
+ * affects how the AvroToArrow code interprets the Avro schema.
+ */
+ private final boolean legacyMode;
+
/**
* Instantiate an instance.
*
@@ -64,6 +70,37 @@ public class AvroToArrowConfig {
this.targetBatchSize = targetBatchSize;
this.provider = provider;
this.skipFieldNames = skipFieldNames;
+
+ // Default values for optional parameters
+ legacyMode = true; // Keep compatibility with old behavior by default
+ }
+
+ /**
+ * Instantiate an instance.
+ *
+ * @param allocator The memory allocator to construct the Arrow vectors with.
+ * @param targetBatchSize The maximum rowCount to read each time when partially convert data.
+ * @param provider The dictionary provider used for enum type, adapter will update this provider.
+ * @param skipFieldNames Field names which to skip.
+ * @param legacyMode Keep compatibility with old behavior (pre-2025)
+ */
+ AvroToArrowConfig(
+ BufferAllocator allocator,
+ int targetBatchSize,
+ DictionaryProvider.MapDictionaryProvider provider,
+ Set skipFieldNames,
+ boolean legacyMode) {
+
+ Preconditions.checkArgument(
+ targetBatchSize == AvroToArrowVectorIterator.NO_LIMIT_BATCH_SIZE || targetBatchSize > 0,
+ "invalid targetBatchSize: %s",
+ targetBatchSize);
+
+ this.allocator = allocator;
+ this.targetBatchSize = targetBatchSize;
+ this.provider = provider;
+ this.skipFieldNames = skipFieldNames;
+ this.legacyMode = legacyMode;
}
public BufferAllocator getAllocator() {
@@ -81,4 +118,8 @@ public DictionaryProvider.MapDictionaryProvider getProvider() {
public Set getSkipFieldNames() {
return skipFieldNames;
}
+
+ public boolean isLegacyMode() {
+ return legacyMode;
+ }
}
diff --git a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/AvroToArrowUtils.java b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/AvroToArrowUtils.java
index ed7642aabd..a6e77e4050 100644
--- a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/AvroToArrowUtils.java
+++ b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/AvroToArrowUtils.java
@@ -41,6 +41,7 @@
import org.apache.arrow.adapter.avro.consumers.AvroLongConsumer;
import org.apache.arrow.adapter.avro.consumers.AvroMapConsumer;
import org.apache.arrow.adapter.avro.consumers.AvroNullConsumer;
+import org.apache.arrow.adapter.avro.consumers.AvroNullableConsumer;
import org.apache.arrow.adapter.avro.consumers.AvroStringConsumer;
import org.apache.arrow.adapter.avro.consumers.AvroStructConsumer;
import org.apache.arrow.adapter.avro.consumers.AvroUnionsConsumer;
@@ -49,17 +50,23 @@
import org.apache.arrow.adapter.avro.consumers.SkipConsumer;
import org.apache.arrow.adapter.avro.consumers.SkipFunction;
import org.apache.arrow.adapter.avro.consumers.logical.AvroDateConsumer;
+import org.apache.arrow.adapter.avro.consumers.logical.AvroDecimal256Consumer;
import org.apache.arrow.adapter.avro.consumers.logical.AvroDecimalConsumer;
import org.apache.arrow.adapter.avro.consumers.logical.AvroTimeMicroConsumer;
import org.apache.arrow.adapter.avro.consumers.logical.AvroTimeMillisConsumer;
import org.apache.arrow.adapter.avro.consumers.logical.AvroTimestampMicrosConsumer;
+import org.apache.arrow.adapter.avro.consumers.logical.AvroTimestampMicrosTzConsumer;
import org.apache.arrow.adapter.avro.consumers.logical.AvroTimestampMillisConsumer;
+import org.apache.arrow.adapter.avro.consumers.logical.AvroTimestampMillisTzConsumer;
+import org.apache.arrow.adapter.avro.consumers.logical.AvroTimestampNanosConsumer;
+import org.apache.arrow.adapter.avro.consumers.logical.AvroTimestampNanosTzConsumer;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.util.Preconditions;
import org.apache.arrow.vector.BaseIntVector;
import org.apache.arrow.vector.BigIntVector;
import org.apache.arrow.vector.BitVector;
import org.apache.arrow.vector.DateDayVector;
+import org.apache.arrow.vector.Decimal256Vector;
import org.apache.arrow.vector.DecimalVector;
import org.apache.arrow.vector.FieldVector;
import org.apache.arrow.vector.FixedSizeBinaryVector;
@@ -69,8 +76,12 @@
import org.apache.arrow.vector.NullVector;
import org.apache.arrow.vector.TimeMicroVector;
import org.apache.arrow.vector.TimeMilliVector;
+import org.apache.arrow.vector.TimeStampMicroTZVector;
import org.apache.arrow.vector.TimeStampMicroVector;
+import org.apache.arrow.vector.TimeStampMilliTZVector;
import org.apache.arrow.vector.TimeStampMilliVector;
+import org.apache.arrow.vector.TimeStampNanoTZVector;
+import org.apache.arrow.vector.TimeStampNanoVector;
import org.apache.arrow.vector.VarBinaryVector;
import org.apache.arrow.vector.VarCharVector;
import org.apache.arrow.vector.VectorSchemaRoot;
@@ -169,42 +180,69 @@ private static Consumer createConsumer(
switch (type) {
case UNION:
- consumer = createUnionConsumer(schema, name, config, consumerVector);
+ boolean nullableUnion =
+ schema.getTypes().stream().anyMatch(t -> t.getType() == Schema.Type.NULL);
+ if (schema.getTypes().size() == 2 && nullableUnion && !config.isLegacyMode()) {
+ // For a simple nullable (null | type), interpret the union as a single nullable field.
+ // Not available in legacy mode, which uses the literal interpretation instead
+ int nullIndex = schema.getTypes().get(0).getType() == Schema.Type.NULL ? 0 : 1;
+ int childIndex = nullIndex == 0 ? 1 : 0;
+ Schema childSchema = schema.getTypes().get(childIndex);
+ Consumer> childConsumer =
+ createConsumer(childSchema, name, true, config, consumerVector);
+ consumer = new AvroNullableConsumer<>(childConsumer, nullIndex);
+ } else {
+ // Literal interpretation of a union, which may or may not include a null element.
+ consumer = createUnionConsumer(schema, name, nullableUnion, config, consumerVector);
+ }
break;
case ARRAY:
- consumer = createArrayConsumer(schema, name, config, consumerVector);
+ consumer = createArrayConsumer(schema, name, nullable, config, consumerVector);
break;
case MAP:
- consumer = createMapConsumer(schema, name, config, consumerVector);
+ consumer = createMapConsumer(schema, name, nullable, config, consumerVector);
break;
case RECORD:
- consumer = createStructConsumer(schema, name, config, consumerVector);
+ consumer = createStructConsumer(schema, name, nullable, config, consumerVector);
break;
case ENUM:
- consumer = createEnumConsumer(schema, name, config, consumerVector);
+ consumer = createEnumConsumer(schema, name, nullable, config, consumerVector);
break;
case STRING:
arrowType = new ArrowType.Utf8();
- fieldType = new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema));
+ fieldType =
+ new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
vector = createVector(consumerVector, fieldType, name, allocator);
consumer = new AvroStringConsumer((VarCharVector) vector);
break;
case FIXED:
- Map extProps = createExternalProps(schema);
+ Map extProps = createExternalProps(schema, config);
if (logicalType instanceof LogicalTypes.Decimal) {
- arrowType = createDecimalArrowType((LogicalTypes.Decimal) logicalType);
+ arrowType = createDecimalArrowType((LogicalTypes.Decimal) logicalType, schema);
fieldType =
new FieldType(
- nullable, arrowType, /* dictionary= */ null, getMetaData(schema, extProps));
+ nullable,
+ arrowType,
+ /* dictionary= */ null,
+ getMetaData(schema, extProps, config));
vector = createVector(consumerVector, fieldType, name, allocator);
- consumer =
- new AvroDecimalConsumer.FixedDecimalConsumer(
- (DecimalVector) vector, schema.getFixedSize());
+ if (schema.getFixedSize() <= 16) {
+ consumer =
+ new AvroDecimalConsumer.FixedDecimalConsumer(
+ (DecimalVector) vector, schema.getFixedSize());
+ } else {
+ consumer =
+ new AvroDecimal256Consumer.FixedDecimal256Consumer(
+ (Decimal256Vector) vector, schema.getFixedSize());
+ }
} else {
arrowType = new ArrowType.FixedSizeBinary(schema.getFixedSize());
fieldType =
new FieldType(
- nullable, arrowType, /* dictionary= */ null, getMetaData(schema, extProps));
+ nullable,
+ arrowType,
+ /* dictionary= */ null,
+ getMetaData(schema, extProps, config));
vector = createVector(consumerVector, fieldType, name, allocator);
consumer = new AvroFixedConsumer((FixedSizeBinaryVector) vector, schema.getFixedSize());
}
@@ -213,26 +251,30 @@ private static Consumer createConsumer(
if (logicalType instanceof LogicalTypes.Date) {
arrowType = new ArrowType.Date(DateUnit.DAY);
fieldType =
- new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema));
+ new FieldType(
+ nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
vector = createVector(consumerVector, fieldType, name, allocator);
consumer = new AvroDateConsumer((DateDayVector) vector);
} else if (logicalType instanceof LogicalTypes.TimeMillis) {
arrowType = new ArrowType.Time(TimeUnit.MILLISECOND, 32);
fieldType =
- new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema));
+ new FieldType(
+ nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
vector = createVector(consumerVector, fieldType, name, allocator);
consumer = new AvroTimeMillisConsumer((TimeMilliVector) vector);
} else {
arrowType = new ArrowType.Int(32, /* isSigned= */ true);
fieldType =
- new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema));
+ new FieldType(
+ nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
vector = createVector(consumerVector, fieldType, name, allocator);
consumer = new AvroIntConsumer((IntVector) vector);
}
break;
case BOOLEAN:
arrowType = new ArrowType.Bool();
- fieldType = new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema));
+ fieldType =
+ new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
vector = createVector(consumerVector, fieldType, name, allocator);
consumer = new AvroBooleanConsumer((BitVector) vector);
break;
@@ -240,60 +282,109 @@ private static Consumer createConsumer(
if (logicalType instanceof LogicalTypes.TimeMicros) {
arrowType = new ArrowType.Time(TimeUnit.MICROSECOND, 64);
fieldType =
- new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema));
+ new FieldType(
+ nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
vector = createVector(consumerVector, fieldType, name, allocator);
consumer = new AvroTimeMicroConsumer((TimeMicroVector) vector);
- } else if (logicalType instanceof LogicalTypes.TimestampMillis) {
+ } else if (logicalType instanceof LogicalTypes.TimestampMillis && !config.isLegacyMode()) {
+ // In legacy mode the timestamp-xxx types are treated as local, there is no zone aware
+ // type
+ arrowType = new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC");
+ fieldType =
+ new FieldType(
+ nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
+ vector = createVector(consumerVector, fieldType, name, allocator);
+ consumer = new AvroTimestampMillisTzConsumer((TimeStampMilliTZVector) vector);
+ } else if (logicalType instanceof LogicalTypes.TimestampMicros && !config.isLegacyMode()) {
+ arrowType = new ArrowType.Timestamp(TimeUnit.MICROSECOND, "UTC");
+ fieldType =
+ new FieldType(
+ nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
+ vector = createVector(consumerVector, fieldType, name, allocator);
+ consumer = new AvroTimestampMicrosTzConsumer((TimeStampMicroTZVector) vector);
+ } else if (logicalType instanceof LogicalTypes.TimestampNanos && !config.isLegacyMode()) {
+ arrowType = new ArrowType.Timestamp(TimeUnit.NANOSECOND, "UTC");
+ fieldType =
+ new FieldType(
+ nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
+ vector = createVector(consumerVector, fieldType, name, allocator);
+ consumer = new AvroTimestampNanosTzConsumer((TimeStampNanoTZVector) vector);
+ } else if (logicalType instanceof LogicalTypes.LocalTimestampMillis
+ || (logicalType instanceof LogicalTypes.TimestampMillis && config.isLegacyMode())) {
arrowType = new ArrowType.Timestamp(TimeUnit.MILLISECOND, null);
fieldType =
- new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema));
+ new FieldType(
+ nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
vector = createVector(consumerVector, fieldType, name, allocator);
consumer = new AvroTimestampMillisConsumer((TimeStampMilliVector) vector);
- } else if (logicalType instanceof LogicalTypes.TimestampMicros) {
+ } else if (logicalType instanceof LogicalTypes.LocalTimestampMicros
+ || (logicalType instanceof LogicalTypes.TimestampMicros && config.isLegacyMode())) {
+ // In legacy mode the timestamp-xxx types are treated as local
arrowType = new ArrowType.Timestamp(TimeUnit.MICROSECOND, null);
fieldType =
- new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema));
+ new FieldType(
+ nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
vector = createVector(consumerVector, fieldType, name, allocator);
consumer = new AvroTimestampMicrosConsumer((TimeStampMicroVector) vector);
+ } else if (logicalType instanceof LogicalTypes.LocalTimestampNanos
+ || (logicalType instanceof LogicalTypes.TimestampNanos && config.isLegacyMode())) {
+ arrowType = new ArrowType.Timestamp(TimeUnit.NANOSECOND, null);
+ fieldType =
+ new FieldType(
+ nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
+ vector = createVector(consumerVector, fieldType, name, allocator);
+ consumer = new AvroTimestampNanosConsumer((TimeStampNanoVector) vector);
} else {
arrowType = new ArrowType.Int(64, /* isSigned= */ true);
fieldType =
- new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema));
+ new FieldType(
+ nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
vector = createVector(consumerVector, fieldType, name, allocator);
consumer = new AvroLongConsumer((BigIntVector) vector);
}
break;
case FLOAT:
arrowType = new ArrowType.FloatingPoint(SINGLE);
- fieldType = new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema));
+ fieldType =
+ new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
vector = createVector(consumerVector, fieldType, name, allocator);
consumer = new AvroFloatConsumer((Float4Vector) vector);
break;
case DOUBLE:
arrowType = new ArrowType.FloatingPoint(DOUBLE);
- fieldType = new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema));
+ fieldType =
+ new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
vector = createVector(consumerVector, fieldType, name, allocator);
consumer = new AvroDoubleConsumer((Float8Vector) vector);
break;
case BYTES:
if (logicalType instanceof LogicalTypes.Decimal) {
- arrowType = createDecimalArrowType((LogicalTypes.Decimal) logicalType);
+ LogicalTypes.Decimal decimalType = (LogicalTypes.Decimal) logicalType;
+ arrowType = createDecimalArrowType(decimalType, schema);
fieldType =
- new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema));
+ new FieldType(
+ nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
vector = createVector(consumerVector, fieldType, name, allocator);
- consumer = new AvroDecimalConsumer.BytesDecimalConsumer((DecimalVector) vector);
+ if (decimalType.getPrecision() <= 38) {
+ consumer = new AvroDecimalConsumer.BytesDecimalConsumer((DecimalVector) vector);
+ } else {
+ consumer =
+ new AvroDecimal256Consumer.BytesDecimal256Consumer((Decimal256Vector) vector);
+ }
} else {
arrowType = new ArrowType.Binary();
fieldType =
- new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema));
+ new FieldType(
+ nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
vector = createVector(consumerVector, fieldType, name, allocator);
consumer = new AvroBytesConsumer((VarBinaryVector) vector);
}
break;
case NULL:
arrowType = new ArrowType.Null();
- fieldType = new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema));
- vector = fieldType.createNewSingleVector(name, allocator, /* schemaCallBack= */ null);
+ fieldType =
+ new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema, config));
+ vector = new NullVector(name, fieldType); // Respect nullability defined in fieldType
consumer = new AvroNullConsumer((NullVector) vector);
break;
default:
@@ -304,19 +395,31 @@ private static Consumer createConsumer(
return consumer;
}
- private static ArrowType createDecimalArrowType(LogicalTypes.Decimal logicalType) {
+ private static ArrowType createDecimalArrowType(LogicalTypes.Decimal logicalType, Schema schema) {
final int scale = logicalType.getScale();
final int precision = logicalType.getPrecision();
Preconditions.checkArgument(
- precision > 0 && precision <= 38, "Precision must be in range of 1 to 38");
- Preconditions.checkArgument(scale >= 0 && scale <= 38, "Scale must be in range of 0 to 38.");
+ precision > 0 && precision <= 76, "Precision must be in range of 1 to 76");
+ Preconditions.checkArgument(scale >= 0 && scale <= 76, "Scale must be in range of 0 to 76.");
Preconditions.checkArgument(
scale <= precision,
"Invalid decimal scale: %s (greater than precision: %s)",
scale,
precision);
- return new ArrowType.Decimal(precision, scale, 128);
+ if (schema.getType() == Schema.Type.FIXED) {
+ if (schema.getFixedSize() <= 16) {
+ return new ArrowType.Decimal(precision, scale, 128);
+ } else {
+ return new ArrowType.Decimal(precision, scale, 256);
+ }
+ } else {
+ if (precision <= 38) {
+ return new ArrowType.Decimal(precision, scale, 128);
+ } else {
+ return new ArrowType.Decimal(precision, scale, 256);
+ }
+ }
}
private static Consumer createSkipConsumer(Schema schema) {
@@ -406,6 +509,30 @@ private static Consumer createSkipConsumer(Schema schema) {
return new SkipConsumer(skipFunction);
}
+ static org.apache.arrow.vector.types.pojo.Schema createArrowSchema(
+ Schema schema, AvroToArrowConfig config) {
+
+ // Create an Arrow schema matching the structure of vectors built by createCompositeConsumer()
+
+ Set skipFieldNames = config.getSkipFieldNames();
+ List arrowFields = new ArrayList<>(schema.getFields().size());
+
+ Schema.Type type = schema.getType();
+ if (type == Schema.Type.RECORD) {
+ for (Schema.Field field : schema.getFields()) {
+ if (!skipFieldNames.contains(field.name())) {
+ Field arrowField = avroSchemaToField(field.schema(), field.name(), config);
+ arrowFields.add(arrowField);
+ }
+ }
+ } else {
+ Field arrowField = avroSchemaToField(schema, schema.getName(), config);
+ arrowFields.add(arrowField);
+ }
+
+ return new org.apache.arrow.vector.types.pojo.Schema(arrowFields);
+ }
+
static CompositeAvroConsumer createCompositeConsumer(Schema schema, AvroToArrowConfig config) {
List consumers = new ArrayList<>();
@@ -442,11 +569,20 @@ private static String getDefaultFieldName(ArrowType type) {
}
private static Field avroSchemaToField(Schema schema, String name, AvroToArrowConfig config) {
- return avroSchemaToField(schema, name, config, null);
+ return avroSchemaToField(schema, name, false, config, null);
}
private static Field avroSchemaToField(
Schema schema, String name, AvroToArrowConfig config, Map externalProps) {
+ return avroSchemaToField(schema, name, false, config, externalProps);
+ }
+
+ private static Field avroSchemaToField(
+ Schema schema,
+ String name,
+ boolean nullable,
+ AvroToArrowConfig config,
+ Map externalProps) {
final Schema.Type type = schema.getType();
final LogicalType logicalType = schema.getLogicalType();
@@ -455,33 +591,53 @@ private static Field avroSchemaToField(
switch (type) {
case UNION:
- for (int i = 0; i < schema.getTypes().size(); i++) {
- Schema childSchema = schema.getTypes().get(i);
- // Union child vector should use default name
- children.add(avroSchemaToField(childSchema, null, config));
+ boolean nullableUnion =
+ schema.getTypes().stream().anyMatch(t -> t.getType() == Schema.Type.NULL);
+ if (nullableUnion && schema.getTypes().size() == 2 && !config.isLegacyMode()) {
+ // For a simple nullable (null | type), interpret the union as a single nullable field.
+ // Not available in legacy mode, which uses the literal interpretation instead
+ Schema childSchema =
+ schema.getTypes().get(0).getType() == Schema.Type.NULL
+ ? schema.getTypes().get(1)
+ : schema.getTypes().get(0);
+ return avroSchemaToField(childSchema, name, true, config, externalProps);
+ } else {
+ // Literal interpretation of a union, which may or may not include a null element.
+ for (int i = 0; i < schema.getTypes().size(); i++) {
+ Schema childSchema = schema.getTypes().get(i);
+ // Union child vector should use default name
+ children.add(avroSchemaToField(childSchema, null, nullableUnion, config, null));
+ }
+ fieldType =
+ createFieldType(
+ new ArrowType.Union(UnionMode.Sparse, null), schema, externalProps, config);
}
- fieldType =
- createFieldType(new ArrowType.Union(UnionMode.Sparse, null), schema, externalProps);
break;
case ARRAY:
Schema elementSchema = schema.getElementType();
- children.add(avroSchemaToField(elementSchema, elementSchema.getName(), config));
- fieldType = createFieldType(new ArrowType.List(), schema, externalProps);
+ children.add(avroSchemaToField(elementSchema, ListVector.DATA_VECTOR_NAME, config));
+ fieldType = createFieldType(nullable, new ArrowType.List(), schema, externalProps, config);
break;
case MAP:
// MapVector internal struct field and key field should be non-nullable
FieldType keyFieldType =
new FieldType(/* nullable= */ false, new ArrowType.Utf8(), /* dictionary= */ null);
- Field keyField = new Field("key", keyFieldType, /* children= */ null);
- Field valueField = avroSchemaToField(schema.getValueType(), "value", config);
+ Field keyField = new Field(MapVector.KEY_NAME, keyFieldType, /* children= */ null);
+ Field valueField = avroSchemaToField(schema.getValueType(), MapVector.VALUE_NAME, config);
FieldType structFieldType =
new FieldType(false, new ArrowType.Struct(), /* dictionary= */ null);
Field structField =
- new Field("internal", structFieldType, Arrays.asList(keyField, valueField));
+ new Field(
+ MapVector.DATA_VECTOR_NAME, structFieldType, Arrays.asList(keyField, valueField));
children.add(structField);
fieldType =
- createFieldType(new ArrowType.Map(/* keysSorted= */ false), schema, externalProps);
+ createFieldType(
+ nullable,
+ new ArrowType.Map(/* keysSorted= */ false),
+ schema,
+ externalProps,
+ config);
break;
case RECORD:
final Set skipFieldNames = config.getSkipFieldNames();
@@ -496,13 +652,14 @@ private static Field avroSchemaToField(
if (doc != null) {
extProps.put("doc", doc);
}
- if (aliases != null) {
+ if (aliases != null && (!aliases.isEmpty() || config.isLegacyMode())) {
extProps.put("aliases", convertAliases(aliases));
}
children.add(avroSchemaToField(childSchema, fullChildName, config, extProps));
}
}
- fieldType = createFieldType(new ArrowType.Struct(), schema, externalProps);
+ fieldType =
+ createFieldType(nullable, new ArrowType.Struct(), schema, externalProps, config);
break;
case ENUM:
DictionaryProvider.MapDictionaryProvider provider = config.getProvider();
@@ -512,23 +669,25 @@ private static Field avroSchemaToField(
fieldType =
createFieldType(
+ nullable,
indexType,
schema,
externalProps,
- new DictionaryEncoding(current, /* ordered= */ false, /* indexType= */ indexType));
+ new DictionaryEncoding(current, /* ordered= */ false, /* indexType= */ indexType),
+ config);
break;
case STRING:
- fieldType = createFieldType(new ArrowType.Utf8(), schema, externalProps);
+ fieldType = createFieldType(nullable, new ArrowType.Utf8(), schema, externalProps, config);
break;
case FIXED:
final ArrowType fixedArrowType;
if (logicalType instanceof LogicalTypes.Decimal) {
- fixedArrowType = createDecimalArrowType((LogicalTypes.Decimal) logicalType);
+ fixedArrowType = createDecimalArrowType((LogicalTypes.Decimal) logicalType, schema);
} else {
fixedArrowType = new ArrowType.FixedSizeBinary(schema.getFixedSize());
}
- fieldType = createFieldType(fixedArrowType, schema, externalProps);
+ fieldType = createFieldType(nullable, fixedArrowType, schema, externalProps, config);
break;
case INT:
final ArrowType intArrowType;
@@ -539,41 +698,62 @@ private static Field avroSchemaToField(
} else {
intArrowType = new ArrowType.Int(32, /* isSigned= */ true);
}
- fieldType = createFieldType(intArrowType, schema, externalProps);
+ fieldType = createFieldType(nullable, intArrowType, schema, externalProps, config);
break;
case BOOLEAN:
- fieldType = createFieldType(new ArrowType.Bool(), schema, externalProps);
+ fieldType = createFieldType(nullable, new ArrowType.Bool(), schema, externalProps, config);
break;
case LONG:
final ArrowType longArrowType;
if (logicalType instanceof LogicalTypes.TimeMicros) {
longArrowType = new ArrowType.Time(TimeUnit.MICROSECOND, 64);
} else if (logicalType instanceof LogicalTypes.TimestampMillis) {
- longArrowType = new ArrowType.Timestamp(TimeUnit.MILLISECOND, null);
+ // In legacy mode the timestamp-xxx types are treated as local
+ String tz = config.isLegacyMode() ? null : "UTC";
+ longArrowType = new ArrowType.Timestamp(TimeUnit.MILLISECOND, tz);
} else if (logicalType instanceof LogicalTypes.TimestampMicros) {
+ String tz = config.isLegacyMode() ? null : "UTC";
+ longArrowType = new ArrowType.Timestamp(TimeUnit.MICROSECOND, tz);
+ } else if (logicalType instanceof LogicalTypes.TimestampNanos) {
+ String tz = config.isLegacyMode() ? null : "UTC";
+ longArrowType = new ArrowType.Timestamp(TimeUnit.NANOSECOND, tz);
+ } else if (logicalType instanceof LogicalTypes.LocalTimestampMillis
+ && !config.isLegacyMode()) {
+ // In legacy mode the local-timestamp-xxx types are not recognized (result is just type =
+ // long)
+ longArrowType = new ArrowType.Timestamp(TimeUnit.MILLISECOND, null);
+ } else if (logicalType instanceof LogicalTypes.LocalTimestampMicros
+ && !config.isLegacyMode()) {
longArrowType = new ArrowType.Timestamp(TimeUnit.MICROSECOND, null);
+ } else if (logicalType instanceof LogicalTypes.LocalTimestampNanos
+ && !config.isLegacyMode()) {
+ longArrowType = new ArrowType.Timestamp(TimeUnit.NANOSECOND, null);
} else {
longArrowType = new ArrowType.Int(64, /* isSigned= */ true);
}
- fieldType = createFieldType(longArrowType, schema, externalProps);
+ fieldType = createFieldType(nullable, longArrowType, schema, externalProps, config);
break;
case FLOAT:
- fieldType = createFieldType(new ArrowType.FloatingPoint(SINGLE), schema, externalProps);
+ fieldType =
+ createFieldType(
+ nullable, new ArrowType.FloatingPoint(SINGLE), schema, externalProps, config);
break;
case DOUBLE:
- fieldType = createFieldType(new ArrowType.FloatingPoint(DOUBLE), schema, externalProps);
+ fieldType =
+ createFieldType(
+ nullable, new ArrowType.FloatingPoint(DOUBLE), schema, externalProps, config);
break;
case BYTES:
final ArrowType bytesArrowType;
if (logicalType instanceof LogicalTypes.Decimal) {
- bytesArrowType = createDecimalArrowType((LogicalTypes.Decimal) logicalType);
+ bytesArrowType = createDecimalArrowType((LogicalTypes.Decimal) logicalType, schema);
} else {
bytesArrowType = new ArrowType.Binary();
}
- fieldType = createFieldType(bytesArrowType, schema, externalProps);
+ fieldType = createFieldType(nullable, bytesArrowType, schema, externalProps, config);
break;
case NULL:
- fieldType = createFieldType(ArrowType.Null.INSTANCE, schema, externalProps);
+ fieldType = createFieldType(ArrowType.Null.INSTANCE, schema, externalProps, config);
break;
default:
// no-op, shouldn't get here
@@ -583,15 +763,24 @@ private static Field avroSchemaToField(
if (name == null) {
name = getDefaultFieldName(fieldType.getType());
}
+ if (name.contains(".") && !config.isLegacyMode()) {
+ // Do not include namespace as part of the field name
+ name = name.substring(name.lastIndexOf(".") + 1);
+ }
return new Field(name, fieldType, children.size() == 0 ? null : children);
}
private static Consumer createArrayConsumer(
- Schema schema, String name, AvroToArrowConfig config, FieldVector consumerVector) {
+ Schema schema,
+ String name,
+ boolean nullable,
+ AvroToArrowConfig config,
+ FieldVector consumerVector) {
ListVector listVector;
if (consumerVector == null) {
- final Field field = avroSchemaToField(schema, name, config);
+ final Field field =
+ avroSchemaToField(schema, name, nullable, config, /* externalProps= */ null);
listVector = (ListVector) field.createVector(config.getAllocator());
} else {
listVector = (ListVector) consumerVector;
@@ -607,13 +796,18 @@ private static Consumer createArrayConsumer(
}
private static Consumer createStructConsumer(
- Schema schema, String name, AvroToArrowConfig config, FieldVector consumerVector) {
+ Schema schema,
+ String name,
+ boolean nullable,
+ AvroToArrowConfig config,
+ FieldVector consumerVector) {
final Set skipFieldNames = config.getSkipFieldNames();
StructVector structVector;
if (consumerVector == null) {
- final Field field = avroSchemaToField(schema, name, config, createExternalProps(schema));
+ final Field field =
+ avroSchemaToField(schema, name, nullable, config, createExternalProps(schema, config));
structVector = (StructVector) field.createVector(config.getAllocator());
} else {
structVector = (StructVector) consumerVector;
@@ -644,11 +838,16 @@ private static Consumer createStructConsumer(
}
private static Consumer createEnumConsumer(
- Schema schema, String name, AvroToArrowConfig config, FieldVector consumerVector) {
+ Schema schema,
+ String name,
+ boolean nullable,
+ AvroToArrowConfig config,
+ FieldVector consumerVector) {
BaseIntVector indexVector;
if (consumerVector == null) {
- final Field field = avroSchemaToField(schema, name, config, createExternalProps(schema));
+ final Field field =
+ avroSchemaToField(schema, name, nullable, config, createExternalProps(schema, config));
indexVector = (BaseIntVector) field.createVector(config.getAllocator());
} else {
indexVector = (BaseIntVector) consumerVector;
@@ -668,11 +867,16 @@ private static Consumer createEnumConsumer(
}
private static Consumer createMapConsumer(
- Schema schema, String name, AvroToArrowConfig config, FieldVector consumerVector) {
+ Schema schema,
+ String name,
+ boolean nullable,
+ AvroToArrowConfig config,
+ FieldVector consumerVector) {
MapVector mapVector;
if (consumerVector == null) {
- final Field field = avroSchemaToField(schema, name, config);
+ final Field field =
+ avroSchemaToField(schema, name, nullable, config, /* externalProps= */ null);
mapVector = (MapVector) field.createVector(config.getAllocator());
} else {
mapVector = (MapVector) consumerVector;
@@ -698,12 +902,13 @@ private static Consumer createMapConsumer(
}
private static Consumer createUnionConsumer(
- Schema schema, String name, AvroToArrowConfig config, FieldVector consumerVector) {
+ Schema schema,
+ String name,
+ boolean nullableUnion,
+ AvroToArrowConfig config,
+ FieldVector consumerVector) {
final int size = schema.getTypes().size();
- final boolean nullable =
- schema.getTypes().stream().anyMatch(t -> t.getType() == Schema.Type.NULL);
-
UnionVector unionVector;
if (consumerVector == null) {
final Field field = avroSchemaToField(schema, name, config);
@@ -720,7 +925,8 @@ private static Consumer createUnionConsumer(
for (int i = 0; i < size; i++) {
FieldVector child = childVectors.get(i);
Schema subSchema = schema.getTypes().get(i);
- Consumer delegate = createConsumer(subSchema, subSchema.getName(), nullable, config, child);
+ Consumer delegate =
+ createConsumer(subSchema, subSchema.getName(), nullableUnion, config, child);
delegates[i] = delegate;
types[i] = child.getMinorType();
}
@@ -785,14 +991,24 @@ static VectorSchemaRoot avroToArrowVectors(
return root;
}
- private static Map getMetaData(Schema schema) {
+ // Do not include props that are part of the Avro format itself as field metadata
+ // These are already represented in the field / type structure and are not custom attributes
+ private static final List AVRO_FORMAT_METADATA =
+ Arrays.asList("logicalType", "precision", "scale");
+
+ private static Map getMetaData(Schema schema, AvroToArrowConfig config) {
Map metadata = new HashMap<>();
- schema.getObjectProps().forEach((k, v) -> metadata.put(k, v.toString()));
+ for (Map.Entry prop : schema.getObjectProps().entrySet()) {
+ if (!AVRO_FORMAT_METADATA.contains(prop.getKey()) || config.isLegacyMode()) {
+ metadata.put(prop.getKey(), prop.getValue().toString());
+ }
+ }
return metadata;
}
- private static Map getMetaData(Schema schema, Map externalProps) {
- Map metadata = getMetaData(schema);
+ private static Map getMetaData(
+ Schema schema, Map externalProps, AvroToArrowConfig config) {
+ Map metadata = getMetaData(schema, config);
if (externalProps != null) {
metadata.putAll(externalProps);
}
@@ -800,37 +1016,63 @@ private static Map getMetaData(Schema schema, Map createExternalProps(Schema schema) {
+ private static Map createExternalProps(Schema schema, AvroToArrowConfig config) {
final Map extProps = new HashMap<>();
String doc = schema.getDoc();
Set aliases = schema.getAliases();
if (doc != null) {
extProps.put("doc", doc);
}
- if (aliases != null) {
+ if (aliases != null && (!aliases.isEmpty() || config.isLegacyMode())) {
extProps.put("aliases", convertAliases(aliases));
}
return extProps;
}
private static FieldType createFieldType(
- ArrowType arrowType, Schema schema, Map externalProps) {
- return createFieldType(arrowType, schema, externalProps, /* dictionary= */ null);
+ ArrowType arrowType,
+ Schema schema,
+ Map externalProps,
+ AvroToArrowConfig config) {
+ return createFieldType(arrowType, schema, externalProps, /* dictionary= */ null, config);
+ }
+
+ private static FieldType createFieldType(
+ boolean nullable,
+ ArrowType arrowType,
+ Schema schema,
+ Map externalProps,
+ AvroToArrowConfig config) {
+ return createFieldType(
+ nullable, arrowType, schema, externalProps, /* dictionary= */ null, config);
}
private static FieldType createFieldType(
ArrowType arrowType,
Schema schema,
Map externalProps,
- DictionaryEncoding dictionary) {
+ DictionaryEncoding dictionary,
+ AvroToArrowConfig config) {
+
+ return createFieldType(
+ /* nullable= */ false, arrowType, schema, externalProps, dictionary, config);
+ }
+
+ private static FieldType createFieldType(
+ boolean nullable,
+ ArrowType arrowType,
+ Schema schema,
+ Map externalProps,
+ DictionaryEncoding dictionary,
+ AvroToArrowConfig config) {
return new FieldType(
- /* nullable= */ false, arrowType, dictionary, getMetaData(schema, externalProps));
+ nullable, arrowType, dictionary, getMetaData(schema, externalProps, config));
}
private static String convertAliases(Set aliases) {
- JsonStringArrayList jsonList = new JsonStringArrayList();
- aliases.stream().forEach(a -> jsonList.add(a));
+ JsonStringArrayList jsonList = new JsonStringArrayList(aliases.size());
+ jsonList.addAll(aliases);
return jsonList.toString();
}
}
diff --git a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/AvroToArrowVectorIterator.java b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/AvroToArrowVectorIterator.java
index 4123370061..e82fdc36fb 100644
--- a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/AvroToArrowVectorIterator.java
+++ b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/AvroToArrowVectorIterator.java
@@ -17,13 +17,14 @@
package org.apache.arrow.adapter.avro;
import java.io.EOFException;
-import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.arrow.adapter.avro.consumers.CompositeAvroConsumer;
+import org.apache.arrow.adapter.avro.consumers.Consumer;
import org.apache.arrow.util.Preconditions;
import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.ValueVector;
import org.apache.arrow.vector.VectorSchemaRoot;
import org.apache.arrow.vector.types.pojo.Field;
import org.apache.arrow.vector.util.ValueVectorUtility;
@@ -75,9 +76,11 @@ public static AvroToArrowVectorIterator create(
private void initialize() {
// create consumers
compositeConsumer = AvroToArrowUtils.createCompositeConsumer(schema, config);
- List vectors = new ArrayList<>();
- compositeConsumer.getConsumers().forEach(c -> vectors.add(c.getVector()));
- List fields = vectors.stream().map(t -> t.getField()).collect(Collectors.toList());
+ List vectors =
+ compositeConsumer.getConsumers().stream()
+ .map(Consumer::getVector)
+ .collect(Collectors.toList());
+ List fields = vectors.stream().map(ValueVector::getField).collect(Collectors.toList());
VectorSchemaRoot root = new VectorSchemaRoot(fields, vectors, 0);
rootSchema = root.getSchema();
diff --git a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/AvroNullableConsumer.java b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/AvroNullableConsumer.java
new file mode 100644
index 0000000000..b67819cb9d
--- /dev/null
+++ b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/AvroNullableConsumer.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.adapter.avro.consumers;
+
+import java.io.IOException;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.avro.io.Decoder;
+
+/**
+ * Consumer wrapper which consumes nullable type values from avro decoder. Write the data to the
+ * underlying {@link FieldVector}.
+ *
+ * @param The vector within consumer or its delegate.
+ */
+public class AvroNullableConsumer extends BaseAvroConsumer {
+
+ private final Consumer delegate;
+ private final int nullIndex;
+
+ /** Instantiate a AvroNullableConsumer. */
+ @SuppressWarnings("unchecked")
+ public AvroNullableConsumer(Consumer delegate, int nullIndex) {
+ super((T) delegate.getVector());
+ this.delegate = delegate;
+ this.nullIndex = nullIndex;
+ }
+
+ @Override
+ public void consume(Decoder decoder) throws IOException {
+ int typeIndex = decoder.readInt();
+ if (typeIndex == nullIndex) {
+ decoder.readNull();
+ delegate.addNull();
+ } else {
+ delegate.consume(decoder);
+ }
+ currentIndex++;
+ }
+
+ @Override
+ public void addNull() {
+ // Can be called by containers of nullable types
+ delegate.addNull();
+ currentIndex++;
+ }
+
+ @Override
+ public void setPosition(int index) {
+ if (index < 0 || index > vector.getValueCount()) {
+ throw new IllegalArgumentException("Index out of bounds");
+ }
+ delegate.setPosition(index);
+ super.setPosition(index);
+ }
+
+ @Override
+ public boolean resetValueVector(T vector) {
+ boolean delegateOk = delegate.resetValueVector(vector);
+ boolean thisOk = super.resetValueVector(vector);
+ return thisOk && delegateOk;
+ }
+
+ @Override
+ public void close() throws Exception {
+ super.close();
+ delegate.close();
+ }
+}
diff --git a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroDecimal256Consumer.java b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroDecimal256Consumer.java
new file mode 100644
index 0000000000..12652833a1
--- /dev/null
+++ b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroDecimal256Consumer.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.adapter.avro.consumers.logical;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.apache.arrow.adapter.avro.consumers.BaseAvroConsumer;
+import org.apache.arrow.util.Preconditions;
+import org.apache.arrow.vector.Decimal256Vector;
+import org.apache.avro.io.Decoder;
+
+/**
+ * Consumer which consume 256-bit decimal type values from avro decoder. Write the data to {@link
+ * Decimal256Vector}.
+ */
+public abstract class AvroDecimal256Consumer extends BaseAvroConsumer {
+
+ protected AvroDecimal256Consumer(Decimal256Vector vector) {
+ super(vector);
+ }
+
+ /** Consumer for decimal logical type with 256 bit width and original bytes type. */
+ public static class BytesDecimal256Consumer extends AvroDecimal256Consumer {
+
+ private ByteBuffer cacheBuffer;
+
+ /** Instantiate a BytesDecimal256Consumer. */
+ public BytesDecimal256Consumer(Decimal256Vector vector) {
+ super(vector);
+ }
+
+ @Override
+ public void consume(Decoder decoder) throws IOException {
+ cacheBuffer = decoder.readBytes(cacheBuffer);
+ byte[] bytes = new byte[cacheBuffer.limit()];
+ Preconditions.checkArgument(bytes.length <= 32, "Decimal bytes length should <= 32.");
+ cacheBuffer.get(bytes);
+ vector.setBigEndian(currentIndex++, bytes);
+ }
+ }
+
+ /** Consumer for decimal logical type with 256 bit width and original fixed type. */
+ public static class FixedDecimal256Consumer extends AvroDecimal256Consumer {
+
+ private final byte[] reuseBytes;
+
+ /** Instantiate a FixedDecimal256Consumer. */
+ public FixedDecimal256Consumer(Decimal256Vector vector, int size) {
+ super(vector);
+ Preconditions.checkArgument(size <= 32, "Decimal bytes length should <= 32.");
+ reuseBytes = new byte[size];
+ }
+
+ @Override
+ public void consume(Decoder decoder) throws IOException {
+ decoder.readFixed(reuseBytes);
+ vector.setBigEndian(currentIndex++, reuseBytes);
+ }
+ }
+}
diff --git a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampMicrosConsumer.java b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampMicrosConsumer.java
index 88acf7b329..5af40ed17d 100644
--- a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampMicrosConsumer.java
+++ b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampMicrosConsumer.java
@@ -22,7 +22,7 @@
import org.apache.avro.io.Decoder;
/**
- * Consumer which consume date timestamp-micro values from avro decoder. Write the data to {@link
+ * Consumer which consumes local-timestamp-micros values from avro decoder. Write the data to {@link
* TimeStampMicroVector}.
*/
public class AvroTimestampMicrosConsumer extends BaseAvroConsumer {
diff --git a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampMicrosTzConsumer.java b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampMicrosTzConsumer.java
new file mode 100644
index 0000000000..a5dede4988
--- /dev/null
+++ b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampMicrosTzConsumer.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.adapter.avro.consumers.logical;
+
+import java.io.IOException;
+import org.apache.arrow.adapter.avro.consumers.BaseAvroConsumer;
+import org.apache.arrow.vector.TimeStampMicroTZVector;
+import org.apache.avro.io.Decoder;
+
+/**
+ * Consumer which consumes timestamp-micros values from avro decoder. Write the data to {@link
+ * TimeStampMicroTZVector}.
+ */
+public class AvroTimestampMicrosTzConsumer extends BaseAvroConsumer {
+
+ /** Instantiate a AvroTimestampMicrosTzConsumer. */
+ public AvroTimestampMicrosTzConsumer(TimeStampMicroTZVector vector) {
+ super(vector);
+ }
+
+ @Override
+ public void consume(Decoder decoder) throws IOException {
+ vector.set(currentIndex++, decoder.readLong());
+ }
+}
diff --git a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampMillisConsumer.java b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampMillisConsumer.java
index ec50d79023..bc451bd1dc 100644
--- a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampMillisConsumer.java
+++ b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampMillisConsumer.java
@@ -22,7 +22,7 @@
import org.apache.avro.io.Decoder;
/**
- * Consumer which consume date timestamp-millis values from avro decoder. Write the data to {@link
+ * Consumer which consume local-timestamp-millis values from avro decoder. Write the data to {@link
* TimeStampMilliVector}.
*/
public class AvroTimestampMillisConsumer extends BaseAvroConsumer {
diff --git a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampMillisTzConsumer.java b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampMillisTzConsumer.java
new file mode 100644
index 0000000000..255fe501fb
--- /dev/null
+++ b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampMillisTzConsumer.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.adapter.avro.consumers.logical;
+
+import java.io.IOException;
+import org.apache.arrow.adapter.avro.consumers.BaseAvroConsumer;
+import org.apache.arrow.vector.TimeStampMilliTZVector;
+import org.apache.avro.io.Decoder;
+
+/**
+ * Consumer which consume timestamp-millis values from avro decoder. Write the data to {@link
+ * TimeStampMilliTZVector}.
+ */
+public class AvroTimestampMillisTzConsumer extends BaseAvroConsumer {
+
+ /** Instantiate a AvroTimestampMillisTzConsumer. */
+ public AvroTimestampMillisTzConsumer(TimeStampMilliTZVector vector) {
+ super(vector);
+ }
+
+ @Override
+ public void consume(Decoder decoder) throws IOException {
+ vector.set(currentIndex++, decoder.readLong());
+ }
+}
diff --git a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampNanosConsumer.java b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampNanosConsumer.java
new file mode 100644
index 0000000000..b5044d221f
--- /dev/null
+++ b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampNanosConsumer.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.adapter.avro.consumers.logical;
+
+import java.io.IOException;
+import org.apache.arrow.adapter.avro.consumers.BaseAvroConsumer;
+import org.apache.arrow.vector.TimeStampNanoVector;
+import org.apache.avro.io.Decoder;
+
+/**
+ * Consumer which consume local-timestamp-nanos values from avro decoder. Write the data to {@link
+ * TimeStampNanoVector}.
+ */
+public class AvroTimestampNanosConsumer extends BaseAvroConsumer {
+
+ /** Instantiate a AvroTimestampNanosConsumer. */
+ public AvroTimestampNanosConsumer(TimeStampNanoVector vector) {
+ super(vector);
+ }
+
+ @Override
+ public void consume(Decoder decoder) throws IOException {
+ vector.set(currentIndex++, decoder.readLong());
+ }
+}
diff --git a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampNanosTzConsumer.java b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampNanosTzConsumer.java
new file mode 100644
index 0000000000..3f42b7ccbb
--- /dev/null
+++ b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/consumers/logical/AvroTimestampNanosTzConsumer.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.adapter.avro.consumers.logical;
+
+import java.io.IOException;
+import org.apache.arrow.adapter.avro.consumers.BaseAvroConsumer;
+import org.apache.arrow.vector.TimeStampNanoTZVector;
+import org.apache.avro.io.Decoder;
+
+/**
+ * Consumer which consume timestamp-nanos values from avro decoder. Write the data to {@link
+ * TimeStampNanoTZVector}.
+ */
+public class AvroTimestampNanosTzConsumer extends BaseAvroConsumer {
+
+ /** Instantiate a AvroTimestampNanosConsumer. */
+ public AvroTimestampNanosTzConsumer(TimeStampNanoTZVector vector) {
+ super(vector);
+ }
+
+ @Override
+ public void consume(Decoder decoder) throws IOException {
+ vector.set(currentIndex++, decoder.readLong());
+ }
+}
diff --git a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/producers/AvroEnumProducer.java b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/producers/AvroEnumProducer.java
index 068566493e..eebfb7d241 100644
--- a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/producers/AvroEnumProducer.java
+++ b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/producers/AvroEnumProducer.java
@@ -17,22 +17,22 @@
package org.apache.arrow.adapter.avro.producers;
import java.io.IOException;
-import org.apache.arrow.vector.IntVector;
+import org.apache.arrow.vector.BaseIntVector;
import org.apache.avro.io.Encoder;
/**
- * Producer that produces enum values from a dictionary-encoded {@link IntVector}, writes data to an
- * Avro encoder.
+ * Producer that produces enum values from a dictionary-encoded {@link BaseIntVector}, writes data
+ * to an Avro encoder.
*/
-public class AvroEnumProducer extends BaseAvroProducer {
+public class AvroEnumProducer extends BaseAvroProducer {
/** Instantiate an AvroEnumProducer. */
- public AvroEnumProducer(IntVector vector) {
+ public AvroEnumProducer(BaseIntVector vector) {
super(vector);
}
@Override
public void produce(Encoder encoder) throws IOException {
- encoder.writeEnum(vector.get(currentIndex++));
+ encoder.writeEnum((int) vector.getValueAsLong(currentIndex++));
}
}
diff --git a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/producers/AvroNullableProducer.java b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/producers/AvroNullableProducer.java
index f4215dbf84..5f8b314f49 100644
--- a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/producers/AvroNullableProducer.java
+++ b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/producers/AvroNullableProducer.java
@@ -21,8 +21,8 @@
import org.apache.avro.io.Encoder;
/**
- * Producer wrapper which producers nullable types to an avro encoder. Write the data to the
- * underlying {@link FieldVector}.
+ * Producer wrapper which produces nullable types to an avro encoder. Read data from the underlying
+ * {@link FieldVector}.
*
* @param The vector within producer or its delegate, used for partially produce purpose.
*/
diff --git a/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/producers/DictionaryDecodingProducer.java b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/producers/DictionaryDecodingProducer.java
new file mode 100644
index 0000000000..afeba08511
--- /dev/null
+++ b/adapter/avro/src/main/java/org/apache/arrow/adapter/avro/producers/DictionaryDecodingProducer.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.adapter.avro.producers;
+
+import java.io.IOException;
+import org.apache.arrow.vector.BaseIntVector;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.avro.io.Encoder;
+
+/**
+ * Producer that decodes values from a dictionary-encoded {@link FieldVector}, writes the resulting
+ * values to an Avro encoder.
+ *
+ * @param Type of the underlying dictionary vector
+ */
+public class DictionaryDecodingProducer
+ extends BaseAvroProducer {
+
+ private final Producer dictProducer;
+
+ /** Instantiate a DictionaryDecodingProducer. */
+ public DictionaryDecodingProducer(BaseIntVector indexVector, Producer dictProducer) {
+ super(indexVector);
+ this.dictProducer = dictProducer;
+ }
+
+ @Override
+ public void produce(Encoder encoder) throws IOException {
+ int dicIndex = (int) vector.getValueAsLong(currentIndex++);
+ dictProducer.setPosition(dicIndex);
+ dictProducer.produce(encoder);
+ }
+}
diff --git a/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/ArrowToAvroDataTest.java b/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/ArrowToAvroDataTest.java
index 2d70b45021..6d66ee9d45 100644
--- a/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/ArrowToAvroDataTest.java
+++ b/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/ArrowToAvroDataTest.java
@@ -76,10 +76,14 @@
import org.apache.arrow.vector.complex.StructVector;
import org.apache.arrow.vector.complex.writer.BaseWriter;
import org.apache.arrow.vector.complex.writer.FieldWriter;
+import org.apache.arrow.vector.dictionary.Dictionary;
+import org.apache.arrow.vector.dictionary.DictionaryEncoder;
+import org.apache.arrow.vector.dictionary.DictionaryProvider;
import org.apache.arrow.vector.types.DateUnit;
import org.apache.arrow.vector.types.FloatingPointPrecision;
import org.apache.arrow.vector.types.TimeUnit;
import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.DictionaryEncoding;
import org.apache.arrow.vector.types.pojo.Field;
import org.apache.arrow.vector.types.pojo.FieldType;
import org.apache.arrow.vector.util.JsonStringArrayList;
@@ -2817,4 +2821,81 @@ record = datumReader.read(record, decoder);
}
}
}
+
+ @Test
+ public void testWriteDictEnumEncoded() throws Exception {
+
+ BufferAllocator allocator = new RootAllocator();
+
+ // Create a dictionary
+ FieldType dictionaryField = new FieldType(false, new ArrowType.Utf8(), null);
+ VarCharVector dictionaryVector =
+ new VarCharVector(new Field("dictionary", dictionaryField, null), allocator);
+
+ dictionaryVector.allocateNew(3);
+ dictionaryVector.set(0, "apple".getBytes());
+ dictionaryVector.set(1, "banana".getBytes());
+ dictionaryVector.set(2, "cherry".getBytes());
+ dictionaryVector.setValueCount(3);
+
+ Dictionary dictionary =
+ new Dictionary(dictionaryVector, new DictionaryEncoding(1L, false, null));
+ DictionaryProvider dictionaries = new DictionaryProvider.MapDictionaryProvider(dictionary);
+
+ // Field definition
+ FieldType stringField = new FieldType(false, new ArrowType.Utf8(), null);
+ VarCharVector stringVector =
+ new VarCharVector(new Field("enumField", stringField, null), allocator);
+ stringVector.allocateNew(10);
+ stringVector.setSafe(0, "apple".getBytes());
+ stringVector.setSafe(1, "banana".getBytes());
+ stringVector.setSafe(2, "cherry".getBytes());
+ stringVector.setSafe(3, "cherry".getBytes());
+ stringVector.setSafe(4, "apple".getBytes());
+ stringVector.setSafe(5, "banana".getBytes());
+ stringVector.setSafe(6, "apple".getBytes());
+ stringVector.setSafe(7, "cherry".getBytes());
+ stringVector.setSafe(8, "banana".getBytes());
+ stringVector.setSafe(9, "apple".getBytes());
+ stringVector.setValueCount(10);
+
+ IntVector encodedVector = (IntVector) DictionaryEncoder.encode(stringVector, dictionary);
+
+ // Set up VSR
+ List vectors = Arrays.asList(encodedVector);
+ int rowCount = 10;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ File dataFile = new File(TMP, "testWriteEnumEncoded.avro");
+
+ // Write an AVRO block using the producer classes
+ try (FileOutputStream fos = new FileOutputStream(dataFile)) {
+ BinaryEncoder encoder = new EncoderFactory().directBinaryEncoder(fos, null);
+ CompositeAvroProducer producer =
+ ArrowToAvroUtils.createCompositeProducer(vectors, dictionaries);
+ for (int row = 0; row < rowCount; row++) {
+ producer.produce(encoder);
+ }
+ encoder.flush();
+ }
+
+ // Set up reading the AVRO block as a GenericRecord
+ Schema schema = ArrowToAvroUtils.createAvroSchema(root.getSchema().getFields(), dictionaries);
+ GenericDatumReader datumReader = new GenericDatumReader<>(schema);
+
+ try (InputStream inputStream = new FileInputStream(dataFile)) {
+
+ BinaryDecoder decoder = DecoderFactory.get().binaryDecoder(inputStream, null);
+ GenericRecord record = null;
+
+ // Read and check values
+ for (int row = 0; row < rowCount; row++) {
+ record = datumReader.read(record, decoder);
+ // Values read from Avro should be the decoded enum values
+ assertEquals(stringVector.getObject(row).toString(), record.get("enumField").toString());
+ }
+ }
+ }
+ }
}
diff --git a/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/ArrowToAvroSchemaTest.java b/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/ArrowToAvroSchemaTest.java
index a05bbc1653..d5e0357a8c 100644
--- a/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/ArrowToAvroSchemaTest.java
+++ b/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/ArrowToAvroSchemaTest.java
@@ -20,11 +20,18 @@
import java.util.Arrays;
import java.util.List;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.BigIntVector;
+import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.dictionary.Dictionary;
+import org.apache.arrow.vector.dictionary.DictionaryProvider;
import org.apache.arrow.vector.types.DateUnit;
import org.apache.arrow.vector.types.FloatingPointPrecision;
import org.apache.arrow.vector.types.TimeUnit;
import org.apache.arrow.vector.types.UnionMode;
import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.DictionaryEncoding;
import org.apache.arrow.vector.types.pojo.Field;
import org.apache.arrow.vector.types.pojo.FieldType;
import org.apache.avro.LogicalTypes;
@@ -319,10 +326,10 @@ public void testConvertDecimalTypes() {
FieldType.notNullable(new ArrowType.Decimal(20, 10, 128)),
null),
new Field(
- "nullableDecimal256", FieldType.nullable(new ArrowType.Decimal(20, 4, 256)), null),
+ "nullableDecimal256", FieldType.nullable(new ArrowType.Decimal(55, 15, 256)), null),
new Field(
"nonNullableDecimal2561",
- FieldType.notNullable(new ArrowType.Decimal(20, 4, 256)),
+ FieldType.notNullable(new ArrowType.Decimal(55, 25, 256)),
null),
new Field(
"nonNullableDecimal2562",
@@ -330,7 +337,7 @@ public void testConvertDecimalTypes() {
null),
new Field(
"nonNullableDecimal2563",
- FieldType.notNullable(new ArrowType.Decimal(30, 15, 256)),
+ FieldType.notNullable(new ArrowType.Decimal(60, 50, 256)),
null));
Schema schema = ArrowToAvroUtils.createAvroSchema(fields, "TestRecord");
@@ -383,9 +390,9 @@ public void testConvertDecimalTypes() {
schema.getField("nullableDecimal256").schema().getTypes().get(0);
assertEquals(Schema.Type.FIXED, nullableDecimal256Schema.getType());
assertEquals(32, nullableDecimal256Schema.getFixedSize());
- assertEquals(LogicalTypes.decimal(20, 4), nullableDecimal256Schema.getLogicalType());
- assertEquals(20, nullableDecimal256Schema.getObjectProp("precision"));
- assertEquals(4, nullableDecimal256Schema.getObjectProp("scale"));
+ assertEquals(LogicalTypes.decimal(55, 15), nullableDecimal256Schema.getLogicalType());
+ assertEquals(55, nullableDecimal256Schema.getObjectProp("precision"));
+ assertEquals(15, nullableDecimal256Schema.getObjectProp("scale"));
assertEquals(
Schema.Type.NULL,
schema.getField("nullableDecimal256").schema().getTypes().get(1).getType());
@@ -394,9 +401,9 @@ public void testConvertDecimalTypes() {
Schema nonNullableDecimal2561Schema = schema.getField("nonNullableDecimal2561").schema();
assertEquals(Schema.Type.FIXED, nonNullableDecimal2561Schema.getType());
assertEquals(32, nonNullableDecimal2561Schema.getFixedSize());
- assertEquals(LogicalTypes.decimal(20, 4), nonNullableDecimal2561Schema.getLogicalType());
- assertEquals(20, nonNullableDecimal2561Schema.getObjectProp("precision"));
- assertEquals(4, nonNullableDecimal2561Schema.getObjectProp("scale"));
+ assertEquals(LogicalTypes.decimal(55, 25), nonNullableDecimal2561Schema.getLogicalType());
+ assertEquals(55, nonNullableDecimal2561Schema.getObjectProp("precision"));
+ assertEquals(25, nonNullableDecimal2561Schema.getObjectProp("scale"));
// Assertions for nonNullableDecimal2562
Schema nonNullableDecimal2562Schema = schema.getField("nonNullableDecimal2562").schema();
@@ -410,9 +417,9 @@ public void testConvertDecimalTypes() {
Schema nonNullableDecimal2563Schema = schema.getField("nonNullableDecimal2563").schema();
assertEquals(Schema.Type.FIXED, nonNullableDecimal2563Schema.getType());
assertEquals(32, nonNullableDecimal2563Schema.getFixedSize());
- assertEquals(LogicalTypes.decimal(30, 15), nonNullableDecimal2563Schema.getLogicalType());
- assertEquals(30, nonNullableDecimal2563Schema.getObjectProp("precision"));
- assertEquals(15, nonNullableDecimal2563Schema.getObjectProp("scale"));
+ assertEquals(LogicalTypes.decimal(60, 50), nonNullableDecimal2563Schema.getLogicalType());
+ assertEquals(60, nonNullableDecimal2563Schema.getObjectProp("precision"));
+ assertEquals(50, nonNullableDecimal2563Schema.getObjectProp("scale"));
}
@Test
@@ -1389,4 +1396,126 @@ public void testConvertUnionTypes() {
Schema.Type.STRING,
schema.getField("nullableDenseUnionField").schema().getTypes().get(3).getType());
}
+
+ @Test
+ public void testWriteDictEnumEncoded() {
+
+ BufferAllocator allocator = new RootAllocator();
+
+ // Create a dictionary
+ FieldType dictionaryField = new FieldType(false, new ArrowType.Utf8(), null);
+ VarCharVector dictionaryVector =
+ new VarCharVector(new Field("dictionary", dictionaryField, null), allocator);
+
+ dictionaryVector.allocateNew(3);
+ dictionaryVector.set(0, "apple".getBytes());
+ dictionaryVector.set(1, "banana".getBytes());
+ dictionaryVector.set(2, "cherry".getBytes());
+ dictionaryVector.setValueCount(3);
+
+ Dictionary dictionary =
+ new Dictionary(
+ dictionaryVector, new DictionaryEncoding(0L, false, new ArrowType.Int(8, true)));
+ DictionaryProvider dictionaries = new DictionaryProvider.MapDictionaryProvider(dictionary);
+
+ List fields =
+ Arrays.asList(
+ new Field(
+ "enumField",
+ new FieldType(false, new ArrowType.Int(8, true), dictionary.getEncoding(), null),
+ null));
+
+ Schema schema = ArrowToAvroUtils.createAvroSchema(fields, "TestRecord", null, dictionaries);
+
+ assertEquals(Schema.Type.RECORD, schema.getType());
+ assertEquals(1, schema.getFields().size());
+
+ Schema.Field enumField = schema.getField("enumField");
+
+ assertEquals(Schema.Type.ENUM, enumField.schema().getType());
+ assertEquals(3, enumField.schema().getEnumSymbols().size());
+ assertEquals("apple", enumField.schema().getEnumSymbols().get(0));
+ assertEquals("banana", enumField.schema().getEnumSymbols().get(1));
+ assertEquals("cherry", enumField.schema().getEnumSymbols().get(2));
+ }
+
+ @Test
+ public void testWriteDictEnumInvalid() {
+
+ BufferAllocator allocator = new RootAllocator();
+
+ // Create a dictionary
+ FieldType dictionaryField = new FieldType(false, new ArrowType.Utf8(), null);
+ VarCharVector dictionaryVector =
+ new VarCharVector(new Field("dictionary", dictionaryField, null), allocator);
+
+ dictionaryVector.allocateNew(3);
+ dictionaryVector.set(0, "passion fruit".getBytes());
+ dictionaryVector.set(1, "banana".getBytes());
+ dictionaryVector.set(2, "cherry".getBytes());
+ dictionaryVector.setValueCount(3);
+
+ Dictionary dictionary =
+ new Dictionary(
+ dictionaryVector, new DictionaryEncoding(0L, false, new ArrowType.Int(8, true)));
+ DictionaryProvider dictionaries = new DictionaryProvider.MapDictionaryProvider(dictionary);
+
+ List fields =
+ Arrays.asList(
+ new Field(
+ "enumField",
+ new FieldType(false, new ArrowType.Int(8, true), dictionary.getEncoding(), null),
+ null));
+
+ // Dictionary field contains values that are not valid enums
+ // Should be decoded and output as a string field
+
+ Schema schema = ArrowToAvroUtils.createAvroSchema(fields, "TestRecord", null, dictionaries);
+
+ assertEquals(Schema.Type.RECORD, schema.getType());
+ assertEquals(1, schema.getFields().size());
+
+ Schema.Field enumField = schema.getField("enumField");
+ assertEquals(Schema.Type.STRING, enumField.schema().getType());
+ }
+
+ @Test
+ public void testWriteDictEnumInvalid2() {
+
+ BufferAllocator allocator = new RootAllocator();
+
+ // Create a dictionary
+ FieldType dictionaryField = new FieldType(false, new ArrowType.Int(64, true), null);
+ BigIntVector dictionaryVector =
+ new BigIntVector(new Field("dictionary", dictionaryField, null), allocator);
+
+ dictionaryVector.allocateNew(3);
+ dictionaryVector.set(0, 123L);
+ dictionaryVector.set(1, 456L);
+ dictionaryVector.set(2, 789L);
+ dictionaryVector.setValueCount(3);
+
+ Dictionary dictionary =
+ new Dictionary(
+ dictionaryVector, new DictionaryEncoding(0L, false, new ArrowType.Int(8, true)));
+ DictionaryProvider dictionaries = new DictionaryProvider.MapDictionaryProvider(dictionary);
+
+ List fields =
+ Arrays.asList(
+ new Field(
+ "enumField",
+ new FieldType(false, new ArrowType.Int(8, true), dictionary.getEncoding(), null),
+ null));
+
+ // Dictionary field encodes LONG values rather than STRING
+ // Should be doecded and output as a LONG field
+
+ Schema schema = ArrowToAvroUtils.createAvroSchema(fields, "TestRecord", null, dictionaries);
+
+ assertEquals(Schema.Type.RECORD, schema.getType());
+ assertEquals(1, schema.getFields().size());
+
+ Schema.Field enumField = schema.getField("enumField");
+ assertEquals(Schema.Type.LONG, enumField.schema().getType());
+ }
}
diff --git a/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/AvroLogicalTypesTest.java b/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/AvroLogicalTypesTest.java
index 173cc855b1..801456d79b 100644
--- a/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/AvroLogicalTypesTest.java
+++ b/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/AvroLogicalTypesTest.java
@@ -173,7 +173,7 @@ public void testInvalidDecimalPrecision() throws Exception {
IllegalArgumentException e =
assertThrows(IllegalArgumentException.class, () -> writeAndRead(schema, data));
- assertTrue(e.getMessage().contains("Precision must be in range of 1 to 38"));
+ assertTrue(e.getMessage().contains("Precision must be in range of 1 to 76"));
}
@Test
diff --git a/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/RoundTripDataTest.java b/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/RoundTripDataTest.java
new file mode 100644
index 0000000000..ceaf59aa72
--- /dev/null
+++ b/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/RoundTripDataTest.java
@@ -0,0 +1,1700 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.adapter.avro;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZonedDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.apache.arrow.adapter.avro.producers.CompositeAvroProducer;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.BigIntVector;
+import org.apache.arrow.vector.BitVector;
+import org.apache.arrow.vector.DateDayVector;
+import org.apache.arrow.vector.Decimal256Vector;
+import org.apache.arrow.vector.DecimalVector;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.FixedSizeBinaryVector;
+import org.apache.arrow.vector.Float4Vector;
+import org.apache.arrow.vector.Float8Vector;
+import org.apache.arrow.vector.IntVector;
+import org.apache.arrow.vector.NullVector;
+import org.apache.arrow.vector.TimeMicroVector;
+import org.apache.arrow.vector.TimeMilliVector;
+import org.apache.arrow.vector.TimeStampMicroTZVector;
+import org.apache.arrow.vector.TimeStampMicroVector;
+import org.apache.arrow.vector.TimeStampMilliTZVector;
+import org.apache.arrow.vector.TimeStampMilliVector;
+import org.apache.arrow.vector.TimeStampNanoTZVector;
+import org.apache.arrow.vector.TimeStampNanoVector;
+import org.apache.arrow.vector.TinyIntVector;
+import org.apache.arrow.vector.VarBinaryVector;
+import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.complex.ListVector;
+import org.apache.arrow.vector.complex.MapVector;
+import org.apache.arrow.vector.complex.StructVector;
+import org.apache.arrow.vector.complex.writer.BaseWriter;
+import org.apache.arrow.vector.complex.writer.FieldWriter;
+import org.apache.arrow.vector.dictionary.Dictionary;
+import org.apache.arrow.vector.dictionary.DictionaryEncoder;
+import org.apache.arrow.vector.dictionary.DictionaryProvider;
+import org.apache.arrow.vector.types.DateUnit;
+import org.apache.arrow.vector.types.FloatingPointPrecision;
+import org.apache.arrow.vector.types.TimeUnit;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.DictionaryEncoding;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.FieldType;
+import org.apache.avro.Schema;
+import org.apache.avro.io.BinaryDecoder;
+import org.apache.avro.io.BinaryEncoder;
+import org.apache.avro.io.DecoderFactory;
+import org.apache.avro.io.EncoderFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class RoundTripDataTest {
+
+ @TempDir public static File TMP;
+
+ private static AvroToArrowConfig basicConfig(
+ BufferAllocator allocator, DictionaryProvider.MapDictionaryProvider dictionaries) {
+ return new AvroToArrowConfig(allocator, 1000, dictionaries, Collections.emptySet(), false);
+ }
+
+ private static VectorSchemaRoot readDataFile(
+ Schema schema,
+ File dataFile,
+ BufferAllocator allocator,
+ DictionaryProvider.MapDictionaryProvider dictionaries)
+ throws Exception {
+
+ try (FileInputStream fis = new FileInputStream(dataFile)) {
+ BinaryDecoder decoder = new DecoderFactory().directBinaryDecoder(fis, null);
+ return AvroToArrow.avroToArrow(schema, decoder, basicConfig(allocator, dictionaries));
+ }
+ }
+
+ private static void roundTripTest(
+ VectorSchemaRoot root, BufferAllocator allocator, File dataFile, int rowCount)
+ throws Exception {
+
+ roundTripTest(root, allocator, dataFile, rowCount, null);
+ }
+
+ private static void roundTripTest(
+ VectorSchemaRoot root,
+ BufferAllocator allocator,
+ File dataFile,
+ int rowCount,
+ DictionaryProvider dictionaries)
+ throws Exception {
+
+ // Write an AVRO block using the producer classes
+ try (FileOutputStream fos = new FileOutputStream(dataFile)) {
+ BinaryEncoder encoder = new EncoderFactory().directBinaryEncoder(fos, null);
+ CompositeAvroProducer producer =
+ ArrowToAvroUtils.createCompositeProducer(root.getFieldVectors(), dictionaries);
+ for (int row = 0; row < rowCount; row++) {
+ producer.produce(encoder);
+ }
+ encoder.flush();
+ }
+
+ // Generate AVRO schema
+ Schema schema = ArrowToAvroUtils.createAvroSchema(root.getSchema().getFields(), dictionaries);
+
+ DictionaryProvider.MapDictionaryProvider roundTripDictionaries =
+ new DictionaryProvider.MapDictionaryProvider();
+
+ // Read back in and compare
+ try (VectorSchemaRoot roundTrip =
+ readDataFile(schema, dataFile, allocator, roundTripDictionaries)) {
+
+ assertEquals(root.getSchema(), roundTrip.getSchema());
+ assertEquals(rowCount, roundTrip.getRowCount());
+
+ // Read and check values
+ for (int row = 0; row < rowCount; row++) {
+ assertEquals(root.getVector(0).getObject(row), roundTrip.getVector(0).getObject(row));
+ }
+
+ if (dictionaries != null) {
+ for (long id : dictionaries.getDictionaryIds()) {
+ Dictionary originalDictionary = dictionaries.lookup(id);
+ Dictionary roundTripDictionary = roundTripDictionaries.lookup(id);
+ assertEquals(
+ originalDictionary.getVector().getValueCount(),
+ roundTripDictionary.getVector().getValueCount());
+ for (int j = 0; j < originalDictionary.getVector().getValueCount(); j++) {
+ assertEquals(
+ originalDictionary.getVector().getObject(j),
+ roundTripDictionary.getVector().getObject(j));
+ }
+ }
+ }
+ }
+ }
+
+ private static void roundTripByteArrayTest(
+ VectorSchemaRoot root, BufferAllocator allocator, File dataFile, int rowCount)
+ throws Exception {
+
+ // Write an AVRO block using the producer classes
+ try (FileOutputStream fos = new FileOutputStream(dataFile)) {
+ BinaryEncoder encoder = new EncoderFactory().directBinaryEncoder(fos, null);
+ CompositeAvroProducer producer =
+ ArrowToAvroUtils.createCompositeProducer(root.getFieldVectors());
+ for (int row = 0; row < rowCount; row++) {
+ producer.produce(encoder);
+ }
+ encoder.flush();
+ }
+
+ // Generate AVRO schema
+ Schema schema = ArrowToAvroUtils.createAvroSchema(root.getSchema().getFields());
+
+ // Read back in and compare
+ try (VectorSchemaRoot roundTrip = readDataFile(schema, dataFile, allocator, null)) {
+
+ assertEquals(root.getSchema(), roundTrip.getSchema());
+ assertEquals(rowCount, roundTrip.getRowCount());
+
+ // Read and check values
+ for (int row = 0; row < rowCount; row++) {
+ byte[] rootBytes = (byte[]) root.getVector(0).getObject(row);
+ byte[] roundTripBytes = (byte[]) roundTrip.getVector(0).getObject(row);
+ assertArrayEquals(rootBytes, roundTripBytes);
+ }
+ }
+ }
+
+ // Data round trip for primitive types, nullable and non-nullable
+
+ @Test
+ public void testRoundTripNullColumn() throws Exception {
+
+ // The current read implementation expects EOF, which never happens for a single null vector
+ // Include a boolean vector with this test for now, so that EOF exception will be triggered
+
+ // Field definition
+ FieldType nullField = new FieldType(false, new ArrowType.Null(), null);
+ FieldType booleanField = new FieldType(false, new ArrowType.Bool(), null);
+
+ // Create empty vector
+ BufferAllocator allocator = new RootAllocator();
+ NullVector nullVector = new NullVector(new Field("nullColumn", nullField, null));
+ BitVector booleanVector = new BitVector(new Field("boolean", booleanField, null), allocator);
+
+ int rowCount = 10;
+
+ // Set up VSR
+ List vectors = Arrays.asList(nullVector, booleanVector);
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set all values to null
+ for (int row = 0; row < rowCount; row++) {
+ nullVector.setNull(row);
+ booleanVector.set(row, 0);
+ }
+
+ File dataFile = new File(TMP, "testRoundTripNullColumn.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripBooleans() throws Exception {
+
+ // Field definition
+ FieldType booleanField = new FieldType(false, new ArrowType.Bool(), null);
+
+ // Create empty vector
+ BufferAllocator allocator = new RootAllocator();
+ BitVector booleanVector = new BitVector(new Field("boolean", booleanField, null), allocator);
+
+ // Set up VSR
+ List vectors = Arrays.asList(booleanVector);
+ int rowCount = 10;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ for (int row = 0; row < rowCount; row++) {
+ booleanVector.set(row, row % 2 == 0 ? 1 : 0);
+ }
+
+ File dataFile = new File(TMP, "testRoundTripBooleans.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripNullableBooleans() throws Exception {
+
+ // Field definition
+ FieldType booleanField = new FieldType(true, new ArrowType.Bool(), null);
+
+ // Create empty vector
+ BufferAllocator allocator = new RootAllocator();
+ BitVector booleanVector = new BitVector(new Field("boolean", booleanField, null), allocator);
+
+ int rowCount = 3;
+
+ // Set up VSR
+ List vectors = Arrays.asList(booleanVector);
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Null value
+ booleanVector.setNull(0);
+
+ // False value
+ booleanVector.set(1, 0);
+
+ // True value
+ booleanVector.set(2, 1);
+
+ File dataFile = new File(TMP, "testRoundTripNullableBooleans.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripIntegers() throws Exception {
+
+ // Field definitions
+ FieldType int32Field = new FieldType(false, new ArrowType.Int(32, true), null);
+ FieldType int64Field = new FieldType(false, new ArrowType.Int(64, true), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ IntVector int32Vector = new IntVector(new Field("int32", int32Field, null), allocator);
+ BigIntVector int64Vector = new BigIntVector(new Field("int64", int64Field, null), allocator);
+
+ // Set up VSR
+ List vectors = Arrays.asList(int32Vector, int64Vector);
+
+ int rowCount = 12;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ for (int row = 0; row < 10; row++) {
+ int32Vector.set(row, 513 * row * (row % 2 == 0 ? 1 : -1));
+ int64Vector.set(row, 3791L * row * (row % 2 == 0 ? 1 : -1));
+ }
+
+ // Min values
+ int32Vector.set(10, Integer.MIN_VALUE);
+ int64Vector.set(10, Long.MIN_VALUE);
+
+ // Max values
+ int32Vector.set(11, Integer.MAX_VALUE);
+ int64Vector.set(11, Long.MAX_VALUE);
+
+ File dataFile = new File(TMP, "testRoundTripIntegers.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripNullableIntegers() throws Exception {
+
+ // Field definitions
+ FieldType int32Field = new FieldType(true, new ArrowType.Int(32, true), null);
+ FieldType int64Field = new FieldType(true, new ArrowType.Int(64, true), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ IntVector int32Vector = new IntVector(new Field("int32", int32Field, null), allocator);
+ BigIntVector int64Vector = new BigIntVector(new Field("int64", int64Field, null), allocator);
+
+ int rowCount = 3;
+
+ // Set up VSR
+ List vectors = Arrays.asList(int32Vector, int64Vector);
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Null values
+ int32Vector.setNull(0);
+ int64Vector.setNull(0);
+
+ // Zero values
+ int32Vector.set(1, 0);
+ int64Vector.set(1, 0);
+
+ // Non-zero values
+ int32Vector.set(2, Integer.MAX_VALUE);
+ int64Vector.set(2, Long.MAX_VALUE);
+
+ File dataFile = new File(TMP, "testRoundTripNullableIntegers.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripFloatingPoints() throws Exception {
+
+ // Field definitions
+ FieldType float32Field =
+ new FieldType(false, new ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE), null);
+ FieldType float64Field =
+ new FieldType(false, new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ Float4Vector float32Vector =
+ new Float4Vector(new Field("float32", float32Field, null), allocator);
+ Float8Vector float64Vector =
+ new Float8Vector(new Field("float64", float64Field, null), allocator);
+
+ // Set up VSR
+ List vectors = Arrays.asList(float32Vector, float64Vector);
+ int rowCount = 15;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ for (int row = 0; row < 10; row++) {
+ float32Vector.set(row, 37.6f * row * (row % 2 == 0 ? 1 : -1));
+ float64Vector.set(row, 37.6d * row * (row % 2 == 0 ? 1 : -1));
+ }
+
+ float32Vector.set(10, Float.MIN_VALUE);
+ float64Vector.set(10, Double.MIN_VALUE);
+
+ float32Vector.set(11, Float.MAX_VALUE);
+ float64Vector.set(11, Double.MAX_VALUE);
+
+ float32Vector.set(12, Float.NaN);
+ float64Vector.set(12, Double.NaN);
+
+ float32Vector.set(13, Float.POSITIVE_INFINITY);
+ float64Vector.set(13, Double.POSITIVE_INFINITY);
+
+ float32Vector.set(14, Float.NEGATIVE_INFINITY);
+ float64Vector.set(14, Double.NEGATIVE_INFINITY);
+
+ File dataFile = new File(TMP, "testRoundTripFloatingPoints.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripNullableFloatingPoints() throws Exception {
+
+ // Field definitions
+ FieldType float32Field =
+ new FieldType(true, new ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE), null);
+ FieldType float64Field =
+ new FieldType(true, new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ Float4Vector float32Vector =
+ new Float4Vector(new Field("float32", float32Field, null), allocator);
+ Float8Vector float64Vector =
+ new Float8Vector(new Field("float64", float64Field, null), allocator);
+
+ int rowCount = 3;
+
+ // Set up VSR
+ List vectors = Arrays.asList(float32Vector, float64Vector);
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Null values
+ float32Vector.setNull(0);
+ float64Vector.setNull(0);
+
+ // Zero values
+ float32Vector.set(1, 0.0f);
+ float64Vector.set(1, 0.0);
+
+ // Non-zero values
+ float32Vector.set(2, 1.0f);
+ float64Vector.set(2, 1.0);
+
+ File dataFile = new File(TMP, "testRoundTripNullableFloatingPoints.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripStrings() throws Exception {
+
+ // Field definition
+ FieldType stringField = new FieldType(false, new ArrowType.Utf8(), null);
+
+ // Create empty vector
+ BufferAllocator allocator = new RootAllocator();
+ VarCharVector stringVector =
+ new VarCharVector(new Field("string", stringField, null), allocator);
+
+ // Set up VSR
+ List vectors = Arrays.asList(stringVector);
+ int rowCount = 5;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ stringVector.setSafe(0, "Hello world!".getBytes());
+ stringVector.setSafe(1, "<%**\r\n\t\\abc\0$$>".getBytes());
+ stringVector.setSafe(2, "ä½ å¥½ä¸–ç•Œ".getBytes());
+ stringVector.setSafe(3, "Ù…Ø±ØØ¨Ø§ بالعالم".getBytes());
+ stringVector.setSafe(4, "(P ∧ P ⇒ Q) ⇒ Q".getBytes());
+
+ File dataFile = new File(TMP, "testRoundTripStrings.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripNullableStrings() throws Exception {
+
+ // Field definition
+ FieldType stringField = new FieldType(true, new ArrowType.Utf8(), null);
+
+ // Create empty vector
+ BufferAllocator allocator = new RootAllocator();
+ VarCharVector stringVector =
+ new VarCharVector(new Field("string", stringField, null), allocator);
+
+ int rowCount = 3;
+
+ // Set up VSR
+ List vectors = Arrays.asList(stringVector);
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ stringVector.setNull(0);
+ stringVector.setSafe(1, "".getBytes());
+ stringVector.setSafe(2, "not empty".getBytes());
+
+ File dataFile = new File(TMP, "testRoundTripNullableStrings.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripBinary() throws Exception {
+
+ // Field definition
+ FieldType binaryField = new FieldType(false, new ArrowType.Binary(), null);
+ FieldType fixedField = new FieldType(false, new ArrowType.FixedSizeBinary(5), null);
+
+ // Create empty vector
+ BufferAllocator allocator = new RootAllocator();
+ VarBinaryVector binaryVector =
+ new VarBinaryVector(new Field("binary", binaryField, null), allocator);
+ FixedSizeBinaryVector fixedVector =
+ new FixedSizeBinaryVector(new Field("fixed", fixedField, null), allocator);
+
+ // Set up VSR
+ List vectors = Arrays.asList(binaryVector, fixedVector);
+ int rowCount = 3;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ binaryVector.setSafe(0, new byte[] {1, 2, 3});
+ binaryVector.setSafe(1, new byte[] {4, 5, 6, 7});
+ binaryVector.setSafe(2, new byte[] {8, 9});
+
+ fixedVector.setSafe(0, new byte[] {1, 2, 3, 4, 5});
+ fixedVector.setSafe(1, new byte[] {4, 5, 6, 7, 8, 9});
+ fixedVector.setSafe(2, new byte[] {8, 9, 10, 11, 12});
+
+ File dataFile = new File(TMP, "testRoundTripBinary.avro");
+
+ roundTripByteArrayTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripNullableBinary() throws Exception {
+
+ // Field definition
+ FieldType binaryField = new FieldType(true, new ArrowType.Binary(), null);
+ FieldType fixedField = new FieldType(true, new ArrowType.FixedSizeBinary(5), null);
+
+ // Create empty vector
+ BufferAllocator allocator = new RootAllocator();
+ VarBinaryVector binaryVector =
+ new VarBinaryVector(new Field("binary", binaryField, null), allocator);
+ FixedSizeBinaryVector fixedVector =
+ new FixedSizeBinaryVector(new Field("fixed", fixedField, null), allocator);
+
+ int rowCount = 3;
+
+ // Set up VSR
+ List vectors = Arrays.asList(binaryVector, fixedVector);
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ binaryVector.setNull(0);
+ binaryVector.setSafe(1, new byte[] {});
+ binaryVector.setSafe(2, new byte[] {10, 11, 12});
+
+ fixedVector.setNull(0);
+ fixedVector.setSafe(1, new byte[] {0, 0, 0, 0, 0});
+ fixedVector.setSafe(2, new byte[] {10, 11, 12, 13, 14});
+
+ File dataFile = new File(TMP, "testRoundTripNullableBinary.avro");
+
+ roundTripByteArrayTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ // Data round trip for logical types, nullable and non-nullable
+
+ @Test
+ public void testRoundTripDecimals() throws Exception {
+
+ // Field definitions
+ FieldType decimal128Field1 = new FieldType(false, new ArrowType.Decimal(38, 10, 128), null);
+ FieldType decimal128Field2 = new FieldType(false, new ArrowType.Decimal(38, 5, 128), null);
+ FieldType decimal256Field1 = new FieldType(false, new ArrowType.Decimal(76, 20, 256), null);
+ FieldType decimal256Field2 = new FieldType(false, new ArrowType.Decimal(76, 10, 256), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ DecimalVector decimal128Vector1 =
+ new DecimalVector(new Field("decimal128_1", decimal128Field1, null), allocator);
+ DecimalVector decimal128Vector2 =
+ new DecimalVector(new Field("decimal128_2", decimal128Field2, null), allocator);
+ Decimal256Vector decimal256Vector1 =
+ new Decimal256Vector(new Field("decimal256_1", decimal256Field1, null), allocator);
+ Decimal256Vector decimal256Vector2 =
+ new Decimal256Vector(new Field("decimal256_2", decimal256Field2, null), allocator);
+
+ // Set up VSR
+ List vectors =
+ Arrays.asList(decimal128Vector1, decimal128Vector2, decimal256Vector1, decimal256Vector2);
+ int rowCount = 3;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ decimal128Vector1.setSafe(
+ 0, new BigDecimal("12345.67890").setScale(10, RoundingMode.UNNECESSARY));
+ decimal128Vector1.setSafe(
+ 1, new BigDecimal("-98765.43210").setScale(10, RoundingMode.UNNECESSARY));
+ decimal128Vector1.setSafe(
+ 2, new BigDecimal("54321.09876").setScale(10, RoundingMode.UNNECESSARY));
+
+ decimal128Vector2.setSafe(
+ 0, new BigDecimal("12345.67890").setScale(5, RoundingMode.UNNECESSARY));
+ decimal128Vector2.setSafe(
+ 1, new BigDecimal("-98765.43210").setScale(5, RoundingMode.UNNECESSARY));
+ decimal128Vector2.setSafe(
+ 2, new BigDecimal("54321.09876").setScale(5, RoundingMode.UNNECESSARY));
+
+ decimal256Vector1.setSafe(
+ 0,
+ new BigDecimal("12345678901234567890.12345678901234567890")
+ .setScale(20, RoundingMode.UNNECESSARY));
+ decimal256Vector1.setSafe(
+ 1,
+ new BigDecimal("-98765432109876543210.98765432109876543210")
+ .setScale(20, RoundingMode.UNNECESSARY));
+ decimal256Vector1.setSafe(
+ 2,
+ new BigDecimal("54321098765432109876.54321098765432109876")
+ .setScale(20, RoundingMode.UNNECESSARY));
+
+ decimal256Vector2.setSafe(
+ 0,
+ new BigDecimal("12345678901234567890.1234567890").setScale(10, RoundingMode.UNNECESSARY));
+ decimal256Vector2.setSafe(
+ 1,
+ new BigDecimal("-98765432109876543210.9876543210")
+ .setScale(10, RoundingMode.UNNECESSARY));
+ decimal256Vector2.setSafe(
+ 2,
+ new BigDecimal("54321098765432109876.5432109876").setScale(10, RoundingMode.UNNECESSARY));
+
+ File dataFile = new File(TMP, "testRoundTripDecimals.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripNullableDecimals() throws Exception {
+
+ // Field definitions
+ FieldType decimal128Field1 = new FieldType(true, new ArrowType.Decimal(38, 10, 128), null);
+ FieldType decimal128Field2 = new FieldType(true, new ArrowType.Decimal(38, 5, 128), null);
+ FieldType decimal256Field1 = new FieldType(true, new ArrowType.Decimal(76, 20, 256), null);
+ FieldType decimal256Field2 = new FieldType(true, new ArrowType.Decimal(76, 10, 256), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ DecimalVector decimal128Vector1 =
+ new DecimalVector(new Field("decimal128_1", decimal128Field1, null), allocator);
+ DecimalVector decimal128Vector2 =
+ new DecimalVector(new Field("decimal128_2", decimal128Field2, null), allocator);
+ Decimal256Vector decimal256Vector1 =
+ new Decimal256Vector(new Field("decimal256_1", decimal256Field1, null), allocator);
+ Decimal256Vector decimal256Vector2 =
+ new Decimal256Vector(new Field("decimal256_2", decimal256Field2, null), allocator);
+
+ int rowCount = 3;
+
+ // Set up VSR
+ List vectors =
+ Arrays.asList(decimal128Vector1, decimal128Vector2, decimal256Vector1, decimal256Vector2);
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ decimal128Vector1.setNull(0);
+ decimal128Vector1.setSafe(1, BigDecimal.ZERO.setScale(10, RoundingMode.UNNECESSARY));
+ decimal128Vector1.setSafe(
+ 2, new BigDecimal("12345.67890").setScale(10, RoundingMode.UNNECESSARY));
+
+ decimal128Vector2.setNull(0);
+ decimal128Vector2.setSafe(1, BigDecimal.ZERO.setScale(5, RoundingMode.UNNECESSARY));
+ decimal128Vector2.setSafe(
+ 2, new BigDecimal("98765.43210").setScale(5, RoundingMode.UNNECESSARY));
+
+ decimal256Vector1.setNull(0);
+ decimal256Vector1.setSafe(1, BigDecimal.ZERO.setScale(20, RoundingMode.UNNECESSARY));
+ decimal256Vector1.setSafe(
+ 2,
+ new BigDecimal("12345678901234567890.12345678901234567890")
+ .setScale(20, RoundingMode.UNNECESSARY));
+
+ decimal256Vector2.setNull(0);
+ decimal256Vector2.setSafe(1, BigDecimal.ZERO.setScale(10, RoundingMode.UNNECESSARY));
+ decimal256Vector2.setSafe(
+ 2,
+ new BigDecimal("98765432109876543210.9876543210").setScale(10, RoundingMode.UNNECESSARY));
+
+ File dataFile = new File(TMP, "testRoundTripNullableDecimals.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripDates() throws Exception {
+
+ // Field definitions
+ FieldType dateDayField = new FieldType(false, new ArrowType.Date(DateUnit.DAY), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ DateDayVector dateDayVector =
+ new DateDayVector(new Field("dateDay", dateDayField, null), allocator);
+
+ // Set up VSR
+ List vectors = Arrays.asList(dateDayVector);
+ int rowCount = 3;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ dateDayVector.setSafe(0, (int) LocalDate.now().toEpochDay());
+ dateDayVector.setSafe(1, (int) LocalDate.now().toEpochDay() + 1);
+ dateDayVector.setSafe(2, (int) LocalDate.now().toEpochDay() + 2);
+
+ File dataFile = new File(TMP, "testRoundTripDates.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripNullableDates() throws Exception {
+
+ // Field definitions
+ FieldType dateDayField = new FieldType(true, new ArrowType.Date(DateUnit.DAY), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ DateDayVector dateDayVector =
+ new DateDayVector(new Field("dateDay", dateDayField, null), allocator);
+
+ int rowCount = 3;
+
+ // Set up VSR
+ List vectors = Arrays.asList(dateDayVector);
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ dateDayVector.setNull(0);
+ dateDayVector.setSafe(1, 0);
+ dateDayVector.setSafe(2, (int) LocalDate.now().toEpochDay());
+
+ File dataFile = new File(TMP, "testRoundTripNullableDates.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripTimes() throws Exception {
+
+ // Field definitions
+ FieldType timeMillisField =
+ new FieldType(false, new ArrowType.Time(TimeUnit.MILLISECOND, 32), null);
+ FieldType timeMicrosField =
+ new FieldType(false, new ArrowType.Time(TimeUnit.MICROSECOND, 64), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ TimeMilliVector timeMillisVector =
+ new TimeMilliVector(new Field("timeMillis", timeMillisField, null), allocator);
+ TimeMicroVector timeMicrosVector =
+ new TimeMicroVector(new Field("timeMicros", timeMicrosField, null), allocator);
+
+ // Set up VSR
+ List vectors = Arrays.asList(timeMillisVector, timeMicrosVector);
+ int rowCount = 3;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ timeMillisVector.setSafe(
+ 0, (int) (ZonedDateTime.now().toLocalTime().toNanoOfDay() / 1000000));
+ timeMillisVector.setSafe(
+ 1, (int) (ZonedDateTime.now().toLocalTime().toNanoOfDay() / 1000000) - 1000);
+ timeMillisVector.setSafe(
+ 2, (int) (ZonedDateTime.now().toLocalTime().toNanoOfDay() / 1000000) - 2000);
+
+ timeMicrosVector.setSafe(0, ZonedDateTime.now().toLocalTime().toNanoOfDay() / 1000);
+ timeMicrosVector.setSafe(1, ZonedDateTime.now().toLocalTime().toNanoOfDay() / 1000 - 1000000);
+ timeMicrosVector.setSafe(2, ZonedDateTime.now().toLocalTime().toNanoOfDay() / 1000 - 2000000);
+
+ File dataFile = new File(TMP, "testRoundTripTimes.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripNullableTimes() throws Exception {
+
+ // Field definitions
+ FieldType timeMillisField =
+ new FieldType(true, new ArrowType.Time(TimeUnit.MILLISECOND, 32), null);
+ FieldType timeMicrosField =
+ new FieldType(true, new ArrowType.Time(TimeUnit.MICROSECOND, 64), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ TimeMilliVector timeMillisVector =
+ new TimeMilliVector(new Field("timeMillis", timeMillisField, null), allocator);
+ TimeMicroVector timeMicrosVector =
+ new TimeMicroVector(new Field("timeMicros", timeMicrosField, null), allocator);
+
+ int rowCount = 3;
+
+ // Set up VSR
+ List vectors = Arrays.asList(timeMillisVector, timeMicrosVector);
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ timeMillisVector.setNull(0);
+ timeMillisVector.setSafe(1, 0);
+ timeMillisVector.setSafe(
+ 2, (int) (ZonedDateTime.now().toLocalTime().toNanoOfDay() / 1000000));
+
+ timeMicrosVector.setNull(0);
+ timeMicrosVector.setSafe(1, 0);
+ timeMicrosVector.setSafe(2, ZonedDateTime.now().toLocalTime().toNanoOfDay() / 1000);
+
+ File dataFile = new File(TMP, "testRoundTripNullableTimes.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripZoneAwareTimestamps() throws Exception {
+
+ // Field definitions
+ FieldType timestampMillisField =
+ new FieldType(false, new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC"), null);
+ FieldType timestampMicrosField =
+ new FieldType(false, new ArrowType.Timestamp(TimeUnit.MICROSECOND, "UTC"), null);
+ FieldType timestampNanosField =
+ new FieldType(false, new ArrowType.Timestamp(TimeUnit.NANOSECOND, "UTC"), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ TimeStampMilliTZVector timestampMillisVector =
+ new TimeStampMilliTZVector(
+ new Field("timestampMillis", timestampMillisField, null), allocator);
+ TimeStampMicroTZVector timestampMicrosVector =
+ new TimeStampMicroTZVector(
+ new Field("timestampMicros", timestampMicrosField, null), allocator);
+ TimeStampNanoTZVector timestampNanosVector =
+ new TimeStampNanoTZVector(
+ new Field("timestampNanos", timestampNanosField, null), allocator);
+
+ // Set up VSR
+ List vectors =
+ Arrays.asList(timestampMillisVector, timestampMicrosVector, timestampNanosVector);
+ int rowCount = 3;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ timestampMillisVector.setSafe(0, (int) Instant.now().toEpochMilli());
+ timestampMillisVector.setSafe(1, (int) Instant.now().toEpochMilli() - 1000);
+ timestampMillisVector.setSafe(2, (int) Instant.now().toEpochMilli() - 2000);
+
+ timestampMicrosVector.setSafe(0, Instant.now().toEpochMilli() * 1000);
+ timestampMicrosVector.setSafe(1, (Instant.now().toEpochMilli() - 1000) * 1000);
+ timestampMicrosVector.setSafe(2, (Instant.now().toEpochMilli() - 2000) * 1000);
+
+ timestampNanosVector.setSafe(0, Instant.now().toEpochMilli() * 1000000);
+ timestampNanosVector.setSafe(1, (Instant.now().toEpochMilli() - 1000) * 1000000);
+ timestampNanosVector.setSafe(2, (Instant.now().toEpochMilli() - 2000) * 1000000);
+
+ File dataFile = new File(TMP, "testRoundTripZoneAwareTimestamps.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripNullableZoneAwareTimestamps() throws Exception {
+
+ // Field definitions
+ FieldType timestampMillisField =
+ new FieldType(true, new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC"), null);
+ FieldType timestampMicrosField =
+ new FieldType(true, new ArrowType.Timestamp(TimeUnit.MICROSECOND, "UTC"), null);
+ FieldType timestampNanosField =
+ new FieldType(true, new ArrowType.Timestamp(TimeUnit.NANOSECOND, "UTC"), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ TimeStampMilliTZVector timestampMillisVector =
+ new TimeStampMilliTZVector(
+ new Field("timestampMillis", timestampMillisField, null), allocator);
+ TimeStampMicroTZVector timestampMicrosVector =
+ new TimeStampMicroTZVector(
+ new Field("timestampMicros", timestampMicrosField, null), allocator);
+ TimeStampNanoTZVector timestampNanosVector =
+ new TimeStampNanoTZVector(
+ new Field("timestampNanos", timestampNanosField, null), allocator);
+
+ int rowCount = 3;
+
+ // Set up VSR
+ List vectors =
+ Arrays.asList(timestampMillisVector, timestampMicrosVector, timestampNanosVector);
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ timestampMillisVector.setNull(0);
+ timestampMillisVector.setSafe(1, 0);
+ timestampMillisVector.setSafe(2, (int) Instant.now().toEpochMilli());
+
+ timestampMicrosVector.setNull(0);
+ timestampMicrosVector.setSafe(1, 0);
+ timestampMicrosVector.setSafe(2, Instant.now().toEpochMilli() * 1000);
+
+ timestampNanosVector.setNull(0);
+ timestampNanosVector.setSafe(1, 0);
+ timestampNanosVector.setSafe(2, Instant.now().toEpochMilli() * 1000000);
+
+ File dataFile = new File(TMP, "testRoundTripNullableZoneAwareTimestamps.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripLocalTimestamps() throws Exception {
+
+ // Field definitions
+ FieldType timestampMillisField =
+ new FieldType(false, new ArrowType.Timestamp(TimeUnit.MILLISECOND, null), null);
+ FieldType timestampMicrosField =
+ new FieldType(false, new ArrowType.Timestamp(TimeUnit.MICROSECOND, null), null);
+ FieldType timestampNanosField =
+ new FieldType(false, new ArrowType.Timestamp(TimeUnit.NANOSECOND, null), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ TimeStampMilliVector timestampMillisVector =
+ new TimeStampMilliVector(
+ new Field("timestampMillis", timestampMillisField, null), allocator);
+ TimeStampMicroVector timestampMicrosVector =
+ new TimeStampMicroVector(
+ new Field("timestampMicros", timestampMicrosField, null), allocator);
+ TimeStampNanoVector timestampNanosVector =
+ new TimeStampNanoVector(new Field("timestampNanos", timestampNanosField, null), allocator);
+
+ // Set up VSR
+ List vectors =
+ Arrays.asList(timestampMillisVector, timestampMicrosVector, timestampNanosVector);
+ int rowCount = 3;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ timestampMillisVector.setSafe(0, (int) Instant.now().toEpochMilli());
+ timestampMillisVector.setSafe(1, (int) Instant.now().toEpochMilli() - 1000);
+ timestampMillisVector.setSafe(2, (int) Instant.now().toEpochMilli() - 2000);
+
+ timestampMicrosVector.setSafe(0, Instant.now().toEpochMilli() * 1000);
+ timestampMicrosVector.setSafe(1, (Instant.now().toEpochMilli() - 1000) * 1000);
+ timestampMicrosVector.setSafe(2, (Instant.now().toEpochMilli() - 2000) * 1000);
+
+ timestampNanosVector.setSafe(0, Instant.now().toEpochMilli() * 1000000);
+ timestampNanosVector.setSafe(1, (Instant.now().toEpochMilli() - 1000) * 1000000);
+ timestampNanosVector.setSafe(2, (Instant.now().toEpochMilli() - 2000) * 1000000);
+
+ File dataFile = new File(TMP, "testRoundTripLocalTimestamps.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripNullableLocalTimestamps() throws Exception {
+
+ // Field definitions
+ FieldType timestampMillisField =
+ new FieldType(true, new ArrowType.Timestamp(TimeUnit.MILLISECOND, null), null);
+ FieldType timestampMicrosField =
+ new FieldType(true, new ArrowType.Timestamp(TimeUnit.MICROSECOND, null), null);
+ FieldType timestampNanosField =
+ new FieldType(true, new ArrowType.Timestamp(TimeUnit.NANOSECOND, null), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ TimeStampMilliVector timestampMillisVector =
+ new TimeStampMilliVector(
+ new Field("timestampMillis", timestampMillisField, null), allocator);
+ TimeStampMicroVector timestampMicrosVector =
+ new TimeStampMicroVector(
+ new Field("timestampMicros", timestampMicrosField, null), allocator);
+ TimeStampNanoVector timestampNanosVector =
+ new TimeStampNanoVector(new Field("timestampNanos", timestampNanosField, null), allocator);
+
+ int rowCount = 3;
+
+ // Set up VSR
+ List vectors =
+ Arrays.asList(timestampMillisVector, timestampMicrosVector, timestampNanosVector);
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ timestampMillisVector.setNull(0);
+ timestampMillisVector.setSafe(1, 0);
+ timestampMillisVector.setSafe(2, (int) Instant.now().toEpochMilli());
+
+ timestampMicrosVector.setNull(0);
+ timestampMicrosVector.setSafe(1, 0);
+ timestampMicrosVector.setSafe(2, Instant.now().toEpochMilli() * 1000);
+
+ timestampNanosVector.setNull(0);
+ timestampNanosVector.setSafe(1, 0);
+ timestampNanosVector.setSafe(2, Instant.now().toEpochMilli() * 1000000);
+
+ File dataFile = new File(TMP, "testRoundTripNullableLocalTimestamps.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ // Data round trip for containers of primitive and logical types, nullable and non-nullable
+
+ @Test
+ public void testRoundTripLists() throws Exception {
+
+ // Field definitions
+ FieldType intListField = new FieldType(false, new ArrowType.List(), null);
+ FieldType stringListField = new FieldType(false, new ArrowType.List(), null);
+ FieldType dateListField = new FieldType(false, new ArrowType.List(), null);
+
+ Field intField = new Field("item", FieldType.notNullable(new ArrowType.Int(32, true)), null);
+ Field stringField = new Field("item", FieldType.notNullable(new ArrowType.Utf8()), null);
+ Field dateField =
+ new Field("item", FieldType.notNullable(new ArrowType.Date(DateUnit.DAY)), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ ListVector intListVector = new ListVector("intList", allocator, intListField, null);
+ ListVector stringListVector = new ListVector("stringList", allocator, stringListField, null);
+ ListVector dateListVector = new ListVector("dateList", allocator, dateListField, null);
+
+ intListVector.initializeChildrenFromFields(Arrays.asList(intField));
+ stringListVector.initializeChildrenFromFields(Arrays.asList(stringField));
+ dateListVector.initializeChildrenFromFields(Arrays.asList(dateField));
+
+ // Set up VSR
+ List vectors = Arrays.asList(intListVector, stringListVector, dateListVector);
+ int rowCount = 3;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ FieldWriter intListWriter = intListVector.getWriter();
+ FieldWriter stringListWriter = stringListVector.getWriter();
+ FieldWriter dateListWriter = dateListVector.getWriter();
+
+ // Set test data for intList
+ for (int i = 0; i < rowCount; i++) {
+ intListWriter.startList();
+ for (int j = 0; j < 5 - i; j++) {
+ intListWriter.writeInt(j);
+ }
+ intListWriter.endList();
+ }
+
+ // Set test data for stringList
+ for (int i = 0; i < rowCount; i++) {
+ stringListWriter.startList();
+ for (int j = 0; j < 5 - i; j++) {
+ stringListWriter.writeVarChar("string" + j);
+ }
+ stringListWriter.endList();
+ }
+
+ // Set test data for dateList
+ for (int i = 0; i < rowCount; i++) {
+ dateListWriter.startList();
+ for (int j = 0; j < 5 - i; j++) {
+ dateListWriter.writeDateDay((int) LocalDate.now().plusDays(j).toEpochDay());
+ }
+ dateListWriter.endList();
+ }
+
+ // Update count for the vectors
+ intListVector.setValueCount(rowCount);
+ stringListVector.setValueCount(rowCount);
+ dateListVector.setValueCount(rowCount);
+
+ File dataFile = new File(TMP, "testRoundTripLists.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripNullableLists() throws Exception {
+
+ // Field definitions
+ FieldType nullListType = new FieldType(true, new ArrowType.List(), null);
+ FieldType nonNullListType = new FieldType(false, new ArrowType.List(), null);
+
+ Field nullFieldType = new Field("item", FieldType.nullable(new ArrowType.Int(32, true)), null);
+ Field nonNullFieldType =
+ new Field("item", FieldType.notNullable(new ArrowType.Int(32, true)), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ ListVector nullEntriesVector =
+ new ListVector("nullEntriesVector", allocator, nonNullListType, null);
+ ListVector nullListVector = new ListVector("nullListVector", allocator, nullListType, null);
+ ListVector nullBothVector = new ListVector("nullBothVector", allocator, nullListType, null);
+
+ nullEntriesVector.initializeChildrenFromFields(Arrays.asList(nullFieldType));
+ nullListVector.initializeChildrenFromFields(Arrays.asList(nonNullFieldType));
+ nullBothVector.initializeChildrenFromFields(Arrays.asList(nullFieldType));
+
+ // Set up VSR
+ List vectors = Arrays.asList(nullEntriesVector, nullListVector, nullBothVector);
+ int rowCount = 4;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data for nullEntriesVector
+ FieldWriter nullEntriesWriter = nullEntriesVector.getWriter();
+ nullEntriesWriter.startList();
+ nullEntriesWriter.integer().writeNull();
+ nullEntriesWriter.integer().writeNull();
+ nullEntriesWriter.endList();
+ nullEntriesWriter.startList();
+ nullEntriesWriter.integer().writeInt(0);
+ nullEntriesWriter.integer().writeInt(0);
+ nullEntriesWriter.endList();
+ nullEntriesWriter.startList();
+ nullEntriesWriter.integer().writeInt(123);
+ nullEntriesWriter.integer().writeInt(456);
+ nullEntriesWriter.endList();
+ nullEntriesWriter.startList();
+ nullEntriesWriter.integer().writeInt(789);
+ nullEntriesWriter.integer().writeInt(789);
+ nullEntriesWriter.endList();
+
+ // Set test data for nullListVector
+ FieldWriter nullListWriter = nullListVector.getWriter();
+ nullListWriter.writeNull();
+ nullListWriter.setPosition(1); // writeNull() does not inc. idx() on list vector
+ nullListWriter.startList();
+ nullListWriter.integer().writeInt(0);
+ nullListWriter.integer().writeInt(0);
+ nullListWriter.endList();
+ nullEntriesWriter.startList();
+ nullEntriesWriter.integer().writeInt(123);
+ nullEntriesWriter.integer().writeInt(456);
+ nullEntriesWriter.endList();
+ nullEntriesWriter.startList();
+ nullEntriesWriter.integer().writeInt(789);
+ nullEntriesWriter.integer().writeInt(789);
+ nullEntriesWriter.endList();
+
+ // Set test data for nullBothVector
+ FieldWriter nullBothWriter = nullBothVector.getWriter();
+ nullBothWriter.writeNull();
+ nullBothWriter.setPosition(1);
+ nullBothWriter.startList();
+ nullBothWriter.integer().writeNull();
+ nullBothWriter.integer().writeNull();
+ nullBothWriter.endList();
+ nullListWriter.startList();
+ nullListWriter.integer().writeInt(0);
+ nullListWriter.integer().writeInt(0);
+ nullListWriter.endList();
+ nullEntriesWriter.startList();
+ nullEntriesWriter.integer().writeInt(123);
+ nullEntriesWriter.integer().writeInt(456);
+ nullEntriesWriter.endList();
+
+ // Update count for the vectors
+ nullListVector.setValueCount(4);
+ nullEntriesVector.setValueCount(4);
+ nullBothVector.setValueCount(4);
+
+ File dataFile = new File(TMP, "testRoundTripNullableLists.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripMap() throws Exception {
+
+ // Field definitions
+ FieldType intMapField = new FieldType(false, new ArrowType.Map(false), null);
+ FieldType stringMapField = new FieldType(false, new ArrowType.Map(false), null);
+ FieldType dateMapField = new FieldType(false, new ArrowType.Map(false), null);
+
+ Field keyField = new Field("key", FieldType.notNullable(new ArrowType.Utf8()), null);
+ Field intField = new Field("value", FieldType.notNullable(new ArrowType.Int(32, true)), null);
+ Field stringField = new Field("value", FieldType.notNullable(new ArrowType.Utf8()), null);
+ Field dateField =
+ new Field("value", FieldType.notNullable(new ArrowType.Date(DateUnit.DAY)), null);
+
+ Field intEntryField =
+ new Field(
+ "entries",
+ FieldType.notNullable(new ArrowType.Struct()),
+ Arrays.asList(keyField, intField));
+ Field stringEntryField =
+ new Field(
+ "entries",
+ FieldType.notNullable(new ArrowType.Struct()),
+ Arrays.asList(keyField, stringField));
+ Field dateEntryField =
+ new Field(
+ "entries",
+ FieldType.notNullable(new ArrowType.Struct()),
+ Arrays.asList(keyField, dateField));
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ MapVector intMapVector = new MapVector("intMap", allocator, intMapField, null);
+ MapVector stringMapVector = new MapVector("stringMap", allocator, stringMapField, null);
+ MapVector dateMapVector = new MapVector("dateMap", allocator, dateMapField, null);
+
+ intMapVector.initializeChildrenFromFields(Arrays.asList(intEntryField));
+ stringMapVector.initializeChildrenFromFields(Arrays.asList(stringEntryField));
+ dateMapVector.initializeChildrenFromFields(Arrays.asList(dateEntryField));
+
+ // Set up VSR
+ List vectors = Arrays.asList(intMapVector, stringMapVector, dateMapVector);
+ int rowCount = 3;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Total number of entries that will be writen to each vector
+ int entryCount = 5 + 4 + 3;
+
+ // Set test data for intList
+ BaseWriter.MapWriter writer = intMapVector.getWriter();
+ for (int i = 0; i < rowCount; i++) {
+ writer.startMap();
+ for (int j = 0; j < 5 - i; j++) {
+ writer.startEntry();
+ writer.key().varChar().writeVarChar("key" + j);
+ writer.value().integer().writeInt(j);
+ writer.endEntry();
+ }
+ writer.endMap();
+ }
+
+ // Update count for data vector (map writer does not do this)
+ intMapVector.getDataVector().setValueCount(entryCount);
+
+ // Set test data for stringList
+ BaseWriter.MapWriter stringWriter = stringMapVector.getWriter();
+ for (int i = 0; i < rowCount; i++) {
+ stringWriter.startMap();
+ for (int j = 0; j < 5 - i; j++) {
+ stringWriter.startEntry();
+ stringWriter.key().varChar().writeVarChar("key" + j);
+ stringWriter.value().varChar().writeVarChar("string" + j);
+ stringWriter.endEntry();
+ }
+ stringWriter.endMap();
+ }
+
+ // Update count for the vectors
+ intMapVector.setValueCount(rowCount);
+ stringMapVector.setValueCount(rowCount);
+ dateMapVector.setValueCount(rowCount);
+
+ // Update count for data vector (map writer does not do this)
+ stringMapVector.getDataVector().setValueCount(entryCount);
+
+ // Set test data for dateList
+ BaseWriter.MapWriter dateWriter = dateMapVector.getWriter();
+ for (int i = 0; i < rowCount; i++) {
+ dateWriter.startMap();
+ for (int j = 0; j < 5 - i; j++) {
+ dateWriter.startEntry();
+ dateWriter.key().varChar().writeVarChar("key" + j);
+ dateWriter.value().dateDay().writeDateDay((int) LocalDate.now().plusDays(j).toEpochDay());
+ dateWriter.endEntry();
+ }
+ dateWriter.endMap();
+ }
+
+ // Update count for data vector (map writer does not do this)
+ dateMapVector.getDataVector().setValueCount(entryCount);
+
+ File dataFile = new File(TMP, "testRoundTripMap.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripNullableMap() throws Exception {
+
+ // Field definitions
+ FieldType nullMapType = new FieldType(true, new ArrowType.Map(false), null);
+ FieldType nonNullMapType = new FieldType(false, new ArrowType.Map(false), null);
+
+ Field keyField = new Field("key", FieldType.notNullable(new ArrowType.Utf8()), null);
+ Field nullFieldType = new Field("value", FieldType.nullable(new ArrowType.Int(32, true)), null);
+ Field nonNullFieldType =
+ new Field("value", FieldType.notNullable(new ArrowType.Int(32, true)), null);
+ Field nullEntryField =
+ new Field(
+ "entries",
+ FieldType.notNullable(new ArrowType.Struct()),
+ Arrays.asList(keyField, nullFieldType));
+ Field nonNullEntryField =
+ new Field(
+ "entries",
+ FieldType.notNullable(new ArrowType.Struct()),
+ Arrays.asList(keyField, nonNullFieldType));
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ MapVector nullEntriesVector =
+ new MapVector("nullEntriesVector", allocator, nonNullMapType, null);
+ MapVector nullMapVector = new MapVector("nullMapVector", allocator, nullMapType, null);
+ MapVector nullBothVector = new MapVector("nullBothVector", allocator, nullMapType, null);
+
+ nullEntriesVector.initializeChildrenFromFields(Arrays.asList(nullEntryField));
+ nullMapVector.initializeChildrenFromFields(Arrays.asList(nonNullEntryField));
+ nullBothVector.initializeChildrenFromFields(Arrays.asList(nullEntryField));
+
+ // Set up VSR
+ List vectors = Arrays.asList(nullEntriesVector, nullMapVector, nullBothVector);
+ int rowCount = 3;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data for intList
+ BaseWriter.MapWriter writer = nullEntriesVector.getWriter();
+ writer.startMap();
+ writer.startEntry();
+ writer.key().varChar().writeVarChar("key0");
+ writer.value().integer().writeNull();
+ writer.endEntry();
+ writer.startEntry();
+ writer.key().varChar().writeVarChar("key1");
+ writer.value().integer().writeNull();
+ writer.endEntry();
+ writer.endMap();
+ writer.startMap();
+ writer.startEntry();
+ writer.key().varChar().writeVarChar("key2");
+ writer.value().integer().writeInt(0);
+ writer.endEntry();
+ writer.startEntry();
+ writer.key().varChar().writeVarChar("key3");
+ writer.value().integer().writeInt(0);
+ writer.endEntry();
+ writer.endMap();
+ writer.startMap();
+ writer.startEntry();
+ writer.key().varChar().writeVarChar("key4");
+ writer.value().integer().writeInt(123);
+ writer.endEntry();
+ writer.startEntry();
+ writer.key().varChar().writeVarChar("key5");
+ writer.value().integer().writeInt(456);
+ writer.endEntry();
+ writer.endMap();
+
+ // Set test data for stringList
+ BaseWriter.MapWriter nullMapWriter = nullMapVector.getWriter();
+ nullMapWriter.writeNull();
+ nullMapWriter.setPosition(1); // writeNull() does not inc. idx() on map (list) vector
+ nullMapWriter.startMap();
+ nullMapWriter.startEntry();
+ nullMapWriter.key().varChar().writeVarChar("key2");
+ nullMapWriter.value().integer().writeInt(0);
+ nullMapWriter.endEntry();
+ writer.startMap();
+ writer.startEntry();
+ writer.key().varChar().writeVarChar("key3");
+ writer.value().integer().writeInt(0);
+ writer.endEntry();
+ nullMapWriter.endMap();
+ nullMapWriter.startMap();
+ writer.startEntry();
+ writer.key().varChar().writeVarChar("key4");
+ writer.value().integer().writeInt(123);
+ writer.endEntry();
+ writer.startEntry();
+ writer.key().varChar().writeVarChar("key5");
+ writer.value().integer().writeInt(456);
+ writer.endEntry();
+ nullMapWriter.endMap();
+
+ // Set test data for dateList
+ BaseWriter.MapWriter nullBothWriter = nullBothVector.getWriter();
+ nullBothWriter.writeNull();
+ nullBothWriter.setPosition(1);
+ nullBothWriter.startMap();
+ nullBothWriter.startEntry();
+ nullBothWriter.key().varChar().writeVarChar("key2");
+ nullBothWriter.value().integer().writeNull();
+ nullBothWriter.endEntry();
+ nullBothWriter.startEntry();
+ nullBothWriter.key().varChar().writeVarChar("key3");
+ nullBothWriter.value().integer().writeNull();
+ nullBothWriter.endEntry();
+ nullBothWriter.endMap();
+ nullBothWriter.startMap();
+ writer.startEntry();
+ writer.key().varChar().writeVarChar("key4");
+ writer.value().integer().writeInt(123);
+ writer.endEntry();
+ writer.startEntry();
+ writer.key().varChar().writeVarChar("key5");
+ writer.value().integer().writeInt(456);
+ writer.endEntry();
+ nullBothWriter.endMap();
+
+ // Update count for the vectors
+ nullEntriesVector.setValueCount(3);
+ nullMapVector.setValueCount(3);
+ nullBothVector.setValueCount(3);
+
+ File dataFile = new File(TMP, "testRoundTripNullableMap.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripStruct() throws Exception {
+
+ // Field definitions
+ FieldType structFieldType = new FieldType(false, new ArrowType.Struct(), null);
+ Field intField =
+ new Field("intField", FieldType.notNullable(new ArrowType.Int(32, true)), null);
+ Field stringField = new Field("stringField", FieldType.notNullable(new ArrowType.Utf8()), null);
+ Field dateField =
+ new Field("dateField", FieldType.notNullable(new ArrowType.Date(DateUnit.DAY)), null);
+
+ // Create empty vector
+ BufferAllocator allocator = new RootAllocator();
+ StructVector structVector = new StructVector("struct", allocator, structFieldType, null);
+ structVector.initializeChildrenFromFields(Arrays.asList(intField, stringField, dateField));
+
+ // Set up VSR
+ List vectors = Arrays.asList(structVector);
+ int rowCount = 3;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data
+ BaseWriter.StructWriter structWriter = structVector.getWriter();
+
+ for (int i = 0; i < rowCount; i++) {
+ structWriter.start();
+ structWriter.integer("intField").writeInt(i);
+ structWriter.varChar("stringField").writeVarChar("string" + i);
+ structWriter.dateDay("dateField").writeDateDay((int) LocalDate.now().toEpochDay() + i);
+ structWriter.end();
+ }
+
+ File dataFile = new File(TMP, "testRoundTripStruct.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripNullableStructs() throws Exception {
+
+ // Field definitions
+ FieldType structFieldType = new FieldType(false, new ArrowType.Struct(), null);
+ FieldType nullableStructFieldType = new FieldType(true, new ArrowType.Struct(), null);
+ Field intField =
+ new Field("intField", FieldType.notNullable(new ArrowType.Int(32, true)), null);
+ Field nullableIntField =
+ new Field("nullableIntField", FieldType.nullable(new ArrowType.Int(32, true)), null);
+
+ // Create empty vectors
+ BufferAllocator allocator = new RootAllocator();
+ StructVector structVector = new StructVector("struct", allocator, structFieldType, null);
+ StructVector nullableStructVector =
+ new StructVector("nullableStruct", allocator, nullableStructFieldType, null);
+ structVector.initializeChildrenFromFields(Arrays.asList(intField, nullableIntField));
+ nullableStructVector.initializeChildrenFromFields(Arrays.asList(intField, nullableIntField));
+
+ // Set up VSR
+ List vectors = Arrays.asList(structVector, nullableStructVector);
+ int rowCount = 4;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ root.setRowCount(rowCount);
+ root.allocateNew();
+
+ // Set test data for structVector
+ BaseWriter.StructWriter structWriter = structVector.getWriter();
+ for (int i = 0; i < rowCount; i++) {
+ structWriter.setPosition(i);
+ structWriter.start();
+ structWriter.integer("intField").writeInt(i);
+ if (i % 2 == 0) {
+ structWriter.integer("nullableIntField").writeInt(i * 10);
+ } else {
+ structWriter.integer("nullableIntField").writeNull();
+ }
+ structWriter.end();
+ }
+
+ // Set test data for nullableStructVector
+ BaseWriter.StructWriter nullableStructWriter = nullableStructVector.getWriter();
+ for (int i = 0; i < rowCount; i++) {
+ nullableStructWriter.setPosition(i);
+ if (i >= 2) {
+ nullableStructWriter.start();
+ nullableStructWriter.integer("intField").writeInt(i);
+ if (i % 2 == 0) {
+ nullableStructWriter.integer("nullableIntField").writeInt(i * 10);
+ } else {
+ nullableStructWriter.integer("nullableIntField").writeNull();
+ }
+ nullableStructWriter.end();
+ } else {
+ nullableStructWriter.writeNull();
+ }
+ }
+
+ // Update count for the vector
+ structVector.setValueCount(rowCount);
+ nullableStructVector.setValueCount(rowCount);
+
+ File dataFile = new File(TMP, "testRoundTripNullableStructs.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount);
+ }
+ }
+
+ @Test
+ public void testRoundTripEnum() throws Exception {
+
+ BufferAllocator allocator = new RootAllocator();
+
+ // Create a dictionary
+ FieldType dictionaryField = new FieldType(false, new ArrowType.Utf8(), null);
+ VarCharVector dictionaryVector =
+ new VarCharVector(new Field("dictionary", dictionaryField, null), allocator);
+
+ dictionaryVector.allocateNew(3);
+ dictionaryVector.set(0, "apple".getBytes());
+ dictionaryVector.set(1, "banana".getBytes());
+ dictionaryVector.set(2, "cherry".getBytes());
+ dictionaryVector.setValueCount(3);
+
+ // For simplicity, ensure the index type matches what will be decoded during Avro enum decoding
+ Dictionary dictionary =
+ new Dictionary(
+ dictionaryVector, new DictionaryEncoding(0L, false, new ArrowType.Int(8, true)));
+ DictionaryProvider dictionaries = new DictionaryProvider.MapDictionaryProvider(dictionary);
+
+ // Field definition
+ FieldType stringField = new FieldType(false, new ArrowType.Utf8(), null);
+ VarCharVector stringVector =
+ new VarCharVector(new Field("enumField", stringField, null), allocator);
+ stringVector.allocateNew(10);
+ stringVector.setSafe(0, "apple".getBytes());
+ stringVector.setSafe(1, "banana".getBytes());
+ stringVector.setSafe(2, "cherry".getBytes());
+ stringVector.setSafe(3, "cherry".getBytes());
+ stringVector.setSafe(4, "apple".getBytes());
+ stringVector.setSafe(5, "banana".getBytes());
+ stringVector.setSafe(6, "apple".getBytes());
+ stringVector.setSafe(7, "cherry".getBytes());
+ stringVector.setSafe(8, "banana".getBytes());
+ stringVector.setSafe(9, "apple".getBytes());
+ stringVector.setValueCount(10);
+
+ TinyIntVector encodedVector =
+ (TinyIntVector) DictionaryEncoder.encode(stringVector, dictionary);
+
+ // Set up VSR
+ List vectors = Arrays.asList(encodedVector);
+ int rowCount = 10;
+
+ try (VectorSchemaRoot root = new VectorSchemaRoot(vectors)) {
+
+ File dataFile = new File(TMP, "testRoundTripEnums.avro");
+
+ roundTripTest(root, allocator, dataFile, rowCount, dictionaries);
+ }
+ }
+}
diff --git a/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/RoundTripSchemaTest.java b/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/RoundTripSchemaTest.java
new file mode 100644
index 0000000000..37c0b4d9fe
--- /dev/null
+++ b/adapter/avro/src/test/java/org/apache/arrow/adapter/avro/RoundTripSchemaTest.java
@@ -0,0 +1,500 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.adapter.avro;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.dictionary.Dictionary;
+import org.apache.arrow.vector.dictionary.DictionaryProvider;
+import org.apache.arrow.vector.types.DateUnit;
+import org.apache.arrow.vector.types.FloatingPointPrecision;
+import org.apache.arrow.vector.types.TimeUnit;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.DictionaryEncoding;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.FieldType;
+import org.apache.avro.Schema;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class RoundTripSchemaTest {
+
+ private void doRoundTripTest(List fields) {
+ doRoundTripTest(fields, null);
+ }
+
+ private void doRoundTripTest(List fields, DictionaryProvider dictionaries) {
+
+ DictionaryProvider.MapDictionaryProvider decodeDictionaries =
+ new DictionaryProvider.MapDictionaryProvider();
+ AvroToArrowConfig decodeConfig =
+ new AvroToArrowConfig(null, 1, decodeDictionaries, Collections.emptySet(), false);
+
+ Schema avroSchema = ArrowToAvroUtils.createAvroSchema(fields, "TestRecord", null, dictionaries);
+ org.apache.arrow.vector.types.pojo.Schema arrowSchema =
+ AvroToArrowUtils.createArrowSchema(avroSchema, decodeConfig);
+
+ // Compare string representations - equality not defined for logical types
+ assertEquals(fields, arrowSchema.getFields());
+
+ for (int i = 0; i < fields.size(); i++) {
+ Field field = fields.get(i);
+ Field rtField = arrowSchema.getFields().get(i);
+ if (field.getDictionary() != null) {
+ // Dictionary content is not decoded until the data is consumed
+ Assertions.assertNotNull(rtField.getDictionary());
+ }
+ }
+ }
+
+ // Schema round trip for primitive types, nullable and non-nullable
+
+ @Test
+ public void testRoundTripNullType() {
+
+ List fields =
+ Arrays.asList(new Field("nullType", FieldType.notNullable(new ArrowType.Null()), null));
+
+ doRoundTripTest(fields);
+ }
+
+ @Test
+ public void testRoundTripBooleanType() {
+
+ List fields =
+ Arrays.asList(
+ new Field("nullableBool", FieldType.nullable(new ArrowType.Bool()), null),
+ new Field("nonNullableBool", FieldType.notNullable(new ArrowType.Bool()), null));
+
+ doRoundTripTest(fields);
+ }
+
+ @Test
+ public void testRoundTripIntegerTypes() {
+
+ AvroToArrowConfig config = new AvroToArrowConfig(null, 1, null, Collections.emptySet(), false);
+
+ // Only round trip types with direct equivalent in Avro
+
+ List fields =
+ Arrays.asList(
+ new Field("nullableInt32", FieldType.nullable(new ArrowType.Int(32, true)), null),
+ new Field("nonNullableInt32", FieldType.notNullable(new ArrowType.Int(32, true)), null),
+ new Field("nullableInt64", FieldType.nullable(new ArrowType.Int(64, true)), null),
+ new Field(
+ "nonNullableInt64", FieldType.notNullable(new ArrowType.Int(64, true)), null));
+
+ Schema avroSchema = ArrowToAvroUtils.createAvroSchema(fields, "TestRecord");
+ org.apache.arrow.vector.types.pojo.Schema arrowSchema =
+ AvroToArrowUtils.createArrowSchema(avroSchema, config);
+
+ // Exact match on fields after round trip
+ assertEquals(fields, arrowSchema.getFields());
+ }
+
+ @Test
+ public void testRoundTripFloatingPointTypes() {
+
+ // Only round trip types with direct equivalent in Avro
+
+ List fields =
+ Arrays.asList(
+ new Field(
+ "nullableFloat32",
+ FieldType.nullable(new ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE)),
+ null),
+ new Field(
+ "nonNullableFloat32",
+ FieldType.notNullable(new ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE)),
+ null),
+ new Field(
+ "nullableFloat64",
+ FieldType.nullable(new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE)),
+ null),
+ new Field(
+ "nonNullableFloat64",
+ FieldType.notNullable(new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE)),
+ null));
+
+ doRoundTripTest(fields);
+ }
+
+ @Test
+ public void testRoundTripStringTypes() {
+
+ List fields =
+ Arrays.asList(
+ new Field("nullableUtf8", FieldType.nullable(new ArrowType.Utf8()), null),
+ new Field("nonNullableUtf8", FieldType.notNullable(new ArrowType.Utf8()), null));
+
+ doRoundTripTest(fields);
+ }
+
+ @Test
+ public void testRoundTripBinaryTypes() {
+
+ List fields =
+ Arrays.asList(
+ new Field("nullableBinary", FieldType.nullable(new ArrowType.Binary()), null),
+ new Field("nonNullableBinary", FieldType.notNullable(new ArrowType.Binary()), null));
+
+ doRoundTripTest(fields);
+ }
+
+ @Test
+ public void testRoundTripFixedSizeBinaryTypes() {
+
+ List fields =
+ Arrays.asList(
+ new Field(
+ "nullableFixedSizeBinary",
+ FieldType.nullable(new ArrowType.FixedSizeBinary(10)),
+ null),
+ new Field(
+ "nonNullableFixedSizeBinary",
+ FieldType.notNullable(new ArrowType.FixedSizeBinary(10)),
+ null));
+
+ doRoundTripTest(fields);
+ }
+
+ // Schema round trip for logical types, nullable and non-nullable
+
+ @Test
+ public void testRoundTripDecimalTypes() {
+
+ List fields =
+ Arrays.asList(
+ new Field(
+ "nullableDecimal128", FieldType.nullable(new ArrowType.Decimal(10, 2, 128)), null),
+ new Field(
+ "nonNullableDecimal1281",
+ FieldType.notNullable(new ArrowType.Decimal(10, 2, 128)),
+ null),
+ new Field(
+ "nonNullableDecimal1282",
+ FieldType.notNullable(new ArrowType.Decimal(15, 5, 128)),
+ null),
+ new Field(
+ "nonNullableDecimal1283",
+ FieldType.notNullable(new ArrowType.Decimal(20, 10, 128)),
+ null),
+ new Field(
+ "nullableDecimal256", FieldType.nullable(new ArrowType.Decimal(55, 15, 256)), null),
+ new Field(
+ "nonNullableDecimal2561",
+ FieldType.notNullable(new ArrowType.Decimal(55, 25, 256)),
+ null),
+ new Field(
+ "nonNullableDecimal2562",
+ FieldType.notNullable(new ArrowType.Decimal(25, 8, 256)),
+ null),
+ new Field(
+ "nonNullableDecimal2563",
+ FieldType.notNullable(new ArrowType.Decimal(60, 50, 256)),
+ null));
+
+ doRoundTripTest(fields);
+ }
+
+ @Test
+ public void testRoundTripDateTypes() {
+
+ List fields =
+ Arrays.asList(
+ new Field(
+ "nullableDateDay", FieldType.nullable(new ArrowType.Date(DateUnit.DAY)), null),
+ new Field(
+ "nonNullableDateDay",
+ FieldType.notNullable(new ArrowType.Date(DateUnit.DAY)),
+ null));
+
+ doRoundTripTest(fields);
+ }
+
+ @Test
+ public void testRoundTripTimeTypes() {
+
+ List fields =
+ Arrays.asList(
+ new Field(
+ "nullableTimeMillis",
+ FieldType.nullable(new ArrowType.Time(TimeUnit.MILLISECOND, 32)),
+ null),
+ new Field(
+ "nonNullableTimeMillis",
+ FieldType.notNullable(new ArrowType.Time(TimeUnit.MILLISECOND, 32)),
+ null),
+ new Field(
+ "nullableTimeMicros",
+ FieldType.nullable(new ArrowType.Time(TimeUnit.MICROSECOND, 64)),
+ null),
+ new Field(
+ "nonNullableTimeMicros",
+ FieldType.notNullable(new ArrowType.Time(TimeUnit.MICROSECOND, 64)),
+ null));
+
+ doRoundTripTest(fields);
+ }
+
+ @Test
+ public void testRoundTripZoneAwareTimestampTypes() {
+
+ List fields =
+ Arrays.asList(
+ new Field(
+ "nullableTimestampMillisTz",
+ FieldType.nullable(new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC")),
+ null),
+ new Field(
+ "nonNullableTimestampMillisTz",
+ FieldType.notNullable(new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC")),
+ null),
+ new Field(
+ "nullableTimestampMicrosTz",
+ FieldType.nullable(new ArrowType.Timestamp(TimeUnit.MICROSECOND, "UTC")),
+ null),
+ new Field(
+ "nonNullableTimestampMicrosTz",
+ FieldType.notNullable(new ArrowType.Timestamp(TimeUnit.MICROSECOND, "UTC")),
+ null),
+ new Field(
+ "nullableTimestampNanosTz",
+ FieldType.nullable(new ArrowType.Timestamp(TimeUnit.NANOSECOND, "UTC")),
+ null),
+ new Field(
+ "nonNullableTimestampNanosTz",
+ FieldType.notNullable(new ArrowType.Timestamp(TimeUnit.NANOSECOND, "UTC")),
+ null));
+
+ doRoundTripTest(fields);
+ }
+
+ @Test
+ public void testRoundTripLocalTimestampTypes() {
+
+ List fields =
+ Arrays.asList(
+ new Field(
+ "nullableTimestampMillis",
+ FieldType.nullable(new ArrowType.Timestamp(TimeUnit.MILLISECOND, null)),
+ null),
+ new Field(
+ "nonNullableTimestampMillis",
+ FieldType.notNullable(new ArrowType.Timestamp(TimeUnit.MILLISECOND, null)),
+ null),
+ new Field(
+ "nullableTimestampMicros",
+ FieldType.nullable(new ArrowType.Timestamp(TimeUnit.MICROSECOND, null)),
+ null),
+ new Field(
+ "nonNullableTimestampMicros",
+ FieldType.notNullable(new ArrowType.Timestamp(TimeUnit.MICROSECOND, null)),
+ null),
+ new Field(
+ "nullableTimestampNanos",
+ FieldType.nullable(new ArrowType.Timestamp(TimeUnit.NANOSECOND, null)),
+ null),
+ new Field(
+ "nonNullableTimestampNanos",
+ FieldType.notNullable(new ArrowType.Timestamp(TimeUnit.NANOSECOND, null)),
+ null));
+
+ doRoundTripTest(fields);
+ }
+
+ // Schema round trip for complex types, where the contents are primitive and logical types
+
+ @Test
+ public void testRoundTripListType() {
+
+ List fields =
+ Arrays.asList(
+ new Field(
+ "nullableIntList",
+ FieldType.nullable(new ArrowType.List()),
+ Arrays.asList(
+ new Field("$data$", FieldType.nullable(new ArrowType.Int(32, true)), null))),
+ new Field(
+ "nullableDoubleList",
+ FieldType.nullable(new ArrowType.List()),
+ Arrays.asList(
+ new Field(
+ "$data$",
+ FieldType.notNullable(
+ new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE)),
+ null))),
+ new Field(
+ "nonNullableDecimalList",
+ FieldType.notNullable(new ArrowType.List()),
+ Arrays.asList(
+ new Field(
+ "$data$", FieldType.nullable(new ArrowType.Decimal(10, 2, 128)), null))),
+ new Field(
+ "nonNullableTimestampList",
+ FieldType.notNullable(new ArrowType.List()),
+ Arrays.asList(
+ new Field(
+ "$data$",
+ FieldType.notNullable(new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC")),
+ null))));
+
+ doRoundTripTest(fields);
+ }
+
+ @Test
+ public void testRoundTripMapType() {
+
+ List fields =
+ Arrays.asList(
+ new Field(
+ "nullableMapWithNullableInt",
+ FieldType.nullable(new ArrowType.Map(false)),
+ Arrays.asList(
+ new Field(
+ "entries",
+ FieldType.notNullable(new ArrowType.Struct()),
+ Arrays.asList(
+ new Field("key", FieldType.notNullable(new ArrowType.Utf8()), null),
+ new Field(
+ "value", FieldType.nullable(new ArrowType.Int(32, true)), null))))),
+ new Field(
+ "nullableMapWithNonNullableDouble",
+ FieldType.nullable(new ArrowType.Map(false)),
+ Arrays.asList(
+ new Field(
+ "entries",
+ FieldType.notNullable(new ArrowType.Struct()),
+ Arrays.asList(
+ new Field("key", FieldType.notNullable(new ArrowType.Utf8()), null),
+ new Field(
+ "value",
+ FieldType.notNullable(
+ new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE)),
+ null))))),
+ new Field(
+ "nonNullableMapWithNullableDecimal",
+ FieldType.notNullable(new ArrowType.Map(false)),
+ Arrays.asList(
+ new Field(
+ "entries",
+ FieldType.notNullable(new ArrowType.Struct()),
+ Arrays.asList(
+ new Field("key", FieldType.notNullable(new ArrowType.Utf8()), null),
+ new Field(
+ "value",
+ FieldType.nullable(new ArrowType.Decimal(10, 2, 128)),
+ null))))),
+ new Field(
+ "nonNullableMapWithNonNullableTimestamp",
+ FieldType.notNullable(new ArrowType.Map(false)),
+ Arrays.asList(
+ new Field(
+ "entries",
+ FieldType.notNullable(new ArrowType.Struct()),
+ Arrays.asList(
+ new Field("key", FieldType.notNullable(new ArrowType.Utf8()), null),
+ new Field(
+ "value",
+ FieldType.notNullable(
+ new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC")),
+ null))))));
+
+ doRoundTripTest(fields);
+ }
+
+ @Test
+ public void testRoundTripStructType() {
+
+ List fields =
+ Arrays.asList(
+ new Field(
+ "nullableRecord",
+ FieldType.nullable(new ArrowType.Struct()),
+ Arrays.asList(
+ new Field("field1", FieldType.nullable(new ArrowType.Int(32, true)), null),
+ new Field(
+ "field2",
+ FieldType.notNullable(
+ new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE)),
+ null),
+ new Field(
+ "field3", FieldType.nullable(new ArrowType.Decimal(10, 2, 128)), null),
+ new Field(
+ "field4",
+ FieldType.notNullable(new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC")),
+ null))),
+ new Field(
+ "nonNullableRecord",
+ FieldType.notNullable(new ArrowType.Struct()),
+ Arrays.asList(
+ new Field("field1", FieldType.nullable(new ArrowType.Int(32, true)), null),
+ new Field(
+ "field2",
+ FieldType.notNullable(
+ new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE)),
+ null),
+ new Field(
+ "field3", FieldType.nullable(new ArrowType.Decimal(10, 2, 128)), null),
+ new Field(
+ "field4",
+ FieldType.notNullable(new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC")),
+ null))));
+
+ doRoundTripTest(fields);
+ }
+
+ @Test
+ public void testRoundTripEnumType() {
+
+ BufferAllocator allocator = new RootAllocator();
+
+ FieldType dictionaryField = new FieldType(false, new ArrowType.Utf8(), null);
+ VarCharVector dictionaryVector =
+ new VarCharVector(new Field("dictionary", dictionaryField, null), allocator);
+
+ dictionaryVector.allocateNew(3);
+ dictionaryVector.set(0, "apple".getBytes());
+ dictionaryVector.set(1, "banana".getBytes());
+ dictionaryVector.set(2, "cherry".getBytes());
+ dictionaryVector.setValueCount(3);
+
+ // For simplicity, ensure the index type matches what will be decoded during Avro enum decoding
+ Dictionary dictionary =
+ new Dictionary(
+ dictionaryVector, new DictionaryEncoding(0L, false, new ArrowType.Int(8, true)));
+ DictionaryProvider dictionaries = new DictionaryProvider.MapDictionaryProvider(dictionary);
+
+ List fields =
+ Arrays.asList(
+ new Field(
+ "enumField",
+ new FieldType(
+ true,
+ new ArrowType.Int(8, true),
+ new DictionaryEncoding(0L, false, new ArrowType.Int(8, true))),
+ null));
+
+ doRoundTripTest(fields, dictionaries);
+ }
+}
diff --git a/adapter/avro/src/test/resources/schema/logical/test_decimal_invalid1.avsc b/adapter/avro/src/test/resources/schema/logical/test_decimal_invalid1.avsc
index 18d7d63fc7..c1867811c7 100644
--- a/adapter/avro/src/test/resources/schema/logical/test_decimal_invalid1.avsc
+++ b/adapter/avro/src/test/resources/schema/logical/test_decimal_invalid1.avsc
@@ -20,6 +20,6 @@
"name": "test",
"type": "bytes",
"logicalType" : "decimal",
- "precision": 39,
+ "precision": 77,
"scale": 2
}
diff --git a/adapter/avro/src/test/resources/schema/logical/test_local_timestamp_micros.avsc b/adapter/avro/src/test/resources/schema/logical/test_local_timestamp_micros.avsc
new file mode 100644
index 0000000000..db456e8a84
--- /dev/null
+++ b/adapter/avro/src/test/resources/schema/logical/test_local_timestamp_micros.avsc
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{
+ "namespace": "org.apache.arrow.avro",
+ "name": "test",
+ "type": "long",
+ "logicalType" : "local-timestamp-micros"
+}
diff --git a/adapter/avro/src/test/resources/schema/logical/test_local_timestamp_millis.avsc b/adapter/avro/src/test/resources/schema/logical/test_local_timestamp_millis.avsc
new file mode 100644
index 0000000000..6a3cf9bccb
--- /dev/null
+++ b/adapter/avro/src/test/resources/schema/logical/test_local_timestamp_millis.avsc
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{
+ "namespace": "org.apache.arrow.avro",
+ "name": "test",
+ "type": "long",
+ "logicalType" : "local-timestamp-millis"
+}
diff --git a/adapter/avro/src/test/resources/schema/logical/test_local_timestamp_nanos.avsc b/adapter/avro/src/test/resources/schema/logical/test_local_timestamp_nanos.avsc
new file mode 100644
index 0000000000..96ca8bbfa4
--- /dev/null
+++ b/adapter/avro/src/test/resources/schema/logical/test_local_timestamp_nanos.avsc
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{
+ "namespace": "org.apache.arrow.avro",
+ "name": "test",
+ "type": "long",
+ "logicalType" : "local-timestamp-nanos"
+}
diff --git a/adapter/avro/src/test/resources/schema/logical/test_timestamp_nanos.avsc b/adapter/avro/src/test/resources/schema/logical/test_timestamp_nanos.avsc
new file mode 100644
index 0000000000..9e05eab408
--- /dev/null
+++ b/adapter/avro/src/test/resources/schema/logical/test_timestamp_nanos.avsc
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{
+ "namespace": "org.apache.arrow.avro",
+ "name": "test",
+ "type": "long",
+ "logicalType" : "timestamp-nanos"
+}
diff --git a/adapter/jdbc/pom.xml b/adapter/jdbc/pom.xml
index 2f621d7a05..a8ac19721d 100644
--- a/adapter/jdbc/pom.xml
+++ b/adapter/jdbc/pom.xml
@@ -23,7 +23,7 @@ under the License.
org.apache.arrowarrow-java-root
- 19.0.0-SNAPSHOT
+ 20.0.0-SNAPSHOT../../pom.xml
@@ -59,7 +59,7 @@ under the License.
com.h2databaseh2
- 2.3.232
+ 2.4.240test
diff --git a/adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/BinaryConsumer.java b/adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/BinaryConsumer.java
index edbc6360df..73ec04b8a0 100644
--- a/adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/BinaryConsumer.java
+++ b/adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/BinaryConsumer.java
@@ -51,13 +51,15 @@ public BinaryConsumer(VarBinaryVector vector, int index) {
/** consume a InputStream. */
public void consume(InputStream is) throws IOException {
+ while (currentIndex >= vector.getValueCapacity()) {
+ vector.reallocValidityAndOffsetBuffers();
+ }
+
+ final int startOffset = vector.getStartOffset(currentIndex);
+ final ArrowBuf offsetBuffer = vector.getOffsetBuffer();
+ int dataLength = 0;
+
if (is != null) {
- while (currentIndex >= vector.getValueCapacity()) {
- vector.reallocValidityAndOffsetBuffers();
- }
- final int startOffset = vector.getStartOffset(currentIndex);
- final ArrowBuf offsetBuffer = vector.getOffsetBuffer();
- int dataLength = 0;
int read;
while ((read = is.read(reuseBytes)) != -1) {
while (vector.getDataBuffer().capacity() < (startOffset + dataLength + read)) {
@@ -66,11 +68,12 @@ public void consume(InputStream is) throws IOException {
vector.getDataBuffer().setBytes(startOffset + dataLength, reuseBytes, 0, read);
dataLength += read;
}
- offsetBuffer.setInt(
- (currentIndex + 1) * ((long) VarBinaryVector.OFFSET_WIDTH), startOffset + dataLength);
+
BitVectorHelper.setBit(vector.getValidityBuffer(), currentIndex);
- vector.setLastSet(currentIndex);
}
+ offsetBuffer.setInt(
+ (currentIndex + 1) * ((long) VarBinaryVector.OFFSET_WIDTH), startOffset + dataLength);
+ vector.setLastSet(currentIndex);
}
public void moveWriterPosition() {
@@ -95,9 +98,7 @@ public NullableBinaryConsumer(VarBinaryVector vector, int index) {
@Override
public void consume(ResultSet resultSet) throws SQLException, IOException {
InputStream is = resultSet.getBinaryStream(columnIndexInResultSet);
- if (!resultSet.wasNull()) {
- consume(is);
- }
+ consume(is);
moveWriterPosition();
}
}
diff --git a/adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/CompositeJdbcConsumer.java b/adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/CompositeJdbcConsumer.java
index 2366116fd0..b8389ee27c 100644
--- a/adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/CompositeJdbcConsumer.java
+++ b/adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/CompositeJdbcConsumer.java
@@ -24,7 +24,6 @@
import org.apache.arrow.util.AutoCloseables;
import org.apache.arrow.vector.ValueVector;
import org.apache.arrow.vector.VectorSchemaRoot;
-import org.apache.arrow.vector.types.pojo.ArrowType;
/** Composite consumer which hold all consumers. It manages the consume and cleanup process. */
public class CompositeJdbcConsumer implements JdbcConsumer {
@@ -46,9 +45,9 @@ public void consume(ResultSet rs) throws SQLException, IOException {
BaseConsumer consumer = (BaseConsumer) consumers[i];
JdbcFieldInfo fieldInfo =
new JdbcFieldInfo(rs.getMetaData(), consumer.columnIndexInResultSet);
- ArrowType arrowType = consumer.vector.getMinorType().getType();
+
throw new JdbcConsumerException(
- "Exception while consuming JDBC value", e, fieldInfo, arrowType);
+ "Exception while consuming JDBC value", e, fieldInfo, consumer.vector.getField());
} else {
throw e;
}
diff --git a/adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/exceptions/JdbcConsumerException.java b/adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/exceptions/JdbcConsumerException.java
index 04e26d640c..98927f416c 100644
--- a/adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/exceptions/JdbcConsumerException.java
+++ b/adapter/jdbc/src/main/java/org/apache/arrow/adapter/jdbc/consumer/exceptions/JdbcConsumerException.java
@@ -17,7 +17,7 @@
package org.apache.arrow.adapter.jdbc.consumer.exceptions;
import org.apache.arrow.adapter.jdbc.JdbcFieldInfo;
-import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
/**
* Exception while consuming JDBC data. This exception stores the JdbcFieldInfo for the column and
@@ -25,7 +25,7 @@
*/
public class JdbcConsumerException extends RuntimeException {
final JdbcFieldInfo fieldInfo;
- final ArrowType arrowType;
+ final Field field;
/**
* Construct JdbcConsumerException with all fields.
@@ -33,17 +33,17 @@ public class JdbcConsumerException extends RuntimeException {
* @param message error message
* @param cause original exception
* @param fieldInfo JdbcFieldInfo for the column
- * @param arrowType ArrowType for the corresponding vector
+ * @param field ArrowType for the corresponding vector
*/
public JdbcConsumerException(
- String message, Throwable cause, JdbcFieldInfo fieldInfo, ArrowType arrowType) {
+ String message, Throwable cause, JdbcFieldInfo fieldInfo, Field field) {
super(message, cause);
this.fieldInfo = fieldInfo;
- this.arrowType = arrowType;
+ this.field = field;
}
- public ArrowType getArrowType() {
- return this.arrowType;
+ public Field getField() {
+ return this.field;
}
public JdbcFieldInfo getFieldInfo() {
diff --git a/adapter/jdbc/src/test/java/org/apache/arrow/adapter/jdbc/ResultSetUtilityTest.java b/adapter/jdbc/src/test/java/org/apache/arrow/adapter/jdbc/ResultSetUtilityTest.java
index c7dc9b2791..e5039ccf59 100644
--- a/adapter/jdbc/src/test/java/org/apache/arrow/adapter/jdbc/ResultSetUtilityTest.java
+++ b/adapter/jdbc/src/test/java/org/apache/arrow/adapter/jdbc/ResultSetUtilityTest.java
@@ -43,15 +43,19 @@ public void testZeroRowResultSet() throws Exception {
.setReuseVectorSchemaRoot(reuseVectorSchemaRoot)
.build();
- ArrowVectorIterator iter = JdbcToArrow.sqlToArrowVectorIterator(rs, config);
- assertTrue(iter.hasNext(), "Iterator on zero row ResultSet should haveNext() before use");
- VectorSchemaRoot root = iter.next();
- assertNotNull(root, "VectorSchemaRoot from first next() result should never be null");
- assertEquals(
- 0, root.getRowCount(), "VectorSchemaRoot from empty ResultSet should have zero rows");
- assertFalse(
- iter.hasNext(),
- "hasNext() should return false on empty ResultSets after initial next() call");
+ try (ArrowVectorIterator iter = JdbcToArrow.sqlToArrowVectorIterator(rs, config)) {
+ assertTrue(iter.hasNext(), "Iterator on zero row ResultSet should haveNext() before use");
+ VectorSchemaRoot root = iter.next();
+ assertNotNull(root, "VectorSchemaRoot from first next() result should never be null");
+ assertEquals(
+ 0, root.getRowCount(), "VectorSchemaRoot from empty ResultSet should have zero rows");
+ assertFalse(
+ iter.hasNext(),
+ "hasNext() should return false on empty ResultSets after initial next() call");
+ if (!reuseVectorSchemaRoot) {
+ root.close();
+ }
+ }
}
}
}
diff --git a/adapter/jdbc/src/test/java/org/apache/arrow/adapter/jdbc/consumer/BinaryConsumerTest.java b/adapter/jdbc/src/test/java/org/apache/arrow/adapter/jdbc/consumer/BinaryConsumerTest.java
index b1e253794d..bb836578e2 100644
--- a/adapter/jdbc/src/test/java/org/apache/arrow/adapter/jdbc/consumer/BinaryConsumerTest.java
+++ b/adapter/jdbc/src/test/java/org/apache/arrow/adapter/jdbc/consumer/BinaryConsumerTest.java
@@ -22,6 +22,7 @@
import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.io.InputStream;
import org.apache.arrow.vector.BaseValueVector;
import org.apache.arrow.vector.VarBinaryVector;
import org.junit.jupiter.api.Test;
@@ -65,7 +66,11 @@ public void testConsumeInputStream(byte[][] values, boolean nullable) throws IOE
nullable,
binaryConsumer -> {
for (byte[] value : values) {
- binaryConsumer.consume(new ByteArrayInputStream(value));
+ if (value != null) {
+ binaryConsumer.consume(new ByteArrayInputStream(value));
+ } else {
+ binaryConsumer.consume((InputStream) null);
+ }
binaryConsumer.moveWriterPosition();
}
},
@@ -119,5 +124,9 @@ public void testConsumeInputStream() throws IOException {
testRecords[i] = createBytes(DEFAULT_RECORD_BYTE_COUNT);
}
testConsumeInputStream(testRecords, false);
+
+ byte[] bytes1 = new byte[] {1, 2, 3};
+ byte[] bytes2 = new byte[] {4, 5, 6};
+ testConsumeInputStream(new byte[][] {bytes1, null, bytes2}, true);
}
}
diff --git a/adapter/orc/pom.xml b/adapter/orc/pom.xml
index e3ae7d5163..50a9b3a603 100644
--- a/adapter/orc/pom.xml
+++ b/adapter/orc/pom.xml
@@ -23,7 +23,7 @@ under the License.
org.apache.arrowarrow-java-root
- 19.0.0-SNAPSHOT
+ 20.0.0-SNAPSHOT../../pom.xml
@@ -61,7 +61,7 @@ under the License.
org.apache.orcorc-core
- 2.1.1
+ 2.3.0test
diff --git a/algorithm/pom.xml b/algorithm/pom.xml
index 898c2605b6..24adcefa6f 100644
--- a/algorithm/pom.xml
+++ b/algorithm/pom.xml
@@ -22,7 +22,7 @@ under the License.
org.apache.arrowarrow-java-root
- 19.0.0-SNAPSHOT
+ 20.0.0-SNAPSHOTarrow-algorithmArrow Algorithms
diff --git a/arrow-format/FlightSql.proto b/arrow-format/FlightSql.proto
index 3568d851cb..b1dc57b33b 100644
--- a/arrow-format/FlightSql.proto
+++ b/arrow-format/FlightSql.proto
@@ -1212,6 +1212,7 @@ message CommandGetDbSchemas {
* - ARROW:FLIGHT:SQL:IS_CASE_SENSITIVE - "1" indicates if the column is case-sensitive, "0" otherwise.
* - ARROW:FLIGHT:SQL:IS_READ_ONLY - "1" indicates if the column is read only, "0" otherwise.
* - ARROW:FLIGHT:SQL:IS_SEARCHABLE - "1" indicates if the column is searchable via WHERE clause, "0" otherwise.
+ * - ARROW:FLIGHT:SQL:REMARKS - A comment describing the column.
* The returned data should be ordered by catalog_name, db_schema_name, table_name, then table_type, followed by table_schema if requested.
*/
message CommandGetTables {
@@ -1549,6 +1550,11 @@ message ActionCreatePreparedStatementResult {
// If the query provided contained parameters, parameter_schema contains the
// schema of the expected parameters. It should be an IPC-encapsulated Schema, as described in Schema.fbs.
bytes parameter_schema = 3;
+
+ // When set to true, the query should be executed with CommandPreparedStatementUpdate,
+ // when set to false, the query should be executed with CommandPreparedStatementQuery.
+ // If not set, the client can choose how to execute the query.
+ optional bool is_update = 4;
}
/*
@@ -1678,6 +1684,7 @@ message ActionEndSavepointRequest {
* - ARROW:FLIGHT:SQL:IS_CASE_SENSITIVE - "1" indicates if the column is case-sensitive, "0" otherwise.
* - ARROW:FLIGHT:SQL:IS_READ_ONLY - "1" indicates if the column is read only, "0" otherwise.
* - ARROW:FLIGHT:SQL:IS_SEARCHABLE - "1" indicates if the column is searchable via WHERE clause, "0" otherwise.
+ * - ARROW:FLIGHT:SQL:REMARKS - A comment describing the column.
* - GetFlightInfo: execute the query.
*/
message CommandStatementQuery {
@@ -1703,6 +1710,7 @@ message CommandStatementQuery {
* - ARROW:FLIGHT:SQL:IS_CASE_SENSITIVE - "1" indicates if the column is case-sensitive, "0" otherwise.
* - ARROW:FLIGHT:SQL:IS_READ_ONLY - "1" indicates if the column is read only, "0" otherwise.
* - ARROW:FLIGHT:SQL:IS_SEARCHABLE - "1" indicates if the column is searchable via WHERE clause, "0" otherwise.
+ * - ARROW:FLIGHT:SQL:REMARKS - A comment describing the column.
* - GetFlightInfo: execute the query.
* - DoPut: execute the query.
*/
@@ -1739,6 +1747,7 @@ message TicketStatementQuery {
* - ARROW:FLIGHT:SQL:IS_CASE_SENSITIVE - "1" indicates if the column is case-sensitive, "0" otherwise.
* - ARROW:FLIGHT:SQL:IS_READ_ONLY - "1" indicates if the column is read only, "0" otherwise.
* - ARROW:FLIGHT:SQL:IS_SEARCHABLE - "1" indicates if the column is searchable via WHERE clause, "0" otherwise.
+ * - ARROW:FLIGHT:SQL:REMARKS - A comment describing the column.
*
* If the schema is retrieved after parameter values have been bound with DoPut, then the server should account
* for the parameters when determining the schema.
diff --git a/arrow-variant/pom.xml b/arrow-variant/pom.xml
new file mode 100644
index 0000000000..e578626dd4
--- /dev/null
+++ b/arrow-variant/pom.xml
@@ -0,0 +1,51 @@
+
+
+
+ 4.0.0
+
+ org.apache.arrow
+ arrow-java-root
+ 20.0.0-SNAPSHOT
+
+ arrow-variant
+ Arrow Variant
+ Arrow Variant type support.
+
+
+
+ org.apache.arrow
+ arrow-memory-core
+
+
+ org.apache.arrow
+ arrow-vector
+
+
+ org.apache.parquet
+ parquet-variant
+ ${dep.parquet.version}
+
+
+ org.apache.arrow
+ arrow-memory-unsafe
+ test
+
+
+
diff --git a/arrow-variant/src/main/java/module-info.java b/arrow-variant/src/main/java/module-info.java
new file mode 100644
index 0000000000..da94173969
--- /dev/null
+++ b/arrow-variant/src/main/java/module-info.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@SuppressWarnings("requires-automatic")
+module org.apache.arrow.variant {
+ exports org.apache.arrow.variant;
+ exports org.apache.arrow.variant.extension;
+ exports org.apache.arrow.variant.impl;
+ exports org.apache.arrow.variant.holders;
+
+ requires org.apache.arrow.memory.core;
+ requires org.apache.arrow.vector;
+ requires parquet.variant;
+}
diff --git a/arrow-variant/src/main/java/org/apache/arrow/variant/Variant.java b/arrow-variant/src/main/java/org/apache/arrow/variant/Variant.java
new file mode 100644
index 0000000000..fa05cdd93f
--- /dev/null
+++ b/arrow-variant/src/main/java/org/apache/arrow/variant/Variant.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.variant;
+
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+import java.util.UUID;
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+
+/**
+ * Wrapper around parquet-variant's Variant implementation.
+ *
+ *
This wrapper exists to isolate the parquet-variant dependency from Arrow's public API,
+ * allowing the vector module to expose variant functionality without requiring users to depend on
+ * parquet-variant directly. It also ensures that nested variant values (from arrays and objects)
+ * are consistently wrapped.
+ */
+public class Variant {
+
+ private final org.apache.parquet.variant.Variant delegate;
+
+ /** Creates a Variant from raw metadata and value byte arrays. */
+ public Variant(byte[] metadata, byte[] value) {
+ this.delegate = new org.apache.parquet.variant.Variant(value, metadata);
+ }
+
+ /** Creates a Variant by copying data from ArrowBuf instances. */
+ public Variant(
+ ArrowBuf metadataBuffer,
+ int metadataStart,
+ int metadataEnd,
+ ArrowBuf valueBuffer,
+ int valueStart,
+ int valueEnd) {
+ byte[] metadata = new byte[metadataEnd - metadataStart];
+ byte[] value = new byte[valueEnd - valueStart];
+ metadataBuffer.getBytes(metadataStart, metadata);
+ valueBuffer.getBytes(valueStart, value);
+ this.delegate = new org.apache.parquet.variant.Variant(value, metadata);
+ }
+
+ private Variant(org.apache.parquet.variant.Variant delegate) {
+ this.delegate = delegate;
+ }
+
+ /** Constructs a Variant from a NullableVariantHolder. */
+ public Variant(NullableVariantHolder holder) {
+ this(
+ holder.metadataBuffer,
+ holder.metadataStart,
+ holder.metadataEnd,
+ holder.valueBuffer,
+ holder.valueStart,
+ holder.valueEnd);
+ }
+
+ public ByteBuffer getValueBuffer() {
+ return delegate.getValueBuffer();
+ }
+
+ public ByteBuffer getMetadataBuffer() {
+ return delegate.getMetadataBuffer();
+ }
+
+ public boolean getBoolean() {
+ return delegate.getBoolean();
+ }
+
+ public byte getByte() {
+ return delegate.getByte();
+ }
+
+ public short getShort() {
+ return delegate.getShort();
+ }
+
+ public int getInt() {
+ return delegate.getInt();
+ }
+
+ public long getLong() {
+ return delegate.getLong();
+ }
+
+ public double getDouble() {
+ return delegate.getDouble();
+ }
+
+ public BigDecimal getDecimal() {
+ return delegate.getDecimal();
+ }
+
+ public float getFloat() {
+ return delegate.getFloat();
+ }
+
+ public ByteBuffer getBinary() {
+ return delegate.getBinary();
+ }
+
+ public UUID getUUID() {
+ return delegate.getUUID();
+ }
+
+ public String getString() {
+ return delegate.getString();
+ }
+
+ public Type getType() {
+ return Type.fromParquet(delegate.getType());
+ }
+
+ public int numObjectElements() {
+ return delegate.numObjectElements();
+ }
+
+ public Variant getFieldByKey(String key) {
+ org.apache.parquet.variant.Variant result = delegate.getFieldByKey(key);
+ return result != null ? wrap(result) : null;
+ }
+
+ public ObjectField getFieldAtIndex(int idx) {
+ org.apache.parquet.variant.Variant.ObjectField field = delegate.getFieldAtIndex(idx);
+ return new ObjectField(field.key, wrap(field.value));
+ }
+
+ public int numArrayElements() {
+ return delegate.numArrayElements();
+ }
+
+ public Variant getElementAtIndex(int index) {
+ org.apache.parquet.variant.Variant result = delegate.getElementAtIndex(index);
+ return result != null ? wrap(result) : null;
+ }
+
+ private static Variant wrap(org.apache.parquet.variant.Variant parquetVariant) {
+ return new Variant(parquetVariant);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Variant variant = (Variant) o;
+ return delegate.getMetadataBuffer().equals(variant.delegate.getMetadataBuffer())
+ && delegate.getValueBuffer().equals(variant.delegate.getValueBuffer());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(delegate.getMetadataBuffer(), delegate.getValueBuffer());
+ }
+
+ @Override
+ public String toString() {
+ return "Variant{type=" + getType() + '}';
+ }
+
+ public enum Type {
+ OBJECT,
+ ARRAY,
+ NULL,
+ BOOLEAN,
+ BYTE,
+ SHORT,
+ INT,
+ LONG,
+ STRING,
+ DOUBLE,
+ DECIMAL4,
+ DECIMAL8,
+ DECIMAL16,
+ DATE,
+ TIMESTAMP_TZ,
+ TIMESTAMP_NTZ,
+ FLOAT,
+ BINARY,
+ TIME,
+ TIMESTAMP_NANOS_TZ,
+ TIMESTAMP_NANOS_NTZ,
+ UUID;
+
+ static Type fromParquet(org.apache.parquet.variant.Variant.Type parquetType) {
+ return Type.valueOf(parquetType.name());
+ }
+ }
+
+ public static final class ObjectField {
+ public final String key;
+ public final Variant value;
+
+ public ObjectField(String key, Variant value) {
+ this.key = key;
+ this.value = value;
+ }
+ }
+}
diff --git a/arrow-variant/src/main/java/org/apache/arrow/variant/extension/VariantType.java b/arrow-variant/src/main/java/org/apache/arrow/variant/extension/VariantType.java
new file mode 100644
index 0000000000..3deb70cdc0
--- /dev/null
+++ b/arrow-variant/src/main/java/org/apache/arrow/variant/extension/VariantType.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.variant.extension;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.variant.impl.VariantWriterImpl;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.ValueVector;
+import org.apache.arrow.vector.complex.writer.FieldWriter;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.ArrowType.ExtensionType;
+import org.apache.arrow.vector.types.pojo.ExtensionTypeRegistry;
+import org.apache.arrow.vector.types.pojo.FieldType;
+
+/**
+ * Arrow extension type for Parquet
+ * Variant binary encoding. The type itself does not support shredded variant data.
+ */
+public final class VariantType extends ExtensionType {
+
+ public static final VariantType INSTANCE = new VariantType();
+
+ public static final String EXTENSION_NAME = "parquet.variant";
+
+ static {
+ ExtensionTypeRegistry.register(INSTANCE);
+ }
+
+ private VariantType() {}
+
+ @Override
+ public ArrowType storageType() {
+ return ArrowType.Struct.INSTANCE;
+ }
+
+ @Override
+ public String extensionName() {
+ return EXTENSION_NAME;
+ }
+
+ @Override
+ public boolean extensionEquals(ExtensionType other) {
+ return other instanceof VariantType;
+ }
+
+ @Override
+ public String serialize() {
+ return "";
+ }
+
+ @Override
+ public ArrowType deserialize(ArrowType storageType, String serializedData) {
+ if (!storageType.equals(this.storageType())) {
+ throw new UnsupportedOperationException(
+ "Cannot construct VariantType from underlying type " + storageType);
+ }
+ return INSTANCE;
+ }
+
+ @Override
+ public FieldVector getNewVector(String name, FieldType fieldType, BufferAllocator allocator) {
+ return new VariantVector(name, allocator);
+ }
+
+ @Override
+ public boolean isComplex() {
+ // The type itself is not complex meaning we need separate functions to convert/extract
+ // different types.
+ // Meanwhile, the containing vector is complex in terms of containing multiple values (metadata
+ // and value)
+ return false;
+ }
+
+ @Override
+ public FieldWriter getNewFieldWriter(ValueVector vector) {
+ return new VariantWriterImpl((VariantVector) vector);
+ }
+}
diff --git a/arrow-variant/src/main/java/org/apache/arrow/variant/extension/VariantVector.java b/arrow-variant/src/main/java/org/apache/arrow/variant/extension/VariantVector.java
new file mode 100644
index 0000000000..1bbf1a6bdb
--- /dev/null
+++ b/arrow-variant/src/main/java/org/apache/arrow/variant/extension/VariantVector.java
@@ -0,0 +1,348 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.variant.extension;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.util.hash.ArrowBufHasher;
+import org.apache.arrow.variant.Variant;
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+import org.apache.arrow.variant.holders.VariantHolder;
+import org.apache.arrow.vector.BitVectorHelper;
+import org.apache.arrow.vector.ExtensionTypeVector;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.ValueVector;
+import org.apache.arrow.vector.VarBinaryVector;
+import org.apache.arrow.vector.complex.AbstractStructVector;
+import org.apache.arrow.vector.complex.StructVector;
+import org.apache.arrow.vector.complex.reader.FieldReader;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.ArrowType.Binary;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.FieldType;
+import org.apache.arrow.vector.util.CallBack;
+import org.apache.arrow.vector.util.TransferPair;
+
+/**
+ * Arrow vector for storing {@link VariantType} values.
+ *
+ *
Stores semi-structured data (like JSON) as metadata + value binary pairs, allowing
+ * type-flexible columnar storage within Arrow's type system.
+ */
+public class VariantVector extends ExtensionTypeVector {
+
+ public static final String METADATA_VECTOR_NAME = "metadata";
+ public static final String VALUE_VECTOR_NAME = "value";
+
+ private final Field rootField;
+
+ /**
+ * Constructs a new VariantVector with the given name and allocator.
+ *
+ * @param name the name of the vector
+ * @param allocator the buffer allocator for memory management
+ */
+ public VariantVector(String name, BufferAllocator allocator) {
+ super(
+ name,
+ allocator,
+ new StructVector(
+ name,
+ allocator,
+ FieldType.nullable(ArrowType.Struct.INSTANCE),
+ null,
+ AbstractStructVector.ConflictPolicy.CONFLICT_ERROR,
+ false));
+ rootField = createVariantField(name);
+ ((FieldVector) this.getUnderlyingVector())
+ .initializeChildrenFromFields(rootField.getChildren());
+ }
+
+ /**
+ * Creates a new VariantVector with the given name. The Variant Field schema has to be the same
+ * everywhere, otherwise ArrowBuffer loading might fail during serialization/deserialization and
+ * schema mismatches can occur. This includes CompleteType's VARIANT and VARIANT_REQUIRED types.
+ */
+ public static Field createVariantField(String name) {
+ return new Field(
+ name, new FieldType(true, VariantType.INSTANCE, null), createVariantChildFields());
+ }
+
+ /**
+ * Creates the child fields for the VariantVector. Metadata vector will be index 0 and value
+ * vector will be index 1.
+ */
+ public static List createVariantChildFields() {
+ return List.of(
+ new Field(METADATA_VECTOR_NAME, new FieldType(false, Binary.INSTANCE, null), null),
+ new Field(VALUE_VECTOR_NAME, new FieldType(false, Binary.INSTANCE, null), null));
+ }
+
+ @Override
+ public void initializeChildrenFromFields(List children) {
+ // No-op, as children are initialized in the constructor
+ }
+
+ @Override
+ public Field getField() {
+ return rootField;
+ }
+
+ public VarBinaryVector getMetadataVector() {
+ return getUnderlyingVector().getChild(METADATA_VECTOR_NAME, VarBinaryVector.class);
+ }
+
+ public VarBinaryVector getValueVector() {
+ return getUnderlyingVector().getChild(VALUE_VECTOR_NAME, VarBinaryVector.class);
+ }
+
+ @Override
+ public TransferPair makeTransferPair(ValueVector target) {
+ return new VariantTransferPair(this, (VariantVector) target);
+ }
+
+ @Override
+ public TransferPair getTransferPair(Field field, BufferAllocator allocator) {
+ return new VariantTransferPair(this, new VariantVector(field.getName(), allocator));
+ }
+
+ @Override
+ public TransferPair getTransferPair(Field field, BufferAllocator allocator, CallBack callBack) {
+ return getTransferPair(field, allocator);
+ }
+
+ @Override
+ public TransferPair getTransferPair(String ref, BufferAllocator allocator) {
+ return new VariantTransferPair(this, new VariantVector(ref, allocator));
+ }
+
+ @Override
+ public TransferPair getTransferPair(String ref, BufferAllocator allocator, CallBack callBack) {
+ return getTransferPair(ref, allocator);
+ }
+
+ @Override
+ public TransferPair getTransferPair(BufferAllocator allocator) {
+ return getTransferPair(this.getField().getName(), allocator);
+ }
+
+ @Override
+ public void copyFrom(int fromIndex, int thisIndex, ValueVector from) {
+ getUnderlyingVector()
+ .copyFrom(fromIndex, thisIndex, ((VariantVector) from).getUnderlyingVector());
+ }
+
+ @Override
+ public void copyFromSafe(int fromIndex, int thisIndex, ValueVector from) {
+ getUnderlyingVector()
+ .copyFromSafe(fromIndex, thisIndex, ((VariantVector) from).getUnderlyingVector());
+ }
+
+ @Override
+ public Object getObject(int index) {
+ if (isNull(index)) {
+ return null;
+ }
+ VarBinaryVector metadataVector = getMetadataVector();
+ VarBinaryVector valueVector = getValueVector();
+
+ int metadataStart = metadataVector.getStartOffset(index);
+ int metadataEnd = metadataVector.getEndOffset(index);
+ int valueStart = valueVector.getStartOffset(index);
+ int valueEnd = valueVector.getEndOffset(index);
+
+ return new Variant(
+ metadataVector.getDataBuffer(),
+ metadataStart,
+ metadataEnd,
+ valueVector.getDataBuffer(),
+ valueStart,
+ valueEnd);
+ }
+
+ /**
+ * Retrieves the variant value at the specified index into the provided holder.
+ *
+ * @param index the index of the value to retrieve
+ * @param holder the holder to populate with the variant data
+ */
+ public void get(int index, NullableVariantHolder holder) {
+ if (isNull(index)) {
+ holder.isSet = 0;
+ } else {
+ holder.isSet = 1;
+ VarBinaryVector metadataVector = getMetadataVector();
+ VarBinaryVector valueVector = getValueVector();
+ assert !metadataVector.isNull(index) && !valueVector.isNull(index);
+
+ holder.metadataStart = metadataVector.getStartOffset(index);
+ holder.metadataEnd = metadataVector.getEndOffset(index);
+ holder.metadataBuffer = metadataVector.getDataBuffer();
+ holder.valueStart = valueVector.getStartOffset(index);
+ holder.valueEnd = valueVector.getEndOffset(index);
+ holder.valueBuffer = valueVector.getDataBuffer();
+ }
+ }
+
+ /**
+ * Retrieves the variant value at the specified index into the provided non-nullable holder.
+ *
+ * @param index the index of the value to retrieve
+ * @param holder the holder to populate with the variant data
+ */
+ public void get(int index, VariantHolder holder) {
+ VarBinaryVector metadataVector = getMetadataVector();
+ VarBinaryVector valueVector = getValueVector();
+ assert !metadataVector.isNull(index) && !valueVector.isNull(index);
+
+ holder.metadataStart = metadataVector.getStartOffset(index);
+ holder.metadataEnd = metadataVector.getEndOffset(index);
+ holder.metadataBuffer = metadataVector.getDataBuffer();
+ holder.valueStart = valueVector.getStartOffset(index);
+ holder.valueEnd = valueVector.getEndOffset(index);
+ holder.valueBuffer = valueVector.getDataBuffer();
+ }
+
+ /**
+ * Sets the variant value at the specified index from the provided holder.
+ *
+ * @param index the index at which to set the value
+ * @param holder the holder containing the variant data to set
+ */
+ public void set(int index, VariantHolder holder) {
+ BitVectorHelper.setBit(getUnderlyingVector().getValidityBuffer(), index);
+ getMetadataVector()
+ .set(index, 1, holder.metadataStart, holder.metadataEnd, holder.metadataBuffer);
+ getValueVector().set(index, 1, holder.valueStart, holder.valueEnd, holder.valueBuffer);
+ }
+
+ /**
+ * Sets the variant value at the specified index from the provided nullable holder.
+ *
+ * @param index the index at which to set the value
+ * @param holder the nullable holder containing the variant data to set
+ */
+ public void set(int index, NullableVariantHolder holder) {
+ BitVectorHelper.setValidityBit(getUnderlyingVector().getValidityBuffer(), index, holder.isSet);
+ if (holder.isSet == 0) {
+ return;
+ }
+ getMetadataVector()
+ .set(index, 1, holder.metadataStart, holder.metadataEnd, holder.metadataBuffer);
+ getValueVector().set(index, 1, holder.valueStart, holder.valueEnd, holder.valueBuffer);
+ }
+
+ /**
+ * Sets the variant value at the specified index from the provided holder, with bounds checking.
+ *
+ * @param index the index at which to set the value
+ * @param holder the holder containing the variant data to set
+ */
+ public void setSafe(int index, VariantHolder holder) {
+ getUnderlyingVector().setIndexDefined(index);
+ getMetadataVector()
+ .setSafe(index, 1, holder.metadataStart, holder.metadataEnd, holder.metadataBuffer);
+ getValueVector().setSafe(index, 1, holder.valueStart, holder.valueEnd, holder.valueBuffer);
+ }
+
+ /**
+ * Sets the variant value at the specified index from the provided nullable holder, with bounds
+ * checking.
+ *
+ * @param index the index at which to set the value
+ * @param holder the nullable holder containing the variant data to set
+ */
+ public void setSafe(int index, NullableVariantHolder holder) {
+ if (holder.isSet == 0) {
+ getUnderlyingVector().setNull(index);
+ return;
+ }
+ getUnderlyingVector().setIndexDefined(index);
+ getMetadataVector()
+ .setSafe(index, 1, holder.metadataStart, holder.metadataEnd, holder.metadataBuffer);
+ getValueVector().setSafe(index, 1, holder.valueStart, holder.valueEnd, holder.valueBuffer);
+ }
+
+ /** Sets the value at the given index from the provided Variant. */
+ public void setSafe(int index, Variant variant) {
+ ByteBuffer metadataBuffer = variant.getMetadataBuffer();
+ ByteBuffer valueBuffer = variant.getValueBuffer();
+ int metadataLength = metadataBuffer.remaining();
+ int valueLength = valueBuffer.remaining();
+ try (ArrowBuf metaBuf = getAllocator().buffer(metadataLength);
+ ArrowBuf valBuf = getAllocator().buffer(valueLength)) {
+ metaBuf.setBytes(0, metadataBuffer.duplicate());
+ valBuf.setBytes(0, valueBuffer.duplicate());
+ getUnderlyingVector().setIndexDefined(index);
+ getMetadataVector().setSafe(index, 1, 0, metadataLength, metaBuf);
+ getValueVector().setSafe(index, 1, 0, valueLength, valBuf);
+ }
+ }
+
+ @Override
+ protected FieldReader getReaderImpl() {
+ return new org.apache.arrow.variant.impl.VariantReaderImpl(this);
+ }
+
+ @Override
+ public int hashCode(int index) {
+ return hashCode(index, null);
+ }
+
+ @Override
+ public int hashCode(int index, ArrowBufHasher hasher) {
+ return getUnderlyingVector().hashCode(index, hasher);
+ }
+
+ /**
+ * VariantTransferPair is a transfer pair for VariantVector. It transfers the metadata and value
+ * together using the underlyingVector's transfer pair.
+ */
+ protected static class VariantTransferPair implements TransferPair {
+ private final TransferPair pair;
+ private final VariantVector from;
+ private final VariantVector to;
+
+ public VariantTransferPair(VariantVector from, VariantVector to) {
+ this.from = from;
+ this.to = to;
+ this.pair = from.getUnderlyingVector().makeTransferPair((to).getUnderlyingVector());
+ }
+
+ @Override
+ public void transfer() {
+ pair.transfer();
+ }
+
+ @Override
+ public void splitAndTransfer(int startIndex, int length) {
+ pair.splitAndTransfer(startIndex, length);
+ }
+
+ @Override
+ public ValueVector getTo() {
+ return to;
+ }
+
+ @Override
+ public void copyValueSafe(int from, int to) {
+ pair.copyValueSafe(from, to);
+ }
+ }
+}
diff --git a/arrow-variant/src/main/java/org/apache/arrow/variant/holders/NullableVariantHolder.java b/arrow-variant/src/main/java/org/apache/arrow/variant/holders/NullableVariantHolder.java
new file mode 100644
index 0000000000..b78d4a2013
--- /dev/null
+++ b/arrow-variant/src/main/java/org/apache/arrow/variant/holders/NullableVariantHolder.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.variant.holders;
+
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.variant.extension.VariantType;
+import org.apache.arrow.vector.holders.ExtensionHolder;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+
+@SuppressWarnings("checkstyle:VisibilityModifier")
+public final class NullableVariantHolder extends ExtensionHolder {
+
+ public int isSet;
+ public int metadataStart;
+ public int metadataEnd;
+ public ArrowBuf metadataBuffer;
+ public int valueStart;
+ public int valueEnd;
+ public ArrowBuf valueBuffer;
+
+ public NullableVariantHolder() {}
+
+ @Override
+ public boolean equals(Object obj) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int hashCode() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ArrowType type() {
+ return VariantType.INSTANCE;
+ }
+}
diff --git a/arrow-variant/src/main/java/org/apache/arrow/variant/holders/VariantHolder.java b/arrow-variant/src/main/java/org/apache/arrow/variant/holders/VariantHolder.java
new file mode 100644
index 0000000000..e3947ac439
--- /dev/null
+++ b/arrow-variant/src/main/java/org/apache/arrow/variant/holders/VariantHolder.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.variant.holders;
+
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.variant.extension.VariantType;
+import org.apache.arrow.vector.holders.ExtensionHolder;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+
+@SuppressWarnings("checkstyle:VisibilityModifier")
+public final class VariantHolder extends ExtensionHolder {
+
+ public final int isSet = 1;
+ public int metadataStart;
+ public int metadataEnd;
+ public ArrowBuf metadataBuffer;
+ public int valueStart;
+ public int valueEnd;
+ public ArrowBuf valueBuffer;
+
+ public VariantHolder() {}
+
+ @Override
+ public boolean equals(Object obj) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int hashCode() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ArrowType type() {
+ return VariantType.INSTANCE;
+ }
+}
diff --git a/arrow-variant/src/main/java/org/apache/arrow/variant/impl/NullableVariantHolderReaderImpl.java b/arrow-variant/src/main/java/org/apache/arrow/variant/impl/NullableVariantHolderReaderImpl.java
new file mode 100644
index 0000000000..1645529c0c
--- /dev/null
+++ b/arrow-variant/src/main/java/org/apache/arrow/variant/impl/NullableVariantHolderReaderImpl.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.variant.impl;
+
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+import org.apache.arrow.vector.complex.impl.AbstractFieldReader;
+import org.apache.arrow.vector.types.Types;
+
+public class NullableVariantHolderReaderImpl extends AbstractFieldReader {
+ private final NullableVariantHolder holder;
+
+ public NullableVariantHolderReaderImpl(NullableVariantHolder holder) {
+ this.holder = holder;
+ }
+
+ @Override
+ public int size() {
+ throw new UnsupportedOperationException("You can't call size on a Holder value reader.");
+ }
+
+ @Override
+ public boolean next() {
+ throw new UnsupportedOperationException("You can't call next on a single value reader.");
+ }
+
+ @Override
+ public void setPosition(int index) {
+ throw new UnsupportedOperationException("You can't call setPosition on a single value reader.");
+ }
+
+ @Override
+ public Types.MinorType getMinorType() {
+ return Types.MinorType.EXTENSIONTYPE;
+ }
+
+ @Override
+ public boolean isSet() {
+ return holder.isSet == 1;
+ }
+
+ /**
+ * Reads the variant holder data into the provided holder.
+ *
+ * @param h the holder to read into
+ */
+ public void read(NullableVariantHolder h) {
+ h.metadataStart = this.holder.metadataStart;
+ h.metadataEnd = this.holder.metadataEnd;
+ h.metadataBuffer = this.holder.metadataBuffer;
+ h.valueStart = this.holder.valueStart;
+ h.valueEnd = this.holder.valueEnd;
+ h.valueBuffer = this.holder.valueBuffer;
+ h.isSet = this.isSet() ? 1 : 0;
+ }
+}
diff --git a/arrow-variant/src/main/java/org/apache/arrow/variant/impl/VariantReaderImpl.java b/arrow-variant/src/main/java/org/apache/arrow/variant/impl/VariantReaderImpl.java
new file mode 100644
index 0000000000..670104b7d1
--- /dev/null
+++ b/arrow-variant/src/main/java/org/apache/arrow/variant/impl/VariantReaderImpl.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.variant.impl;
+
+import org.apache.arrow.variant.extension.VariantVector;
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+import org.apache.arrow.variant.holders.VariantHolder;
+import org.apache.arrow.vector.complex.impl.AbstractFieldReader;
+import org.apache.arrow.vector.holders.ExtensionHolder;
+import org.apache.arrow.vector.types.Types;
+import org.apache.arrow.vector.types.pojo.Field;
+
+public class VariantReaderImpl extends AbstractFieldReader {
+ private final VariantVector vector;
+
+ public VariantReaderImpl(VariantVector vector) {
+ this.vector = vector;
+ }
+
+ @Override
+ public Types.MinorType getMinorType() {
+ return this.vector.getMinorType();
+ }
+
+ @Override
+ public Field getField() {
+ return this.vector.getField();
+ }
+
+ @Override
+ public boolean isSet() {
+ return !this.vector.isNull(this.idx());
+ }
+
+ @Override
+ public void read(ExtensionHolder holder) {
+ if (holder instanceof VariantHolder) {
+ vector.get(idx(), (VariantHolder) holder);
+ } else if (holder instanceof NullableVariantHolder) {
+ vector.get(idx(), (NullableVariantHolder) holder);
+ } else {
+ throw new IllegalArgumentException(
+ "Unsupported holder type for VariantReader: " + holder.getClass());
+ }
+ }
+
+ public void read(VariantHolder h) {
+ this.vector.get(this.idx(), h);
+ }
+
+ public void read(NullableVariantHolder h) {
+ this.vector.get(this.idx(), h);
+ }
+
+ @Override
+ public Object readObject() {
+ return this.vector.getObject(this.idx());
+ }
+}
diff --git a/arrow-variant/src/main/java/org/apache/arrow/variant/impl/VariantWriterImpl.java b/arrow-variant/src/main/java/org/apache/arrow/variant/impl/VariantWriterImpl.java
new file mode 100644
index 0000000000..266ddb75d2
--- /dev/null
+++ b/arrow-variant/src/main/java/org/apache/arrow/variant/impl/VariantWriterImpl.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.variant.impl;
+
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.variant.Variant;
+import org.apache.arrow.variant.extension.VariantVector;
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+import org.apache.arrow.variant.holders.VariantHolder;
+import org.apache.arrow.vector.complex.impl.AbstractExtensionTypeWriter;
+import org.apache.arrow.vector.holders.ExtensionHolder;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+
+/**
+ * Writer implementation for VARIANT extension type vectors.
+ *
+ *
This writer handles writing variant data to a {@link VariantVector}. It accepts both {@link
+ * VariantHolder} and {@link NullableVariantHolder} objects containing metadata and value buffers
+ * and writes them to the appropriate position in the vector.
+ */
+public class VariantWriterImpl extends AbstractExtensionTypeWriter {
+
+ private static final String UNSUPPORTED_TYPE_TEMPLATE = "Unsupported type for Variant: %s";
+
+ /**
+ * Constructs a new VariantWriterImpl for the given vector.
+ *
+ * @param vector the variant vector to write to
+ */
+ public VariantWriterImpl(VariantVector vector) {
+ super(vector);
+ }
+
+ /**
+ * Writes an extension type or variant value to the vector.
+ *
+ *
This method handles {@link ExtensionHolder} by delegating to {@link #write(ExtensionHolder)}
+ * and {@link Variant} by delegating to {@link #writeVariant(Variant)}.
+ *
+ * @param object the object to write, must be an {@link ExtensionHolder} or {@link Variant}
+ * @throws IllegalArgumentException if the object is not an {@link ExtensionHolder} or {@link
+ * Variant}
+ */
+ @Override
+ public void writeExtension(Object object) {
+ if (object instanceof ExtensionHolder) {
+ write((ExtensionHolder) object);
+ } else if (object instanceof Variant) {
+ writeVariant((Variant) object);
+ } else {
+ throw new IllegalArgumentException(
+ String.format(UNSUPPORTED_TYPE_TEMPLATE, object.getClass().getName()));
+ }
+ }
+
+ private void writeVariant(Variant variant) {
+ java.nio.ByteBuffer metadataBuffer = variant.getMetadataBuffer();
+ java.nio.ByteBuffer valueBuffer = variant.getValueBuffer();
+ int metadataLength = metadataBuffer.remaining();
+ int valueLength = valueBuffer.remaining();
+ try (ArrowBuf metadataBuf = vector.getAllocator().buffer(metadataLength);
+ ArrowBuf valueBuf = vector.getAllocator().buffer(valueLength)) {
+ metadataBuf.setBytes(0, metadataBuffer.duplicate());
+ valueBuf.setBytes(0, valueBuffer.duplicate());
+ NullableVariantHolder holder = new NullableVariantHolder();
+ holder.isSet = 1;
+ holder.metadataBuffer = metadataBuf;
+ holder.metadataStart = 0;
+ holder.metadataEnd = metadataLength;
+ holder.valueBuffer = valueBuf;
+ holder.valueStart = 0;
+ holder.valueEnd = valueLength;
+ vector.setSafe(getPosition(), holder);
+ vector.setValueCount(getPosition() + 1);
+ }
+ }
+
+ @Override
+ public void writeExtension(Object value, ArrowType type) {
+ writeExtension(value);
+ }
+
+ /**
+ * Writes a variant holder to the vector at the current position.
+ *
+ *
The holder can be either a {@link VariantHolder} (non-nullable, always set) or a {@link
+ * NullableVariantHolder} (nullable, may be null). The data is written using {@link
+ * VariantVector#setSafe(int, NullableVariantHolder)} which handles buffer allocation and copying.
+ *
+ * @param extensionHolder the variant holder to write, must be a {@link VariantHolder} or {@link
+ * NullableVariantHolder}
+ * @throws IllegalArgumentException if the holder is neither a {@link VariantHolder} nor a {@link
+ * NullableVariantHolder}
+ */
+ @Override
+ public void write(ExtensionHolder extensionHolder) {
+ if (extensionHolder instanceof VariantHolder) {
+ vector.setSafe(getPosition(), (VariantHolder) extensionHolder);
+ } else if (extensionHolder instanceof NullableVariantHolder) {
+ vector.setSafe(getPosition(), (NullableVariantHolder) extensionHolder);
+ } else {
+ throw new IllegalArgumentException(
+ String.format(UNSUPPORTED_TYPE_TEMPLATE, extensionHolder.getClass().getName()));
+ }
+ vector.setValueCount(getPosition() + 1);
+ }
+}
diff --git a/arrow-variant/src/test/java/org/apache/arrow/variant/TestVariant.java b/arrow-variant/src/test/java/org/apache/arrow/variant/TestVariant.java
new file mode 100644
index 0000000000..bc46a68616
--- /dev/null
+++ b/arrow-variant/src/test/java/org/apache/arrow/variant/TestVariant.java
@@ -0,0 +1,439 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.variant;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.util.UUID;
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.parquet.variant.VariantBuilder;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class TestVariant {
+
+ private BufferAllocator allocator;
+
+ @BeforeEach
+ void beforeEach() {
+ allocator = new RootAllocator();
+ }
+
+ @AfterEach
+ void afterEach() {
+ allocator.close();
+ }
+
+ static Variant buildVariant(VariantBuilder builder) {
+ org.apache.parquet.variant.Variant parquetVariant = builder.build();
+ ByteBuffer valueBuf = parquetVariant.getValueBuffer();
+ ByteBuffer metaBuf = parquetVariant.getMetadataBuffer();
+ byte[] valueBytes = new byte[valueBuf.remaining()];
+ byte[] metaBytes = new byte[metaBuf.remaining()];
+ valueBuf.get(valueBytes);
+ metaBuf.get(metaBytes);
+ return new Variant(metaBytes, valueBytes);
+ }
+
+ public static Variant variantString(String value) {
+ VariantBuilder builder = new VariantBuilder();
+ builder.appendString(value);
+ return buildVariant(builder);
+ }
+
+ @Test
+ void testConstructionWithArrowBuf() {
+ VariantBuilder builder = new VariantBuilder();
+ builder.appendInt(42);
+ Variant source = buildVariant(builder);
+ int metaLen = source.getMetadataBuffer().remaining();
+ int valueLen = source.getValueBuffer().remaining();
+
+ try (ArrowBuf metadataArrowBuf = allocator.buffer(metaLen + 2);
+ ArrowBuf valueArrowBuf = allocator.buffer(valueLen + 3)) {
+ metadataArrowBuf.setBytes(2, source.getMetadataBuffer());
+ valueArrowBuf.setBytes(3, source.getValueBuffer());
+
+ Variant variant =
+ new Variant(metadataArrowBuf, 2, 2 + metaLen, valueArrowBuf, 3, 3 + valueLen);
+
+ assertEquals(Variant.Type.INT, variant.getType());
+ assertEquals(42, variant.getInt());
+ }
+ }
+
+ @Test
+ void testNullType() {
+ VariantBuilder builder = new VariantBuilder();
+ builder.appendNull();
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.NULL, variant.getType());
+ }
+
+ @Test
+ void testBooleanType() {
+ VariantBuilder builder = new VariantBuilder();
+ builder.appendBoolean(true);
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.BOOLEAN, variant.getType());
+ assertTrue(variant.getBoolean());
+
+ builder = new VariantBuilder();
+ builder.appendBoolean(false);
+ variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.BOOLEAN, variant.getType());
+ assertFalse(variant.getBoolean());
+ }
+
+ @Test
+ void testByteType() {
+ VariantBuilder builder = new VariantBuilder();
+ builder.appendByte((byte) 42);
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.BYTE, variant.getType());
+ assertEquals((byte) 42, variant.getByte());
+ }
+
+ @Test
+ void testShortType() {
+ VariantBuilder builder = new VariantBuilder();
+ builder.appendShort((short) 1234);
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.SHORT, variant.getType());
+ assertEquals((short) 1234, variant.getShort());
+ }
+
+ @Test
+ void testIntType() {
+ VariantBuilder builder = new VariantBuilder();
+ builder.appendInt(123456);
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.INT, variant.getType());
+ assertEquals(123456, variant.getInt());
+ }
+
+ @Test
+ void testLongType() {
+ VariantBuilder builder = new VariantBuilder();
+ builder.appendLong(9876543210L);
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.LONG, variant.getType());
+ assertEquals(9876543210L, variant.getLong());
+ }
+
+ @Test
+ void testFloatType() {
+ VariantBuilder builder = new VariantBuilder();
+ builder.appendFloat(3.14f);
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.FLOAT, variant.getType());
+ assertEquals(3.14f, variant.getFloat(), 0.001f);
+ }
+
+ @Test
+ void testDoubleType() {
+ VariantBuilder builder = new VariantBuilder();
+ builder.appendDouble(3.14159265359);
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.DOUBLE, variant.getType());
+ assertEquals(3.14159265359, variant.getDouble(), 0.0000001);
+ }
+
+ @Test
+ void testStringType() {
+ VariantBuilder builder = new VariantBuilder();
+ builder.appendString("hello world");
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.STRING, variant.getType());
+ assertEquals("hello world", variant.getString());
+ }
+
+ @Test
+ void testDecimalType() {
+ VariantBuilder builder = new VariantBuilder();
+ builder.appendDecimal(new BigDecimal("123.456"));
+ Variant variant = buildVariant(builder);
+
+ assertTrue(
+ variant.getType() == Variant.Type.DECIMAL4
+ || variant.getType() == Variant.Type.DECIMAL8
+ || variant.getType() == Variant.Type.DECIMAL16);
+ assertEquals(new BigDecimal("123.456"), variant.getDecimal());
+ }
+
+ @Test
+ void testBinaryType() {
+ VariantBuilder builder = new VariantBuilder();
+ byte[] data = new byte[] {1, 2, 3, 4, 5};
+ builder.appendBinary(ByteBuffer.wrap(data));
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.BINARY, variant.getType());
+ ByteBuffer result = variant.getBinary();
+ byte[] resultBytes = new byte[result.remaining()];
+ result.get(resultBytes);
+ assertArrayEquals(data, resultBytes);
+ }
+
+ @Test
+ void testUuidType() {
+ VariantBuilder builder = new VariantBuilder();
+ UUID uuid = UUID.randomUUID();
+ builder.appendUUID(uuid);
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.UUID, variant.getType());
+ assertEquals(uuid, variant.getUUID());
+ }
+
+ @Test
+ void testDateType() {
+ VariantBuilder builder = new VariantBuilder();
+ int daysSinceEpoch = 19000;
+ builder.appendDate(daysSinceEpoch);
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.DATE, variant.getType());
+ }
+
+ @Test
+ void testTimestampTzType() {
+ VariantBuilder builder = new VariantBuilder();
+ long micros = System.currentTimeMillis() * 1000;
+ builder.appendTimestampTz(micros);
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.TIMESTAMP_TZ, variant.getType());
+ }
+
+ @Test
+ void testTimestampNtzType() {
+ VariantBuilder builder = new VariantBuilder();
+ long micros = System.currentTimeMillis() * 1000;
+ builder.appendTimestampNtz(micros);
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.TIMESTAMP_NTZ, variant.getType());
+ }
+
+ @Test
+ void testTimeType() {
+ VariantBuilder builder = new VariantBuilder();
+ long micros = 12345678L;
+ builder.appendTime(micros);
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.TIME, variant.getType());
+ }
+
+ @Test
+ void testObjectType() {
+ VariantBuilder builder = new VariantBuilder();
+ var objBuilder = builder.startObject();
+ objBuilder.appendKey("name");
+ objBuilder.appendString("test");
+ objBuilder.appendKey("value");
+ objBuilder.appendInt(42);
+ builder.endObject();
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.OBJECT, variant.getType());
+ assertEquals(2, variant.numObjectElements());
+
+ Variant nameField = variant.getFieldByKey("name");
+ assertNotNull(nameField);
+ assertEquals(Variant.Type.STRING, nameField.getType());
+ assertEquals("test", nameField.getString());
+
+ Variant valueField = variant.getFieldByKey("value");
+ assertNotNull(valueField);
+ assertEquals(Variant.Type.INT, valueField.getType());
+ assertEquals(42, valueField.getInt());
+
+ assertNull(variant.getFieldByKey("nonexistent"));
+
+ // Empty object
+ builder = new VariantBuilder();
+ builder.startObject();
+ builder.endObject();
+ Variant emptyObj = buildVariant(builder);
+ assertEquals(Variant.Type.OBJECT, emptyObj.getType());
+ assertEquals(0, emptyObj.numObjectElements());
+ }
+
+ @Test
+ void testObjectFieldAtIndex() {
+ VariantBuilder builder = new VariantBuilder();
+ var objBuilder = builder.startObject();
+ objBuilder.appendKey("alpha");
+ objBuilder.appendInt(1);
+ objBuilder.appendKey("beta");
+ objBuilder.appendInt(2);
+ builder.endObject();
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.OBJECT, variant.getType());
+ assertEquals(2, variant.numObjectElements());
+
+ Variant.ObjectField field0 = variant.getFieldAtIndex(0);
+ assertNotNull(field0);
+ assertNotNull(field0.key);
+ assertNotNull(field0.value);
+
+ Variant.ObjectField field1 = variant.getFieldAtIndex(1);
+ assertNotNull(field1);
+ assertNotNull(field1.key);
+ assertNotNull(field1.value);
+ }
+
+ @Test
+ void testArrayType() {
+ VariantBuilder builder = new VariantBuilder();
+ var arrayBuilder = builder.startArray();
+ arrayBuilder.appendInt(1);
+ arrayBuilder.appendInt(2);
+ arrayBuilder.appendInt(3);
+ builder.endArray();
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.ARRAY, variant.getType());
+ assertEquals(3, variant.numArrayElements());
+
+ Variant elem0 = variant.getElementAtIndex(0);
+ assertNotNull(elem0);
+ assertEquals(Variant.Type.INT, elem0.getType());
+ assertEquals(1, elem0.getInt());
+
+ Variant elem1 = variant.getElementAtIndex(1);
+ assertEquals(2, elem1.getInt());
+
+ Variant elem2 = variant.getElementAtIndex(2);
+ assertEquals(3, elem2.getInt());
+
+ assertNull(variant.getElementAtIndex(-1));
+ assertNull(variant.getElementAtIndex(3));
+
+ // Empty array
+ builder = new VariantBuilder();
+ builder.startArray();
+ builder.endArray();
+ Variant emptyArr = buildVariant(builder);
+ assertEquals(Variant.Type.ARRAY, emptyArr.getType());
+ assertEquals(0, emptyArr.numArrayElements());
+ }
+
+ @Test
+ void testNestedStructure() {
+ VariantBuilder builder = new VariantBuilder();
+ var objBuilder = builder.startObject();
+ objBuilder.appendKey("items");
+ var arrayBuilder = objBuilder.startArray();
+ arrayBuilder.appendString("a");
+ arrayBuilder.appendString("b");
+ objBuilder.endArray();
+ builder.endObject();
+ Variant variant = buildVariant(builder);
+
+ assertEquals(Variant.Type.OBJECT, variant.getType());
+ Variant items = variant.getFieldByKey("items");
+ assertNotNull(items);
+ assertEquals(Variant.Type.ARRAY, items.getType());
+ assertEquals(2, items.numArrayElements());
+ assertEquals("a", items.getElementAtIndex(0).getString());
+ assertEquals("b", items.getElementAtIndex(1).getString());
+ }
+
+ @Test
+ void testEquals() {
+ VariantBuilder builder1 = new VariantBuilder();
+ builder1.appendString("test");
+ Variant variant1 = buildVariant(builder1);
+
+ VariantBuilder builder2 = new VariantBuilder();
+ builder2.appendString("test");
+ Variant variant2 = buildVariant(builder2);
+
+ VariantBuilder builder3 = new VariantBuilder();
+ builder3.appendString("different");
+ Variant variant3 = buildVariant(builder3);
+
+ assertEquals(variant1, variant1);
+ assertEquals(variant1, variant2);
+ assertNotEquals(variant1, variant3);
+ assertNotEquals(variant1, null);
+ assertNotEquals(variant1, "not a variant");
+ }
+
+ @Test
+ void testHashCode() {
+ VariantBuilder builder1 = new VariantBuilder();
+ builder1.appendInt(42);
+ Variant variant1 = buildVariant(builder1);
+
+ VariantBuilder builder2 = new VariantBuilder();
+ builder2.appendInt(42);
+ Variant variant2 = buildVariant(builder2);
+
+ assertEquals(variant1.hashCode(), variant2.hashCode());
+ }
+
+ @Test
+ void testToString() {
+ VariantBuilder builder = new VariantBuilder();
+ builder.appendString("test");
+ Variant variant = buildVariant(builder);
+
+ String str = variant.toString();
+ assertNotNull(str);
+ assertTrue(str.contains("type="));
+ }
+
+ @Test
+ void testTypeEnumsMatch() {
+ for (Variant.Type arrowType : Variant.Type.values()) {
+ org.apache.parquet.variant.Variant.Type parquetType =
+ org.apache.parquet.variant.Variant.Type.valueOf(arrowType.name());
+ assertEquals(arrowType, Variant.Type.fromParquet(parquetType));
+ }
+ for (org.apache.parquet.variant.Variant.Type parquetType :
+ org.apache.parquet.variant.Variant.Type.values()) {
+ Variant.Type arrowType = Variant.Type.valueOf(parquetType.name());
+ assertEquals(parquetType.name(), arrowType.name());
+ }
+ }
+}
diff --git a/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantExtensionType.java b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantExtensionType.java
new file mode 100644
index 0000000000..f3213d523a
--- /dev/null
+++ b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantExtensionType.java
@@ -0,0 +1,249 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.variant.extension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.Collections;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.variant.TestVariant;
+import org.apache.arrow.variant.Variant;
+import org.apache.arrow.vector.ExtensionTypeVector;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.VarBinaryVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.compare.Range;
+import org.apache.arrow.vector.compare.RangeEqualsVisitor;
+import org.apache.arrow.vector.complex.StructVector;
+import org.apache.arrow.vector.complex.writer.BaseWriter;
+import org.apache.arrow.vector.ipc.ArrowFileReader;
+import org.apache.arrow.vector.ipc.ArrowFileWriter;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.ArrowType.ExtensionType;
+import org.apache.arrow.vector.types.pojo.ExtensionTypeRegistry;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.FieldType;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.apache.arrow.vector.util.VectorBatchAppender;
+import org.apache.arrow.vector.validate.ValidateVectorVisitor;
+import org.junit.jupiter.api.Test;
+
+public class TestVariantExtensionType {
+
+ private static void ensureRegistered(ArrowType.ExtensionType type) {
+ if (ExtensionTypeRegistry.lookup(type.extensionName()) == null) {
+ ExtensionTypeRegistry.register(type);
+ }
+ }
+
+ @Test
+ public void roundtripVariant() throws IOException {
+ ensureRegistered(VariantType.INSTANCE);
+ final Schema schema =
+ new Schema(Collections.singletonList(Field.nullable("a", VariantType.INSTANCE)));
+ try (final BufferAllocator allocator = new RootAllocator(Integer.MAX_VALUE);
+ final VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator)) {
+ VariantVector vector = (VariantVector) root.getVector("a");
+ vector.allocateNew();
+
+ vector.setSafe(0, TestVariant.variantString("hello"));
+ vector.setSafe(1, TestVariant.variantString("world"));
+ vector.setValueCount(2);
+ root.setRowCount(2);
+
+ final File file = File.createTempFile("varianttest", ".arrow");
+ try (final WritableByteChannel channel =
+ FileChannel.open(Paths.get(file.getAbsolutePath()), StandardOpenOption.WRITE);
+ final ArrowFileWriter writer = new ArrowFileWriter(root, null, channel)) {
+ writer.start();
+ writer.writeBatch();
+ writer.end();
+ }
+
+ try (final SeekableByteChannel channel =
+ Files.newByteChannel(Paths.get(file.getAbsolutePath()));
+ final ArrowFileReader reader = new ArrowFileReader(channel, allocator)) {
+ reader.loadNextBatch();
+ final VectorSchemaRoot readerRoot = reader.getVectorSchemaRoot();
+ assertEquals(root.getSchema(), readerRoot.getSchema());
+
+ final Field field = readerRoot.getSchema().getFields().get(0);
+ final VariantType expectedType = VariantType.INSTANCE;
+ assertEquals(
+ field.getMetadata().get(ExtensionType.EXTENSION_METADATA_KEY_NAME),
+ expectedType.extensionName());
+ assertEquals(
+ field.getMetadata().get(ExtensionType.EXTENSION_METADATA_KEY_METADATA),
+ expectedType.serialize());
+
+ final ExtensionTypeVector deserialized =
+ (ExtensionTypeVector) readerRoot.getFieldVectors().get(0);
+ assertEquals(vector.getValueCount(), deserialized.getValueCount());
+ for (int i = 0; i < vector.getValueCount(); i++) {
+ assertEquals(vector.isNull(i), deserialized.isNull(i));
+ if (!vector.isNull(i)) {
+ assertEquals(vector.getObject(i), deserialized.getObject(i));
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ public void readVariantAsUnderlyingType() throws IOException {
+ ensureRegistered(VariantType.INSTANCE);
+ final Schema schema =
+ new Schema(Collections.singletonList(VariantVector.createVariantField("a")));
+ try (final BufferAllocator allocator = new RootAllocator(Integer.MAX_VALUE);
+ final VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator)) {
+ VariantVector vector = (VariantVector) root.getVector("a");
+ vector.allocateNew();
+
+ vector.setSafe(0, TestVariant.variantString("hello"));
+ vector.setValueCount(1);
+ root.setRowCount(1);
+
+ final File file = File.createTempFile("varianttest", ".arrow");
+ try (final WritableByteChannel channel =
+ FileChannel.open(Paths.get(file.getAbsolutePath()), StandardOpenOption.WRITE);
+ final ArrowFileWriter writer = new ArrowFileWriter(root, null, channel)) {
+ writer.start();
+ writer.writeBatch();
+ writer.end();
+ }
+
+ ExtensionTypeRegistry.unregister(VariantType.INSTANCE);
+
+ try (final SeekableByteChannel channel =
+ Files.newByteChannel(Paths.get(file.getAbsolutePath()));
+ final ArrowFileReader reader = new ArrowFileReader(channel, allocator)) {
+ reader.loadNextBatch();
+ VectorSchemaRoot readRoot = reader.getVectorSchemaRoot();
+
+ // Verify schema properties
+ assertEquals(1, readRoot.getSchema().getFields().size());
+ assertEquals("a", readRoot.getSchema().getFields().get(0).getName());
+ assertTrue(readRoot.getSchema().getFields().get(0).getType() instanceof ArrowType.Struct);
+
+ // Verify extension metadata is preserved
+ final Field field = readRoot.getSchema().getFields().get(0);
+ assertEquals(
+ VariantType.EXTENSION_NAME,
+ field.getMetadata().get(ExtensionType.EXTENSION_METADATA_KEY_NAME));
+ assertEquals("", field.getMetadata().get(ExtensionType.EXTENSION_METADATA_KEY_METADATA));
+
+ // Verify vector type and row count
+ assertEquals(1, readRoot.getRowCount());
+ FieldVector readVector = readRoot.getVector("a");
+ assertEquals(StructVector.class, readVector.getClass());
+
+ // Verify value count matches
+ StructVector structVector = (StructVector) readVector;
+ assertEquals(vector.getValueCount(), structVector.getValueCount());
+
+ // Verify the underlying data can be accessed from child vectors
+ VarBinaryVector metadataVector =
+ structVector.getChild(VariantVector.METADATA_VECTOR_NAME, VarBinaryVector.class);
+ VarBinaryVector valueVector =
+ structVector.getChild(VariantVector.VALUE_VECTOR_NAME, VarBinaryVector.class);
+ assertNotNull(metadataVector);
+ assertNotNull(valueVector);
+ assertEquals(1, metadataVector.getValueCount());
+ assertEquals(1, valueVector.getValueCount());
+ }
+ }
+ }
+
+ @Test
+ public void testVariantVectorCompare() {
+ VariantType variantType = VariantType.INSTANCE;
+ ExtensionTypeRegistry.register(variantType);
+ Variant hello = TestVariant.variantString("hello");
+ Variant world = TestVariant.variantString("world");
+ try (final BufferAllocator allocator = new RootAllocator(Integer.MAX_VALUE);
+ VariantVector a1 =
+ (VariantVector)
+ variantType.getNewVector("a", FieldType.nullable(variantType), allocator);
+ VariantVector a2 =
+ (VariantVector)
+ variantType.getNewVector("a", FieldType.nullable(variantType), allocator);
+ VariantVector bb =
+ (VariantVector)
+ variantType.getNewVector("a", FieldType.nullable(variantType), allocator)) {
+
+ ValidateVectorVisitor validateVisitor = new ValidateVectorVisitor();
+ validateVisitor.visit(a1, null);
+
+ a1.allocateNew();
+ a2.allocateNew();
+ bb.allocateNew();
+
+ a1.setSafe(0, hello);
+ a1.setSafe(1, world);
+ a1.setValueCount(2);
+
+ a2.setSafe(0, hello);
+ a2.setSafe(1, world);
+ a2.setValueCount(2);
+
+ bb.setSafe(0, world);
+ bb.setSafe(1, hello);
+ bb.setValueCount(2);
+
+ Range range = new Range(0, 0, a1.getValueCount());
+ RangeEqualsVisitor visitor = new RangeEqualsVisitor(a1, a2);
+ assertTrue(visitor.rangeEquals(range));
+
+ visitor = new RangeEqualsVisitor(a1, bb);
+ assertFalse(visitor.rangeEquals(range));
+
+ VectorBatchAppender.batchAppend(a1, a2, bb);
+ assertEquals(6, a1.getValueCount());
+ validateVisitor.visit(a1, null);
+ }
+ }
+
+ @Test
+ public void testVariantCopyAsValueThrowsException() {
+ ensureRegistered(VariantType.INSTANCE);
+ try (BufferAllocator allocator = new RootAllocator(Integer.MAX_VALUE);
+ VariantVector vector = new VariantVector("variant", allocator)) {
+ vector.allocateNew();
+ vector.setSafe(0, TestVariant.variantString("hello"));
+ vector.setValueCount(1);
+
+ var reader = vector.getReader();
+ reader.setPosition(0);
+
+ assertThrows(
+ IllegalArgumentException.class, () -> reader.copyAsValue((BaseWriter.StructWriter) null));
+ }
+ }
+}
diff --git a/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantInListVector.java b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantInListVector.java
new file mode 100644
index 0000000000..8b6000bc46
--- /dev/null
+++ b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantInListVector.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.variant.extension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.variant.TestVariant;
+import org.apache.arrow.variant.Variant;
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+import org.apache.arrow.vector.complex.ListVector;
+import org.apache.arrow.vector.complex.impl.UnionListReader;
+import org.apache.arrow.vector.complex.impl.UnionListWriter;
+import org.apache.arrow.vector.complex.reader.FieldReader;
+import org.apache.arrow.vector.complex.writer.BaseWriter.ExtensionWriter;
+import org.apache.arrow.vector.types.pojo.FieldType;
+import org.apache.arrow.vector.util.TransferPair;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class TestVariantInListVector {
+
+ private BufferAllocator allocator;
+
+ @BeforeEach
+ public void init() {
+ allocator = new RootAllocator(Long.MAX_VALUE);
+ }
+
+ @AfterEach
+ public void terminate() throws Exception {
+ allocator.close();
+ }
+
+ @Test
+ public void testListVectorWithVariantExtensionType() {
+ final FieldType type = FieldType.nullable(VariantType.INSTANCE);
+ try (ListVector inVector = new ListVector("input", allocator, type, null)) {
+ Variant variant1 = TestVariant.variantString("hello");
+ Variant variant2 = TestVariant.variantString("bye");
+
+ UnionListWriter writer = inVector.getWriter();
+ writer.allocate();
+
+ writer.setPosition(0);
+ writer.startList();
+ ExtensionWriter extensionWriter = writer.extension(VariantType.INSTANCE);
+ extensionWriter.writeExtension(variant1);
+ extensionWriter.writeExtension(variant2);
+ writer.endList();
+ inVector.setValueCount(1);
+
+ ArrayList resultSet = (ArrayList) inVector.getObject(0);
+ assertEquals(2, resultSet.size());
+ assertEquals(variant1, resultSet.get(0));
+ assertEquals(variant2, resultSet.get(1));
+ }
+ }
+
+ @Test
+ public void testListVectorReaderForVariantExtensionType() {
+ try (ListVector inVector = ListVector.empty("input", allocator)) {
+ Variant variant1 = TestVariant.variantString("hello");
+ Variant variant2 = TestVariant.variantString("bye");
+
+ UnionListWriter writer = inVector.getWriter();
+ writer.allocate();
+
+ writer.setPosition(0);
+ writer.startList();
+ ExtensionWriter extensionWriter = writer.extension(VariantType.INSTANCE);
+ extensionWriter.writeExtension(variant1);
+ writer.endList();
+
+ writer.setPosition(1);
+ writer.startList();
+ extensionWriter.writeExtension(variant2);
+ extensionWriter.writeExtension(variant2);
+ writer.endList();
+
+ inVector.setValueCount(2);
+
+ UnionListReader reader = inVector.getReader();
+ reader.setPosition(0);
+ assertTrue(reader.next());
+ FieldReader variantReader = reader.reader();
+ NullableVariantHolder resultHolder = new NullableVariantHolder();
+ variantReader.read(resultHolder);
+ assertEquals(variant1, new Variant(resultHolder));
+
+ reader.setPosition(1);
+ assertTrue(reader.next());
+ variantReader = reader.reader();
+ variantReader.read(resultHolder);
+ assertEquals(variant2, new Variant(resultHolder));
+
+ assertTrue(reader.next());
+ variantReader = reader.reader();
+ variantReader.read(resultHolder);
+ assertEquals(variant2, new Variant(resultHolder));
+ }
+ }
+
+ @Test
+ public void testCopyFromForVariantExtensionType() {
+ try (ListVector inVector = ListVector.empty("input", allocator);
+ ListVector outVector = ListVector.empty("output", allocator)) {
+ Variant variant1 = TestVariant.variantString("hello");
+ Variant variant2 = TestVariant.variantString("bye");
+
+ UnionListWriter writer = inVector.getWriter();
+ writer.allocate();
+
+ writer.setPosition(0);
+ writer.startList();
+ ExtensionWriter extensionWriter = writer.extension(VariantType.INSTANCE);
+ extensionWriter.writeExtension(variant1);
+ writer.endList();
+
+ writer.setPosition(1);
+ writer.startList();
+ extensionWriter.writeExtension(variant2);
+ extensionWriter.writeExtension(variant2);
+ writer.endList();
+
+ inVector.setValueCount(2);
+
+ outVector.allocateNew();
+ outVector.copyFrom(0, 0, inVector);
+ outVector.copyFrom(1, 1, inVector);
+ outVector.setValueCount(2);
+
+ ArrayList resultSet0 = (ArrayList) outVector.getObject(0);
+ assertEquals(1, resultSet0.size());
+ assertEquals(variant1, resultSet0.get(0));
+
+ ArrayList resultSet1 = (ArrayList) outVector.getObject(1);
+ assertEquals(2, resultSet1.size());
+ assertEquals(variant2, resultSet1.get(0));
+ assertEquals(variant2, resultSet1.get(1));
+ }
+ }
+
+ @Test
+ public void testCopyValueSafeForVariantExtensionType() {
+ try (ListVector inVector = ListVector.empty("input", allocator)) {
+ Variant variant1 = TestVariant.variantString("hello");
+ Variant variant2 = TestVariant.variantString("bye");
+
+ UnionListWriter writer = inVector.getWriter();
+ writer.allocate();
+
+ writer.setPosition(0);
+ writer.startList();
+ ExtensionWriter extensionWriter = writer.extension(VariantType.INSTANCE);
+ extensionWriter.writeExtension(variant1);
+ writer.endList();
+
+ writer.setPosition(1);
+ writer.startList();
+ extensionWriter.writeExtension(variant2);
+ extensionWriter.writeExtension(variant2);
+ writer.endList();
+
+ inVector.setValueCount(2);
+
+ try (ListVector outVector = (ListVector) inVector.getTransferPair(allocator).getTo()) {
+ TransferPair tp = inVector.makeTransferPair(outVector);
+ tp.copyValueSafe(0, 0);
+ tp.copyValueSafe(1, 1);
+ outVector.setValueCount(2);
+
+ ArrayList resultSet0 = (ArrayList) outVector.getObject(0);
+ assertEquals(1, resultSet0.size());
+ assertEquals(variant1, resultSet0.get(0));
+
+ ArrayList resultSet1 = (ArrayList) outVector.getObject(1);
+ assertEquals(2, resultSet1.size());
+ assertEquals(variant2, resultSet1.get(0));
+ assertEquals(variant2, resultSet1.get(1));
+ }
+ }
+ }
+}
diff --git a/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantInMapVector.java b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantInMapVector.java
new file mode 100644
index 0000000000..dd925810de
--- /dev/null
+++ b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantInMapVector.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.variant.extension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.variant.TestVariant;
+import org.apache.arrow.variant.Variant;
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+import org.apache.arrow.vector.complex.MapVector;
+import org.apache.arrow.vector.complex.impl.UnionMapReader;
+import org.apache.arrow.vector.complex.impl.UnionMapWriter;
+import org.apache.arrow.vector.complex.reader.FieldReader;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class TestVariantInMapVector {
+
+ private BufferAllocator allocator;
+
+ @BeforeEach
+ public void init() {
+ allocator = new RootAllocator(Long.MAX_VALUE);
+ }
+
+ @AfterEach
+ public void terminate() {
+ allocator.close();
+ }
+
+ @Test
+ public void testMapVectorWithVariantExtensionType() {
+ Variant variant1 = TestVariant.variantString("hello");
+ Variant variant2 = TestVariant.variantString("world");
+ try (final MapVector inVector = MapVector.empty("map", allocator, false)) {
+ inVector.allocateNew();
+ UnionMapWriter writer = inVector.getWriter();
+ writer.setPosition(0);
+
+ writer.startMap();
+ writer.startEntry();
+ writer.key().bigInt().writeBigInt(0);
+ writer.value().extension(VariantType.INSTANCE).writeExtension(variant1, VariantType.INSTANCE);
+ writer.endEntry();
+ writer.startEntry();
+ writer.key().bigInt().writeBigInt(1);
+ writer.value().extension(VariantType.INSTANCE).writeExtension(variant2, VariantType.INSTANCE);
+ writer.endEntry();
+ writer.endMap();
+
+ writer.setValueCount(1);
+
+ UnionMapReader mapReader = inVector.getReader();
+ mapReader.setPosition(0);
+ mapReader.next();
+ FieldReader variantReader = mapReader.value();
+ NullableVariantHolder holder = new NullableVariantHolder();
+ variantReader.read(holder);
+ assertEquals(variant1, new Variant(holder));
+
+ mapReader.next();
+ variantReader = mapReader.value();
+ variantReader.read(holder);
+ assertEquals(variant2, new Variant(holder));
+ }
+ }
+
+ @Test
+ public void testCopyFromForVariantExtensionType() {
+ Variant variant1 = TestVariant.variantString("hello");
+ Variant variant2 = TestVariant.variantString("world");
+ try (final MapVector inVector = MapVector.empty("in", allocator, false);
+ final MapVector outVector = MapVector.empty("out", allocator, false)) {
+ inVector.allocateNew();
+ UnionMapWriter writer = inVector.getWriter();
+ writer.setPosition(0);
+
+ writer.startMap();
+ writer.startEntry();
+ writer.key().bigInt().writeBigInt(0);
+ writer.value().extension(VariantType.INSTANCE).writeExtension(variant1, VariantType.INSTANCE);
+ writer.endEntry();
+ writer.startEntry();
+ writer.key().bigInt().writeBigInt(1);
+ writer.value().extension(VariantType.INSTANCE).writeExtension(variant2, VariantType.INSTANCE);
+ writer.endEntry();
+ writer.endMap();
+
+ writer.setValueCount(1);
+ outVector.allocateNew();
+ outVector.copyFrom(0, 0, inVector);
+ outVector.setValueCount(1);
+
+ UnionMapReader mapReader = outVector.getReader();
+ mapReader.setPosition(0);
+ mapReader.next();
+ FieldReader variantReader = mapReader.value();
+ NullableVariantHolder holder = new NullableVariantHolder();
+ variantReader.read(holder);
+ assertEquals(variant1, new Variant(holder));
+
+ mapReader.next();
+ variantReader = mapReader.value();
+ variantReader.read(holder);
+ assertEquals(variant2, new Variant(holder));
+ }
+ }
+}
diff --git a/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantType.java b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantType.java
new file mode 100644
index 0000000000..017e71224b
--- /dev/null
+++ b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantType.java
@@ -0,0 +1,308 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.variant.extension;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.dictionary.DictionaryProvider;
+import org.apache.arrow.vector.ipc.ArrowStreamReader;
+import org.apache.arrow.vector.ipc.ArrowStreamWriter;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.ExtensionTypeRegistry;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.FieldType;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TestVariantType {
+ BufferAllocator allocator;
+
+ @BeforeEach
+ void beforeEach() {
+ allocator = new RootAllocator();
+ }
+
+ @AfterEach
+ void afterEach() {
+ allocator.close();
+ }
+
+ @Test
+ void testConstants() {
+ assertNotNull(VariantType.INSTANCE);
+ }
+
+ @Test
+ void testStorageType() {
+ VariantType type = VariantType.INSTANCE;
+ assertEquals(ArrowType.Struct.INSTANCE, type.storageType());
+ assertInstanceOf(ArrowType.Struct.class, type.storageType());
+ }
+
+ @Test
+ void testExtensionName() {
+ VariantType type = VariantType.INSTANCE;
+ assertEquals("parquet.variant", type.extensionName());
+ }
+
+ @Test
+ void testExtensionEquals() {
+ VariantType type1 = VariantType.INSTANCE;
+ VariantType type2 = VariantType.INSTANCE;
+
+ assertTrue(type1.extensionEquals(type2));
+ }
+
+ @Test
+ void testIsComplex() {
+ VariantType type = VariantType.INSTANCE;
+ assertFalse(type.isComplex());
+ }
+
+ @Test
+ void testSerialize() {
+ VariantType type = VariantType.INSTANCE;
+ String serialized = type.serialize();
+ assertEquals("", serialized);
+ }
+
+ @Test
+ void testDeserializeValid() {
+ VariantType type = VariantType.INSTANCE;
+ ArrowType storageType = ArrowType.Struct.INSTANCE;
+
+ ArrowType deserialized = assertDoesNotThrow(() -> type.deserialize(storageType, ""));
+ assertInstanceOf(VariantType.class, deserialized);
+ assertEquals(VariantType.INSTANCE, deserialized);
+ }
+
+ @Test
+ void testDeserializeInvalidStorageType() {
+ VariantType type = VariantType.INSTANCE;
+ ArrowType wrongStorageType = ArrowType.Utf8.INSTANCE;
+
+ assertThrows(UnsupportedOperationException.class, () -> type.deserialize(wrongStorageType, ""));
+ }
+
+ @Test
+ void testGetNewVector() {
+ VariantType type = VariantType.INSTANCE;
+ try (FieldVector vector =
+ type.getNewVector("variant_field", FieldType.nullable(type), allocator)) {
+ assertInstanceOf(VariantVector.class, vector);
+ assertEquals("variant_field", vector.getField().getName());
+ assertEquals(type, vector.getField().getType());
+ }
+ }
+
+ @Test
+ void testGetNewVectorWithNullableFieldType() {
+ VariantType type = VariantType.INSTANCE;
+ FieldType nullableFieldType = FieldType.nullable(type);
+
+ try (FieldVector vector = type.getNewVector("nullable_variant", nullableFieldType, allocator)) {
+ assertInstanceOf(VariantVector.class, vector);
+ assertEquals("nullable_variant", vector.getField().getName());
+ assertTrue(vector.getField().isNullable());
+ }
+ }
+
+ @Test
+ void testGetNewVectorWithNonNullableFieldType() {
+ VariantType type = VariantType.INSTANCE;
+ FieldType nonNullableFieldType = FieldType.notNullable(type);
+
+ try (FieldVector vector =
+ type.getNewVector("non_nullable_variant", nonNullableFieldType, allocator)) {
+ assertInstanceOf(VariantVector.class, vector);
+ assertEquals("non_nullable_variant", vector.getField().getName());
+ }
+ }
+
+ @Test
+ void testIpcRoundTrip() {
+ VariantType type = VariantType.INSTANCE;
+
+ Schema schema = new Schema(Collections.singletonList(Field.nullable("variant", type)));
+ byte[] serialized = schema.serializeAsMessage();
+ Schema deserialized = Schema.deserializeMessage(ByteBuffer.wrap(serialized));
+ assertEquals(schema, deserialized);
+ }
+
+ @Test
+ void testVectorIpcRoundTrip() throws IOException {
+ VariantType type = VariantType.INSTANCE;
+
+ try (FieldVector vector = type.getNewVector("field", FieldType.nullable(type), allocator);
+ ArrowBuf metadataBuf1 = allocator.buffer(10);
+ ArrowBuf valueBuf1 = allocator.buffer(10);
+ ArrowBuf metadataBuf2 = allocator.buffer(10);
+ ArrowBuf valueBuf2 = allocator.buffer(10)) {
+ VariantVector variantVector = (VariantVector) vector;
+
+ byte[] metadata1 = new byte[] {1, 2, 3};
+ byte[] value1 = new byte[] {4, 5, 6, 7};
+ metadataBuf1.setBytes(0, metadata1);
+ valueBuf1.setBytes(0, value1);
+
+ byte[] metadata2 = new byte[] {8, 9};
+ byte[] value2 = new byte[] {10, 11, 12};
+ metadataBuf2.setBytes(0, metadata2);
+ valueBuf2.setBytes(0, value2);
+
+ NullableVariantHolder holder1 = new NullableVariantHolder();
+ holder1.isSet = 1;
+ holder1.metadataStart = 0;
+ holder1.metadataEnd = metadata1.length;
+ holder1.metadataBuffer = metadataBuf1;
+ holder1.valueStart = 0;
+ holder1.valueEnd = value1.length;
+ holder1.valueBuffer = valueBuf1;
+
+ NullableVariantHolder holder2 = new NullableVariantHolder();
+ holder2.isSet = 1;
+ holder2.metadataStart = 0;
+ holder2.metadataEnd = metadata2.length;
+ holder2.metadataBuffer = metadataBuf2;
+ holder2.valueStart = 0;
+ holder2.valueEnd = value2.length;
+ holder2.valueBuffer = valueBuf2;
+
+ variantVector.setSafe(0, holder1);
+ variantVector.setNull(1);
+ variantVector.setSafe(2, holder2);
+ variantVector.setValueCount(3);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (VectorSchemaRoot root = new VectorSchemaRoot(Collections.singletonList(variantVector));
+ ArrowStreamWriter writer =
+ new ArrowStreamWriter(root, new DictionaryProvider.MapDictionaryProvider(), baos)) {
+ writer.start();
+ writer.writeBatch();
+ }
+
+ try (ArrowStreamReader reader =
+ new ArrowStreamReader(new ByteArrayInputStream(baos.toByteArray()), allocator)) {
+ assertTrue(reader.loadNextBatch());
+ VectorSchemaRoot root = reader.getVectorSchemaRoot();
+ assertEquals(3, root.getRowCount());
+ assertEquals(
+ new Schema(Collections.singletonList(variantVector.getField())), root.getSchema());
+
+ VariantVector actual = assertInstanceOf(VariantVector.class, root.getVector("field"));
+ assertFalse(actual.isNull(0));
+ assertTrue(actual.isNull(1));
+ assertFalse(actual.isNull(2));
+
+ NullableVariantHolder result1 = new NullableVariantHolder();
+ actual.get(0, result1);
+ assertEquals(1, result1.isSet);
+ assertEquals(metadata1.length, result1.metadataEnd - result1.metadataStart);
+ assertEquals(value1.length, result1.valueEnd - result1.valueStart);
+
+ assertNull(actual.getObject(1));
+
+ NullableVariantHolder result2 = new NullableVariantHolder();
+ actual.get(2, result2);
+ assertEquals(1, result2.isSet);
+ assertEquals(metadata2.length, result2.metadataEnd - result2.metadataStart);
+ assertEquals(value2.length, result2.valueEnd - result2.valueStart);
+ }
+ }
+ }
+
+ @Test
+ void testSingleton() {
+ VariantType type1 = VariantType.INSTANCE;
+ VariantType type2 = VariantType.INSTANCE;
+
+ // Same instance
+ assertSame(type1, type2);
+ assertTrue(type1.extensionEquals(type2));
+ }
+
+ @Test
+ void testExtensionTypeRegistry() {
+ // VariantType should be automatically registered via static initializer
+ ArrowType.ExtensionType registeredType =
+ ExtensionTypeRegistry.lookup(VariantType.EXTENSION_NAME);
+ assertNotNull(registeredType);
+ assertInstanceOf(VariantType.class, registeredType);
+ assertEquals(VariantType.INSTANCE, registeredType);
+ }
+
+ @Test
+ void testFieldMetadata() {
+ Map metadata = new HashMap<>();
+ metadata.put("key1", "value1");
+ metadata.put("key2", "value2");
+
+ FieldType fieldType = new FieldType(true, VariantType.INSTANCE, null, metadata);
+ try (VariantVector vector = new VariantVector("test", allocator)) {
+ Field field = new Field("test", fieldType, VariantVector.createVariantChildFields());
+
+ // Field metadata includes both custom metadata and extension type metadata
+ Map fieldMetadata = field.getMetadata();
+ assertEquals("value1", fieldMetadata.get("key1"));
+ assertEquals("value2", fieldMetadata.get("key2"));
+ // Extension type metadata is also present
+ assertTrue(fieldMetadata.containsKey("ARROW:extension:name"));
+ assertTrue(fieldMetadata.containsKey("ARROW:extension:metadata"));
+ }
+ }
+
+ @Test
+ void testFieldChildren() {
+ try (VariantVector vector = new VariantVector("test", allocator)) {
+ Field field = vector.getField();
+
+ assertNotNull(field.getChildren());
+ assertEquals(2, field.getChildren().size());
+
+ Field metadataField = field.getChildren().get(0);
+ assertEquals(VariantVector.METADATA_VECTOR_NAME, metadataField.getName());
+ assertEquals(ArrowType.Binary.INSTANCE, metadataField.getType());
+
+ Field valueField = field.getChildren().get(1);
+ assertEquals(VariantVector.VALUE_VECTOR_NAME, valueField.getName());
+ assertEquals(ArrowType.Binary.INSTANCE, valueField.getType());
+ }
+ }
+}
diff --git a/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantVector.java b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantVector.java
new file mode 100644
index 0000000000..1c172e304f
--- /dev/null
+++ b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantVector.java
@@ -0,0 +1,844 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.variant.extension;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.variant.Variant;
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+import org.apache.arrow.variant.holders.VariantHolder;
+import org.apache.arrow.variant.impl.VariantReaderImpl;
+import org.apache.arrow.variant.impl.VariantWriterImpl;
+import org.apache.arrow.vector.holders.ExtensionHolder;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Tests for VariantVector, VariantWriterImpl, and VariantReaderImpl. */
+class TestVariantVector {
+
+ private BufferAllocator allocator;
+
+ @BeforeEach
+ void beforeEach() {
+ allocator = new RootAllocator();
+ }
+
+ @AfterEach
+ void afterEach() {
+ allocator.close();
+ }
+
+ private VariantHolder createHolder(
+ ArrowBuf metadataBuf, byte[] metadata, ArrowBuf valueBuf, byte[] value) {
+ VariantHolder holder = new VariantHolder();
+ holder.metadataStart = 0;
+ holder.metadataEnd = metadata.length;
+ holder.metadataBuffer = metadataBuf;
+ holder.valueStart = 0;
+ holder.valueEnd = value.length;
+ holder.valueBuffer = valueBuf;
+ return holder;
+ }
+
+ private NullableVariantHolder createNullableHolder(
+ ArrowBuf metadataBuf, byte[] metadata, ArrowBuf valueBuf, byte[] value) {
+ NullableVariantHolder holder = new NullableVariantHolder();
+ holder.isSet = 1;
+ holder.metadataStart = 0;
+ holder.metadataEnd = metadata.length;
+ holder.metadataBuffer = metadataBuf;
+ holder.valueStart = 0;
+ holder.valueEnd = value.length;
+ holder.valueBuffer = valueBuf;
+ return holder;
+ }
+
+ private NullableVariantHolder createNullHolder() {
+ NullableVariantHolder holder = new NullableVariantHolder();
+ holder.isSet = 0;
+ return holder;
+ }
+
+ // ========== Basic Vector Tests ==========
+
+ @Test
+ void testVectorCreation() {
+ try (VariantVector vector = new VariantVector("test", allocator)) {
+ assertNotNull(vector);
+ assertEquals("test", vector.getField().getName());
+ assertNotNull(vector.getMetadataVector());
+ assertNotNull(vector.getValueVector());
+ }
+ }
+
+ @Test
+ void testSetAndGet() {
+ try (VariantVector vector = new VariantVector("test", allocator);
+ ArrowBuf metadataBuf = allocator.buffer(10);
+ ArrowBuf valueBuf = allocator.buffer(10)) {
+
+ byte[] metadata = new byte[] {1, 2, 3};
+ byte[] value = new byte[] {4, 5, 6, 7};
+ metadataBuf.setBytes(0, metadata);
+ valueBuf.setBytes(0, value);
+
+ NullableVariantHolder holder = createNullableHolder(metadataBuf, metadata, valueBuf, value);
+
+ vector.setSafe(0, holder);
+ vector.setValueCount(1);
+
+ // Retrieve and verify
+ NullableVariantHolder result = new NullableVariantHolder();
+ vector.get(0, result);
+
+ assertEquals(1, result.isSet);
+ assertEquals(metadata.length, result.metadataEnd - result.metadataStart);
+ assertEquals(value.length, result.valueEnd - result.valueStart);
+
+ byte[] actualMetadata = new byte[metadata.length];
+ byte[] actualValue = new byte[value.length];
+ result.metadataBuffer.getBytes(result.metadataStart, actualMetadata);
+ result.valueBuffer.getBytes(result.valueStart, actualValue);
+
+ assertArrayEquals(metadata, actualMetadata);
+ assertArrayEquals(value, actualValue);
+ }
+ }
+
+ @Test
+ void testSetNull() {
+ try (VariantVector vector = new VariantVector("test", allocator)) {
+ NullableVariantHolder holder = createNullHolder();
+
+ vector.setSafe(0, holder);
+ vector.setValueCount(1);
+
+ assertTrue(vector.isNull(0));
+
+ NullableVariantHolder result = new NullableVariantHolder();
+ vector.get(0, result);
+ assertEquals(0, result.isSet);
+ }
+ }
+
+ @Test
+ void testMultipleValues() {
+ try (VariantVector vector = new VariantVector("test", allocator);
+ ArrowBuf metadataBuf1 = allocator.buffer(10);
+ ArrowBuf valueBuf1 = allocator.buffer(10);
+ ArrowBuf metadataBuf2 = allocator.buffer(10);
+ ArrowBuf valueBuf2 = allocator.buffer(10)) {
+
+ byte[] metadata1 = new byte[] {1, 2};
+ byte[] value1 = new byte[] {3, 4, 5};
+ metadataBuf1.setBytes(0, metadata1);
+ valueBuf1.setBytes(0, value1);
+
+ NullableVariantHolder holder1 =
+ createNullableHolder(metadataBuf1, metadata1, valueBuf1, value1);
+
+ byte[] metadata2 = new byte[] {6, 7, 8};
+ byte[] value2 = new byte[] {9, 10};
+ metadataBuf2.setBytes(0, metadata2);
+ valueBuf2.setBytes(0, value2);
+
+ NullableVariantHolder holder2 =
+ createNullableHolder(metadataBuf2, metadata2, valueBuf2, value2);
+
+ vector.setSafe(0, holder1);
+ vector.setSafe(1, holder2);
+ vector.setValueCount(2);
+
+ // Verify first value
+ NullableVariantHolder result1 = new NullableVariantHolder();
+ vector.get(0, result1);
+ assertEquals(1, result1.isSet);
+
+ byte[] actualMetadata1 = new byte[metadata1.length];
+ byte[] actualValue1 = new byte[value1.length];
+ result1.metadataBuffer.getBytes(result1.metadataStart, actualMetadata1);
+ result1.valueBuffer.getBytes(result1.valueStart, actualValue1);
+ assertArrayEquals(metadata1, actualMetadata1);
+ assertArrayEquals(value1, actualValue1);
+
+ // Verify second value
+ NullableVariantHolder result2 = new NullableVariantHolder();
+ vector.get(1, result2);
+ assertEquals(1, result2.isSet);
+
+ byte[] actualMetadata2 = new byte[metadata2.length];
+ byte[] actualValue2 = new byte[value2.length];
+ result2.metadataBuffer.getBytes(result2.metadataStart, actualMetadata2);
+ result2.valueBuffer.getBytes(result2.valueStart, actualValue2);
+ assertArrayEquals(metadata2, actualMetadata2);
+ assertArrayEquals(value2, actualValue2);
+ }
+ }
+
+ @Test
+ void testNonNullableHolder() {
+ try (VariantVector vector = new VariantVector("test", allocator);
+ ArrowBuf metadataBuf = allocator.buffer(10);
+ ArrowBuf valueBuf = allocator.buffer(10)) {
+
+ byte[] metadata = new byte[] {1, 2, 3};
+ byte[] value = new byte[] {4, 5, 6};
+ metadataBuf.setBytes(0, metadata);
+ valueBuf.setBytes(0, value);
+
+ VariantHolder holder = createHolder(metadataBuf, metadata, valueBuf, value);
+
+ vector.setSafe(0, holder);
+ vector.setValueCount(1);
+
+ assertFalse(vector.isNull(0));
+
+ NullableVariantHolder result = new NullableVariantHolder();
+ vector.get(0, result);
+ assertEquals(1, result.isSet);
+ }
+ }
+
+ // ========== Writer Tests ==========
+
+ @Test
+ void testWriteWithVariantHolder() {
+ try (VariantVector vector = new VariantVector("test", allocator);
+ VariantWriterImpl writer = new VariantWriterImpl(vector);
+ ArrowBuf metadataBuf = allocator.buffer(10);
+ ArrowBuf valueBuf = allocator.buffer(10)) {
+
+ byte[] metadata = new byte[] {1, 2};
+ byte[] value = new byte[] {3, 4, 5};
+ metadataBuf.setBytes(0, metadata);
+ valueBuf.setBytes(0, value);
+
+ VariantHolder holder = createHolder(metadataBuf, metadata, valueBuf, value);
+
+ writer.setPosition(0);
+ writer.write(holder);
+
+ assertEquals(1, vector.getValueCount());
+ assertFalse(vector.isNull(0));
+ }
+ }
+
+ @Test
+ void testWriteWithNullableVariantHolder() {
+ try (VariantVector vector = new VariantVector("test", allocator);
+ VariantWriterImpl writer = new VariantWriterImpl(vector);
+ ArrowBuf metadataBuf = allocator.buffer(10);
+ ArrowBuf valueBuf = allocator.buffer(10)) {
+
+ byte[] metadata = new byte[] {1, 2};
+ byte[] value = new byte[] {3, 4, 5};
+ metadataBuf.setBytes(0, metadata);
+ valueBuf.setBytes(0, value);
+
+ NullableVariantHolder holder = createNullableHolder(metadataBuf, metadata, valueBuf, value);
+
+ writer.setPosition(0);
+ writer.write(holder);
+
+ assertEquals(1, vector.getValueCount());
+ assertFalse(vector.isNull(0));
+ }
+ }
+
+ @Test
+ void testWriteWithNullableVariantHolderNull() {
+ try (VariantVector vector = new VariantVector("test", allocator);
+ VariantWriterImpl writer = new VariantWriterImpl(vector)) {
+
+ NullableVariantHolder holder = createNullHolder();
+
+ writer.setPosition(0);
+ writer.write(holder);
+
+ assertEquals(1, vector.getValueCount());
+ assertTrue(vector.isNull(0));
+ }
+ }
+
+ @Test
+ void testWriteExtensionWithUnsupportedType() {
+ try (VariantVector vector = new VariantVector("test", allocator);
+ VariantWriterImpl writer = new VariantWriterImpl(vector)) {
+
+ writer.setPosition(0);
+
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () -> writer.writeExtension("invalid-type"));
+
+ assertTrue(exception.getMessage().contains("Unsupported type for Variant"));
+ }
+ }
+
+ @Test
+ void testWriteWithUnsupportedHolder() {
+ try (VariantVector vector = new VariantVector("test", allocator);
+ VariantWriterImpl writer = new VariantWriterImpl(vector)) {
+
+ ExtensionHolder unsupportedHolder =
+ new ExtensionHolder() {
+ @Override
+ public ArrowType type() {
+ return VariantType.INSTANCE;
+ }
+ };
+
+ writer.setPosition(0);
+
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () -> writer.write(unsupportedHolder));
+
+ assertTrue(exception.getMessage().contains("Unsupported type for Variant"));
+ }
+ }
+
+ // ========== Reader Tests ==========
+
+ @Test
+ void testReaderReadWithNullableVariantHolder() {
+ try (VariantVector vector = new VariantVector("test", allocator);
+ ArrowBuf metadataBuf = allocator.buffer(10);
+ ArrowBuf valueBuf = allocator.buffer(10)) {
+
+ byte[] metadata = new byte[] {1, 2, 3};
+ byte[] value = new byte[] {4, 5, 6};
+ metadataBuf.setBytes(0, metadata);
+ valueBuf.setBytes(0, value);
+
+ NullableVariantHolder holder = createNullableHolder(metadataBuf, metadata, valueBuf, value);
+
+ vector.setSafe(0, holder);
+ vector.setValueCount(1);
+
+ VariantReaderImpl reader = (VariantReaderImpl) vector.getReader();
+ reader.setPosition(0);
+
+ NullableVariantHolder result = new NullableVariantHolder();
+ reader.read(result);
+
+ assertEquals(1, result.isSet);
+ assertEquals(metadata.length, result.metadataEnd - result.metadataStart);
+ assertEquals(value.length, result.valueEnd - result.valueStart);
+ }
+ }
+
+ @Test
+ void testReaderReadWithNullableVariantHolderNull() {
+ try (VariantVector vector = new VariantVector("test", allocator)) {
+ vector.setNull(0);
+ vector.setValueCount(1);
+
+ VariantReaderImpl reader = (VariantReaderImpl) vector.getReader();
+ reader.setPosition(0);
+
+ NullableVariantHolder holder = new NullableVariantHolder();
+ reader.read(holder);
+
+ assertEquals(0, holder.isSet);
+ }
+ }
+
+ @Test
+ void testReaderIsSet() {
+ try (VariantVector vector = new VariantVector("test", allocator);
+ ArrowBuf metadataBuf = allocator.buffer(10);
+ ArrowBuf valueBuf = allocator.buffer(10)) {
+
+ byte[] metadata = new byte[] {1};
+ byte[] value = new byte[] {2};
+ metadataBuf.setBytes(0, metadata);
+ valueBuf.setBytes(0, value);
+
+ NullableVariantHolder holder = createNullableHolder(metadataBuf, metadata, valueBuf, value);
+
+ vector.setSafe(0, holder);
+ vector.setNull(1);
+ vector.setValueCount(2);
+
+ VariantReaderImpl reader = (VariantReaderImpl) vector.getReader();
+
+ reader.setPosition(0);
+ assertTrue(reader.isSet());
+
+ reader.setPosition(1);
+ assertFalse(reader.isSet());
+ }
+ }
+
+ @Test
+ void testReaderGetMinorType() {
+ try (VariantVector vector = new VariantVector("test", allocator)) {
+ VariantReaderImpl reader = (VariantReaderImpl) vector.getReader();
+ assertEquals(vector.getMinorType(), reader.getMinorType());
+ }
+ }
+
+ @Test
+ void testReaderGetField() {
+ try (VariantVector vector = new VariantVector("test", allocator)) {
+ VariantReaderImpl reader = (VariantReaderImpl) vector.getReader();
+ assertEquals(vector.getField(), reader.getField());
+ assertEquals("test", reader.getField().getName());
+ }
+ }
+
+ @Test
+ void testReaderReadWithNonNullableVariantHolder() {
+ try (VariantVector vector = new VariantVector("test", allocator);
+ ArrowBuf metadataBuf = allocator.buffer(10);
+ ArrowBuf valueBuf = allocator.buffer(10)) {
+
+ byte[] metadata = new byte[] {1, 2, 3};
+ byte[] value = new byte[] {4, 5, 6};
+ metadataBuf.setBytes(0, metadata);
+ valueBuf.setBytes(0, value);
+
+ NullableVariantHolder holder = createNullableHolder(metadataBuf, metadata, valueBuf, value);
+
+ vector.setSafe(0, holder);
+ vector.setValueCount(1);
+
+ VariantReaderImpl reader = (VariantReaderImpl) vector.getReader();
+ reader.setPosition(0);
+
+ VariantHolder result = new VariantHolder();
+ reader.read(result);
+
+ // Verify the data was read correctly
+ byte[] actualMetadata = new byte[metadata.length];
+ byte[] actualValue = new byte[value.length];
+ result.metadataBuffer.getBytes(result.metadataStart, actualMetadata);
+ result.valueBuffer.getBytes(result.valueStart, actualValue);
+
+ assertArrayEquals(metadata, actualMetadata);
+ assertArrayEquals(value, actualValue);
+ assertEquals(1, result.isSet);
+ }
+ }
+
+ // ========== Transfer Pair Tests ==========
+
+ @Test
+ void testTransferPair() {
+ try (VariantVector fromVector = new VariantVector("from", allocator);
+ ArrowBuf metadataBuf = allocator.buffer(10);
+ ArrowBuf valueBuf = allocator.buffer(10)) {
+
+ byte[] metadata = new byte[] {1, 2, 3};
+ byte[] value = new byte[] {4, 5, 6, 7};
+ metadataBuf.setBytes(0, metadata);
+ valueBuf.setBytes(0, value);
+
+ NullableVariantHolder holder = createNullableHolder(metadataBuf, metadata, valueBuf, value);
+
+ fromVector.setSafe(0, holder);
+ fromVector.setValueCount(1);
+
+ org.apache.arrow.vector.util.TransferPair transferPair =
+ fromVector.getTransferPair(allocator);
+ VariantVector toVector = (VariantVector) transferPair.getTo();
+
+ transferPair.transfer();
+
+ assertEquals(0, fromVector.getValueCount());
+ assertEquals(1, toVector.getValueCount());
+
+ NullableVariantHolder result = new NullableVariantHolder();
+ toVector.get(0, result);
+ assertEquals(1, result.isSet);
+
+ byte[] actualMetadata = new byte[metadata.length];
+ byte[] actualValue = new byte[value.length];
+ result.metadataBuffer.getBytes(result.metadataStart, actualMetadata);
+ result.valueBuffer.getBytes(result.valueStart, actualValue);
+
+ assertArrayEquals(metadata, actualMetadata);
+ assertArrayEquals(value, actualValue);
+
+ toVector.close();
+ }
+ }
+
+ @Test
+ void testSplitAndTransfer() {
+ try (VariantVector fromVector = new VariantVector("from", allocator);
+ ArrowBuf metadataBuf1 = allocator.buffer(10);
+ ArrowBuf valueBuf1 = allocator.buffer(10);
+ ArrowBuf metadataBuf2 = allocator.buffer(10);
+ ArrowBuf valueBuf2 = allocator.buffer(10);
+ ArrowBuf metadataBuf3 = allocator.buffer(10);
+ ArrowBuf valueBuf3 = allocator.buffer(10)) {
+
+ byte[] metadata1 = new byte[] {1};
+ byte[] value1 = new byte[] {2, 3};
+ metadataBuf1.setBytes(0, metadata1);
+ valueBuf1.setBytes(0, value1);
+
+ byte[] metadata2 = new byte[] {4, 5};
+ byte[] value2 = new byte[] {6};
+ metadataBuf2.setBytes(0, metadata2);
+ valueBuf2.setBytes(0, value2);
+
+ byte[] metadata3 = new byte[] {7, 8, 9};
+ byte[] value3 = new byte[] {10, 11, 12};
+ metadataBuf3.setBytes(0, metadata3);
+ valueBuf3.setBytes(0, value3);
+
+ NullableVariantHolder holder1 =
+ createNullableHolder(metadataBuf1, metadata1, valueBuf1, value1);
+ NullableVariantHolder holder2 =
+ createNullableHolder(metadataBuf2, metadata2, valueBuf2, value2);
+ NullableVariantHolder holder3 =
+ createNullableHolder(metadataBuf3, metadata3, valueBuf3, value3);
+
+ fromVector.setSafe(0, holder1);
+ fromVector.setSafe(1, holder2);
+ fromVector.setSafe(2, holder3);
+ fromVector.setValueCount(3);
+
+ org.apache.arrow.vector.util.TransferPair transferPair =
+ fromVector.getTransferPair(allocator);
+ VariantVector toVector = (VariantVector) transferPair.getTo();
+
+ // Split and transfer indices 1-2 (middle and last)
+ transferPair.splitAndTransfer(1, 2);
+
+ assertEquals(2, toVector.getValueCount());
+
+ // Verify transferred values
+ NullableVariantHolder result1 = new NullableVariantHolder();
+ toVector.get(0, result1);
+ assertEquals(1, result1.isSet);
+
+ byte[] actualMetadata1 = new byte[metadata2.length];
+ byte[] actualValue1 = new byte[value2.length];
+ result1.metadataBuffer.getBytes(result1.metadataStart, actualMetadata1);
+ result1.valueBuffer.getBytes(result1.valueStart, actualValue1);
+ assertArrayEquals(metadata2, actualMetadata1);
+ assertArrayEquals(value2, actualValue1);
+
+ NullableVariantHolder result2 = new NullableVariantHolder();
+ toVector.get(1, result2);
+ assertEquals(1, result2.isSet);
+
+ byte[] actualMetadata2 = new byte[metadata3.length];
+ byte[] actualValue2 = new byte[value3.length];
+ result2.metadataBuffer.getBytes(result2.metadataStart, actualMetadata2);
+ result2.valueBuffer.getBytes(result2.valueStart, actualValue2);
+ assertArrayEquals(metadata3, actualMetadata2);
+ assertArrayEquals(value3, actualValue2);
+
+ toVector.close();
+ }
+ }
+
+ @Test
+ void testCopyValueSafe() {
+ try (VariantVector fromVector = new VariantVector("from", allocator);
+ VariantVector toVector = new VariantVector("to", allocator);
+ ArrowBuf metadataBuf = allocator.buffer(10);
+ ArrowBuf valueBuf = allocator.buffer(10)) {
+
+ byte[] metadata = new byte[] {1, 2};
+ byte[] value = new byte[] {3, 4, 5};
+ metadataBuf.setBytes(0, metadata);
+ valueBuf.setBytes(0, value);
+
+ NullableVariantHolder holder = createNullableHolder(metadataBuf, metadata, valueBuf, value);
+
+ fromVector.setSafe(0, holder);
+ fromVector.setValueCount(1);
+
+ org.apache.arrow.vector.util.TransferPair transferPair =
+ fromVector.makeTransferPair(toVector);
+
+ transferPair.copyValueSafe(0, 0);
+ toVector.setValueCount(1);
+
+ // Verify the value was copied
+ NullableVariantHolder result = new NullableVariantHolder();
+ toVector.get(0, result);
+ assertEquals(1, result.isSet);
+
+ byte[] actualMetadata = new byte[metadata.length];
+ byte[] actualValue = new byte[value.length];
+ result.metadataBuffer.getBytes(result.metadataStart, actualMetadata);
+ result.valueBuffer.getBytes(result.valueStart, actualValue);
+
+ assertArrayEquals(metadata, actualMetadata);
+ assertArrayEquals(value, actualValue);
+
+ // Original vector should still have the value
+ NullableVariantHolder originalResult = new NullableVariantHolder();
+ fromVector.get(0, originalResult);
+ assertEquals(1, originalResult.isSet);
+ }
+ }
+
+ @Test
+ void testGetTransferPairWithField() {
+ try (VariantVector fromVector = new VariantVector("from", allocator);
+ ArrowBuf metadataBuf = allocator.buffer(10);
+ ArrowBuf valueBuf = allocator.buffer(10)) {
+
+ byte[] metadata = new byte[] {1};
+ byte[] value = new byte[] {2};
+ metadataBuf.setBytes(0, metadata);
+ valueBuf.setBytes(0, value);
+
+ NullableVariantHolder holder = createNullableHolder(metadataBuf, metadata, valueBuf, value);
+
+ fromVector.setSafe(0, holder);
+ fromVector.setValueCount(1);
+
+ org.apache.arrow.vector.util.TransferPair transferPair =
+ fromVector.getTransferPair(fromVector.getField(), allocator);
+ VariantVector toVector = (VariantVector) transferPair.getTo();
+
+ transferPair.transfer();
+
+ assertEquals(1, toVector.getValueCount());
+ assertEquals(fromVector.getField().getName(), toVector.getField().getName());
+
+ toVector.close();
+ }
+ }
+
+ // ========== Copy Operations Tests ==========
+
+ @Test
+ void testCopyFrom() {
+ try (VariantVector fromVector = new VariantVector("from", allocator);
+ VariantVector toVector = new VariantVector("to", allocator);
+ ArrowBuf metadataBuf = allocator.buffer(10);
+ ArrowBuf valueBuf = allocator.buffer(10)) {
+
+ byte[] metadata = new byte[] {1, 2, 3};
+ byte[] value = new byte[] {4, 5};
+ metadataBuf.setBytes(0, metadata);
+ valueBuf.setBytes(0, value);
+
+ NullableVariantHolder holder = createNullableHolder(metadataBuf, metadata, valueBuf, value);
+
+ fromVector.setSafe(0, holder);
+ fromVector.setValueCount(1);
+
+ toVector.allocateNew();
+ toVector.copyFrom(0, 0, fromVector);
+ toVector.setValueCount(1);
+
+ NullableVariantHolder result = new NullableVariantHolder();
+ toVector.get(0, result);
+ assertEquals(1, result.isSet);
+
+ byte[] actualMetadata = new byte[metadata.length];
+ byte[] actualValue = new byte[value.length];
+ result.metadataBuffer.getBytes(result.metadataStart, actualMetadata);
+ result.valueBuffer.getBytes(result.valueStart, actualValue);
+
+ assertArrayEquals(metadata, actualMetadata);
+ assertArrayEquals(value, actualValue);
+ }
+ }
+
+ @Test
+ void testCopyFromSafe() {
+ try (VariantVector fromVector = new VariantVector("from", allocator);
+ VariantVector toVector = new VariantVector("to", allocator);
+ ArrowBuf metadataBuf1 = allocator.buffer(10);
+ ArrowBuf valueBuf1 = allocator.buffer(10);
+ ArrowBuf metadataBuf2 = allocator.buffer(10);
+ ArrowBuf valueBuf2 = allocator.buffer(10)) {
+
+ byte[] metadata1 = new byte[] {1};
+ byte[] value1 = new byte[] {2, 3};
+ metadataBuf1.setBytes(0, metadata1);
+ valueBuf1.setBytes(0, value1);
+
+ NullableVariantHolder holder1 =
+ createNullableHolder(metadataBuf1, metadata1, valueBuf1, value1);
+
+ byte[] metadata2 = new byte[] {4, 5};
+ byte[] value2 = new byte[] {6};
+ metadataBuf2.setBytes(0, metadata2);
+ valueBuf2.setBytes(0, value2);
+
+ NullableVariantHolder holder2 =
+ createNullableHolder(metadataBuf2, metadata2, valueBuf2, value2);
+
+ fromVector.setSafe(0, holder1);
+ fromVector.setSafe(1, holder2);
+ fromVector.setValueCount(2);
+
+ // Copy without pre-allocating toVector
+ for (int i = 0; i < 2; i++) {
+ toVector.copyFromSafe(i, i, fromVector);
+ }
+ toVector.setValueCount(2);
+
+ // Verify both values
+ NullableVariantHolder result1 = new NullableVariantHolder();
+ toVector.get(0, result1);
+ assertEquals(1, result1.isSet);
+
+ byte[] actualMetadata1 = new byte[metadata1.length];
+ byte[] actualValue1 = new byte[value1.length];
+ result1.metadataBuffer.getBytes(result1.metadataStart, actualMetadata1);
+ result1.valueBuffer.getBytes(result1.valueStart, actualValue1);
+ assertArrayEquals(metadata1, actualMetadata1);
+ assertArrayEquals(value1, actualValue1);
+
+ NullableVariantHolder result2 = new NullableVariantHolder();
+ toVector.get(1, result2);
+ assertEquals(1, result2.isSet);
+
+ byte[] actualMetadata2 = new byte[metadata2.length];
+ byte[] actualValue2 = new byte[value2.length];
+ result2.metadataBuffer.getBytes(result2.metadataStart, actualMetadata2);
+ result2.valueBuffer.getBytes(result2.valueStart, actualValue2);
+ assertArrayEquals(metadata2, actualMetadata2);
+ assertArrayEquals(value2, actualValue2);
+ }
+ }
+
+ @Test
+ void testCopyFromWithNulls() {
+ try (VariantVector fromVector = new VariantVector("from", allocator);
+ VariantVector toVector = new VariantVector("to", allocator);
+ ArrowBuf metadataBuf = allocator.buffer(10);
+ ArrowBuf valueBuf = allocator.buffer(10)) {
+
+ byte[] metadata = new byte[] {1};
+ byte[] value = new byte[] {2};
+ metadataBuf.setBytes(0, metadata);
+ valueBuf.setBytes(0, value);
+
+ NullableVariantHolder holder = createNullableHolder(metadataBuf, metadata, valueBuf, value);
+
+ fromVector.setSafe(0, holder);
+ fromVector.setNull(1);
+ fromVector.setSafe(2, holder);
+ fromVector.setValueCount(3);
+
+ toVector.allocateNew();
+ for (int i = 0; i < 3; i++) {
+ toVector.copyFromSafe(i, i, fromVector);
+ }
+ toVector.setValueCount(3);
+
+ assertFalse(toVector.isNull(0));
+ assertTrue(toVector.isNull(1));
+ assertFalse(toVector.isNull(2));
+ }
+ }
+
+ // ========== GetObject Tests ==========
+
+ @Test
+ void testGetObject() {
+ try (VariantVector vector = new VariantVector("test", allocator);
+ ArrowBuf metadataBuf = allocator.buffer(10);
+ ArrowBuf valueBuf = allocator.buffer(10)) {
+
+ byte[] metadata = new byte[] {1, 2};
+ byte[] value = new byte[] {3, 4, 5};
+ metadataBuf.setBytes(0, metadata);
+ valueBuf.setBytes(0, value);
+
+ NullableVariantHolder holder = createNullableHolder(metadataBuf, metadata, valueBuf, value);
+
+ vector.setSafe(0, holder);
+ vector.setValueCount(1);
+
+ Object obj = vector.getObject(0);
+ assertNotNull(obj);
+ assertTrue(obj instanceof Variant);
+ assertEquals(new Variant(metadata, value), obj);
+ }
+ }
+
+ @Test
+ void testGetObjectNull() {
+ try (VariantVector vector = new VariantVector("test", allocator)) {
+ vector.setNull(0);
+ vector.setValueCount(1);
+
+ Object obj = vector.getObject(0);
+ assertNull(obj);
+ }
+ }
+
+ // ========== Allocate and Capacity Tests ==========
+
+ @Test
+ void testAllocateNew() {
+ try (VariantVector vector = new VariantVector("test", allocator)) {
+ vector.allocateNew();
+ assertTrue(vector.getValueCapacity() > 0);
+ }
+ }
+
+ @Test
+ void testSetInitialCapacity() {
+ try (VariantVector vector = new VariantVector("test", allocator)) {
+ vector.setInitialCapacity(100);
+ vector.allocateNew();
+ assertTrue(vector.getValueCapacity() >= 100);
+ }
+ }
+
+ @Test
+ void testClearAndReuse() {
+ try (VariantVector vector = new VariantVector("test", allocator);
+ ArrowBuf metadataBuf = allocator.buffer(10);
+ ArrowBuf valueBuf = allocator.buffer(10)) {
+
+ byte[] metadata = new byte[] {1};
+ byte[] value = new byte[] {2};
+ metadataBuf.setBytes(0, metadata);
+ valueBuf.setBytes(0, value);
+
+ NullableVariantHolder holder = createNullableHolder(metadataBuf, metadata, valueBuf, value);
+
+ vector.setSafe(0, holder);
+ vector.setValueCount(1);
+
+ assertFalse(vector.isNull(0));
+
+ vector.clear();
+ vector.allocateNew();
+
+ // After clear, vector should be empty
+ assertEquals(0, vector.getValueCount());
+ }
+ }
+}
diff --git a/bom/pom.xml b/bom/pom.xml
index 171d0bc5e9..2d1085b160 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -23,13 +23,13 @@ under the License.
org.apacheapache
- 33
-
+ 38
+ org.apache.arrowarrow-bom
- 19.0.0-SNAPSHOT
+ 20.0.0-SNAPSHOTpomArrow Bill of Materials
@@ -78,12 +78,12 @@ under the License.
-
+
- 11
- 11
- 11
- 11
+ 17
+ 17
+ 17
+ 17
@@ -165,7 +165,7 @@ under the License.
${project.version}
- org.apache.arrow
+ org.apache.arrow.gandivaarrow-gandiva${project.version}
@@ -194,6 +194,11 @@ under the License.
arrow-tools${project.version}
+
+ org.apache.arrow
+ arrow-variant
+ ${project.version}
+
@@ -203,12 +208,12 @@ under the License.
com.diffplug.spotlessspotless-maven-plugin
- 2.44.3
+ 3.8.0org.codehaus.mojoversions-maven-plugin
- 2.18.0
+ 2.21.0
@@ -230,7 +235,7 @@ under the License.
${maven.multiModuleProjectDirectory}/dev/license/asf-xml.license(<configuration|<project)
-
+
diff --git a/c/pom.xml b/c/pom.xml
index c90b6dc0ef..27b6619c4c 100644
--- a/c/pom.xml
+++ b/c/pom.xml
@@ -22,7 +22,7 @@ under the License.
org.apache.arrowarrow-java-root
- 19.0.0-SNAPSHOT
+ 20.0.0-SNAPSHOTarrow-c-data
diff --git a/c/src/main/cpp/jni_wrapper.cc b/c/src/main/cpp/jni_wrapper.cc
index 35c2b7787e..3d7a194563 100644
--- a/c/src/main/cpp/jni_wrapper.cc
+++ b/c/src/main/cpp/jni_wrapper.cc
@@ -205,8 +205,9 @@ void TryCopyLastError(JNIEnv* env, InnerPrivateData* private_data) {
return;
}
+ jsize error_bytes_len = env->GetArrayLength(arr);
char* error_str = reinterpret_cast(error_bytes);
- private_data->last_error_ = std::string(error_str, std::strlen(error_str));
+ private_data->last_error_ = std::string(error_str, error_bytes_len);
env->ReleaseByteArrayElements(arr, error_bytes, JNI_ABORT);
}
@@ -326,19 +327,20 @@ void ArrowArrayStreamRelease(ArrowArrayStream* stream) {
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
- if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION) != JNI_OK) {
- return JNI_ERR;
+ const int err_code = vm->GetEnv(reinterpret_cast(&env), JNI_VERSION);
+ if (err_code != JNI_OK) {
+ return err_code;
}
JNI_METHOD_START
- kObjectClass = CreateGlobalClassReference(env, "Ljava/lang/Object;");
+ kObjectClass = CreateGlobalClassReference(env, "java/lang/Object");
kRuntimeExceptionClass =
- CreateGlobalClassReference(env, "Ljava/lang/RuntimeException;");
+ CreateGlobalClassReference(env, "java/lang/RuntimeException");
kPrivateDataClass =
- CreateGlobalClassReference(env, "Lorg/apache/arrow/c/jni/PrivateData;");
+ CreateGlobalClassReference(env, "org/apache/arrow/c/jni/PrivateData");
kCDataExceptionClass =
- CreateGlobalClassReference(env, "Lorg/apache/arrow/c/jni/CDataJniException;");
+ CreateGlobalClassReference(env, "org/apache/arrow/c/jni/CDataJniException");
kStreamPrivateDataClass = CreateGlobalClassReference(
- env, "Lorg/apache/arrow/c/ArrayStreamExporter$ExportedArrayStreamPrivateData;");
+ env, "org/apache/arrow/c/ArrayStreamExporter$ExportedArrayStreamPrivateData");
kPrivateDataLastErrorField =
GetFieldID(env, kStreamPrivateDataClass, "lastError", "[B");
diff --git a/c/src/main/java/org/apache/arrow/c/ArrayImporter.java b/c/src/main/java/org/apache/arrow/c/ArrayImporter.java
index b74fb1b473..f31a8a1faa 100644
--- a/c/src/main/java/org/apache/arrow/c/ArrayImporter.java
+++ b/c/src/main/java/org/apache/arrow/c/ArrayImporter.java
@@ -58,7 +58,6 @@ void importArray(ArrowArray src) {
ArrowArray ownedArray = ArrowArray.allocateNew(allocator);
ownedArray.save(snapshot);
src.markReleased();
- src.close();
recursionLevel = 0;
diff --git a/c/src/main/java/org/apache/arrow/c/ArrowArrayStreamReader.java b/c/src/main/java/org/apache/arrow/c/ArrowArrayStreamReader.java
index 07a88cd8d7..34a9c4ec03 100644
--- a/c/src/main/java/org/apache/arrow/c/ArrowArrayStreamReader.java
+++ b/c/src/main/java/org/apache/arrow/c/ArrowArrayStreamReader.java
@@ -44,7 +44,6 @@ final class ArrowArrayStreamReader extends ArrowReader {
this.ownedStream = ArrowArrayStream.allocateNew(allocator);
this.ownedStream.save(snapshot);
stream.markReleased();
- stream.close();
}
@Override
diff --git a/c/src/main/java/org/apache/arrow/c/BufferImportTypeVisitor.java b/c/src/main/java/org/apache/arrow/c/BufferImportTypeVisitor.java
index 5ca398c9f9..10f690fc87 100644
--- a/c/src/main/java/org/apache/arrow/c/BufferImportTypeVisitor.java
+++ b/c/src/main/java/org/apache/arrow/c/BufferImportTypeVisitor.java
@@ -228,9 +228,8 @@ public List visit(ArrowType.Utf8 type) {
type,
start,
end);
- final int len = end - start;
offsets.getReferenceManager().retain();
- return Arrays.asList(maybeImportBitmap(type), offsets, importData(type, len));
+ return Arrays.asList(maybeImportBitmap(type), offsets, importData(type, end));
}
}
@@ -279,9 +278,8 @@ public List visit(ArrowType.LargeUtf8 type) {
type,
start,
end);
- final long len = end - start;
offsets.getReferenceManager().retain();
- return Arrays.asList(maybeImportBitmap(type), offsets, importData(type, len));
+ return Arrays.asList(maybeImportBitmap(type), offsets, importData(type, end));
}
}
@@ -296,9 +294,8 @@ public List visit(ArrowType.Binary type) {
type,
start,
end);
- final int len = end - start;
offsets.getReferenceManager().retain();
- return Arrays.asList(maybeImportBitmap(type), offsets, importData(type, len));
+ return Arrays.asList(maybeImportBitmap(type), offsets, importData(type, end));
}
}
@@ -320,9 +317,8 @@ public List visit(ArrowType.LargeBinary type) {
type,
start,
end);
- final long len = end - start;
offsets.getReferenceManager().retain();
- return Arrays.asList(maybeImportBitmap(type), offsets, importData(type, len));
+ return Arrays.asList(maybeImportBitmap(type), offsets, importData(type, end));
}
}
diff --git a/c/src/main/java/org/apache/arrow/c/Data.java b/c/src/main/java/org/apache/arrow/c/Data.java
index 0b4da33b4e..f9d2ee4542 100644
--- a/c/src/main/java/org/apache/arrow/c/Data.java
+++ b/c/src/main/java/org/apache/arrow/c/Data.java
@@ -231,6 +231,22 @@ public static void exportArrayStream(
new ArrayStreamExporter(allocator).export(out, reader);
}
+ /**
+ * Equivalent to calling {@link #importField(BufferAllocator, ArrowSchema,
+ * CDataDictionaryProvider, boolean) importField(allocator, schema, provider, true)}.
+ *
+ * @param allocator Buffer allocator for allocating dictionary vectors
+ * @param schema C data interface struct representing the field [inout]
+ * @param provider A dictionary provider will be initialized with empty dictionary vectors
+ * (optional)
+ * @return Imported field object
+ * @see #importField(BufferAllocator, ArrowSchema, CDataDictionaryProvider, boolean)
+ */
+ public static Field importField(
+ BufferAllocator allocator, ArrowSchema schema, CDataDictionaryProvider provider) {
+ return importField(allocator, schema, provider, true);
+ }
+
/**
* Import Java Field from the C data interface.
*
@@ -241,19 +257,42 @@ public static void exportArrayStream(
* @param schema C data interface struct representing the field [inout]
* @param provider A dictionary provider will be initialized with empty dictionary vectors
* (optional)
+ * @param closeImportedStructs if true, the ArrowSchema struct will be closed when this method
+ * completes.
* @return Imported field object
*/
public static Field importField(
- BufferAllocator allocator, ArrowSchema schema, CDataDictionaryProvider provider) {
+ BufferAllocator allocator,
+ ArrowSchema schema,
+ CDataDictionaryProvider provider,
+ boolean closeImportedStructs) {
try {
SchemaImporter importer = new SchemaImporter(allocator);
return importer.importField(schema, provider);
} finally {
schema.release();
- schema.close();
+ if (closeImportedStructs) {
+ schema.close();
+ }
}
}
+ /**
+ * Equivalent to calling {@link #importSchema(BufferAllocator, ArrowSchema,
+ * CDataDictionaryProvider, boolean) importSchema(allocator, schema, provider, true)}.
+ *
+ * @param allocator Buffer allocator for allocating dictionary vectors
+ * @param schema C data interface struct representing the field
+ * @param provider A dictionary provider will be initialized with empty dictionary vectors
+ * (optional)
+ * @return Imported schema object
+ * @see #importSchema(BufferAllocator, ArrowSchema, CDataDictionaryProvider, boolean)
+ */
+ public static Schema importSchema(
+ BufferAllocator allocator, ArrowSchema schema, CDataDictionaryProvider provider) {
+ return importSchema(allocator, schema, provider, true);
+ }
+
/**
* Import Java Schema from the C data interface.
*
@@ -264,11 +303,16 @@ public static Field importField(
* @param schema C data interface struct representing the field
* @param provider A dictionary provider will be initialized with empty dictionary vectors
* (optional)
+ * @param closeImportedStructs if true, the ArrowSchema struct will be closed when this method
+ * completes.
* @return Imported schema object
*/
public static Schema importSchema(
- BufferAllocator allocator, ArrowSchema schema, CDataDictionaryProvider provider) {
- Field structField = importField(allocator, schema, provider);
+ BufferAllocator allocator,
+ ArrowSchema schema,
+ CDataDictionaryProvider provider,
+ boolean closeImportedStructs) {
+ Field structField = importField(allocator, schema, provider, closeImportedStructs);
if (structField.getType().getTypeID() != ArrowTypeID.Struct) {
throw new IllegalArgumentException(
"Cannot import schema: ArrowSchema describes non-struct type");
@@ -276,24 +320,67 @@ public static Schema importSchema(
return new Schema(structField.getChildren(), structField.getMetadata());
}
+ /**
+ * Equivalent to calling {@link #importIntoVector(BufferAllocator, ArrowArray, FieldVector,
+ * DictionaryProvider, boolean)} importIntoVector(allocator, array, vector, provider, true)}.
+ *
+ * @param allocator Buffer allocator
+ * @param array C data interface struct holding the array data
+ * @param vector Imported vector object [out]
+ * @param provider Dictionary provider to load dictionary vectors to (optional)
+ * @see #importIntoVector(BufferAllocator, ArrowArray, FieldVector, DictionaryProvider, boolean)
+ */
+ public static void importIntoVector(
+ BufferAllocator allocator,
+ ArrowArray array,
+ FieldVector vector,
+ DictionaryProvider provider) {
+ importIntoVector(allocator, array, vector, provider, true);
+ }
+
/**
* Import Java vector from the C data interface.
*
- *
The ArrowArray struct has its contents moved (as per the C data interface specification) to
- * a private object held alive by the resulting array.
+ *
On successful completion, the ArrowArray struct will have been moved (as per the C data
+ * interface specification) to a private object held alive by the resulting array.
*
* @param allocator Buffer allocator
* @param array C data interface struct holding the array data
* @param vector Imported vector object [out]
* @param provider Dictionary provider to load dictionary vectors to (optional)
+ * @param closeImportedStructs if true, the ArrowArray struct will be closed when this method
+ * completes successfully.
*/
public static void importIntoVector(
BufferAllocator allocator,
ArrowArray array,
FieldVector vector,
- DictionaryProvider provider) {
+ DictionaryProvider provider,
+ boolean closeImportedStructs) {
ArrayImporter importer = new ArrayImporter(allocator, vector, provider);
importer.importArray(array);
+ if (closeImportedStructs) {
+ array.close();
+ }
+ }
+
+ /**
+ * Equivalent to calling {@link #importVector(BufferAllocator, ArrowArray, ArrowSchema,
+ * CDataDictionaryProvider, boolean) importVector(allocator, array, schema, provider, true)}.
+ *
+ * @param allocator Buffer allocator for allocating the output FieldVector
+ * @param array C data interface struct holding the array data
+ * @param schema C data interface struct holding the array type
+ * @param provider Dictionary provider to load dictionary vectors to (optional)
+ * @return Imported vector object
+ * @see #importVector(BufferAllocator, ArrowArray, ArrowSchema, CDataDictionaryProvider, boolean)
+ */
+ public static FieldVector importVector(
+ BufferAllocator allocator,
+ ArrowArray array,
+ ArrowSchema schema,
+ CDataDictionaryProvider provider) {
+ return importVector(allocator, array, schema, provider, true);
}
/**
@@ -307,19 +394,42 @@ public static void importIntoVector(
* @param array C data interface struct holding the array data
* @param schema C data interface struct holding the array type
* @param provider Dictionary provider to load dictionary vectors to (optional)
+ * @param closeImportedStructs if true, the ArrowArray struct will be closed when this method
+ * completes successfully and the ArrowSchema struct will be always be closed.
* @return Imported vector object
*/
public static FieldVector importVector(
BufferAllocator allocator,
ArrowArray array,
ArrowSchema schema,
- CDataDictionaryProvider provider) {
- Field field = importField(allocator, schema, provider);
+ CDataDictionaryProvider provider,
+ boolean closeImportedStructs) {
+ Field field = importField(allocator, schema, provider, closeImportedStructs);
FieldVector vector = field.createVector(allocator);
- importIntoVector(allocator, array, vector, provider);
+ importIntoVector(allocator, array, vector, provider, closeImportedStructs);
return vector;
}
+ /**
+ * Equivalent to calling {@link #importIntoVectorSchemaRoot(BufferAllocator, ArrowArray,
+ * VectorSchemaRoot, DictionaryProvider, boolean) importIntoVectorSchemaRoot(allocator, array,
+ * root, provider, true)}.
+ *
+ * @param allocator Buffer allocator
+ * @param array C data interface struct holding the record batch data
+ * @param root vector schema root to load into
+ * @param provider Dictionary provider to load dictionary vectors to (optional)
+ * @see #importIntoVectorSchemaRoot(BufferAllocator, ArrowArray, VectorSchemaRoot,
+ * DictionaryProvider, boolean)
+ */
+ public static void importIntoVectorSchemaRoot(
+ BufferAllocator allocator,
+ ArrowArray array,
+ VectorSchemaRoot root,
+ DictionaryProvider provider) {
+ importIntoVectorSchemaRoot(allocator, array, root, provider, true);
+ }
+
/**
* Import record batch from the C data interface into vector schema root.
*
@@ -333,15 +443,18 @@ public static FieldVector importVector(
* @param array C data interface struct holding the record batch data
* @param root vector schema root to load into
* @param provider Dictionary provider to load dictionary vectors to (optional)
+ * @param closeImportedStructs if true, the ArrowArray struct will be closed when this method
+ * completes successfully
*/
public static void importIntoVectorSchemaRoot(
BufferAllocator allocator,
ArrowArray array,
VectorSchemaRoot root,
- DictionaryProvider provider) {
+ DictionaryProvider provider,
+ boolean closeImportedStructs) {
try (StructVector structVector = StructVector.emptyWithDuplicates("", allocator)) {
structVector.initializeChildrenFromFields(root.getSchema().getFields());
- importIntoVector(allocator, array, structVector, provider);
+ importIntoVector(allocator, array, structVector, provider, closeImportedStructs);
StructVectorUnloader unloader = new StructVectorUnloader(structVector);
VectorLoader loader = new VectorLoader(root);
try (ArrowRecordBatch recordBatch = unloader.getRecordBatch()) {
@@ -350,6 +463,21 @@ public static void importIntoVectorSchemaRoot(
}
}
+ /**
+ * Equivalent to calling {@link #importVectorSchemaRoot(BufferAllocator, ArrowSchema,
+ * CDataDictionaryProvider, boolean) importVectorSchemaRoot(allocator, schema, provider, true)}.
+ *
+ * @param allocator Buffer allocator for allocating the output VectorSchemaRoot
+ * @param schema C data interface struct holding the record batch schema
+ * @param provider Dictionary provider to load dictionary vectors to (optional)
+ * @return Imported vector schema root
+ * @see #importVectorSchemaRoot(BufferAllocator, ArrowSchema, CDataDictionaryProvider, boolean)
+ */
+ public static VectorSchemaRoot importVectorSchemaRoot(
+ BufferAllocator allocator, ArrowSchema schema, CDataDictionaryProvider provider) {
+ return importVectorSchemaRoot(allocator, schema, provider, true);
+ }
+
/**
* Import Java vector schema root from a C data interface Schema.
*
@@ -360,11 +488,37 @@ public static void importIntoVectorSchemaRoot(
* @param allocator Buffer allocator for allocating the output VectorSchemaRoot
* @param schema C data interface struct holding the record batch schema
* @param provider Dictionary provider to load dictionary vectors to (optional)
+ * @param closeImportedStructs if true, the ArrowSchema struct will be closed when this method
+ * completes
* @return Imported vector schema root
*/
public static VectorSchemaRoot importVectorSchemaRoot(
- BufferAllocator allocator, ArrowSchema schema, CDataDictionaryProvider provider) {
- return importVectorSchemaRoot(allocator, null, schema, provider);
+ BufferAllocator allocator,
+ ArrowSchema schema,
+ CDataDictionaryProvider provider,
+ boolean closeImportedStructs) {
+ return importVectorSchemaRoot(allocator, null, schema, provider, closeImportedStructs);
+ }
+
+ /**
+ * Equivalent to calling {@link #importVectorSchemaRoot(BufferAllocator, ArrowArray, ArrowSchema,
+ * CDataDictionaryProvider, boolean) importVectorSchemaRoot(allocator, array, schema, provider,
+ * true)}.
+ *
+ * @param allocator Buffer allocator for allocating the output VectorSchemaRoot
+ * @param array C data interface struct holding the record batch data (optional)
+ * @param schema C data interface struct holding the record batch schema
+ * @param provider Dictionary provider to load dictionary vectors to (optional)
+ * @return Imported vector schema root
+ * @see #importVectorSchemaRoot(BufferAllocator, ArrowArray, ArrowSchema, CDataDictionaryProvider,
+ * boolean)
+ */
+ public static VectorSchemaRoot importVectorSchemaRoot(
+ BufferAllocator allocator,
+ ArrowArray array,
+ ArrowSchema schema,
+ CDataDictionaryProvider provider) {
+ return importVectorSchemaRoot(allocator, array, schema, provider, true);
}
/**
@@ -383,29 +537,56 @@ public static VectorSchemaRoot importVectorSchemaRoot(
* @param array C data interface struct holding the record batch data (optional)
* @param schema C data interface struct holding the record batch schema
* @param provider Dictionary provider to load dictionary vectors to (optional)
+ * @param closeImportedStructs if true, the ArrowArray struct will be closed when this method
+ * completes successfully and the ArrowSchema struct will be always be closed.
* @return Imported vector schema root
*/
public static VectorSchemaRoot importVectorSchemaRoot(
BufferAllocator allocator,
ArrowArray array,
ArrowSchema schema,
- CDataDictionaryProvider provider) {
+ CDataDictionaryProvider provider,
+ boolean closeImportedStructs) {
VectorSchemaRoot vsr =
- VectorSchemaRoot.create(importSchema(allocator, schema, provider), allocator);
+ VectorSchemaRoot.create(
+ importSchema(allocator, schema, provider, closeImportedStructs), allocator);
if (array != null) {
- importIntoVectorSchemaRoot(allocator, array, vsr, provider);
+ importIntoVectorSchemaRoot(allocator, array, vsr, provider, closeImportedStructs);
}
return vsr;
}
/**
- * Import an ArrowArrayStream as an {@link ArrowReader}.
+ * Equivalent to calling {@link #importArrayStream(BufferAllocator, ArrowArrayStream, boolean)
+ * importArrayStream(allocator, stream, true)}.
*
* @param allocator Buffer allocator for allocating the output data.
* @param stream C stream interface struct to import.
* @return Imported reader
+ * @see #importArrayStream(BufferAllocator, ArrowArrayStream, boolean)
*/
public static ArrowReader importArrayStream(BufferAllocator allocator, ArrowArrayStream stream) {
- return new ArrowArrayStreamReader(allocator, stream);
+ return importArrayStream(allocator, stream, true);
+ }
+
+ /**
+ * Import an ArrowArrayStream as an {@link ArrowReader}.
+ *
+ *
On successful completion, the ArrowArrayStream struct will have been moved (as per the C
+ * data interface specification) to a private object held alive by the resulting ArrowReader.
+ *
+ * @param allocator Buffer allocator for allocating the output data.
+ * @param stream C stream interface struct to import.
+ * @param closeImportedStructs if true, the ArrowArrayStream struct will be closed when this
+ * method completes successfully
+ * @return Imported reader
+ */
+ public static ArrowReader importArrayStream(
+ BufferAllocator allocator, ArrowArrayStream stream, boolean closeImportedStructs) {
+ ArrowArrayStreamReader reader = new ArrowArrayStreamReader(allocator, stream);
+ if (closeImportedStructs) {
+ stream.close();
+ }
+ return reader;
}
}
diff --git a/c/src/main/java/org/apache/arrow/c/ReferenceCountedArrowArray.java b/c/src/main/java/org/apache/arrow/c/ReferenceCountedArrowArray.java
index cf50f9417b..f51fb25105 100644
--- a/c/src/main/java/org/apache/arrow/c/ReferenceCountedArrowArray.java
+++ b/c/src/main/java/org/apache/arrow/c/ReferenceCountedArrowArray.java
@@ -64,13 +64,18 @@ void release() {
*/
ArrowBuf unsafeAssociateAllocation(
BufferAllocator trackingAllocator, long capacity, long memoryAddress) {
+ // Retain only after wrapForeignAllocation succeeds. On the allocator-limit OOM path,
+ // wrapForeignAllocation throws before the ForeignAllocation is associated, so release0()
+ // is not called; retaining first would leave the count elevated with no matching release0().
+ ArrowBuf buf =
+ trackingAllocator.wrapForeignAllocation(
+ new ForeignAllocation(capacity, memoryAddress) {
+ @Override
+ protected void release0() {
+ ReferenceCountedArrowArray.this.release();
+ }
+ });
retain();
- return trackingAllocator.wrapForeignAllocation(
- new ForeignAllocation(capacity, memoryAddress) {
- @Override
- protected void release0() {
- ReferenceCountedArrowArray.this.release();
- }
- });
+ return buf;
}
}
diff --git a/c/src/main/java/org/apache/arrow/c/jni/JniLoader.java b/c/src/main/java/org/apache/arrow/c/jni/JniLoader.java
index f712b400bf..46c93f5541 100644
--- a/c/src/main/java/org/apache/arrow/c/jni/JniLoader.java
+++ b/c/src/main/java/org/apache/arrow/c/jni/JniLoader.java
@@ -75,8 +75,23 @@ private synchronized void loadRemaining() {
}
private void load(String name) {
- final String libraryToLoad =
- name + "/" + getNormalizedArch() + "/" + System.mapLibraryName(name);
+ String libraryName = System.mapLibraryName(name);
+
+ // If 'arrow.cdata.library.path' is defined, try to load the native library from there
+ String libraryPath = System.getProperty("arrow.cdata.library.path");
+ if (libraryPath != null) {
+ try {
+ File libraryFile = new File(libraryPath, libraryName);
+ if (libraryFile.isFile()) {
+ System.load(libraryFile.getAbsolutePath());
+ return;
+ }
+ } catch (UnsatisfiedLinkError e) {
+ // Ignore this error and fall back to extracting from the JAR file
+ }
+ }
+
+ final String libraryToLoad = name + "/" + getNormalizedArch() + "/" + libraryName;
try {
File temp =
File.createTempFile("jnilib-", ".tmp", new File(System.getProperty("java.io.tmpdir")));
diff --git a/c/src/test/java/org/apache/arrow/c/ExceptionTest.java b/c/src/test/java/org/apache/arrow/c/ExceptionTest.java
new file mode 100644
index 0000000000..5bc96a8f99
--- /dev/null
+++ b/c/src/test/java/org/apache/arrow/c/ExceptionTest.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.arrow.c;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowableOfType;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.VectorLoader;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.dictionary.Dictionary;
+import org.apache.arrow.vector.dictionary.DictionaryProvider;
+import org.apache.arrow.vector.ipc.ArrowReader;
+import org.apache.arrow.vector.ipc.message.ArrowRecordBatch;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.junit.jupiter.api.Test;
+
+// Regression test for https://github.com/apache/arrow-java/issues/759
+final class ExceptionTest {
+ @Test
+ public void testException() throws IOException {
+ final Schema schema =
+ new Schema(Collections.singletonList(Field.nullable("ints", new ArrowType.Int(32, true))));
+ final List