diff --git a/.github/actions/setup-rust-runtime/action.yaml b/.github/actions/setup-rust-runtime/action.yaml
index cd18be9890315..b6fb2c898bf2f 100644
--- a/.github/actions/setup-rust-runtime/action.yaml
+++ b/.github/actions/setup-rust-runtime/action.yaml
@@ -20,8 +20,10 @@ description: 'Setup Rust Runtime Environment'
runs:
using: "composite"
steps:
- - name: Run sccache-cache
- uses: mozilla-actions/sccache-action@v0.0.4
+ # https://github.com/apache/datafusion/issues/15535
+ # disabled because neither version nor git hash works with apache github policy
+ #- name: Run sccache-cache
+ # uses: mozilla-actions/sccache-action@65101d47ea8028ed0c98a1cdea8dd9182e9b5133 # v0.0.8
- name: Configure runtime env
shell: bash
# do not produce debug symbols to keep memory usage down
@@ -30,9 +32,11 @@ runs:
#
# Set debuginfo=line-tables-only as debuginfo=0 causes immensely slow build
# See for more details: https://github.com/rust-lang/rust/issues/119560
+ #
+ # readd the following to the run below once sccache-cache is re-enabled
+ # echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
+ # echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV
run: |
- echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
- echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV
echo "RUST_BACKTRACE=1" >> $GITHUB_ENV
echo "RUSTFLAGS=-C debuginfo=line-tables-only -C incremental=false" >> $GITHUB_ENV
diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml
index 0d65b1aa809ff..491fa27c2a56a 100644
--- a/.github/workflows/audit.yml
+++ b/.github/workflows/audit.yml
@@ -26,6 +26,8 @@ on:
paths:
- "**/Cargo.toml"
- "**/Cargo.lock"
+ branches:
+ - main
pull_request:
paths:
@@ -40,4 +42,6 @@ jobs:
- name: Install cargo-audit
run: cargo install cargo-audit
- name: Run audit check
- run: cargo audit
+ # Ignored until https://github.com/apache/datafusion/issues/15571
+ # ignored py03 warning until arrow 55 upgrade
+ run: cargo audit --ignore RUSTSEC-2024-0370 --ignore RUSTSEC-2025-0020
diff --git a/.github/workflows/extended.yml b/.github/workflows/extended.yml
index a5d68ff079b56..d80fdb75d932d 100644
--- a/.github/workflows/extended.yml
+++ b/.github/workflows/extended.yml
@@ -47,7 +47,7 @@ on:
permissions:
contents: read
checks: write
-
+
jobs:
# Check crate compiles and base cargo check passes
@@ -58,6 +58,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
+ ref: ${{ github.event.inputs.pr_head_sha }} # will be empty if triggered by push
submodules: true
fetch-depth: 1
- name: Install Rust
@@ -81,6 +82,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
+ ref: ${{ github.event.inputs.pr_head_sha }} # will be empty if triggered by push
submodules: true
fetch-depth: 1
- name: Free Disk Space (Ubuntu)
@@ -114,6 +116,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
+ ref: ${{ github.event.inputs.pr_head_sha }} # will be empty if triggered by push
submodules: true
fetch-depth: 1
- name: Setup Rust toolchain
@@ -134,6 +137,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
+ ref: ${{ github.event.inputs.pr_head_sha }} # will be empty if triggered by push
submodules: true
fetch-depth: 1
- name: Setup Rust toolchain
@@ -161,14 +165,14 @@ jobs:
echo "workflow_status=completed" >> $GITHUB_OUTPUT
echo "conclusion=success" >> $GITHUB_OUTPUT
fi
-
+
- name: Update check run
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const workflowRunUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
-
+
await github.rest.checks.update({
owner: context.repo.owner,
repo: context.repo.repo,
diff --git a/.github/workflows/pr_comment_commands.yml b/.github/workflows/pr_comment_commands.yml
index a20a5b15965dd..6aa6caaf34d02 100644
--- a/.github/workflows/pr_comment_commands.yml
+++ b/.github/workflows/pr_comment_commands.yml
@@ -44,12 +44,12 @@ jobs:
repo: context.repo.repo,
pull_number: context.payload.issue.number
});
-
+
// Extract the branch name
const branchName = pullRequest.head.ref;
const headSha = pullRequest.head.sha;
const workflowRunsUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions?query=workflow%3A%22Datafusion+extended+tests%22+branch%3A${branchName}`;
-
+
// Create a check run that links to the Actions tab so the run will be visible in GitHub UI
const check = await github.rest.checks.create({
owner: context.repo.owner,
@@ -69,7 +69,7 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'extended.yml',
- ref: branchName,
+ ref: 'main',
inputs: {
pr_number: context.payload.issue.number.toString(),
check_run_id: check.data.id.toString(),
@@ -77,7 +77,7 @@ jobs:
}
});
- - name: Add reaction to comment
+ - name: Add reaction to comment
uses: actions/github-script@v7
with:
script: |
@@ -86,4 +86,4 @@ jobs:
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'rocket'
- });
\ No newline at end of file
+ });
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 1e6cd97acea33..f3b7e19a4970b 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -384,25 +384,25 @@ jobs:
run: ci/scripts/rust_docs.sh
linux-wasm-pack:
- name: build with wasm-pack
- runs-on: ubuntu-latest
- container:
- image: amd64/rust
+ name: build and run with wasm-pack
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- - name: Setup Rust toolchain
- uses: ./.github/actions/setup-builder
- with:
- rust-version: stable
+ - name: Setup for wasm32
+ run: |
+ rustup target add wasm32-unknown-unknown
- name: Install dependencies
run: |
- apt-get update -qq
- apt-get install -y -qq clang
- - name: Install wasm-pack
- run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- - name: Build with wasm-pack
+ sudo apt-get update -qq
+ sudo apt-get install -y -qq clang
+ - name: Setup wasm-pack
+ run: |
+ cargo install wasm-pack
+ - name: Run tests with headless mode
working-directory: ./datafusion/wasmtest
- run: wasm-pack build --dev
+ run: |
+ wasm-pack test --headless --firefox
+ wasm-pack test --headless --chrome --chromedriver $CHROMEWEBDRIVER/chromedriver
# verify that the benchmark queries return the correct results
verify-benchmark-results:
@@ -693,6 +693,11 @@ jobs:
# If you encounter an error, run './dev/update_function_docs.sh' and commit
./dev/update_function_docs.sh
git diff --exit-code
+ - name: Check if runtime_configs.md has been modified
+ run: |
+ # If you encounter an error, run './dev/update_runtime_config_docs.sh' and commit
+ ./dev/update_runtime_config_docs.sh
+ git diff --exit-code
# Verify MSRV for the crates which are directly used by other projects:
# - datafusion
diff --git a/Cargo.lock b/Cargo.lock
index 8aba95bdcca4a..299ea0dc4c6fd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -246,9 +246,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "arrow"
-version = "54.2.1"
+version = "55.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc208515aa0151028e464cc94a692156e945ce5126abd3537bb7fd6ba2143ed1"
+checksum = "3095aaf545942ff5abd46654534f15b03a90fba78299d661e045e5d587222f0d"
dependencies = [
"arrow-arith",
"arrow-array",
@@ -265,14 +265,14 @@ dependencies = [
"arrow-string",
"half",
"pyo3",
- "rand 0.8.5",
+ "rand 0.9.0",
]
[[package]]
name = "arrow-arith"
-version = "54.2.1"
+version = "55.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e07e726e2b3f7816a85c6a45b6ec118eeeabf0b2a8c208122ad949437181f49a"
+checksum = "00752064ff47cee746e816ddb8450520c3a52cbad1e256f6fa861a35f86c45e7"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -284,9 +284,9 @@ dependencies = [
[[package]]
name = "arrow-array"
-version = "54.2.1"
+version = "55.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2262eba4f16c78496adfd559a29fe4b24df6088efc9985a873d58e92be022d5"
+checksum = "cebfe926794fbc1f49ddd0cdaf898956ca9f6e79541efce62dabccfd81380472"
dependencies = [
"ahash 0.8.11",
"arrow-buffer",
@@ -301,9 +301,9 @@ dependencies = [
[[package]]
name = "arrow-buffer"
-version = "54.2.1"
+version = "55.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e899dade2c3b7f5642eb8366cfd898958bcca099cde6dfea543c7e8d3ad88d4"
+checksum = "0303c7ec4cf1a2c60310fc4d6bbc3350cd051a17bf9e9c0a8e47b4db79277824"
dependencies = [
"bytes",
"half",
@@ -312,9 +312,9 @@ dependencies = [
[[package]]
name = "arrow-cast"
-version = "54.2.1"
+version = "55.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4103d88c5b441525ed4ac23153be7458494c2b0c9a11115848fdb9b81f6f886a"
+checksum = "335f769c5a218ea823d3760a743feba1ef7857cba114c01399a891c2fff34285"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -333,9 +333,9 @@ dependencies = [
[[package]]
name = "arrow-csv"
-version = "54.2.1"
+version = "55.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43d3cb0914486a3cae19a5cad2598e44e225d53157926d0ada03c20521191a65"
+checksum = "510db7dfbb4d5761826516cc611d97b3a68835d0ece95b034a052601109c0b1b"
dependencies = [
"arrow-array",
"arrow-cast",
@@ -349,9 +349,9 @@ dependencies = [
[[package]]
name = "arrow-data"
-version = "54.2.1"
+version = "55.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a329fb064477c9ec5f0870d2f5130966f91055c7c5bce2b3a084f116bc28c3b"
+checksum = "e8affacf3351a24039ea24adab06f316ded523b6f8c3dbe28fbac5f18743451b"
dependencies = [
"arrow-buffer",
"arrow-schema",
@@ -361,9 +361,9 @@ dependencies = [
[[package]]
name = "arrow-flight"
-version = "54.2.1"
+version = "55.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7408f2bf3b978eddda272c7699f439760ebc4ac70feca25fefa82c5b8ce808d"
+checksum = "e2e0fad280f41a918d53ba48288a246ff04202d463b3b380fbc0edecdcb52cfd"
dependencies = [
"arrow-arith",
"arrow-array",
@@ -388,9 +388,9 @@ dependencies = [
[[package]]
name = "arrow-ipc"
-version = "54.2.1"
+version = "55.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddecdeab02491b1ce88885986e25002a3da34dd349f682c7cfe67bab7cc17b86"
+checksum = "69880a9e6934d9cba2b8630dd08a3463a91db8693b16b499d54026b6137af284"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -402,9 +402,9 @@ dependencies = [
[[package]]
name = "arrow-json"
-version = "54.2.1"
+version = "55.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d03b9340013413eb84868682ace00a1098c81a5ebc96d279f7ebf9a4cac3c0fd"
+checksum = "d8dafd17a05449e31e0114d740530e0ada7379d7cb9c338fd65b09a8130960b0"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -413,18 +413,20 @@ dependencies = [
"arrow-schema",
"chrono",
"half",
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
"lexical-core",
+ "memchr",
"num",
"serde",
"serde_json",
+ "simdutf8",
]
[[package]]
name = "arrow-ord"
-version = "54.2.1"
+version = "55.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f841bfcc1997ef6ac48ee0305c4dfceb1f7c786fe31e67c1186edf775e1f1160"
+checksum = "895644523af4e17502d42c3cb6b27cb820f0cb77954c22d75c23a85247c849e1"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -435,9 +437,9 @@ dependencies = [
[[package]]
name = "arrow-row"
-version = "54.2.1"
+version = "55.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1eeb55b0a0a83851aa01f2ca5ee5648f607e8506ba6802577afdda9d75cdedcd"
+checksum = "9be8a2a4e5e7d9c822b2b8095ecd77010576d824f654d347817640acfc97d229"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -448,9 +450,9 @@ dependencies = [
[[package]]
name = "arrow-schema"
-version = "54.2.1"
+version = "55.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85934a9d0261e0fa5d4e2a5295107d743b543a6e0484a835d4b8db2da15306f9"
+checksum = "7450c76ab7c5a6805be3440dc2e2096010da58f7cab301fdc996a4ee3ee74e49"
dependencies = [
"bitflags 2.8.0",
"serde",
@@ -458,9 +460,9 @@ dependencies = [
[[package]]
name = "arrow-select"
-version = "54.2.1"
+version = "55.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e2932aece2d0c869dd2125feb9bd1709ef5c445daa3838ac4112dcfa0fda52c"
+checksum = "aa5f5a93c75f46ef48e4001535e7b6c922eeb0aa20b73cf58d09e13d057490d8"
dependencies = [
"ahash 0.8.11",
"arrow-array",
@@ -472,9 +474,9 @@ dependencies = [
[[package]]
name = "arrow-string"
-version = "54.2.1"
+version = "55.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "912e38bd6a7a7714c1d9b61df80315685553b7455e8a6045c27531d8ecd5b458"
+checksum = "6e7005d858d84b56428ba2a98a107fe88c0132c61793cf6b8232a1f9bfc0452b"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -1047,9 +1049,9 @@ dependencies = [
[[package]]
name = "bigdecimal"
-version = "0.4.7"
+version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f31f3af01c5c65a07985c804d3366560e6fa7883d640a122819b14ec327482c"
+checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013"
dependencies = [
"autocfg",
"libm",
@@ -1117,9 +1119,9 @@ dependencies = [
[[package]]
name = "blake3"
-version = "1.7.0"
+version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b17679a8d69b6d7fd9cd9801a536cec9fa5e5970b69f9d4747f70b39b031f5e7"
+checksum = "389a099b34312839e16420d499a9cad9650541715937ffbdd40d36f49e77eeb3"
dependencies = [
"arrayref",
"arrayvec",
@@ -1361,9 +1363,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
-version = "0.4.39"
+version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
+checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -1371,7 +1373,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
- "windows-targets 0.52.6",
+ "windows-link",
]
[[package]]
@@ -1446,9 +1448,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.34"
+version = "4.5.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff"
+checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
dependencies = [
"clap_builder",
"clap_derive",
@@ -1456,9 +1458,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.5.34"
+version = "4.5.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489"
+checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
dependencies = [
"anstream",
"anstyle",
@@ -1640,7 +1642,7 @@ dependencies = [
"anes",
"cast",
"ciborium",
- "clap 4.5.34",
+ "clap 4.5.36",
"criterion-plot",
"futures",
"is-terminal",
@@ -1671,9 +1673,9 @@ dependencies = [
[[package]]
name = "crossbeam-channel"
-version = "0.5.14"
+version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
@@ -1807,7 +1809,7 @@ dependencies = [
[[package]]
name = "datafusion"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"arrow-ipc",
@@ -1877,7 +1879,7 @@ dependencies = [
[[package]]
name = "datafusion-benchmarks"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"datafusion",
@@ -1901,7 +1903,7 @@ dependencies = [
[[package]]
name = "datafusion-catalog"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"async-trait",
@@ -1925,7 +1927,7 @@ dependencies = [
[[package]]
name = "datafusion-catalog-listing"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"async-trait",
@@ -1947,14 +1949,14 @@ dependencies = [
[[package]]
name = "datafusion-cli"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"assert_cmd",
"async-trait",
"aws-config",
"aws-credential-types",
- "clap 4.5.34",
+ "clap 4.5.36",
"ctor",
"datafusion",
"dirs",
@@ -1976,7 +1978,7 @@ dependencies = [
[[package]]
name = "datafusion-common"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"ahash 0.8.11",
"apache-avro",
@@ -1986,7 +1988,7 @@ dependencies = [
"chrono",
"half",
"hashbrown 0.14.5",
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
"insta",
"libc",
"log",
@@ -2003,7 +2005,7 @@ dependencies = [
[[package]]
name = "datafusion-common-runtime"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"futures",
"log",
@@ -2012,7 +2014,7 @@ dependencies = [
[[package]]
name = "datafusion-datasource"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"async-compression",
@@ -2020,6 +2022,7 @@ dependencies = [
"bytes",
"bzip2 0.5.2",
"chrono",
+ "criterion",
"datafusion-common",
"datafusion-common-runtime",
"datafusion-execution",
@@ -2046,7 +2049,7 @@ dependencies = [
[[package]]
name = "datafusion-datasource-avro"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"apache-avro",
"arrow",
@@ -2071,7 +2074,7 @@ dependencies = [
[[package]]
name = "datafusion-datasource-csv"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"async-trait",
@@ -2094,7 +2097,7 @@ dependencies = [
[[package]]
name = "datafusion-datasource-json"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"async-trait",
@@ -2117,7 +2120,7 @@ dependencies = [
[[package]]
name = "datafusion-datasource-parquet"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"async-trait",
@@ -2147,11 +2150,11 @@ dependencies = [
[[package]]
name = "datafusion-doc"
-version = "46.0.1"
+version = "47.0.0"
[[package]]
name = "datafusion-examples"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"arrow-flight",
@@ -2160,6 +2163,7 @@ dependencies = [
"bytes",
"dashmap",
"datafusion",
+ "datafusion-ffi",
"datafusion-proto",
"env_logger",
"futures",
@@ -2180,7 +2184,7 @@ dependencies = [
[[package]]
name = "datafusion-execution"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"chrono",
@@ -2198,7 +2202,7 @@ dependencies = [
[[package]]
name = "datafusion-expr"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"chrono",
@@ -2210,7 +2214,7 @@ dependencies = [
"datafusion-functions-window-common",
"datafusion-physical-expr-common",
"env_logger",
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
"paste",
"recursive",
"serde_json",
@@ -2219,21 +2223,22 @@ dependencies = [
[[package]]
name = "datafusion-expr-common"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"datafusion-common",
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
"itertools 0.14.0",
"paste",
]
[[package]]
name = "datafusion-ffi"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"abi_stable",
"arrow",
+ "arrow-schema",
"async-ffi",
"async-trait",
"datafusion",
@@ -2248,7 +2253,7 @@ dependencies = [
[[package]]
name = "datafusion-functions"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"arrow-buffer",
@@ -2277,7 +2282,7 @@ dependencies = [
[[package]]
name = "datafusion-functions-aggregate"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"ahash 0.8.11",
"arrow",
@@ -2298,7 +2303,7 @@ dependencies = [
[[package]]
name = "datafusion-functions-aggregate-common"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"ahash 0.8.11",
"arrow",
@@ -2311,7 +2316,7 @@ dependencies = [
[[package]]
name = "datafusion-functions-nested"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"arrow-ord",
@@ -2332,7 +2337,7 @@ dependencies = [
[[package]]
name = "datafusion-functions-table"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"async-trait",
@@ -2346,7 +2351,7 @@ dependencies = [
[[package]]
name = "datafusion-functions-window"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"datafusion-common",
@@ -2362,7 +2367,7 @@ dependencies = [
[[package]]
name = "datafusion-functions-window-common"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"datafusion-common",
"datafusion-physical-expr-common",
@@ -2370,7 +2375,7 @@ dependencies = [
[[package]]
name = "datafusion-macros"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"datafusion-expr",
"quote",
@@ -2379,11 +2384,12 @@ dependencies = [
[[package]]
name = "datafusion-optimizer"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"async-trait",
"chrono",
+ "criterion",
"ctor",
"datafusion-common",
"datafusion-expr",
@@ -2393,7 +2399,8 @@ dependencies = [
"datafusion-physical-expr",
"datafusion-sql",
"env_logger",
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
+ "insta",
"itertools 0.14.0",
"log",
"recursive",
@@ -2403,7 +2410,7 @@ dependencies = [
[[package]]
name = "datafusion-physical-expr"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"ahash 0.8.11",
"arrow",
@@ -2416,7 +2423,8 @@ dependencies = [
"datafusion-physical-expr-common",
"half",
"hashbrown 0.14.5",
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
+ "insta",
"itertools 0.14.0",
"log",
"paste",
@@ -2427,7 +2435,7 @@ dependencies = [
[[package]]
name = "datafusion-physical-expr-common"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"ahash 0.8.11",
"arrow",
@@ -2439,7 +2447,7 @@ dependencies = [
[[package]]
name = "datafusion-physical-optimizer"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"datafusion-common",
@@ -2458,7 +2466,7 @@ dependencies = [
[[package]]
name = "datafusion-physical-plan"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"ahash 0.8.11",
"arrow",
@@ -2479,7 +2487,7 @@ dependencies = [
"futures",
"half",
"hashbrown 0.14.5",
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
"insta",
"itertools 0.14.0",
"log",
@@ -2488,12 +2496,13 @@ dependencies = [
"rand 0.8.5",
"rstest",
"rstest_reuse",
+ "tempfile",
"tokio",
]
[[package]]
name = "datafusion-proto"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"chrono",
@@ -2516,7 +2525,7 @@ dependencies = [
[[package]]
name = "datafusion-proto-common"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"datafusion-common",
@@ -2529,7 +2538,7 @@ dependencies = [
[[package]]
name = "datafusion-session"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"async-trait",
@@ -2551,7 +2560,7 @@ dependencies = [
[[package]]
name = "datafusion-sql"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"bigdecimal",
@@ -2563,7 +2572,8 @@ dependencies = [
"datafusion-functions-nested",
"datafusion-functions-window",
"env_logger",
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
+ "insta",
"log",
"paste",
"recursive",
@@ -2574,14 +2584,14 @@ dependencies = [
[[package]]
name = "datafusion-sqllogictest"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"arrow",
"async-trait",
"bigdecimal",
"bytes",
"chrono",
- "clap 4.5.34",
+ "clap 4.5.36",
"datafusion",
"env_logger",
"futures",
@@ -2605,7 +2615,7 @@ dependencies = [
[[package]]
name = "datafusion-substrait"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"async-recursion",
"async-trait",
@@ -2625,7 +2635,7 @@ dependencies = [
[[package]]
name = "datafusion-wasmtest"
-version = "46.0.1"
+version = "47.0.0"
dependencies = [
"chrono",
"console_error_panic_hook",
@@ -2795,9 +2805,9 @@ dependencies = [
[[package]]
name = "env_logger"
-version = "0.11.7"
+version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697"
+checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
@@ -2918,21 +2928,22 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flatbuffers"
-version = "24.12.23"
+version = "25.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f1baf0dbf96932ec9a3038d57900329c015b0bfb7b63d904f3bc27e2b02a096"
+checksum = "1045398c1bfd89168b5fd3f1fc11f6e70b34f6f66300c87d44d3de849463abf1"
dependencies = [
- "bitflags 1.3.2",
+ "bitflags 2.8.0",
"rustc_version",
]
[[package]]
name = "flate2"
-version = "1.1.0"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
+checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
dependencies = [
"crc32fast",
+ "libz-rs-sys",
"miniz_oxide",
]
@@ -3179,7 +3190,7 @@ dependencies = [
"futures-core",
"futures-sink",
"http 1.2.0",
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
"slab",
"tokio",
"tokio-util",
@@ -3188,9 +3199,9 @@ dependencies = [
[[package]]
name = "half"
-version = "2.5.0"
+version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1"
+checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
dependencies = [
"cfg-if",
"crunchy",
@@ -3628,9 +3639,9 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.8.0"
+version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
+checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
@@ -3867,9 +3878,9 @@ dependencies = [
[[package]]
name = "libc"
-version = "0.2.171"
+version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
+checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libflate"
@@ -3923,9 +3934,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "libmimalloc-sys"
-version = "0.1.40"
+version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07d0e07885d6a754b9c7993f2625187ad694ee985d60f23355ff0e7077261502"
+checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4"
dependencies = [
"cc",
"libc",
@@ -3950,10 +3961,19 @@ checksum = "5297962ef19edda4ce33aaa484386e0a5b3d7f2f4e037cbeee00503ef6b29d33"
dependencies = [
"anstream",
"anstyle",
- "clap 4.5.34",
+ "clap 4.5.36",
"escape8259",
]
+[[package]]
+name = "libz-rs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a"
+dependencies = [
+ "zlib-rs",
+]
+
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@@ -4000,7 +4020,7 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5"
dependencies = [
- "twox-hash",
+ "twox-hash 1.6.3",
]
[[package]]
@@ -4047,9 +4067,9 @@ dependencies = [
[[package]]
name = "mimalloc"
-version = "0.1.44"
+version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99585191385958383e13f6b822e6b6d8d9cf928e7d286ceb092da92b43c87bc1"
+checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af"
dependencies = [
"libmimalloc-sys",
]
@@ -4078,9 +4098,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
-version = "0.8.4"
+version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
+checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
dependencies = [
"adler2",
]
@@ -4245,6 +4265,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925"
+dependencies = [
+ "bitflags 2.8.0",
+]
+
[[package]]
name = "object"
version = "0.36.7"
@@ -4256,18 +4285,21 @@ dependencies = [
[[package]]
name = "object_store"
-version = "0.11.2"
+version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3cfccb68961a56facde1163f9319e0d15743352344e7808a11795fb99698dcaf"
+checksum = "e9ce831b09395f933addbc56d894d889e4b226eba304d4e7adbab591e26daf1e"
dependencies = [
"async-trait",
"base64 0.22.1",
"bytes",
"chrono",
+ "form_urlencoded",
"futures",
+ "http 1.2.0",
+ "http-body-util",
"humantime",
"hyper",
- "itertools 0.13.0",
+ "itertools 0.14.0",
"md-5",
"parking_lot",
"percent-encoding",
@@ -4278,7 +4310,8 @@ dependencies = [
"rustls-pemfile",
"serde",
"serde_json",
- "snafu",
+ "serde_urlencoded",
+ "thiserror 2.0.12",
"tokio",
"tracing",
"url",
@@ -4361,9 +4394,9 @@ dependencies = [
[[package]]
name = "parquet"
-version = "54.2.1"
+version = "55.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f88838dca3b84d41444a0341b19f347e8098a3898b0f21536654b8b799e11abd"
+checksum = "cd31a8290ac5b19f09ad77ee7a1e6a541f1be7674ad410547d5f1eef6eef4a9c"
dependencies = [
"ahash 0.8.11",
"arrow-array",
@@ -4391,9 +4424,8 @@ dependencies = [
"snap",
"thrift",
"tokio",
- "twox-hash",
+ "twox-hash 2.1.0",
"zstd",
- "zstd-sys",
]
[[package]]
@@ -4486,7 +4518,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
dependencies = [
"fixedbitset",
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
]
[[package]]
@@ -4840,9 +4872,9 @@ dependencies = [
[[package]]
name = "pyo3"
-version = "0.23.5"
+version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872"
+checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219"
dependencies = [
"cfg-if",
"indoc",
@@ -4858,9 +4890,9 @@ dependencies = [
[[package]]
name = "pyo3-build-config"
-version = "0.23.5"
+version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb"
+checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999"
dependencies = [
"once_cell",
"target-lexicon",
@@ -4868,9 +4900,9 @@ dependencies = [
[[package]]
name = "pyo3-ffi"
-version = "0.23.5"
+version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d"
+checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33"
dependencies = [
"libc",
"pyo3-build-config",
@@ -4878,9 +4910,9 @@ dependencies = [
[[package]]
name = "pyo3-macros"
-version = "0.23.5"
+version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da"
+checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
@@ -4890,9 +4922,9 @@ dependencies = [
[[package]]
name = "pyo3-macros-backend"
-version = "0.23.5"
+version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028"
+checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -5689,7 +5721,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
"serde",
"serde_derive",
"serde_json",
@@ -5715,7 +5747,7 @@ version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
"itoa",
"ryu",
"serde",
@@ -5790,27 +5822,6 @@ version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
-[[package]]
-name = "snafu"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019"
-dependencies = [
- "snafu-derive",
-]
-
-[[package]]
-name = "snafu-derive"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917"
-dependencies = [
- "heck 0.5.0",
- "proc-macro2",
- "quote",
- "syn 2.0.100",
-]
-
[[package]]
name = "snap"
version = "1.1.1"
@@ -5847,9 +5858,9 @@ dependencies = [
[[package]]
name = "sqllogictest"
-version = "0.28.0"
+version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17b2f0b80fc250ed3fdd82fc88c0ada5ad62ee1ed5314ac5474acfa52082f518"
+checksum = "ee6199c1e008acc669b1e5873c138bf3ad4f8709ccd5c5d88913e664ae4f75de"
dependencies = [
"async-trait",
"educe",
@@ -6108,15 +6119,14 @@ dependencies = [
[[package]]
name = "sysinfo"
-version = "0.33.1"
+version = "0.34.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01"
+checksum = "a4b93974b3d3aeaa036504b8eefd4c039dced109171c1ae973f1dc63b2c7e4b2"
dependencies = [
- "core-foundation-sys",
"libc",
"memchr",
"ntapi",
- "rayon",
+ "objc2-core-foundation",
"windows",
]
@@ -6128,9 +6138,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "target-lexicon"
-version = "0.12.16"
+version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
[[package]]
name = "tempfile"
@@ -6461,7 +6471,7 @@ version = "0.22.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
dependencies = [
- "indexmap 2.8.0",
+ "indexmap 2.9.0",
"toml_datetime",
"winnow",
]
@@ -6631,6 +6641,12 @@ dependencies = [
"static_assertions",
]
+[[package]]
+name = "twox-hash"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7b17f197b3050ba473acf9181f7b1d3b66d1cf7356c6cc57886662276e65908"
+
[[package]]
name = "typed-arena"
version = "2.0.2"
@@ -7123,6 +7139,12 @@ dependencies = [
"syn 2.0.100",
]
+[[package]]
+name = "windows-link"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
+
[[package]]
name = "windows-registry"
version = "0.2.0"
@@ -7489,6 +7511,12 @@ dependencies = [
"syn 2.0.100",
]
+[[package]]
+name = "zlib-rs"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8"
+
[[package]]
name = "zstd"
version = "0.13.3"
diff --git a/Cargo.toml b/Cargo.toml
index b6164f89d31e8..5a735666f8e7e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -75,7 +75,7 @@ repository = "https://github.com/apache/datafusion"
# Define Minimum Supported Rust Version (MSRV)
rust-version = "1.82.0"
# Define DataFusion version
-version = "46.0.1"
+version = "47.0.0"
[workspace.dependencies]
# We turn off default-features for some dependencies here so the workspaces which inherit them can
@@ -87,69 +87,69 @@ ahash = { version = "0.8", default-features = false, features = [
"runtime-rng",
] }
apache-avro = { version = "0.17", default-features = false }
-arrow = { version = "54.2.1", features = [
+arrow = { version = "55.0.0", features = [
"prettyprint",
"chrono-tz",
] }
-arrow-buffer = { version = "54.1.0", default-features = false }
-arrow-flight = { version = "54.2.1", features = [
+arrow-buffer = { version = "55.0.0", default-features = false }
+arrow-flight = { version = "55.0.0", features = [
"flight-sql-experimental",
] }
-arrow-ipc = { version = "54.2.0", default-features = false, features = [
+arrow-ipc = { version = "55.0.0", default-features = false, features = [
"lz4",
] }
-arrow-ord = { version = "54.1.0", default-features = false }
-arrow-schema = { version = "54.1.0", default-features = false }
+arrow-ord = { version = "55.0.0", default-features = false }
+arrow-schema = { version = "55.0.0", default-features = false }
async-trait = "0.1.88"
-bigdecimal = "0.4.7"
+bigdecimal = "0.4.8"
bytes = "1.10"
chrono = { version = "0.4.38", default-features = false }
criterion = "0.5.1"
ctor = "0.2.9"
dashmap = "6.0.1"
-datafusion = { path = "datafusion/core", version = "46.0.1", default-features = false }
-datafusion-catalog = { path = "datafusion/catalog", version = "46.0.1" }
-datafusion-catalog-listing = { path = "datafusion/catalog-listing", version = "46.0.1" }
-datafusion-common = { path = "datafusion/common", version = "46.0.1", default-features = false }
-datafusion-common-runtime = { path = "datafusion/common-runtime", version = "46.0.1" }
-datafusion-datasource = { path = "datafusion/datasource", version = "46.0.1", default-features = false }
-datafusion-datasource-avro = { path = "datafusion/datasource-avro", version = "46.0.1", default-features = false }
-datafusion-datasource-csv = { path = "datafusion/datasource-csv", version = "46.0.1", default-features = false }
-datafusion-datasource-json = { path = "datafusion/datasource-json", version = "46.0.1", default-features = false }
-datafusion-datasource-parquet = { path = "datafusion/datasource-parquet", version = "46.0.1", default-features = false }
-datafusion-doc = { path = "datafusion/doc", version = "46.0.1" }
-datafusion-execution = { path = "datafusion/execution", version = "46.0.1" }
-datafusion-expr = { path = "datafusion/expr", version = "46.0.1" }
-datafusion-expr-common = { path = "datafusion/expr-common", version = "46.0.1" }
-datafusion-ffi = { path = "datafusion/ffi", version = "46.0.1" }
-datafusion-functions = { path = "datafusion/functions", version = "46.0.1" }
-datafusion-functions-aggregate = { path = "datafusion/functions-aggregate", version = "46.0.1" }
-datafusion-functions-aggregate-common = { path = "datafusion/functions-aggregate-common", version = "46.0.1" }
-datafusion-functions-nested = { path = "datafusion/functions-nested", version = "46.0.1" }
-datafusion-functions-table = { path = "datafusion/functions-table", version = "46.0.1" }
-datafusion-functions-window = { path = "datafusion/functions-window", version = "46.0.1" }
-datafusion-functions-window-common = { path = "datafusion/functions-window-common", version = "46.0.1" }
-datafusion-macros = { path = "datafusion/macros", version = "46.0.1" }
-datafusion-optimizer = { path = "datafusion/optimizer", version = "46.0.1", default-features = false }
-datafusion-physical-expr = { path = "datafusion/physical-expr", version = "46.0.1", default-features = false }
-datafusion-physical-expr-common = { path = "datafusion/physical-expr-common", version = "46.0.1", default-features = false }
-datafusion-physical-optimizer = { path = "datafusion/physical-optimizer", version = "46.0.1" }
-datafusion-physical-plan = { path = "datafusion/physical-plan", version = "46.0.1" }
-datafusion-proto = { path = "datafusion/proto", version = "46.0.1" }
-datafusion-proto-common = { path = "datafusion/proto-common", version = "46.0.1" }
-datafusion-session = { path = "datafusion/session", version = "46.0.1" }
-datafusion-sql = { path = "datafusion/sql", version = "46.0.1" }
+datafusion = { path = "datafusion/core", version = "47.0.0", default-features = false }
+datafusion-catalog = { path = "datafusion/catalog", version = "47.0.0" }
+datafusion-catalog-listing = { path = "datafusion/catalog-listing", version = "47.0.0" }
+datafusion-common = { path = "datafusion/common", version = "47.0.0", default-features = false }
+datafusion-common-runtime = { path = "datafusion/common-runtime", version = "47.0.0" }
+datafusion-datasource = { path = "datafusion/datasource", version = "47.0.0", default-features = false }
+datafusion-datasource-avro = { path = "datafusion/datasource-avro", version = "47.0.0", default-features = false }
+datafusion-datasource-csv = { path = "datafusion/datasource-csv", version = "47.0.0", default-features = false }
+datafusion-datasource-json = { path = "datafusion/datasource-json", version = "47.0.0", default-features = false }
+datafusion-datasource-parquet = { path = "datafusion/datasource-parquet", version = "47.0.0", default-features = false }
+datafusion-doc = { path = "datafusion/doc", version = "47.0.0" }
+datafusion-execution = { path = "datafusion/execution", version = "47.0.0" }
+datafusion-expr = { path = "datafusion/expr", version = "47.0.0" }
+datafusion-expr-common = { path = "datafusion/expr-common", version = "47.0.0" }
+datafusion-ffi = { path = "datafusion/ffi", version = "47.0.0" }
+datafusion-functions = { path = "datafusion/functions", version = "47.0.0" }
+datafusion-functions-aggregate = { path = "datafusion/functions-aggregate", version = "47.0.0" }
+datafusion-functions-aggregate-common = { path = "datafusion/functions-aggregate-common", version = "47.0.0" }
+datafusion-functions-nested = { path = "datafusion/functions-nested", version = "47.0.0" }
+datafusion-functions-table = { path = "datafusion/functions-table", version = "47.0.0" }
+datafusion-functions-window = { path = "datafusion/functions-window", version = "47.0.0" }
+datafusion-functions-window-common = { path = "datafusion/functions-window-common", version = "47.0.0" }
+datafusion-macros = { path = "datafusion/macros", version = "47.0.0" }
+datafusion-optimizer = { path = "datafusion/optimizer", version = "47.0.0", default-features = false }
+datafusion-physical-expr = { path = "datafusion/physical-expr", version = "47.0.0", default-features = false }
+datafusion-physical-expr-common = { path = "datafusion/physical-expr-common", version = "47.0.0", default-features = false }
+datafusion-physical-optimizer = { path = "datafusion/physical-optimizer", version = "47.0.0" }
+datafusion-physical-plan = { path = "datafusion/physical-plan", version = "47.0.0" }
+datafusion-proto = { path = "datafusion/proto", version = "47.0.0" }
+datafusion-proto-common = { path = "datafusion/proto-common", version = "47.0.0" }
+datafusion-session = { path = "datafusion/session", version = "47.0.0" }
+datafusion-sql = { path = "datafusion/sql", version = "47.0.0" }
doc-comment = "0.3"
env_logger = "0.11"
futures = "0.3"
-half = { version = "2.5.0", default-features = false }
+half = { version = "2.6.0", default-features = false }
hashbrown = { version = "0.14.5", features = ["raw"] }
-indexmap = "2.8.0"
+indexmap = "2.9.0"
itertools = "0.14"
log = "^0.4"
-object_store = { version = "0.11.0", default-features = false }
+object_store = { version = "0.12.0", default-features = false }
parking_lot = "0.12"
-parquet = { version = "54.2.1", default-features = false, features = [
+parquet = { version = "55.0.0", default-features = false, features = [
"arrow",
"async",
"object_store",
@@ -191,13 +191,20 @@ strip = false # Retain debug info for flamegraphs
inherits = "dev"
incremental = false
-# ci turns off debug info, etc for dependencies to allow for smaller binaries making caching more effective
+# ci turns off debug info, etc. for dependencies to allow for smaller binaries making caching more effective
[profile.ci.package."*"]
debug = false
debug-assertions = false
strip = "debuginfo"
incremental = false
+# release inherited profile keeping debug information and symbols
+# for mem/cpu profiling
+[profile.profiling]
+inherits = "release"
+debug = true
+strip = false
+
[workspace.lints.clippy]
# Detects large stack-allocated futures that may cause stack overflow crashes (see threshold in clippy.toml)
large_futures = "warn"
diff --git a/benchmarks/README.md b/benchmarks/README.md
index 8acaa298bd3ad..86b2e1b3b958f 100644
--- a/benchmarks/README.md
+++ b/benchmarks/README.md
@@ -200,6 +200,16 @@ cargo run --release --bin tpch -- convert --input ./data --output /mnt/tpch-parq
Or if you want to verify and run all the queries in the benchmark, you can just run `cargo test`.
+#### Sorted Conversion
+
+The TPCH tables generated by the dbgen utility are sorted by their first column (their primary key for most tables, the `l_orderkey` column for the `lineitem` table.)
+
+To preserve this sorted order information during conversion (useful for benchmarking execution on pre-sorted data) include the `--sort` flag:
+
+```bash
+cargo run --release --bin tpch -- convert --input ./data --output /mnt/tpch-sorted-parquet --format parquet --sort
+```
+
### Comparing results between runs
Any `dfbench` execution with `-o
` argument will produce a
@@ -445,20 +455,29 @@ Test performance of end-to-end sort SQL queries. (While the `Sort` benchmark foc
Sort integration benchmark runs whole table sort queries on TPCH `lineitem` table, with different characteristics. For example, different number of sort keys, different sort key cardinality, different number of payload columns, etc.
+If the TPCH tables have been converted as sorted on their first column (see [Sorted Conversion](#sorted-conversion)), you can use the `--sorted` flag to indicate that the input data is pre-sorted, allowing DataFusion to leverage that order during query execution.
+
+Additionally, an optional `--limit` flag is available for the sort benchmark. When specified, this flag appends a `LIMIT n` clause to the SQL query, effectively converting the query into a TopK query. Combining the `--sorted` and `--limit` options enables benchmarking of TopK queries on pre-sorted inputs.
+
See [`sort_tpch.rs`](src/sort_tpch.rs) for more details.
### Sort TPCH Benchmark Example Runs
1. Run all queries with default setting:
```bash
- cargo run --release --bin dfbench -- sort-tpch -p '....../datafusion/benchmarks/data/tpch_sf1' -o '/tmp/sort_tpch.json'
+ cargo run --release --bin dfbench -- sort-tpch -p './datafusion/benchmarks/data/tpch_sf1' -o '/tmp/sort_tpch.json'
```
2. Run a specific query:
```bash
- cargo run --release --bin dfbench -- sort-tpch -p '....../datafusion/benchmarks/data/tpch_sf1' -o '/tmp/sort_tpch.json' --query 2
+ cargo run --release --bin dfbench -- sort-tpch -p './datafusion/benchmarks/data/tpch_sf1' -o '/tmp/sort_tpch.json' --query 2
```
-3. Run all queries with `bench.sh` script:
+3. Run all queries as TopK queries on presorted data:
+```bash
+ cargo run --release --bin dfbench -- sort-tpch --sorted --limit 10 -p './datafusion/benchmarks/data/tpch_sf1' -o '/tmp/sort_tpch.json'
+```
+
+4. Run all queries with `bench.sh` script:
```bash
./bench.sh run sort_tpch
```
diff --git a/benchmarks/bench.sh b/benchmarks/bench.sh
index 5be825eb0dafd..5d3ad3446ddb9 100755
--- a/benchmarks/bench.sh
+++ b/benchmarks/bench.sh
@@ -412,7 +412,10 @@ run_tpch() {
echo "Running tpch benchmark..."
# Optional query filter to run specific query
QUERY=$([ -n "$ARG3" ] && echo "--query $ARG3" || echo "")
+ # debug the target command
+ set -x
$CARGO_COMMAND --bin tpch -- benchmark datafusion --iterations 5 --path "${TPCH_DIR}" --prefer_hash_join "${PREFER_HASH_JOIN}" --format parquet -o "${RESULTS_FILE}" $QUERY
+ set +x
}
# Runs the tpch in memory
@@ -427,9 +430,13 @@ run_tpch_mem() {
RESULTS_FILE="${RESULTS_DIR}/tpch_mem_sf${SCALE_FACTOR}.json"
echo "RESULTS_FILE: ${RESULTS_FILE}"
echo "Running tpch_mem benchmark..."
+ # Optional query filter to run specific query
QUERY=$([ -n "$ARG3" ] && echo "--query $ARG3" || echo "")
+ # debug the target command
+ set -x
# -m means in memory
$CARGO_COMMAND --bin tpch -- benchmark datafusion --iterations 5 --path "${TPCH_DIR}" --prefer_hash_join "${PREFER_HASH_JOIN}" -m --format parquet -o "${RESULTS_FILE}" $QUERY
+ set +x
}
# Runs the cancellation benchmark
diff --git a/benchmarks/queries/clickbench/README.md b/benchmarks/queries/clickbench/README.md
index 6797797409c1a..fdb7d1676be0f 100644
--- a/benchmarks/queries/clickbench/README.md
+++ b/benchmarks/queries/clickbench/README.md
@@ -93,12 +93,14 @@ LIMIT 10;
Results look like
+```
+-------------+---------------------+---+------+------+------+
| ClientIP | WatchID | c | tmin | tmed | tmax |
+-------------+---------------------+---+------+------+------+
| 1611957945 | 6655575552203051303 | 2 | 0 | 0 | 0 |
| -1402644643 | 8566928176839891583 | 2 | 0 | 0 | 0 |
+-------------+---------------------+---+------+------+------+
+```
### Q5: Response start time distribution analysis (p95)
@@ -120,13 +122,42 @@ LIMIT 10;
```
Results look like
-
+```
+-------------+---------------------+---+------+------+------+
| ClientIP | WatchID | c | tmin | tp95 | tmax |
+-------------+---------------------+---+------+------+------+
| 1611957945 | 6655575552203051303 | 2 | 0 | 0 | 0 |
| -1402644643 | 8566928176839891583 | 2 | 0 | 0 | 0 |
+-------------+---------------------+---+------+------+------+
+```
+
+### Q6: How many social shares meet complex multi-stage filtering criteria?
+**Question**: What is the count of sharing actions from iPhone mobile users on specific social networks, within common timezones, participating in seasonal campaigns, with high screen resolutions and closely matched UTM parameters?
+**Important Query Properties**: Simple filter with high-selectivity, Costly string matching, A large number of filters with high overhead are positioned relatively later in the process
+
+```sql
+SELECT COUNT(*) AS ShareCount
+FROM hits
+WHERE
+ -- Stage 1: High-selectivity filters (fast)
+ "IsMobile" = 1 -- Filter mobile users
+ AND "MobilePhoneModel" LIKE 'iPhone%' -- Match iPhone models
+ AND "SocialAction" = 'share' -- Identify social sharing actions
+
+ -- Stage 2: Moderate filters (cheap)
+ AND "SocialSourceNetworkID" IN (5, 12) -- Filter specific social networks
+ AND "ClientTimeZone" BETWEEN -5 AND 5 -- Restrict to common timezones
+
+ -- Stage 3: Heavy computations (expensive)
+ AND regexp_match("Referer", '\/campaign\/(spring|summer)_promo') IS NOT NULL -- Find campaign-specific referrers
+ AND CASE
+ WHEN split_part(split_part("URL", 'resolution=', 2), '&', 1) ~ '^\d+$'
+ THEN split_part(split_part("URL", 'resolution=', 2), '&', 1)::INT
+ ELSE 0
+ END > 1920 -- Extract and validate resolution parameter
+ AND levenshtein(CAST("UTMSource" AS STRING), CAST("UTMCampaign" AS STRING)) < 3 -- Verify UTM parameter similarity
+```
+Result is empty,Since it has already been filtered by `"SocialAction" = 'share'`.
## Data Notes
diff --git a/benchmarks/queries/clickbench/extended.sql b/benchmarks/queries/clickbench/extended.sql
index fbabaf2a70218..e967583fd6442 100644
--- a/benchmarks/queries/clickbench/extended.sql
+++ b/benchmarks/queries/clickbench/extended.sql
@@ -3,4 +3,5 @@ SELECT COUNT(DISTINCT "HitColor"), COUNT(DISTINCT "BrowserCountry"), COUNT(DISTI
SELECT "BrowserCountry", COUNT(DISTINCT "SocialNetwork"), COUNT(DISTINCT "HitColor"), COUNT(DISTINCT "BrowserLanguage"), COUNT(DISTINCT "SocialAction") FROM hits GROUP BY 1 ORDER BY 2 DESC LIMIT 10;
SELECT "SocialSourceNetworkID", "RegionID", COUNT(*), AVG("Age"), AVG("ParamPrice"), STDDEV("ParamPrice") as s, VAR("ParamPrice") FROM hits GROUP BY "SocialSourceNetworkID", "RegionID" HAVING s IS NOT NULL ORDER BY s DESC LIMIT 10;
SELECT "ClientIP", "WatchID", COUNT(*) c, MIN("ResponseStartTiming") tmin, MEDIAN("ResponseStartTiming") tmed, MAX("ResponseStartTiming") tmax FROM hits WHERE "JavaEnable" = 0 GROUP BY "ClientIP", "WatchID" HAVING c > 1 ORDER BY tmed DESC LIMIT 10;
-SELECT "ClientIP", "WatchID", COUNT(*) c, MIN("ResponseStartTiming") tmin, APPROX_PERCENTILE_CONT("ResponseStartTiming", 0.95) tp95, MAX("ResponseStartTiming") tmax FROM 'hits' WHERE "JavaEnable" = 0 GROUP BY "ClientIP", "WatchID" HAVING c > 1 ORDER BY tp95 DESC LIMIT 10;
\ No newline at end of file
+SELECT "ClientIP", "WatchID", COUNT(*) c, MIN("ResponseStartTiming") tmin, APPROX_PERCENTILE_CONT("ResponseStartTiming", 0.95) tp95, MAX("ResponseStartTiming") tmax FROM 'hits' WHERE "JavaEnable" = 0 GROUP BY "ClientIP", "WatchID" HAVING c > 1 ORDER BY tp95 DESC LIMIT 10;
+SELECT COUNT(*) AS ShareCount FROM hits WHERE "IsMobile" = 1 AND "MobilePhoneModel" LIKE 'iPhone%' AND "SocialAction" = 'share' AND "SocialSourceNetworkID" IN (5, 12) AND "ClientTimeZone" BETWEEN -5 AND 5 AND regexp_match("Referer", '\/campaign\/(spring|summer)_promo') IS NOT NULL AND CASE WHEN split_part(split_part("URL", 'resolution=', 2), '&', 1) ~ '^\d+$' THEN split_part(split_part("URL", 'resolution=', 2), '&', 1)::INT ELSE 0 END > 1920 AND levenshtein(CAST("UTMSource" AS STRING), CAST("UTMCampaign" AS STRING)) < 3;
diff --git a/benchmarks/queries/clickbench/queries.sql b/benchmarks/queries/clickbench/queries.sql
index 52e72e02e1e0d..9a183cd6e259c 100644
--- a/benchmarks/queries/clickbench/queries.sql
+++ b/benchmarks/queries/clickbench/queries.sql
@@ -4,7 +4,7 @@ SELECT SUM("AdvEngineID"), COUNT(*), AVG("ResolutionWidth") FROM hits;
SELECT AVG("UserID") FROM hits;
SELECT COUNT(DISTINCT "UserID") FROM hits;
SELECT COUNT(DISTINCT "SearchPhrase") FROM hits;
-SELECT MIN("EventDate"::INT::DATE), MAX("EventDate"::INT::DATE) FROM hits;
+SELECT MIN("EventDate"), MAX("EventDate") FROM hits;
SELECT "AdvEngineID", COUNT(*) FROM hits WHERE "AdvEngineID" <> 0 GROUP BY "AdvEngineID" ORDER BY COUNT(*) DESC;
SELECT "RegionID", COUNT(DISTINCT "UserID") AS u FROM hits GROUP BY "RegionID" ORDER BY u DESC LIMIT 10;
SELECT "RegionID", SUM("AdvEngineID"), COUNT(*) AS c, AVG("ResolutionWidth"), COUNT(DISTINCT "UserID") FROM hits GROUP BY "RegionID" ORDER BY c DESC LIMIT 10;
@@ -21,10 +21,10 @@ SELECT "UserID" FROM hits WHERE "UserID" = 435090932899640449;
SELECT COUNT(*) FROM hits WHERE "URL" LIKE '%google%';
SELECT "SearchPhrase", MIN("URL"), COUNT(*) AS c FROM hits WHERE "URL" LIKE '%google%' AND "SearchPhrase" <> '' GROUP BY "SearchPhrase" ORDER BY c DESC LIMIT 10;
SELECT "SearchPhrase", MIN("URL"), MIN("Title"), COUNT(*) AS c, COUNT(DISTINCT "UserID") FROM hits WHERE "Title" LIKE '%Google%' AND "URL" NOT LIKE '%.google.%' AND "SearchPhrase" <> '' GROUP BY "SearchPhrase" ORDER BY c DESC LIMIT 10;
-SELECT * FROM hits WHERE "URL" LIKE '%google%' ORDER BY to_timestamp_seconds("EventTime") LIMIT 10;
-SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY to_timestamp_seconds("EventTime") LIMIT 10;
+SELECT * FROM hits WHERE "URL" LIKE '%google%' ORDER BY "EventTime" LIMIT 10;
+SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "EventTime" LIMIT 10;
SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10;
-SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY to_timestamp_seconds("EventTime"), "SearchPhrase" LIMIT 10;
+SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "EventTime", "SearchPhrase" LIMIT 10;
SELECT "CounterID", AVG(length("URL")) AS l, COUNT(*) AS c FROM hits WHERE "URL" <> '' GROUP BY "CounterID" HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25;
SELECT REGEXP_REPLACE("Referer", '^https?://(?:www\.)?([^/]+)/.*$', '\1') AS k, AVG(length("Referer")) AS l, COUNT(*) AS c, MIN("Referer") FROM hits WHERE "Referer" <> '' GROUP BY k HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25;
SELECT SUM("ResolutionWidth"), SUM("ResolutionWidth" + 1), SUM("ResolutionWidth" + 2), SUM("ResolutionWidth" + 3), SUM("ResolutionWidth" + 4), SUM("ResolutionWidth" + 5), SUM("ResolutionWidth" + 6), SUM("ResolutionWidth" + 7), SUM("ResolutionWidth" + 8), SUM("ResolutionWidth" + 9), SUM("ResolutionWidth" + 10), SUM("ResolutionWidth" + 11), SUM("ResolutionWidth" + 12), SUM("ResolutionWidth" + 13), SUM("ResolutionWidth" + 14), SUM("ResolutionWidth" + 15), SUM("ResolutionWidth" + 16), SUM("ResolutionWidth" + 17), SUM("ResolutionWidth" + 18), SUM("ResolutionWidth" + 19), SUM("ResolutionWidth" + 20), SUM("ResolutionWidth" + 21), SUM("ResolutionWidth" + 22), SUM("ResolutionWidth" + 23), SUM("ResolutionWidth" + 24), SUM("ResolutionWidth" + 25), SUM("ResolutionWidth" + 26), SUM("ResolutionWidth" + 27), SUM("ResolutionWidth" + 28), SUM("ResolutionWidth" + 29), SUM("ResolutionWidth" + 30), SUM("ResolutionWidth" + 31), SUM("ResolutionWidth" + 32), SUM("ResolutionWidth" + 33), SUM("ResolutionWidth" + 34), SUM("ResolutionWidth" + 35), SUM("ResolutionWidth" + 36), SUM("ResolutionWidth" + 37), SUM("ResolutionWidth" + 38), SUM("ResolutionWidth" + 39), SUM("ResolutionWidth" + 40), SUM("ResolutionWidth" + 41), SUM("ResolutionWidth" + 42), SUM("ResolutionWidth" + 43), SUM("ResolutionWidth" + 44), SUM("ResolutionWidth" + 45), SUM("ResolutionWidth" + 46), SUM("ResolutionWidth" + 47), SUM("ResolutionWidth" + 48), SUM("ResolutionWidth" + 49), SUM("ResolutionWidth" + 50), SUM("ResolutionWidth" + 51), SUM("ResolutionWidth" + 52), SUM("ResolutionWidth" + 53), SUM("ResolutionWidth" + 54), SUM("ResolutionWidth" + 55), SUM("ResolutionWidth" + 56), SUM("ResolutionWidth" + 57), SUM("ResolutionWidth" + 58), SUM("ResolutionWidth" + 59), SUM("ResolutionWidth" + 60), SUM("ResolutionWidth" + 61), SUM("ResolutionWidth" + 62), SUM("ResolutionWidth" + 63), SUM("ResolutionWidth" + 64), SUM("ResolutionWidth" + 65), SUM("ResolutionWidth" + 66), SUM("ResolutionWidth" + 67), SUM("ResolutionWidth" + 68), SUM("ResolutionWidth" + 69), SUM("ResolutionWidth" + 70), SUM("ResolutionWidth" + 71), SUM("ResolutionWidth" + 72), SUM("ResolutionWidth" + 73), SUM("ResolutionWidth" + 74), SUM("ResolutionWidth" + 75), SUM("ResolutionWidth" + 76), SUM("ResolutionWidth" + 77), SUM("ResolutionWidth" + 78), SUM("ResolutionWidth" + 79), SUM("ResolutionWidth" + 80), SUM("ResolutionWidth" + 81), SUM("ResolutionWidth" + 82), SUM("ResolutionWidth" + 83), SUM("ResolutionWidth" + 84), SUM("ResolutionWidth" + 85), SUM("ResolutionWidth" + 86), SUM("ResolutionWidth" + 87), SUM("ResolutionWidth" + 88), SUM("ResolutionWidth" + 89) FROM hits;
@@ -34,10 +34,10 @@ SELECT "WatchID", "ClientIP", COUNT(*) AS c, SUM("IsRefresh"), AVG("ResolutionWi
SELECT "URL", COUNT(*) AS c FROM hits GROUP BY "URL" ORDER BY c DESC LIMIT 10;
SELECT 1, "URL", COUNT(*) AS c FROM hits GROUP BY 1, "URL" ORDER BY c DESC LIMIT 10;
SELECT "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3, COUNT(*) AS c FROM hits GROUP BY "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3 ORDER BY c DESC LIMIT 10;
-SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "URL" <> '' GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10;
-SELECT "Title", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "Title" <> '' GROUP BY "Title" ORDER BY PageViews DESC LIMIT 10;
-SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 AND "IsLink" <> 0 AND "IsDownload" = 0 GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10 OFFSET 1000;
-SELECT "TraficSourceID", "SearchEngineID", "AdvEngineID", CASE WHEN ("SearchEngineID" = 0 AND "AdvEngineID" = 0) THEN "Referer" ELSE '' END AS Src, "URL" AS Dst, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 GROUP BY "TraficSourceID", "SearchEngineID", "AdvEngineID", Src, Dst ORDER BY PageViews DESC LIMIT 10 OFFSET 1000;
-SELECT "URLHash", "EventDate"::INT::DATE, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 AND "TraficSourceID" IN (-1, 6) AND "RefererHash" = 3594120000172545465 GROUP BY "URLHash", "EventDate"::INT::DATE ORDER BY PageViews DESC LIMIT 10 OFFSET 100;
-SELECT "WindowClientWidth", "WindowClientHeight", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 AND "DontCountHits" = 0 AND "URLHash" = 2868770270353813622 GROUP BY "WindowClientWidth", "WindowClientHeight" ORDER BY PageViews DESC LIMIT 10 OFFSET 10000;
-SELECT DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) AS M, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-14' AND "EventDate"::INT::DATE <= '2013-07-15' AND "IsRefresh" = 0 AND "DontCountHits" = 0 GROUP BY DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) ORDER BY DATE_TRUNC('minute', M) LIMIT 10 OFFSET 1000;
+SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "URL" <> '' GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10;
+SELECT "Title", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "Title" <> '' GROUP BY "Title" ORDER BY PageViews DESC LIMIT 10;
+SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 AND "IsLink" <> 0 AND "IsDownload" = 0 GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10 OFFSET 1000;
+SELECT "TraficSourceID", "SearchEngineID", "AdvEngineID", CASE WHEN ("SearchEngineID" = 0 AND "AdvEngineID" = 0) THEN "Referer" ELSE '' END AS Src, "URL" AS Dst, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 GROUP BY "TraficSourceID", "SearchEngineID", "AdvEngineID", Src, Dst ORDER BY PageViews DESC LIMIT 10 OFFSET 1000;
+SELECT "URLHash", "EventDate", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 AND "TraficSourceID" IN (-1, 6) AND "RefererHash" = 3594120000172545465 GROUP BY "URLHash", "EventDate" ORDER BY PageViews DESC LIMIT 10 OFFSET 100;
+SELECT "WindowClientWidth", "WindowClientHeight", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 AND "DontCountHits" = 0 AND "URLHash" = 2868770270353813622 GROUP BY "WindowClientWidth", "WindowClientHeight" ORDER BY PageViews DESC LIMIT 10 OFFSET 10000;
+SELECT DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) AS M, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-14' AND "EventDate" <= '2013-07-15' AND "IsRefresh" = 0 AND "DontCountHits" = 0 GROUP BY DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) ORDER BY DATE_TRUNC('minute', M) LIMIT 10 OFFSET 1000;
diff --git a/benchmarks/src/sort_tpch.rs b/benchmarks/src/sort_tpch.rs
index 956bb92b6c78d..176234eca541c 100644
--- a/benchmarks/src/sort_tpch.rs
+++ b/benchmarks/src/sort_tpch.rs
@@ -63,6 +63,15 @@ pub struct RunOpt {
/// Load the data into a MemTable before executing the query
#[structopt(short = "m", long = "mem-table")]
mem_table: bool,
+
+ /// Mark the first column of each table as sorted in ascending order.
+ /// The tables should have been created with the `--sort` option for this to have any effect.
+ #[structopt(short = "t", long = "sorted")]
+ sorted: bool,
+
+ /// Append a `LIMIT n` clause to the query
+ #[structopt(short = "l", long = "limit")]
+ limit: Option,
}
struct QueryResult {
@@ -163,7 +172,7 @@ impl RunOpt {
r#"
SELECT l_shipmode, l_comment, l_partkey
FROM lineitem
- ORDER BY l_shipmode;
+ ORDER BY l_shipmode
"#,
];
@@ -212,9 +221,14 @@ impl RunOpt {
let start = Instant::now();
let query_idx = query_id - 1; // 1-indexed -> 0-indexed
- let sql = Self::SORT_QUERIES[query_idx];
+ let base_sql = Self::SORT_QUERIES[query_idx].to_string();
+ let sql = if let Some(limit) = self.limit {
+ format!("{base_sql} LIMIT {limit}")
+ } else {
+ base_sql
+ };
- let row_count = self.execute_query(&ctx, sql).await?;
+ let row_count = self.execute_query(&ctx, sql.as_str()).await?;
let elapsed = start.elapsed(); //.as_secs_f64() * 1000.0;
let ms = elapsed.as_secs_f64() * 1000.0;
@@ -315,8 +329,18 @@ impl RunOpt {
.with_collect_stat(state.config().collect_statistics());
let table_path = ListingTableUrl::parse(path)?;
- let config = ListingTableConfig::new(table_path).with_listing_options(options);
- let config = config.infer_schema(&state).await?;
+ let schema = options.infer_schema(&state, &table_path).await?;
+ let options = if self.sorted {
+ let key_column_name = schema.fields()[0].name();
+ options
+ .with_file_sort_order(vec![vec![col(key_column_name).sort(true, false)]])
+ } else {
+ options
+ };
+
+ let config = ListingTableConfig::new(table_path)
+ .with_listing_options(options)
+ .with_schema(schema);
Ok(Arc::new(ListingTable::try_new(config)?))
}
diff --git a/benchmarks/src/tpch/convert.rs b/benchmarks/src/tpch/convert.rs
index 7f391d930045a..5219e09cd3052 100644
--- a/benchmarks/src/tpch/convert.rs
+++ b/benchmarks/src/tpch/convert.rs
@@ -22,15 +22,14 @@ use std::path::{Path, PathBuf};
use datafusion::common::not_impl_err;
+use super::get_tbl_tpch_table_schema;
+use super::TPCH_TABLES;
use datafusion::error::Result;
use datafusion::prelude::*;
use parquet::basic::Compression;
use parquet::file::properties::WriterProperties;
use structopt::StructOpt;
-use super::get_tbl_tpch_table_schema;
-use super::TPCH_TABLES;
-
/// Convert tpch .slt files to .parquet or .csv files
#[derive(Debug, StructOpt)]
pub struct ConvertOpt {
@@ -57,6 +56,10 @@ pub struct ConvertOpt {
/// Batch size when reading CSV or Parquet files
#[structopt(short = "s", long = "batch-size", default_value = "8192")]
batch_size: usize,
+
+ /// Sort each table by its first column in ascending order.
+ #[structopt(short = "t", long = "sort")]
+ sort: bool,
}
impl ConvertOpt {
@@ -70,6 +73,7 @@ impl ConvertOpt {
for table in TPCH_TABLES {
let start = Instant::now();
let schema = get_tbl_tpch_table_schema(table);
+ let key_column_name = schema.fields()[0].name();
let input_path = format!("{input_path}/{table}.tbl");
let options = CsvReadOptions::new()
@@ -77,6 +81,13 @@ impl ConvertOpt {
.has_header(false)
.delimiter(b'|')
.file_extension(".tbl");
+ let options = if self.sort {
+ // indicated that the file is already sorted by its first column to speed up the conversion
+ options
+ .file_sort_order(vec![vec![col(key_column_name).sort(true, false)]])
+ } else {
+ options
+ };
let config = SessionConfig::new().with_batch_size(self.batch_size);
let ctx = SessionContext::new_with_config(config);
@@ -99,6 +110,11 @@ impl ConvertOpt {
if partitions > 1 {
csv = csv.repartition(Partitioning::RoundRobinBatch(partitions))?
}
+ let csv = if self.sort {
+ csv.sort_by(vec![col(key_column_name)])?
+ } else {
+ csv
+ };
// create the physical plan
let csv = csv.create_physical_plan().await?;
diff --git a/benchmarks/src/tpch/run.rs b/benchmarks/src/tpch/run.rs
index eb9db821db02f..752a5a1a6ba01 100644
--- a/benchmarks/src/tpch/run.rs
+++ b/benchmarks/src/tpch/run.rs
@@ -90,6 +90,11 @@ pub struct RunOpt {
/// True by default.
#[structopt(short = "j", long = "prefer_hash_join", default_value = "true")]
prefer_hash_join: BoolDefaultTrue,
+
+ /// Mark the first column of each table as sorted in ascending order.
+ /// The tables should have been created with the `--sort` option for this to have any effect.
+ #[structopt(short = "t", long = "sorted")]
+ sorted: bool,
}
const TPCH_QUERY_START_ID: usize = 1;
@@ -275,20 +280,28 @@ impl RunOpt {
}
};
+ let table_path = ListingTableUrl::parse(path)?;
let options = ListingOptions::new(format)
.with_file_extension(extension)
.with_target_partitions(target_partitions)
.with_collect_stat(state.config().collect_statistics());
-
- let table_path = ListingTableUrl::parse(path)?;
- let config = ListingTableConfig::new(table_path).with_listing_options(options);
-
- let config = match table_format {
- "parquet" => config.infer_schema(&state).await?,
- "tbl" => config.with_schema(Arc::new(get_tbl_tpch_table_schema(table))),
- "csv" => config.with_schema(Arc::new(get_tpch_table_schema(table))),
+ let schema = match table_format {
+ "parquet" => options.infer_schema(&state, &table_path).await?,
+ "tbl" => Arc::new(get_tbl_tpch_table_schema(table)),
+ "csv" => Arc::new(get_tpch_table_schema(table)),
_ => unreachable!(),
};
+ let options = if self.sorted {
+ let key_column_name = schema.fields()[0].name();
+ options
+ .with_file_sort_order(vec![vec![col(key_column_name).sort(true, false)]])
+ } else {
+ options
+ };
+
+ let config = ListingTableConfig::new(table_path)
+ .with_listing_options(options)
+ .with_schema(schema);
Ok(Arc::new(ListingTable::try_new(config)?))
}
@@ -357,6 +370,7 @@ mod tests {
output_path: None,
disable_statistics: false,
prefer_hash_join: true,
+ sorted: false,
};
opt.register_tables(&ctx).await?;
let queries = get_query_sql(query)?;
@@ -393,6 +407,7 @@ mod tests {
output_path: None,
disable_statistics: false,
prefer_hash_join: true,
+ sorted: false,
};
opt.register_tables(&ctx).await?;
let queries = get_query_sql(query)?;
diff --git a/datafusion-cli/Cargo.toml b/datafusion-cli/Cargo.toml
index c70e3fc1caec5..e21c005cee5bf 100644
--- a/datafusion-cli/Cargo.toml
+++ b/datafusion-cli/Cargo.toml
@@ -39,7 +39,7 @@ arrow = { workspace = true }
async-trait = { workspace = true }
aws-config = "1.6.1"
aws-credential-types = "1.2.0"
-clap = { version = "4.5.34", features = ["derive", "cargo"] }
+clap = { version = "4.5.36", features = ["derive", "cargo"] }
datafusion = { workspace = true, features = [
"avro",
"crypto_expressions",
diff --git a/datafusion-cli/src/main.rs b/datafusion-cli/src/main.rs
index e21006312d85a..dad2d15f01a11 100644
--- a/datafusion-cli/src/main.rs
+++ b/datafusion-cli/src/main.rs
@@ -25,6 +25,7 @@ use datafusion::error::{DataFusionError, Result};
use datafusion::execution::context::SessionConfig;
use datafusion::execution::memory_pool::{FairSpillPool, GreedyMemoryPool, MemoryPool};
use datafusion::execution::runtime_env::RuntimeEnvBuilder;
+use datafusion::execution::DiskManager;
use datafusion::prelude::SessionContext;
use datafusion_cli::catalog::DynamicObjectStoreCatalog;
use datafusion_cli::functions::ParquetMetadataFunc;
@@ -37,6 +38,9 @@ use datafusion_cli::{
};
use clap::Parser;
+use datafusion::common::config_err;
+use datafusion::config::ConfigOptions;
+use datafusion::execution::disk_manager::DiskManagerConfig;
use mimalloc::MiMalloc;
#[global_allocator]
@@ -123,6 +127,14 @@ struct Args {
#[clap(long, help = "Enables console syntax highlighting")]
color: bool,
+
+ #[clap(
+ short = 'd',
+ long,
+ help = "Available disk space for spilling queries (e.g. '10g'), default to None (uses DataFusion's default value of '100g')",
+ value_parser(extract_disk_limit)
+ )]
+ disk_limit: Option,
}
#[tokio::main]
@@ -150,11 +162,7 @@ async fn main_inner() -> Result<()> {
env::set_current_dir(p).unwrap();
};
- let mut session_config = SessionConfig::from_env()?.with_information_schema(true);
-
- if let Some(batch_size) = args.batch_size {
- session_config = session_config.with_batch_size(batch_size);
- };
+ let session_config = get_session_config(&args)?;
let mut rt_builder = RuntimeEnvBuilder::new();
// set memory pool size
@@ -167,6 +175,18 @@ async fn main_inner() -> Result<()> {
rt_builder = rt_builder.with_memory_pool(pool)
}
+ // set disk limit
+ if let Some(disk_limit) = args.disk_limit {
+ let disk_manager = DiskManager::try_new(DiskManagerConfig::NewOs)?;
+
+ let disk_manager = Arc::try_unwrap(disk_manager)
+ .expect("DiskManager should be a single instance")
+ .with_max_temp_directory_size(disk_limit.try_into().unwrap())?;
+
+ let disk_config = DiskManagerConfig::new_existing(Arc::new(disk_manager));
+ rt_builder = rt_builder.with_disk_manager(disk_config);
+ }
+
let runtime_env = rt_builder.build_arc()?;
// enable dynamic file query
@@ -226,6 +246,30 @@ async fn main_inner() -> Result<()> {
Ok(())
}
+/// Get the session configuration based on the provided arguments
+/// and environment settings.
+fn get_session_config(args: &Args) -> Result {
+ // Read options from environment variables and merge with command line options
+ let mut config_options = ConfigOptions::from_env()?;
+
+ if let Some(batch_size) = args.batch_size {
+ if batch_size == 0 {
+ return config_err!("batch_size must be greater than 0");
+ }
+ config_options.execution.batch_size = batch_size;
+ };
+
+ // use easier to understand "tree" mode by default
+ // if the user hasn't specified an explain format in the environment
+ if env::var_os("DATAFUSION_EXPLAIN_FORMAT").is_none() {
+ config_options.explain.format = String::from("tree");
+ }
+
+ let session_config =
+ SessionConfig::from(config_options).with_information_schema(true);
+ Ok(session_config)
+}
+
fn parse_valid_file(dir: &str) -> Result {
if Path::new(dir).is_file() {
Ok(dir.to_string())
@@ -278,7 +322,7 @@ impl ByteUnit {
}
}
-fn extract_memory_pool_size(size: &str) -> Result {
+fn parse_size_string(size: &str, label: &str) -> Result {
static BYTE_SUFFIXES: LazyLock> =
LazyLock::new(|| {
let mut m = HashMap::new();
@@ -300,25 +344,33 @@ fn extract_memory_pool_size(size: &str) -> Result {
let lower = size.to_lowercase();
if let Some(caps) = SUFFIX_REGEX.captures(&lower) {
let num_str = caps.get(1).unwrap().as_str();
- let num = num_str.parse::().map_err(|_| {
- format!("Invalid numeric value in memory pool size '{}'", size)
- })?;
+ let num = num_str
+ .parse::()
+ .map_err(|_| format!("Invalid numeric value in {} '{}'", label, size))?;
let suffix = caps.get(2).map(|m| m.as_str()).unwrap_or("b");
- let unit = &BYTE_SUFFIXES
+ let unit = BYTE_SUFFIXES
.get(suffix)
- .ok_or_else(|| format!("Invalid memory pool size '{}'", size))?;
- let memory_pool_size = usize::try_from(unit.multiplier())
+ .ok_or_else(|| format!("Invalid {} '{}'", label, size))?;
+ let total_bytes = usize::try_from(unit.multiplier())
.ok()
.and_then(|multiplier| num.checked_mul(multiplier))
- .ok_or_else(|| format!("Memory pool size '{}' is too large", size))?;
+ .ok_or_else(|| format!("{} '{}' is too large", label, size))?;
- Ok(memory_pool_size)
+ Ok(total_bytes)
} else {
- Err(format!("Invalid memory pool size '{}'", size))
+ Err(format!("Invalid {} '{}'", label, size))
}
}
+pub fn extract_memory_pool_size(size: &str) -> Result {
+ parse_size_string(size, "memory pool size")
+}
+
+pub fn extract_disk_limit(size: &str) -> Result {
+ parse_size_string(size, "disk limit")
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/datafusion-cli/tests/cli_integration.rs b/datafusion-cli/tests/cli_integration.rs
index a54a920e97bbf..9ac09955512b8 100644
--- a/datafusion-cli/tests/cli_integration.rs
+++ b/datafusion-cli/tests/cli_integration.rs
@@ -59,6 +59,16 @@ fn init() {
"batch_size",
["--command", "show datafusion.execution.batch_size", "-q", "-b", "1"],
)]
+#[case::default_explain_plan(
+ "default_explain_plan",
+ // default explain format should be tree
+ ["--command", "EXPLAIN SELECT 123"],
+)]
+#[case::can_see_indent_format(
+ "can_see_indent_format",
+ // can choose the old explain format too
+ ["--command", "EXPLAIN FORMAT indent SELECT 123"],
+)]
#[test]
fn cli_quick_test<'a>(
#[case] snapshot_name: &'a str,
@@ -74,6 +84,21 @@ fn cli_quick_test<'a>(
assert_cmd_snapshot!(cmd);
}
+#[test]
+fn cli_explain_environment_overrides() {
+ let mut settings = make_settings();
+ settings.set_snapshot_suffix("explain_plan_environment_overrides");
+ let _bound = settings.bind_to_scope();
+
+ let mut cmd = cli();
+
+ // should use the environment variable to override the default explain plan
+ cmd.env("DATAFUSION_EXPLAIN_FORMAT", "pgjson")
+ .args(["--command", "EXPLAIN SELECT 123"]);
+
+ assert_cmd_snapshot!(cmd);
+}
+
#[rstest]
#[case("csv")]
#[case("tsv")]
diff --git a/datafusion-cli/tests/snapshots/cli_explain_environment_overrides@explain_plan_environment_overrides.snap b/datafusion-cli/tests/snapshots/cli_explain_environment_overrides@explain_plan_environment_overrides.snap
new file mode 100644
index 0000000000000..6b3a247dd7b82
--- /dev/null
+++ b/datafusion-cli/tests/snapshots/cli_explain_environment_overrides@explain_plan_environment_overrides.snap
@@ -0,0 +1,44 @@
+---
+source: datafusion-cli/tests/cli_integration.rs
+info:
+ program: datafusion-cli
+ args:
+ - "--command"
+ - EXPLAIN SELECT 123
+ env:
+ DATAFUSION_EXPLAIN_FORMAT: pgjson
+snapshot_kind: text
+---
+success: true
+exit_code: 0
+----- stdout -----
+[CLI_VERSION]
++--------------+-----------------------------------------+
+| plan_type | plan |
++--------------+-----------------------------------------+
+| logical_plan | [ |
+| | { |
+| | "Plan": { |
+| | "Expressions": [ |
+| | "Int64(123)" |
+| | ], |
+| | "Node Type": "Projection", |
+| | "Output": [ |
+| | "Int64(123)" |
+| | ], |
+| | "Plans": [ |
+| | { |
+| | "Node Type": "EmptyRelation", |
+| | "Output": [], |
+| | "Plans": [] |
+| | } |
+| | ] |
+| | } |
+| | } |
+| | ] |
++--------------+-----------------------------------------+
+1 row(s) fetched.
+[ELAPSED]
+
+
+----- stderr -----
diff --git a/datafusion-cli/tests/snapshots/cli_quick_test@can_see_indent_format.snap b/datafusion-cli/tests/snapshots/cli_quick_test@can_see_indent_format.snap
new file mode 100644
index 0000000000000..b2fb64709974e
--- /dev/null
+++ b/datafusion-cli/tests/snapshots/cli_quick_test@can_see_indent_format.snap
@@ -0,0 +1,27 @@
+---
+source: datafusion-cli/tests/cli_integration.rs
+info:
+ program: datafusion-cli
+ args:
+ - "--command"
+ - EXPLAIN FORMAT indent SELECT 123
+snapshot_kind: text
+---
+success: true
+exit_code: 0
+----- stdout -----
+[CLI_VERSION]
++---------------+------------------------------------------+
+| plan_type | plan |
++---------------+------------------------------------------+
+| logical_plan | Projection: Int64(123) |
+| | EmptyRelation |
+| physical_plan | ProjectionExec: expr=[123 as Int64(123)] |
+| | PlaceholderRowExec |
+| | |
++---------------+------------------------------------------+
+2 row(s) fetched.
+[ELAPSED]
+
+
+----- stderr -----
diff --git a/datafusion-cli/tests/snapshots/cli_quick_test@default_explain_plan.snap b/datafusion-cli/tests/snapshots/cli_quick_test@default_explain_plan.snap
new file mode 100644
index 0000000000000..46ee6be64f624
--- /dev/null
+++ b/datafusion-cli/tests/snapshots/cli_quick_test@default_explain_plan.snap
@@ -0,0 +1,31 @@
+---
+source: datafusion-cli/tests/cli_integration.rs
+info:
+ program: datafusion-cli
+ args:
+ - "--command"
+ - EXPLAIN SELECT 123
+snapshot_kind: text
+---
+success: true
+exit_code: 0
+----- stdout -----
+[CLI_VERSION]
++---------------+-------------------------------+
+| plan_type | plan |
++---------------+-------------------------------+
+| physical_plan | ┌───────────────────────────┐ |
+| | │ ProjectionExec │ |
+| | │ -------------------- │ |
+| | │ Int64(123): 123 │ |
+| | └─────────────┬─────────────┘ |
+| | ┌─────────────┴─────────────┐ |
+| | │ PlaceholderRowExec │ |
+| | └───────────────────────────┘ |
+| | |
++---------------+-------------------------------+
+1 row(s) fetched.
+[ELAPSED]
+
+
+----- stderr -----
diff --git a/datafusion-examples/Cargo.toml b/datafusion-examples/Cargo.toml
index f6b7d641d1264..2ba1673d97b99 100644
--- a/datafusion-examples/Cargo.toml
+++ b/datafusion-examples/Cargo.toml
@@ -62,6 +62,7 @@ bytes = { workspace = true }
dashmap = { workspace = true }
# note only use main datafusion crate for examples
datafusion = { workspace = true, default-features = true }
+datafusion-ffi = { workspace = true }
datafusion-proto = { workspace = true }
env_logger = { workspace = true }
futures = { workspace = true }
diff --git a/datafusion-examples/examples/advanced_parquet_index.rs b/datafusion-examples/examples/advanced_parquet_index.rs
index b8c303e221618..03ef3d66f9d71 100644
--- a/datafusion-examples/examples/advanced_parquet_index.rs
+++ b/datafusion-examples/examples/advanced_parquet_index.rs
@@ -571,7 +571,9 @@ impl ParquetFileReaderFactory for CachedParquetFileReaderFactory {
.to_string();
let object_store = Arc::clone(&self.object_store);
- let mut inner = ParquetObjectReader::new(object_store, file_meta.object_meta);
+ let mut inner =
+ ParquetObjectReader::new(object_store, file_meta.object_meta.location)
+ .with_file_size(file_meta.object_meta.size);
if let Some(hint) = metadata_size_hint {
inner = inner.with_footer_size_hint(hint)
@@ -599,7 +601,7 @@ struct ParquetReaderWithCache {
impl AsyncFileReader for ParquetReaderWithCache {
fn get_bytes(
&mut self,
- range: Range,
+ range: Range,
) -> BoxFuture<'_, datafusion::parquet::errors::Result> {
println!("get_bytes: {} Reading range {:?}", self.filename, range);
self.inner.get_bytes(range)
@@ -607,7 +609,7 @@ impl AsyncFileReader for ParquetReaderWithCache {
fn get_byte_ranges(
&mut self,
- ranges: Vec>,
+ ranges: Vec>,
) -> BoxFuture<'_, datafusion::parquet::errors::Result>> {
println!(
"get_byte_ranges: {} Reading ranges {:?}",
@@ -618,6 +620,7 @@ impl AsyncFileReader for ParquetReaderWithCache {
fn get_metadata(
&mut self,
+ _options: Option<&ArrowReaderOptions>,
) -> BoxFuture<'_, datafusion::parquet::errors::Result>> {
println!("get_metadata: {} returning cached metadata", self.filename);
diff --git a/datafusion-examples/examples/parquet_index.rs b/datafusion-examples/examples/parquet_index.rs
index 0b6bccc27b1d1..7d6ce4d86af1a 100644
--- a/datafusion-examples/examples/parquet_index.rs
+++ b/datafusion-examples/examples/parquet_index.rs
@@ -685,7 +685,7 @@ fn make_demo_file(path: impl AsRef, value_range: Range) -> Result<()>
let num_values = value_range.len();
let file_names =
- StringArray::from_iter_values(std::iter::repeat(&filename).take(num_values));
+ StringArray::from_iter_values(std::iter::repeat_n(&filename, num_values));
let values = Int32Array::from_iter_values(value_range);
let batch = RecordBatch::try_from_iter(vec![
("file_name", Arc::new(file_names) as ArrayRef),
diff --git a/datafusion-examples/examples/sql_dialect.rs b/datafusion-examples/examples/sql_dialect.rs
index 12141847ca361..840faa63b1a48 100644
--- a/datafusion-examples/examples/sql_dialect.rs
+++ b/datafusion-examples/examples/sql_dialect.rs
@@ -17,10 +17,10 @@
use std::fmt::Display;
-use datafusion::error::Result;
+use datafusion::error::{DataFusionError, Result};
use datafusion::sql::{
parser::{CopyToSource, CopyToStatement, DFParser, DFParserBuilder, Statement},
- sqlparser::{keywords::Keyword, parser::ParserError, tokenizer::Token},
+ sqlparser::{keywords::Keyword, tokenizer::Token},
};
/// This example demonstrates how to use the DFParser to parse a statement in a custom way
@@ -62,7 +62,7 @@ impl<'a> MyParser<'a> {
/// This is the entry point to our parser -- it handles `COPY` statements specially
/// but otherwise delegates to the existing DataFusion parser.
- pub fn parse_statement(&mut self) -> Result {
+ pub fn parse_statement(&mut self) -> Result {
if self.is_copy() {
self.df_parser.parser.next_token(); // COPY
let df_statement = self.df_parser.parse_copy()?;
diff --git a/datafusion-testing b/datafusion-testing
index 243047b9dd682..e9f9e22ccf091 160000
--- a/datafusion-testing
+++ b/datafusion-testing
@@ -1 +1 @@
-Subproject commit 243047b9dd682be688628539c604daaddfe640f9
+Subproject commit e9f9e22ccf09145a7368f80fd6a871f11e2b4481
diff --git a/datafusion/catalog/src/lib.rs b/datafusion/catalog/src/lib.rs
index f160bddd2b9c1..0394b05277dac 100644
--- a/datafusion/catalog/src/lib.rs
+++ b/datafusion/catalog/src/lib.rs
@@ -50,7 +50,7 @@ pub use catalog::*;
pub use datafusion_session::Session;
pub use dynamic_file::catalog::*;
pub use memory::{
- MemoryCatalogProvider, MemoryCatalogProviderList, MemorySchemaProvider,
+ MemTable, MemoryCatalogProvider, MemoryCatalogProviderList, MemorySchemaProvider,
};
pub use r#async::*;
pub use schema::*;
diff --git a/datafusion/catalog/src/memory/mod.rs b/datafusion/catalog/src/memory/mod.rs
index 4c5cf1a9ae9de..541d25b3345b4 100644
--- a/datafusion/catalog/src/memory/mod.rs
+++ b/datafusion/catalog/src/memory/mod.rs
@@ -17,6 +17,12 @@
pub(crate) mod catalog;
pub(crate) mod schema;
+pub(crate) mod table;
pub use catalog::*;
pub use schema::*;
+pub use table::*;
+
+// backward compatibility
+pub use datafusion_datasource::memory::MemorySourceConfig;
+pub use datafusion_datasource::source::DataSourceExec;
diff --git a/datafusion/catalog/src/memory/table.rs b/datafusion/catalog/src/memory/table.rs
new file mode 100644
index 0000000000000..81243e2c4889e
--- /dev/null
+++ b/datafusion/catalog/src/memory/table.rs
@@ -0,0 +1,296 @@
+// 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.
+
+//! [`MemTable`] for querying `Vec` by DataFusion.
+
+use std::any::Any;
+use std::collections::HashMap;
+use std::fmt::Debug;
+use std::sync::Arc;
+
+use crate::TableProvider;
+use datafusion_common::error::Result;
+use datafusion_expr::Expr;
+use datafusion_expr::TableType;
+use datafusion_physical_expr::create_physical_sort_exprs;
+use datafusion_physical_plan::repartition::RepartitionExec;
+use datafusion_physical_plan::{
+ common, ExecutionPlan, ExecutionPlanProperties, Partitioning,
+};
+
+use arrow::datatypes::SchemaRef;
+use arrow::record_batch::RecordBatch;
+use datafusion_common::{not_impl_err, plan_err, Constraints, DFSchema, SchemaExt};
+use datafusion_common_runtime::JoinSet;
+use datafusion_datasource::memory::MemSink;
+use datafusion_datasource::memory::MemorySourceConfig;
+use datafusion_datasource::sink::DataSinkExec;
+use datafusion_datasource::source::DataSourceExec;
+use datafusion_expr::dml::InsertOp;
+use datafusion_expr::SortExpr;
+use datafusion_session::Session;
+
+use async_trait::async_trait;
+use futures::StreamExt;
+use log::debug;
+use parking_lot::Mutex;
+use tokio::sync::RwLock;
+
+// backward compatibility
+pub use datafusion_datasource::memory::PartitionData;
+
+/// In-memory data source for presenting a `Vec` as a
+/// data source that can be queried by DataFusion. This allows data to
+/// be pre-loaded into memory and then repeatedly queried without
+/// incurring additional file I/O overhead.
+#[derive(Debug)]
+pub struct MemTable {
+ schema: SchemaRef,
+ // batches used to be pub(crate), but it's needed to be public for the tests
+ pub batches: Vec,
+ constraints: Constraints,
+ column_defaults: HashMap,
+ /// Optional pre-known sort order(s). Must be `SortExpr`s.
+ /// inserting data into this table removes the order
+ pub sort_order: Arc>>>,
+}
+
+impl MemTable {
+ /// Create a new in-memory table from the provided schema and record batches
+ pub fn try_new(schema: SchemaRef, partitions: Vec>) -> Result {
+ for batches in partitions.iter().flatten() {
+ let batches_schema = batches.schema();
+ if !schema.contains(&batches_schema) {
+ debug!(
+ "mem table schema does not contain batches schema. \
+ Target_schema: {schema:?}. Batches Schema: {batches_schema:?}"
+ );
+ return plan_err!("Mismatch between schema and batches");
+ }
+ }
+
+ Ok(Self {
+ schema,
+ batches: partitions
+ .into_iter()
+ .map(|e| Arc::new(RwLock::new(e)))
+ .collect::>(),
+ constraints: Constraints::empty(),
+ column_defaults: HashMap::new(),
+ sort_order: Arc::new(Mutex::new(vec![])),
+ })
+ }
+
+ /// Assign constraints
+ pub fn with_constraints(mut self, constraints: Constraints) -> Self {
+ self.constraints = constraints;
+ self
+ }
+
+ /// Assign column defaults
+ pub fn with_column_defaults(
+ mut self,
+ column_defaults: HashMap,
+ ) -> Self {
+ self.column_defaults = column_defaults;
+ self
+ }
+
+ /// Specify an optional pre-known sort order(s). Must be `SortExpr`s.
+ ///
+ /// If the data is not sorted by this order, DataFusion may produce
+ /// incorrect results.
+ ///
+ /// DataFusion may take advantage of this ordering to omit sorts
+ /// or use more efficient algorithms.
+ ///
+ /// Note that multiple sort orders are supported, if some are known to be
+ /// equivalent,
+ pub fn with_sort_order(self, mut sort_order: Vec>) -> Self {
+ std::mem::swap(self.sort_order.lock().as_mut(), &mut sort_order);
+ self
+ }
+
+ /// Create a mem table by reading from another data source
+ pub async fn load(
+ t: Arc,
+ output_partitions: Option,
+ state: &dyn Session,
+ ) -> Result {
+ let schema = t.schema();
+ let constraints = t.constraints();
+ let exec = t.scan(state, None, &[], None).await?;
+ let partition_count = exec.output_partitioning().partition_count();
+
+ let mut join_set = JoinSet::new();
+
+ for part_idx in 0..partition_count {
+ let task = state.task_ctx();
+ let exec = Arc::clone(&exec);
+ join_set.spawn(async move {
+ let stream = exec.execute(part_idx, task)?;
+ common::collect(stream).await
+ });
+ }
+
+ let mut data: Vec> =
+ Vec::with_capacity(exec.output_partitioning().partition_count());
+
+ while let Some(result) = join_set.join_next().await {
+ match result {
+ Ok(res) => data.push(res?),
+ Err(e) => {
+ if e.is_panic() {
+ std::panic::resume_unwind(e.into_panic());
+ } else {
+ unreachable!();
+ }
+ }
+ }
+ }
+
+ let mut exec = DataSourceExec::new(Arc::new(MemorySourceConfig::try_new(
+ &data,
+ Arc::clone(&schema),
+ None,
+ )?));
+ if let Some(cons) = constraints {
+ exec = exec.with_constraints(cons.clone());
+ }
+
+ if let Some(num_partitions) = output_partitions {
+ let exec = RepartitionExec::try_new(
+ Arc::new(exec),
+ Partitioning::RoundRobinBatch(num_partitions),
+ )?;
+
+ // execute and collect results
+ let mut output_partitions = vec![];
+ for i in 0..exec.properties().output_partitioning().partition_count() {
+ // execute this *output* partition and collect all batches
+ let task_ctx = state.task_ctx();
+ let mut stream = exec.execute(i, task_ctx)?;
+ let mut batches = vec![];
+ while let Some(result) = stream.next().await {
+ batches.push(result?);
+ }
+ output_partitions.push(batches);
+ }
+
+ return MemTable::try_new(Arc::clone(&schema), output_partitions);
+ }
+ MemTable::try_new(Arc::clone(&schema), data)
+ }
+}
+
+#[async_trait]
+impl TableProvider for MemTable {
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
+ fn schema(&self) -> SchemaRef {
+ Arc::clone(&self.schema)
+ }
+
+ fn constraints(&self) -> Option<&Constraints> {
+ Some(&self.constraints)
+ }
+
+ fn table_type(&self) -> TableType {
+ TableType::Base
+ }
+
+ async fn scan(
+ &self,
+ state: &dyn Session,
+ projection: Option<&Vec>,
+ _filters: &[Expr],
+ _limit: Option,
+ ) -> Result> {
+ let mut partitions = vec![];
+ for arc_inner_vec in self.batches.iter() {
+ let inner_vec = arc_inner_vec.read().await;
+ partitions.push(inner_vec.clone())
+ }
+
+ let mut source =
+ MemorySourceConfig::try_new(&partitions, self.schema(), projection.cloned())?;
+
+ let show_sizes = state.config_options().explain.show_sizes;
+ source = source.with_show_sizes(show_sizes);
+
+ // add sort information if present
+ let sort_order = self.sort_order.lock();
+ if !sort_order.is_empty() {
+ let df_schema = DFSchema::try_from(self.schema.as_ref().clone())?;
+
+ let file_sort_order = sort_order
+ .iter()
+ .map(|sort_exprs| {
+ create_physical_sort_exprs(
+ sort_exprs,
+ &df_schema,
+ state.execution_props(),
+ )
+ })
+ .collect::>>()?;
+ source = source.try_with_sort_information(file_sort_order)?;
+ }
+
+ Ok(DataSourceExec::from_data_source(source))
+ }
+
+ /// Returns an ExecutionPlan that inserts the execution results of a given [`ExecutionPlan`] into this [`MemTable`].
+ ///
+ /// The [`ExecutionPlan`] must have the same schema as this [`MemTable`].
+ ///
+ /// # Arguments
+ ///
+ /// * `state` - The [`SessionState`] containing the context for executing the plan.
+ /// * `input` - The [`ExecutionPlan`] to execute and insert.
+ ///
+ /// # Returns
+ ///
+ /// * A plan that returns the number of rows written.
+ ///
+ /// [`SessionState`]: https://docs.rs/datafusion/latest/datafusion/execution/session_state/struct.SessionState.html
+ async fn insert_into(
+ &self,
+ _state: &dyn Session,
+ input: Arc,
+ insert_op: InsertOp,
+ ) -> Result> {
+ // If we are inserting into the table, any sort order may be messed up so reset it here
+ *self.sort_order.lock() = vec![];
+
+ // Create a physical plan from the logical plan.
+ // Check that the schema of the plan matches the schema of this table.
+ self.schema()
+ .logically_equivalent_names_and_types(&input.schema())?;
+
+ if insert_op != InsertOp::Append {
+ return not_impl_err!("{insert_op} not implemented for MemoryTable yet");
+ }
+ let sink = MemSink::try_new(self.batches.clone(), Arc::clone(&self.schema))?;
+ Ok(Arc::new(DataSinkExec::new(input, Arc::new(sink), None)))
+ }
+
+ fn get_column_default(&self, column: &str) -> Option<&Expr> {
+ self.column_defaults.get(column)
+ }
+}
diff --git a/datafusion/common-runtime/src/common.rs b/datafusion/common-runtime/src/common.rs
index 361f6af95cf13..e7aba1d455ee6 100644
--- a/datafusion/common-runtime/src/common.rs
+++ b/datafusion/common-runtime/src/common.rs
@@ -15,18 +15,25 @@
// specific language governing permissions and limitations
// under the License.
-use std::future::Future;
+use std::{
+ future::Future,
+ pin::Pin,
+ task::{Context, Poll},
+};
-use crate::JoinSet;
-use tokio::task::JoinError;
+use tokio::task::{JoinError, JoinHandle};
+
+use crate::trace_utils::{trace_block, trace_future};
/// Helper that provides a simple API to spawn a single task and join it.
/// Provides guarantees of aborting on `Drop` to keep it cancel-safe.
+/// Note that if the task was spawned with `spawn_blocking`, it will only be
+/// aborted if it hasn't started yet.
///
-/// Technically, it's just a wrapper of `JoinSet` (with size=1).
+/// Technically, it's just a wrapper of a `JoinHandle` overriding drop.
#[derive(Debug)]
pub struct SpawnedTask {
- inner: JoinSet,
+ inner: JoinHandle,
}
impl SpawnedTask {
@@ -36,8 +43,9 @@ impl SpawnedTask {
T: Send + 'static,
R: Send,
{
- let mut inner = JoinSet::new();
- inner.spawn(task);
+ // Ok to use spawn here as SpawnedTask handles aborting/cancelling the task on Drop
+ #[allow(clippy::disallowed_methods)]
+ let inner = tokio::task::spawn(trace_future(task));
Self { inner }
}
@@ -47,22 +55,21 @@ impl SpawnedTask {
T: Send + 'static,
R: Send,
{
- let mut inner = JoinSet::new();
- inner.spawn_blocking(task);
+ // Ok to use spawn_blocking here as SpawnedTask handles aborting/cancelling the task on Drop
+ #[allow(clippy::disallowed_methods)]
+ let inner = tokio::task::spawn_blocking(trace_block(task));
Self { inner }
}
/// Joins the task, returning the result of join (`Result`).
- pub async fn join(mut self) -> Result {
- self.inner
- .join_next()
- .await
- .expect("`SpawnedTask` instance always contains exactly 1 task")
+ /// Same as awaiting the spawned task, but left for backwards compatibility.
+ pub async fn join(self) -> Result {
+ self.await
}
/// Joins the task and unwinds the panic if it happens.
pub async fn join_unwind(self) -> Result {
- self.join().await.map_err(|e| {
+ self.await.map_err(|e| {
// `JoinError` can be caused either by panic or cancellation. We have to handle panics:
if e.is_panic() {
std::panic::resume_unwind(e.into_panic());
@@ -77,17 +84,32 @@ impl SpawnedTask {
}
}
+impl Future for SpawnedTask {
+ type Output = Result;
+
+ fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
+ Pin::new(&mut self.inner).poll(cx)
+ }
+}
+
+impl Drop for SpawnedTask {
+ fn drop(&mut self) {
+ self.inner.abort();
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
use std::future::{pending, Pending};
- use tokio::runtime::Runtime;
+ use tokio::{runtime::Runtime, sync::oneshot};
#[tokio::test]
async fn runtime_shutdown() {
let rt = Runtime::new().unwrap();
+ #[allow(clippy::async_yields_async)]
let task = rt
.spawn(async {
SpawnedTask::spawn(async {
@@ -119,4 +141,36 @@ mod tests {
.await
.ok();
}
+
+ #[tokio::test]
+ async fn cancel_not_started_task() {
+ let (sender, receiver) = oneshot::channel::();
+ let task = SpawnedTask::spawn(async {
+ // Shouldn't be reached.
+ sender.send(42).unwrap();
+ });
+
+ drop(task);
+
+ // If the task was cancelled, the sender was also dropped,
+ // and awaiting the receiver should result in an error.
+ assert!(receiver.await.is_err());
+ }
+
+ #[tokio::test]
+ async fn cancel_ongoing_task() {
+ let (sender, mut receiver) = tokio::sync::mpsc::channel(1);
+ let task = SpawnedTask::spawn(async move {
+ sender.send(1).await.unwrap();
+ // This line will never be reached because the channel has a buffer
+ // of 1.
+ sender.send(2).await.unwrap();
+ });
+ // Let the task start.
+ assert_eq!(receiver.recv().await.unwrap(), 1);
+ drop(task);
+
+ // The sender was dropped so we receive `None`.
+ assert!(receiver.recv().await.is_none());
+ }
}
diff --git a/datafusion/common/Cargo.toml b/datafusion/common/Cargo.toml
index 39b47a96bccf3..d471e48be4e75 100644
--- a/datafusion/common/Cargo.toml
+++ b/datafusion/common/Cargo.toml
@@ -58,12 +58,12 @@ base64 = "0.22.1"
half = { workspace = true }
hashbrown = { workspace = true }
indexmap = { workspace = true }
-libc = "0.2.171"
+libc = "0.2.172"
log = { workspace = true }
object_store = { workspace = true, optional = true }
parquet = { workspace = true, optional = true, default-features = true }
paste = "1.0.15"
-pyo3 = { version = "0.23.5", optional = true }
+pyo3 = { version = "0.24.2", optional = true }
recursive = { workspace = true, optional = true }
sqlparser = { workspace = true }
tokio = { workspace = true }
diff --git a/datafusion/common/src/config.rs b/datafusion/common/src/config.rs
index b0f17630c910c..1e0f63d6d81ca 100644
--- a/datafusion/common/src/config.rs
+++ b/datafusion/common/src/config.rs
@@ -149,9 +149,17 @@ macro_rules! config_namespace {
// $(#[allow(deprecated)])?
{
$(let value = $transform(value);)? // Apply transformation if specified
- $(log::warn!($warn);)? // Log warning if specified
#[allow(deprecated)]
- self.$field_name.set(rem, value.as_ref())
+ let ret = self.$field_name.set(rem, value.as_ref());
+
+ $(if !$warn.is_empty() {
+ let default: $field_type = $default;
+ #[allow(deprecated)]
+ if default != self.$field_name {
+ log::warn!($warn);
+ }
+ })? // Log warning if specified, and the value is not the default
+ ret
}
},
)*
@@ -292,7 +300,7 @@ config_namespace! {
/// concurrency.
///
/// Defaults to the number of CPU cores on the system
- pub target_partitions: usize, default = get_available_parallelism()
+ pub target_partitions: usize, transform = ExecutionOptions::normalized_parallelism, default = get_available_parallelism()
/// The default time zone
///
@@ -308,7 +316,7 @@ config_namespace! {
/// This is mostly use to plan `UNION` children in parallel.
///
/// Defaults to the number of CPU cores on the system
- pub planning_concurrency: usize, default = get_available_parallelism()
+ pub planning_concurrency: usize, transform = ExecutionOptions::normalized_parallelism, default = get_available_parallelism()
/// When set to true, skips verifying that the schema produced by
/// planning the input of `LogicalPlan::Aggregate` exactly matches the
@@ -451,6 +459,14 @@ config_namespace! {
/// BLOB instead.
pub binary_as_string: bool, default = false
+ /// (reading) If true, parquet reader will read columns of
+ /// physical type int96 as originating from a different resolution
+ /// than nanosecond. This is useful for reading data from systems like Spark
+ /// which stores microsecond resolution timestamps in an int96 allowing it
+ /// to write values with a larger date range than 64-bit timestamps with
+ /// nanosecond resolution.
+ pub coerce_int96: Option, transform = str::to_lowercase, default = None
+
// The following options affect writing to parquet files
// and map to parquet::file::properties::WriterProperties
@@ -723,6 +739,19 @@ config_namespace! {
}
}
+impl ExecutionOptions {
+ /// Returns the correct parallelism based on the provided `value`.
+ /// If `value` is `"0"`, returns the default available parallelism, computed with
+ /// `get_available_parallelism`. Otherwise, returns `value`.
+ fn normalized_parallelism(value: &str) -> String {
+ if value.parse::() == Ok(0) {
+ get_available_parallelism().to_string()
+ } else {
+ value.to_owned()
+ }
+ }
+}
+
/// A key value pair, with a corresponding description
#[derive(Debug)]
pub struct ConfigEntry {
@@ -1999,8 +2028,8 @@ mod tests {
use std::collections::HashMap;
use crate::config::{
- ConfigEntry, ConfigExtension, ConfigFileType, ExtensionOptions, Extensions,
- TableOptions,
+ ConfigEntry, ConfigExtension, ConfigField, ConfigFileType, ExtensionOptions,
+ Extensions, TableOptions,
};
#[derive(Default, Debug, Clone)]
@@ -2085,6 +2114,37 @@ mod tests {
assert_eq!(table_config.csv.escape.unwrap() as char, '\'');
}
+ #[test]
+ fn warning_only_not_default() {
+ use std::sync::atomic::AtomicUsize;
+ static COUNT: AtomicUsize = AtomicUsize::new(0);
+ use log::{Level, LevelFilter, Metadata, Record};
+ struct SimpleLogger;
+ impl log::Log for SimpleLogger {
+ fn enabled(&self, metadata: &Metadata) -> bool {
+ metadata.level() <= Level::Info
+ }
+
+ fn log(&self, record: &Record) {
+ if self.enabled(record.metadata()) {
+ COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
+ }
+ }
+ fn flush(&self) {}
+ }
+ log::set_logger(&SimpleLogger).unwrap();
+ log::set_max_level(LevelFilter::Info);
+ let mut sql_parser_options = crate::config::SqlParserOptions::default();
+ sql_parser_options
+ .set("enable_options_value_normalization", "false")
+ .unwrap();
+ assert_eq!(COUNT.load(std::sync::atomic::Ordering::Relaxed), 0);
+ sql_parser_options
+ .set("enable_options_value_normalization", "true")
+ .unwrap();
+ assert_eq!(COUNT.load(std::sync::atomic::Ordering::Relaxed), 1);
+ }
+
#[cfg(feature = "parquet")]
#[test]
fn parquet_table_options() {
diff --git a/datafusion/common/src/dfschema.rs b/datafusion/common/src/dfschema.rs
index 43d082f9dc936..66a26a18c0dc8 100644
--- a/datafusion/common/src/dfschema.rs
+++ b/datafusion/common/src/dfschema.rs
@@ -641,7 +641,7 @@ impl DFSchema {
|| (!DFSchema::datatype_is_semantically_equal(
f1.data_type(),
f2.data_type(),
- ) && !can_cast_types(f2.data_type(), f1.data_type()))
+ ))
{
_plan_err!(
"Schema mismatch: Expected field '{}' with type {:?}, \
@@ -659,9 +659,12 @@ impl DFSchema {
}
/// Checks if two [`DataType`]s are logically equal. This is a notably weaker constraint
- /// than datatype_is_semantically_equal in that a Dictionary type is logically
- /// equal to a plain V type, but not semantically equal. Dictionary is also
- /// logically equal to Dictionary.
+ /// than datatype_is_semantically_equal in that different representations of same data can be
+ /// logically but not semantically equivalent. Semantically equivalent types are always also
+ /// logically equivalent. For example:
+ /// - a Dictionary type is logically equal to a plain V type
+ /// - a Dictionary is also logically equal to Dictionary
+ /// - Utf8 and Utf8View are logically equal
pub fn datatype_is_logically_equal(dt1: &DataType, dt2: &DataType) -> bool {
// check nested fields
match (dt1, dt2) {
@@ -711,12 +714,15 @@ impl DFSchema {
.zip(iter2)
.all(|((t1, f1), (t2, f2))| t1 == t2 && Self::field_is_logically_equal(f1, f2))
}
- _ => dt1 == dt2,
+ // Utf8 and Utf8View are logically equivalent
+ (DataType::Utf8, DataType::Utf8View) => true,
+ (DataType::Utf8View, DataType::Utf8) => true,
+ _ => Self::datatype_is_semantically_equal(dt1, dt2),
}
}
/// Returns true of two [`DataType`]s are semantically equal (same
- /// name and type), ignoring both metadata and nullability.
+ /// name and type), ignoring both metadata and nullability, and decimal precision/scale.
///
/// request to upstream:
pub fn datatype_is_semantically_equal(dt1: &DataType, dt2: &DataType) -> bool {
diff --git a/datafusion/common/src/file_options/parquet_writer.rs b/datafusion/common/src/file_options/parquet_writer.rs
index 939cb5e1a3578..3e33466edf505 100644
--- a/datafusion/common/src/file_options/parquet_writer.rs
+++ b/datafusion/common/src/file_options/parquet_writer.rs
@@ -239,6 +239,7 @@ impl ParquetOptions {
bloom_filter_on_read: _, // reads not used for writer props
schema_force_view_types: _,
binary_as_string: _, // not used for writer props
+ coerce_int96: _, // not used for writer props
skip_arrow_metadata: _,
} = self;
@@ -516,6 +517,7 @@ mod tests {
schema_force_view_types: defaults.schema_force_view_types,
binary_as_string: defaults.binary_as_string,
skip_arrow_metadata: defaults.skip_arrow_metadata,
+ coerce_int96: None,
}
}
@@ -622,6 +624,7 @@ mod tests {
schema_force_view_types: global_options_defaults.schema_force_view_types,
binary_as_string: global_options_defaults.binary_as_string,
skip_arrow_metadata: global_options_defaults.skip_arrow_metadata,
+ coerce_int96: None,
},
column_specific_options,
key_value_metadata,
diff --git a/datafusion/common/src/functional_dependencies.rs b/datafusion/common/src/functional_dependencies.rs
index 5f262d634af37..c4f2805f82856 100644
--- a/datafusion/common/src/functional_dependencies.rs
+++ b/datafusion/common/src/functional_dependencies.rs
@@ -47,11 +47,13 @@ impl Constraints {
Constraints::new_unverified(vec![])
}
- /// Create a new `Constraints` object from the given `constraints`.
- /// Users should use the `empty` or `new_from_table_constraints` functions
- /// for constructing `Constraints`. This constructor is for internal
+ /// Create a new [`Constraints`] object from the given `constraints`.
+ /// Users should use the [`Constraints::empty`] or [`SqlToRel::new_constraint_from_table_constraints`] functions
+ /// for constructing [`Constraints`]. This constructor is for internal
/// purposes only and does not check whether the argument is valid. The user
- /// is responsible for supplying a valid vector of `Constraint` objects.
+ /// is responsible for supplying a valid vector of [`Constraint`] objects.
+ ///
+ /// [`SqlToRel::new_constraint_from_table_constraints`]: https://docs.rs/datafusion/latest/datafusion/sql/planner/struct.SqlToRel.html#method.new_constraint_from_table_constraints
pub fn new_unverified(constraints: Vec) -> Self {
Self { inner: constraints }
}
diff --git a/datafusion/common/src/scalar/mod.rs b/datafusion/common/src/scalar/mod.rs
index 2b758f4568760..b8d9aea810f03 100644
--- a/datafusion/common/src/scalar/mod.rs
+++ b/datafusion/common/src/scalar/mod.rs
@@ -27,7 +27,7 @@ use std::convert::Infallible;
use std::fmt;
use std::hash::Hash;
use std::hash::Hasher;
-use std::iter::repeat;
+use std::iter::repeat_n;
use std::mem::{size_of, size_of_val};
use std::str::FromStr;
use std::sync::Arc;
@@ -802,12 +802,14 @@ fn dict_from_scalar(
let values_array = value.to_array_of_size(1)?;
// Create a key array with `size` elements, each of 0
- let key_array: PrimitiveArray = repeat(if value.is_null() {
- None
- } else {
- Some(K::default_value())
- })
- .take(size)
+ let key_array: PrimitiveArray = repeat_n(
+ if value.is_null() {
+ None
+ } else {
+ Some(K::default_value())
+ },
+ size,
+ )
.collect();
// create a new DictionaryArray
@@ -2189,8 +2191,7 @@ impl ScalarValue {
scale: i8,
size: usize,
) -> Result {
- Ok(repeat(value)
- .take(size)
+ Ok(repeat_n(value, size)
.collect::()
.with_precision_and_scale(precision, scale)?)
}
@@ -2416,53 +2417,47 @@ impl ScalarValue {
}
ScalarValue::Utf8(e) => match e {
Some(value) => {
- Arc::new(StringArray::from_iter_values(repeat(value).take(size)))
+ Arc::new(StringArray::from_iter_values(repeat_n(value, size)))
}
None => new_null_array(&DataType::Utf8, size),
},
ScalarValue::Utf8View(e) => match e {
Some(value) => {
- Arc::new(StringViewArray::from_iter_values(repeat(value).take(size)))
+ Arc::new(StringViewArray::from_iter_values(repeat_n(value, size)))
}
None => new_null_array(&DataType::Utf8View, size),
},
ScalarValue::LargeUtf8(e) => match e {
Some(value) => {
- Arc::new(LargeStringArray::from_iter_values(repeat(value).take(size)))
+ Arc::new(LargeStringArray::from_iter_values(repeat_n(value, size)))
}
None => new_null_array(&DataType::LargeUtf8, size),
},
ScalarValue::Binary(e) => match e {
Some(value) => Arc::new(
- repeat(Some(value.as_slice()))
- .take(size)
- .collect::(),
+ repeat_n(Some(value.as_slice()), size).collect::(),
),
- None => {
- Arc::new(repeat(None::<&str>).take(size).collect::())
- }
+ None => Arc::new(repeat_n(None::<&str>, size).collect::()),
},
ScalarValue::BinaryView(e) => match e {
Some(value) => Arc::new(
- repeat(Some(value.as_slice()))
- .take(size)
- .collect::(),
+ repeat_n(Some(value.as_slice()), size).collect::(),
),
None => {
- Arc::new(repeat(None::<&str>).take(size).collect::())
+ Arc::new(repeat_n(None::<&str>, size).collect::())
}
},
ScalarValue::FixedSizeBinary(s, e) => match e {
Some(value) => Arc::new(
FixedSizeBinaryArray::try_from_sparse_iter_with_size(
- repeat(Some(value.as_slice())).take(size),
+ repeat_n(Some(value.as_slice()), size),
*s,
)
.unwrap(),
),
None => Arc::new(
FixedSizeBinaryArray::try_from_sparse_iter_with_size(
- repeat(None::<&[u8]>).take(size),
+ repeat_n(None::<&[u8]>, size),
*s,
)
.unwrap(),
@@ -2470,15 +2465,11 @@ impl ScalarValue {
},
ScalarValue::LargeBinary(e) => match e {
Some(value) => Arc::new(
- repeat(Some(value.as_slice()))
- .take(size)
- .collect::(),
- ),
- None => Arc::new(
- repeat(None::<&str>)
- .take(size)
- .collect::(),
+ repeat_n(Some(value.as_slice()), size).collect::(),
),
+ None => {
+ Arc::new(repeat_n(None::<&str>, size).collect::())
+ }
},
ScalarValue::List(arr) => {
Self::list_to_array_of_size(arr.as_ref() as &dyn Array, size)?
@@ -2606,7 +2597,7 @@ impl ScalarValue {
child_arrays.push(ar);
new_fields.push(field.clone());
}
- let type_ids = repeat(*v_id).take(size);
+ let type_ids = repeat_n(*v_id, size);
let type_ids = ScalarBuffer::::from_iter(type_ids);
let value_offsets = match mode {
UnionMode::Sparse => None,
@@ -2674,7 +2665,7 @@ impl ScalarValue {
}
fn list_to_array_of_size(arr: &dyn Array, size: usize) -> Result {
- let arrays = repeat(arr).take(size).collect::>();
+ let arrays = repeat_n(arr, size).collect::>();
let ret = match !arrays.is_empty() {
true => arrow::compute::concat(arrays.as_slice())?,
false => arr.slice(0, 0),
@@ -3036,6 +3027,34 @@ impl ScalarValue {
DataType::Timestamp(TimeUnit::Nanosecond, None),
) => ScalarValue::Int64(Some((float_ts * 1_000_000_000_f64).trunc() as i64))
.to_array()?,
+ (
+ ScalarValue::Decimal128(Some(decimal_value), _, scale),
+ DataType::Timestamp(time_unit, None),
+ ) => {
+ let scale_factor = 10_i128.pow(*scale as u32);
+ let seconds = decimal_value / scale_factor;
+ let fraction = decimal_value % scale_factor;
+
+ let timestamp_value = match time_unit {
+ TimeUnit::Second => ScalarValue::Int64(Some(seconds as i64)),
+ TimeUnit::Millisecond => {
+ let millis = seconds * 1_000 + (fraction * 1_000) / scale_factor;
+ ScalarValue::Int64(Some(millis as i64))
+ }
+ TimeUnit::Microsecond => {
+ let micros =
+ seconds * 1_000_000 + (fraction * 1_000_000) / scale_factor;
+ ScalarValue::Int64(Some(micros as i64))
+ }
+ TimeUnit::Nanosecond => {
+ let nanos = seconds * 1_000_000_000
+ + (fraction * 1_000_000_000) / scale_factor;
+ ScalarValue::Int64(Some(nanos as i64))
+ }
+ };
+
+ timestamp_value.to_array()?
+ }
_ => self.to_array()?,
};
diff --git a/datafusion/common/src/stats.rs b/datafusion/common/src/stats.rs
index 5b841db53c5ee..807d885b3a4de 100644
--- a/datafusion/common/src/stats.rs
+++ b/datafusion/common/src/stats.rs
@@ -21,6 +21,7 @@ use std::fmt::{self, Debug, Display};
use crate::{Result, ScalarValue};
+use crate::error::_plan_err;
use arrow::datatypes::{DataType, Schema, SchemaRef};
/// Represents a value with a degree of certainty. `Precision` is used to
@@ -271,11 +272,25 @@ pub struct Statistics {
pub num_rows: Precision,
/// Total bytes of the table rows.
pub total_byte_size: Precision,
- /// Statistics on a column level. It contains a [`ColumnStatistics`] for
- /// each field in the schema of the table to which the [`Statistics`] refer.
+ /// Statistics on a column level.
+ ///
+ /// It must contains a [`ColumnStatistics`] for each field in the schema of
+ /// the table to which the [`Statistics`] refer.
pub column_statistics: Vec,
}
+impl Default for Statistics {
+ /// Returns a new [`Statistics`] instance with all fields set to unknown
+ /// and no columns.
+ fn default() -> Self {
+ Self {
+ num_rows: Precision::Absent,
+ total_byte_size: Precision::Absent,
+ column_statistics: vec![],
+ }
+ }
+}
+
impl Statistics {
/// Returns a [`Statistics`] instance for the given schema by assigning
/// unknown statistics to each column in the schema.
@@ -296,6 +311,24 @@ impl Statistics {
.collect()
}
+ /// Set the number of rows
+ pub fn with_num_rows(mut self, num_rows: Precision) -> Self {
+ self.num_rows = num_rows;
+ self
+ }
+
+ /// Set the total size, in bytes
+ pub fn with_total_byte_size(mut self, total_byte_size: Precision) -> Self {
+ self.total_byte_size = total_byte_size;
+ self
+ }
+
+ /// Add a column to the column statistics
+ pub fn add_column_statistics(mut self, column_stats: ColumnStatistics) -> Self {
+ self.column_statistics.push(column_stats);
+ self
+ }
+
/// If the exactness of a [`Statistics`] instance is lost, this function relaxes
/// the exactness of all information by converting them [`Precision::Inexact`].
pub fn to_inexact(mut self) -> Self {
@@ -351,7 +384,8 @@ impl Statistics {
self
}
- /// Calculates the statistics after `fetch` and `skip` operations apply.
+ /// Calculates the statistics after applying `fetch` and `skip` operations.
+ ///
/// Here, `self` denotes per-partition statistics. Use the `n_partitions`
/// parameter to compute global statistics in a multi-partition setting.
pub fn with_fetch(
@@ -414,6 +448,100 @@ impl Statistics {
self.total_byte_size = Precision::Absent;
Ok(self)
}
+
+ /// Summarize zero or more statistics into a single `Statistics` instance.
+ ///
+ /// Returns an error if the statistics do not match the specified schemas.
+ pub fn try_merge_iter<'a, I>(items: I, schema: &Schema) -> Result
+ where
+ I: IntoIterator- ,
+ {
+ let mut items = items.into_iter();
+
+ let Some(init) = items.next() else {
+ return Ok(Statistics::new_unknown(schema));
+ };
+ items.try_fold(init.clone(), |acc: Statistics, item_stats: &Statistics| {
+ acc.try_merge(item_stats)
+ })
+ }
+
+ /// Merge this Statistics value with another Statistics value.
+ ///
+ /// Returns an error if the statistics do not match (different schemas).
+ ///
+ /// # Example
+ /// ```
+ /// # use datafusion_common::{ColumnStatistics, ScalarValue, Statistics};
+ /// # use arrow::datatypes::{Field, Schema, DataType};
+ /// # use datafusion_common::stats::Precision;
+ /// let stats1 = Statistics::default()
+ /// .with_num_rows(Precision::Exact(1))
+ /// .with_total_byte_size(Precision::Exact(2))
+ /// .add_column_statistics(ColumnStatistics::new_unknown()
+ /// .with_null_count(Precision::Exact(3))
+ /// .with_min_value(Precision::Exact(ScalarValue::from(4)))
+ /// .with_max_value(Precision::Exact(ScalarValue::from(5)))
+ /// );
+ ///
+ /// let stats2 = Statistics::default()
+ /// .with_num_rows(Precision::Exact(10))
+ /// .with_total_byte_size(Precision::Inexact(20))
+ /// .add_column_statistics(ColumnStatistics::new_unknown()
+ /// // absent null count
+ /// .with_min_value(Precision::Exact(ScalarValue::from(40)))
+ /// .with_max_value(Precision::Exact(ScalarValue::from(50)))
+ /// );
+ ///
+ /// let merged_stats = stats1.try_merge(&stats2).unwrap();
+ /// let expected_stats = Statistics::default()
+ /// .with_num_rows(Precision::Exact(11))
+ /// .with_total_byte_size(Precision::Inexact(22)) // inexact in stats2 --> inexact
+ /// .add_column_statistics(
+ /// ColumnStatistics::new_unknown()
+ /// .with_null_count(Precision::Absent) // missing from stats2 --> absent
+ /// .with_min_value(Precision::Exact(ScalarValue::from(4)))
+ /// .with_max_value(Precision::Exact(ScalarValue::from(50)))
+ /// );
+ ///
+ /// assert_eq!(merged_stats, expected_stats)
+ /// ```
+ pub fn try_merge(self, other: &Statistics) -> Result {
+ let Self {
+ mut num_rows,
+ mut total_byte_size,
+ mut column_statistics,
+ } = self;
+
+ // Accumulate statistics for subsequent items
+ num_rows = num_rows.add(&other.num_rows);
+ total_byte_size = total_byte_size.add(&other.total_byte_size);
+
+ if column_statistics.len() != other.column_statistics.len() {
+ return _plan_err!(
+ "Cannot merge statistics with different number of columns: {} vs {}",
+ column_statistics.len(),
+ other.column_statistics.len()
+ );
+ }
+
+ for (item_col_stats, col_stats) in other
+ .column_statistics
+ .iter()
+ .zip(column_statistics.iter_mut())
+ {
+ col_stats.null_count = col_stats.null_count.add(&item_col_stats.null_count);
+ col_stats.max_value = col_stats.max_value.max(&item_col_stats.max_value);
+ col_stats.min_value = col_stats.min_value.min(&item_col_stats.min_value);
+ col_stats.sum_value = col_stats.sum_value.add(&item_col_stats.sum_value);
+ }
+
+ Ok(Statistics {
+ num_rows,
+ total_byte_size,
+ column_statistics,
+ })
+ }
}
/// Creates an estimate of the number of rows in the output using the given
@@ -521,6 +649,36 @@ impl ColumnStatistics {
}
}
+ /// Set the null count
+ pub fn with_null_count(mut self, null_count: Precision) -> Self {
+ self.null_count = null_count;
+ self
+ }
+
+ /// Set the max value
+ pub fn with_max_value(mut self, max_value: Precision) -> Self {
+ self.max_value = max_value;
+ self
+ }
+
+ /// Set the min value
+ pub fn with_min_value(mut self, min_value: Precision) -> Self {
+ self.min_value = min_value;
+ self
+ }
+
+ /// Set the sum value
+ pub fn with_sum_value(mut self, sum_value: Precision) -> Self {
+ self.sum_value = sum_value;
+ self
+ }
+
+ /// Set the distinct count
+ pub fn with_distinct_count(mut self, distinct_count: Precision) -> Self {
+ self.distinct_count = distinct_count;
+ self
+ }
+
/// If the exactness of a [`ColumnStatistics`] instance is lost, this
/// function relaxes the exactness of all information by converting them
/// [`Precision::Inexact`].
@@ -537,6 +695,9 @@ impl ColumnStatistics {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::assert_contains;
+ use arrow::datatypes::Field;
+ use std::sync::Arc;
#[test]
fn test_get_value() {
@@ -798,4 +959,193 @@ mod tests {
distinct_count: Precision::Exact(100),
}
}
+
+ #[test]
+ fn test_try_merge_basic() {
+ // Create a schema with two columns
+ let schema = Arc::new(Schema::new(vec![
+ Field::new("col1", DataType::Int32, false),
+ Field::new("col2", DataType::Int32, false),
+ ]));
+
+ // Create items with statistics
+ let stats1 = Statistics {
+ num_rows: Precision::Exact(10),
+ total_byte_size: Precision::Exact(100),
+ column_statistics: vec![
+ ColumnStatistics {
+ null_count: Precision::Exact(1),
+ max_value: Precision::Exact(ScalarValue::Int32(Some(100))),
+ min_value: Precision::Exact(ScalarValue::Int32(Some(1))),
+ sum_value: Precision::Exact(ScalarValue::Int32(Some(500))),
+ distinct_count: Precision::Absent,
+ },
+ ColumnStatistics {
+ null_count: Precision::Exact(2),
+ max_value: Precision::Exact(ScalarValue::Int32(Some(200))),
+ min_value: Precision::Exact(ScalarValue::Int32(Some(10))),
+ sum_value: Precision::Exact(ScalarValue::Int32(Some(1000))),
+ distinct_count: Precision::Absent,
+ },
+ ],
+ };
+
+ let stats2 = Statistics {
+ num_rows: Precision::Exact(15),
+ total_byte_size: Precision::Exact(150),
+ column_statistics: vec![
+ ColumnStatistics {
+ null_count: Precision::Exact(2),
+ max_value: Precision::Exact(ScalarValue::Int32(Some(120))),
+ min_value: Precision::Exact(ScalarValue::Int32(Some(-10))),
+ sum_value: Precision::Exact(ScalarValue::Int32(Some(600))),
+ distinct_count: Precision::Absent,
+ },
+ ColumnStatistics {
+ null_count: Precision::Exact(3),
+ max_value: Precision::Exact(ScalarValue::Int32(Some(180))),
+ min_value: Precision::Exact(ScalarValue::Int32(Some(5))),
+ sum_value: Precision::Exact(ScalarValue::Int32(Some(1200))),
+ distinct_count: Precision::Absent,
+ },
+ ],
+ };
+
+ let items = vec![stats1, stats2];
+
+ let summary_stats = Statistics::try_merge_iter(&items, &schema).unwrap();
+
+ // Verify the results
+ assert_eq!(summary_stats.num_rows, Precision::Exact(25)); // 10 + 15
+ assert_eq!(summary_stats.total_byte_size, Precision::Exact(250)); // 100 + 150
+
+ // Verify column statistics
+ let col1_stats = &summary_stats.column_statistics[0];
+ assert_eq!(col1_stats.null_count, Precision::Exact(3)); // 1 + 2
+ assert_eq!(
+ col1_stats.max_value,
+ Precision::Exact(ScalarValue::Int32(Some(120)))
+ );
+ assert_eq!(
+ col1_stats.min_value,
+ Precision::Exact(ScalarValue::Int32(Some(-10)))
+ );
+ assert_eq!(
+ col1_stats.sum_value,
+ Precision::Exact(ScalarValue::Int32(Some(1100)))
+ ); // 500 + 600
+
+ let col2_stats = &summary_stats.column_statistics[1];
+ assert_eq!(col2_stats.null_count, Precision::Exact(5)); // 2 + 3
+ assert_eq!(
+ col2_stats.max_value,
+ Precision::Exact(ScalarValue::Int32(Some(200)))
+ );
+ assert_eq!(
+ col2_stats.min_value,
+ Precision::Exact(ScalarValue::Int32(Some(5)))
+ );
+ assert_eq!(
+ col2_stats.sum_value,
+ Precision::Exact(ScalarValue::Int32(Some(2200)))
+ ); // 1000 + 1200
+ }
+
+ #[test]
+ fn test_try_merge_mixed_precision() {
+ // Create a schema with one column
+ let schema = Arc::new(Schema::new(vec![Field::new(
+ "col1",
+ DataType::Int32,
+ false,
+ )]));
+
+ // Create items with different precision levels
+ let stats1 = Statistics {
+ num_rows: Precision::Exact(10),
+ total_byte_size: Precision::Inexact(100),
+ column_statistics: vec![ColumnStatistics {
+ null_count: Precision::Exact(1),
+ max_value: Precision::Exact(ScalarValue::Int32(Some(100))),
+ min_value: Precision::Inexact(ScalarValue::Int32(Some(1))),
+ sum_value: Precision::Exact(ScalarValue::Int32(Some(500))),
+ distinct_count: Precision::Absent,
+ }],
+ };
+
+ let stats2 = Statistics {
+ num_rows: Precision::Inexact(15),
+ total_byte_size: Precision::Exact(150),
+ column_statistics: vec![ColumnStatistics {
+ null_count: Precision::Inexact(2),
+ max_value: Precision::Inexact(ScalarValue::Int32(Some(120))),
+ min_value: Precision::Exact(ScalarValue::Int32(Some(-10))),
+ sum_value: Precision::Absent,
+ distinct_count: Precision::Absent,
+ }],
+ };
+
+ let items = vec![stats1, stats2];
+
+ let summary_stats = Statistics::try_merge_iter(&items, &schema).unwrap();
+
+ assert_eq!(summary_stats.num_rows, Precision::Inexact(25));
+ assert_eq!(summary_stats.total_byte_size, Precision::Inexact(250));
+
+ let col_stats = &summary_stats.column_statistics[0];
+ assert_eq!(col_stats.null_count, Precision::Inexact(3));
+ assert_eq!(
+ col_stats.max_value,
+ Precision::Inexact(ScalarValue::Int32(Some(120)))
+ );
+ assert_eq!(
+ col_stats.min_value,
+ Precision::Inexact(ScalarValue::Int32(Some(-10)))
+ );
+ assert!(matches!(col_stats.sum_value, Precision::Absent));
+ }
+
+ #[test]
+ fn test_try_merge_empty() {
+ let schema = Arc::new(Schema::new(vec![Field::new(
+ "col1",
+ DataType::Int32,
+ false,
+ )]));
+
+ // Empty collection
+ let items: Vec = vec![];
+
+ let summary_stats = Statistics::try_merge_iter(&items, &schema).unwrap();
+
+ // Verify default values for empty collection
+ assert_eq!(summary_stats.num_rows, Precision::Absent);
+ assert_eq!(summary_stats.total_byte_size, Precision::Absent);
+ assert_eq!(summary_stats.column_statistics.len(), 1);
+ assert_eq!(
+ summary_stats.column_statistics[0].null_count,
+ Precision::Absent
+ );
+ }
+
+ #[test]
+ fn test_try_merge_mismatched_size() {
+ // Create a schema with one column
+ let schema = Arc::new(Schema::new(vec![Field::new(
+ "col1",
+ DataType::Int32,
+ false,
+ )]));
+
+ // No column statistics
+ let stats1 = Statistics::default();
+
+ let stats2 =
+ Statistics::default().add_column_statistics(ColumnStatistics::new_unknown());
+
+ let items = vec![stats1, stats2];
+
+ let e = Statistics::try_merge_iter(&items, &schema).unwrap_err();
+ assert_contains!(e.to_string(), "Error during planning: Cannot merge statistics with different number of columns: 0 vs 1");
+ }
}
diff --git a/datafusion/common/src/utils/memory.rs b/datafusion/common/src/utils/memory.rs
index ab73996fcd8b7..7ac081e0beb84 100644
--- a/datafusion/common/src/utils/memory.rs
+++ b/datafusion/common/src/utils/memory.rs
@@ -25,7 +25,7 @@ use std::mem::size_of;
/// # Parameters
/// - `num_elements`: The number of elements expected in the hash table.
/// - `fixed_size`: A fixed overhead size associated with the collection
-/// (e.g., HashSet or HashTable).
+/// (e.g., HashSet or HashTable).
/// - `T`: The type of elements stored in the hash table.
///
/// # Details
diff --git a/datafusion/core/Cargo.toml b/datafusion/core/Cargo.toml
index 56698e4d7e255..edc0d34b539ac 100644
--- a/datafusion/core/Cargo.toml
+++ b/datafusion/core/Cargo.toml
@@ -125,7 +125,7 @@ datafusion-physical-optimizer = { workspace = true }
datafusion-physical-plan = { workspace = true }
datafusion-session = { workspace = true }
datafusion-sql = { workspace = true }
-flate2 = { version = "1.1.0", optional = true }
+flate2 = { version = "1.1.1", optional = true }
futures = { workspace = true }
itertools = { workspace = true }
log = { workspace = true }
@@ -160,7 +160,7 @@ rand_distr = "0.4.3"
regex = { workspace = true }
rstest = { workspace = true }
serde_json = { workspace = true }
-sysinfo = "0.33.1"
+sysinfo = "0.34.2"
test-utils = { path = "../../test-utils" }
tokio = { workspace = true, features = ["rt-multi-thread", "parking_lot", "fs"] }
diff --git a/datafusion/core/benches/aggregate_query_sql.rs b/datafusion/core/benches/aggregate_query_sql.rs
index ebe94450c1f8d..057a0e1d1b54c 100644
--- a/datafusion/core/benches/aggregate_query_sql.rs
+++ b/datafusion/core/benches/aggregate_query_sql.rs
@@ -29,8 +29,7 @@ use parking_lot::Mutex;
use std::sync::Arc;
use tokio::runtime::Runtime;
-fn query(ctx: Arc>, sql: &str) {
- let rt = Runtime::new().unwrap();
+fn query(ctx: Arc>, rt: &Runtime, sql: &str) {
let df = rt.block_on(ctx.lock().sql(sql)).unwrap();
criterion::black_box(rt.block_on(df.collect()).unwrap());
}
@@ -51,11 +50,13 @@ fn criterion_benchmark(c: &mut Criterion) {
let array_len = 32768 * 2; // 2^16
let batch_size = 2048; // 2^11
let ctx = create_context(partitions_len, array_len, batch_size).unwrap();
+ let rt = Runtime::new().unwrap();
c.bench_function("aggregate_query_no_group_by 15 12", |b| {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT MIN(f64), AVG(f64), COUNT(f64) \
FROM t",
)
@@ -66,6 +67,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT MIN(f64), MAX(f64) \
FROM t",
)
@@ -76,6 +78,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT COUNT(DISTINCT u64_wide) \
FROM t",
)
@@ -86,6 +89,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT COUNT(DISTINCT u64_narrow) \
FROM t",
)
@@ -96,6 +100,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT utf8, MIN(f64), AVG(f64), COUNT(f64) \
FROM t GROUP BY utf8",
)
@@ -106,6 +111,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT utf8, MIN(f64), AVG(f64), COUNT(f64) \
FROM t \
WHERE f32 > 10 AND f32 < 20 GROUP BY utf8",
@@ -117,6 +123,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT u64_narrow, MIN(f64), AVG(f64), COUNT(f64) \
FROM t GROUP BY u64_narrow",
)
@@ -127,6 +134,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT u64_narrow, MIN(f64), AVG(f64), COUNT(f64) \
FROM t \
WHERE f32 > 10 AND f32 < 20 GROUP BY u64_narrow",
@@ -138,6 +146,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT u64_wide, utf8, MIN(f64), AVG(f64), COUNT(f64) \
FROM t GROUP BY u64_wide, utf8",
)
@@ -148,7 +157,8 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
- "SELECT utf8, approx_percentile_cont(u64_wide, 0.5, 2500) \
+ &rt,
+ "SELECT utf8, approx_percentile_cont(0.5, 2500) WITHIN GROUP (ORDER BY u64_wide) \
FROM t GROUP BY utf8",
)
})
@@ -158,7 +168,8 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
- "SELECT utf8, approx_percentile_cont(f32, 0.5, 2500) \
+ &rt,
+ "SELECT utf8, approx_percentile_cont(0.5, 2500) WITHIN GROUP (ORDER BY f32) \
FROM t GROUP BY utf8",
)
})
@@ -168,6 +179,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT MEDIAN(DISTINCT u64_wide), MEDIAN(DISTINCT u64_narrow) \
FROM t",
)
@@ -178,6 +190,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT first_value(u64_wide order by f64, u64_narrow, utf8),\
last_value(u64_wide order by f64, u64_narrow, utf8) \
FROM t GROUP BY u64_narrow",
@@ -189,6 +202,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT first_value(u64_wide ignore nulls order by f64, u64_narrow, utf8), \
last_value(u64_wide ignore nulls order by f64, u64_narrow, utf8) \
FROM t GROUP BY u64_narrow",
@@ -200,6 +214,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT first_value(u64_wide order by f64), \
last_value(u64_wide order by f64) \
FROM t GROUP BY u64_narrow",
diff --git a/datafusion/core/benches/csv_load.rs b/datafusion/core/benches/csv_load.rs
index 2d42121ec9b25..3f984757466d5 100644
--- a/datafusion/core/benches/csv_load.rs
+++ b/datafusion/core/benches/csv_load.rs
@@ -32,8 +32,12 @@ use std::time::Duration;
use test_utils::AccessLogGenerator;
use tokio::runtime::Runtime;
-fn load_csv(ctx: Arc>, path: &str, options: CsvReadOptions) {
- let rt = Runtime::new().unwrap();
+fn load_csv(
+ ctx: Arc>,
+ rt: &Runtime,
+ path: &str,
+ options: CsvReadOptions,
+) {
let df = rt.block_on(ctx.lock().read_csv(path, options)).unwrap();
criterion::black_box(rt.block_on(df.collect()).unwrap());
}
@@ -61,6 +65,7 @@ fn generate_test_file() -> TestCsvFile {
fn criterion_benchmark(c: &mut Criterion) {
let ctx = create_context().unwrap();
+ let rt = Runtime::new().unwrap();
let test_file = generate_test_file();
let mut group = c.benchmark_group("load csv testing");
@@ -70,6 +75,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
load_csv(
ctx.clone(),
+ &rt,
test_file.path().to_str().unwrap(),
CsvReadOptions::default(),
)
@@ -80,6 +86,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
load_csv(
ctx.clone(),
+ &rt,
test_file.path().to_str().unwrap(),
CsvReadOptions::default().null_regex(Some("^NULL$|^$".to_string())),
)
diff --git a/datafusion/core/benches/data_utils/mod.rs b/datafusion/core/benches/data_utils/mod.rs
index 38f6a2c76df6d..fc5f8945c4392 100644
--- a/datafusion/core/benches/data_utils/mod.rs
+++ b/datafusion/core/benches/data_utils/mod.rs
@@ -19,7 +19,8 @@
use arrow::array::{
builder::{Int64Builder, StringBuilder},
- Float32Array, Float64Array, RecordBatch, StringArray, UInt64Array,
+ ArrayRef, Float32Array, Float64Array, RecordBatch, StringArray, StringViewBuilder,
+ UInt64Array,
};
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
use datafusion::datasource::MemTable;
@@ -158,6 +159,31 @@ pub fn create_record_batches(
.collect::>()
}
+/// An enum that wraps either a regular StringBuilder or a GenericByteViewBuilder
+/// so that both can be used interchangeably.
+enum TraceIdBuilder {
+ Utf8(StringBuilder),
+ Utf8View(StringViewBuilder),
+}
+
+impl TraceIdBuilder {
+ /// Append a value to the builder.
+ fn append_value(&mut self, value: &str) {
+ match self {
+ TraceIdBuilder::Utf8(builder) => builder.append_value(value),
+ TraceIdBuilder::Utf8View(builder) => builder.append_value(value),
+ }
+ }
+
+ /// Finish building and return the ArrayRef.
+ fn finish(self) -> ArrayRef {
+ match self {
+ TraceIdBuilder::Utf8(mut builder) => Arc::new(builder.finish()),
+ TraceIdBuilder::Utf8View(mut builder) => Arc::new(builder.finish()),
+ }
+ }
+}
+
/// Create time series data with `partition_cnt` partitions and `sample_cnt` rows per partition
/// in ascending order, if `asc` is true, otherwise randomly sampled using a Pareto distribution
#[allow(dead_code)]
@@ -165,6 +191,7 @@ pub(crate) fn make_data(
partition_cnt: i32,
sample_cnt: i32,
asc: bool,
+ use_view: bool,
) -> Result<(Arc, Vec>), DataFusionError> {
// constants observed from trace data
let simultaneous_group_cnt = 2000;
@@ -177,11 +204,17 @@ pub(crate) fn make_data(
let mut rng = rand::rngs::SmallRng::from_seed([0; 32]);
// populate data
- let schema = test_schema();
+ let schema = test_schema(use_view);
let mut partitions = vec![];
let mut cur_time = 16909000000000i64;
for _ in 0..partition_cnt {
- let mut id_builder = StringBuilder::new();
+ // Choose the appropriate builder based on use_view.
+ let mut id_builder = if use_view {
+ TraceIdBuilder::Utf8View(StringViewBuilder::new())
+ } else {
+ TraceIdBuilder::Utf8(StringBuilder::new())
+ };
+
let mut ts_builder = Int64Builder::new();
let gen_id = |rng: &mut rand::rngs::SmallRng| {
rng.gen::<[u8; 16]>()
@@ -230,10 +263,19 @@ pub(crate) fn make_data(
Ok((schema, partitions))
}
-/// The Schema used by make_data
-fn test_schema() -> SchemaRef {
- Arc::new(Schema::new(vec![
- Field::new("trace_id", DataType::Utf8, false),
- Field::new("timestamp_ms", DataType::Int64, false),
- ]))
+/// Returns a Schema based on the use_view flag
+fn test_schema(use_view: bool) -> SchemaRef {
+ if use_view {
+ // Return Utf8View schema
+ Arc::new(Schema::new(vec![
+ Field::new("trace_id", DataType::Utf8View, false),
+ Field::new("timestamp_ms", DataType::Int64, false),
+ ]))
+ } else {
+ // Return regular Utf8 schema
+ Arc::new(Schema::new(vec![
+ Field::new("trace_id", DataType::Utf8, false),
+ Field::new("timestamp_ms", DataType::Int64, false),
+ ]))
+ }
}
diff --git a/datafusion/core/benches/dataframe.rs b/datafusion/core/benches/dataframe.rs
index 03078e05e1054..832553ebed82a 100644
--- a/datafusion/core/benches/dataframe.rs
+++ b/datafusion/core/benches/dataframe.rs
@@ -44,9 +44,7 @@ fn create_context(field_count: u32) -> datafusion_common::Result) {
- let rt = Runtime::new().unwrap();
-
+fn run(column_count: u32, ctx: Arc, rt: &Runtime) {
criterion::black_box(rt.block_on(async {
let mut data_frame = ctx.table("t").await.unwrap();
@@ -67,11 +65,13 @@ fn run(column_count: u32, ctx: Arc) {
}
fn criterion_benchmark(c: &mut Criterion) {
+ let rt = Runtime::new().unwrap();
+
for column_count in [10, 100, 200, 500] {
let ctx = create_context(column_count).unwrap();
c.bench_function(&format!("with_column_{column_count}"), |b| {
- b.iter(|| run(column_count, ctx.clone()))
+ b.iter(|| run(column_count, ctx.clone(), &rt))
});
}
}
diff --git a/datafusion/core/benches/distinct_query_sql.rs b/datafusion/core/benches/distinct_query_sql.rs
index c242798a56f00..c7056aab86897 100644
--- a/datafusion/core/benches/distinct_query_sql.rs
+++ b/datafusion/core/benches/distinct_query_sql.rs
@@ -33,8 +33,7 @@ use parking_lot::Mutex;
use std::{sync::Arc, time::Duration};
use tokio::runtime::Runtime;
-fn query(ctx: Arc>, sql: &str) {
- let rt = Runtime::new().unwrap();
+fn query(ctx: Arc>, rt: &Runtime, sql: &str) {
let df = rt.block_on(ctx.lock().sql(sql)).unwrap();
criterion::black_box(rt.block_on(df.collect()).unwrap());
}
@@ -55,6 +54,7 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) {
let array_len = 1 << 26; // 64 M
let batch_size = 8192;
let ctx = create_context(partitions_len, array_len, batch_size).unwrap();
+ let rt = Runtime::new().unwrap();
let mut group = c.benchmark_group("custom-measurement-time");
group.measurement_time(Duration::from_secs(40));
@@ -63,6 +63,7 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT DISTINCT u64_narrow FROM t GROUP BY u64_narrow LIMIT 10",
)
})
@@ -72,6 +73,7 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT DISTINCT u64_narrow FROM t GROUP BY u64_narrow LIMIT 100",
)
})
@@ -81,6 +83,7 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT DISTINCT u64_narrow FROM t GROUP BY u64_narrow LIMIT 1000",
)
})
@@ -90,6 +93,7 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT DISTINCT u64_narrow FROM t GROUP BY u64_narrow LIMIT 10000",
)
})
@@ -99,6 +103,7 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT u64_narrow, u64_wide, utf8, f64 FROM t GROUP BY 1, 2, 3, 4 LIMIT 10",
)
})
@@ -118,12 +123,9 @@ async fn distinct_with_limit(
Ok(())
}
-fn run(plan: Arc, ctx: Arc) {
- let rt = Runtime::new().unwrap();
- criterion::black_box(
- rt.block_on(async { distinct_with_limit(plan.clone(), ctx.clone()).await }),
- )
- .unwrap();
+fn run(rt: &Runtime, plan: Arc, ctx: Arc) {
+ criterion::black_box(rt.block_on(distinct_with_limit(plan.clone(), ctx.clone())))
+ .unwrap();
}
pub async fn create_context_sampled_data(
@@ -131,7 +133,8 @@ pub async fn create_context_sampled_data(
partition_cnt: i32,
sample_cnt: i32,
) -> Result<(Arc, Arc)> {
- let (schema, parts) = make_data(partition_cnt, sample_cnt, false /* asc */).unwrap();
+ let (schema, parts) =
+ make_data(partition_cnt, sample_cnt, false /* asc */, false).unwrap();
let mem_table = Arc::new(MemTable::try_new(schema, parts).unwrap());
// Create the DataFrame
@@ -145,58 +148,47 @@ pub async fn create_context_sampled_data(
fn criterion_benchmark_limited_distinct_sampled(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
-
let limit = 10;
let partitions = 100;
let samples = 100_000;
let sql =
format!("select DISTINCT trace_id from traces group by trace_id limit {limit};");
-
- let distinct_trace_id_100_partitions_100_000_samples_limit_100 = rt.block_on(async {
- create_context_sampled_data(sql.as_str(), partitions, samples)
- .await
- .unwrap()
- });
-
c.bench_function(
format!("distinct query with {} partitions and {} samples per partition with limit {}", partitions, samples, limit).as_str(),
- |b| b.iter(|| run(distinct_trace_id_100_partitions_100_000_samples_limit_100.0.clone(),
- distinct_trace_id_100_partitions_100_000_samples_limit_100.1.clone())),
+ |b| b.iter(|| {
+ let (plan, ctx) = rt.block_on(
+ create_context_sampled_data(sql.as_str(), partitions, samples)
+ ).unwrap();
+ run(&rt, plan.clone(), ctx.clone())
+ }),
);
let partitions = 10;
let samples = 1_000_000;
let sql =
format!("select DISTINCT trace_id from traces group by trace_id limit {limit};");
-
- let distinct_trace_id_10_partitions_1_000_000_samples_limit_10 = rt.block_on(async {
- create_context_sampled_data(sql.as_str(), partitions, samples)
- .await
- .unwrap()
- });
-
c.bench_function(
format!("distinct query with {} partitions and {} samples per partition with limit {}", partitions, samples, limit).as_str(),
- |b| b.iter(|| run(distinct_trace_id_10_partitions_1_000_000_samples_limit_10.0.clone(),
- distinct_trace_id_10_partitions_1_000_000_samples_limit_10.1.clone())),
+ |b| b.iter(|| {
+ let (plan, ctx) = rt.block_on(
+ create_context_sampled_data(sql.as_str(), partitions, samples)
+ ).unwrap();
+ run(&rt, plan.clone(), ctx.clone())
+ }),
);
let partitions = 1;
let samples = 10_000_000;
let sql =
format!("select DISTINCT trace_id from traces group by trace_id limit {limit};");
-
- let rt = Runtime::new().unwrap();
- let distinct_trace_id_1_partition_10_000_000_samples_limit_10 = rt.block_on(async {
- create_context_sampled_data(sql.as_str(), partitions, samples)
- .await
- .unwrap()
- });
-
c.bench_function(
format!("distinct query with {} partitions and {} samples per partition with limit {}", partitions, samples, limit).as_str(),
- |b| b.iter(|| run(distinct_trace_id_1_partition_10_000_000_samples_limit_10.0.clone(),
- distinct_trace_id_1_partition_10_000_000_samples_limit_10.1.clone())),
+ |b| b.iter(|| {
+ let (plan, ctx) = rt.block_on(
+ create_context_sampled_data(sql.as_str(), partitions, samples)
+ ).unwrap();
+ run(&rt, plan.clone(), ctx.clone())
+ }),
);
}
diff --git a/datafusion/core/benches/filter_query_sql.rs b/datafusion/core/benches/filter_query_sql.rs
index 0e09ae09d7c2e..c82a1607184dc 100644
--- a/datafusion/core/benches/filter_query_sql.rs
+++ b/datafusion/core/benches/filter_query_sql.rs
@@ -27,9 +27,7 @@ use futures::executor::block_on;
use std::sync::Arc;
use tokio::runtime::Runtime;
-async fn query(ctx: &SessionContext, sql: &str) {
- let rt = Runtime::new().unwrap();
-
+async fn query(ctx: &SessionContext, rt: &Runtime, sql: &str) {
// execute the query
let df = rt.block_on(ctx.sql(sql)).unwrap();
criterion::black_box(rt.block_on(df.collect()).unwrap());
@@ -68,10 +66,11 @@ fn create_context(array_len: usize, batch_size: usize) -> Result
fn criterion_benchmark(c: &mut Criterion) {
let array_len = 524_288; // 2^19
let batch_size = 4096; // 2^12
+ let rt = Runtime::new().unwrap();
c.bench_function("filter_array", |b| {
let ctx = create_context(array_len, batch_size).unwrap();
- b.iter(|| block_on(query(&ctx, "select f32, f64 from t where f32 >= f64")))
+ b.iter(|| block_on(query(&ctx, &rt, "select f32, f64 from t where f32 >= f64")))
});
c.bench_function("filter_scalar", |b| {
@@ -79,6 +78,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
block_on(query(
&ctx,
+ &rt,
"select f32, f64 from t where f32 >= 250 and f64 > 250",
))
})
@@ -89,6 +89,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
block_on(query(
&ctx,
+ &rt,
"select f32, f64 from t where f32 in (10, 20, 30, 40)",
))
})
diff --git a/datafusion/core/benches/math_query_sql.rs b/datafusion/core/benches/math_query_sql.rs
index 92c59d5066401..76824850c114c 100644
--- a/datafusion/core/benches/math_query_sql.rs
+++ b/datafusion/core/benches/math_query_sql.rs
@@ -36,9 +36,7 @@ use datafusion::datasource::MemTable;
use datafusion::error::Result;
use datafusion::execution::context::SessionContext;
-fn query(ctx: Arc>, sql: &str) {
- let rt = Runtime::new().unwrap();
-
+fn query(ctx: Arc>, rt: &Runtime, sql: &str) {
// execute the query
let df = rt.block_on(ctx.lock().sql(sql)).unwrap();
rt.block_on(df.collect()).unwrap();
@@ -81,29 +79,31 @@ fn criterion_benchmark(c: &mut Criterion) {
let array_len = 1048576; // 2^20
let batch_size = 512; // 2^9
let ctx = create_context(array_len, batch_size).unwrap();
+ let rt = Runtime::new().unwrap();
+
c.bench_function("sqrt_20_9", |b| {
- b.iter(|| query(ctx.clone(), "SELECT sqrt(f32) FROM t"))
+ b.iter(|| query(ctx.clone(), &rt, "SELECT sqrt(f32) FROM t"))
});
let array_len = 1048576; // 2^20
let batch_size = 4096; // 2^12
let ctx = create_context(array_len, batch_size).unwrap();
c.bench_function("sqrt_20_12", |b| {
- b.iter(|| query(ctx.clone(), "SELECT sqrt(f32) FROM t"))
+ b.iter(|| query(ctx.clone(), &rt, "SELECT sqrt(f32) FROM t"))
});
let array_len = 4194304; // 2^22
let batch_size = 4096; // 2^12
let ctx = create_context(array_len, batch_size).unwrap();
c.bench_function("sqrt_22_12", |b| {
- b.iter(|| query(ctx.clone(), "SELECT sqrt(f32) FROM t"))
+ b.iter(|| query(ctx.clone(), &rt, "SELECT sqrt(f32) FROM t"))
});
let array_len = 4194304; // 2^22
let batch_size = 16384; // 2^14
let ctx = create_context(array_len, batch_size).unwrap();
c.bench_function("sqrt_22_14", |b| {
- b.iter(|| query(ctx.clone(), "SELECT sqrt(f32) FROM t"))
+ b.iter(|| query(ctx.clone(), &rt, "SELECT sqrt(f32) FROM t"))
});
}
diff --git a/datafusion/core/benches/physical_plan.rs b/datafusion/core/benches/physical_plan.rs
index aae1457ab9e6d..0a65c52f72def 100644
--- a/datafusion/core/benches/physical_plan.rs
+++ b/datafusion/core/benches/physical_plan.rs
@@ -42,6 +42,7 @@ use datafusion_physical_expr_common::sort_expr::LexOrdering;
// as inputs. All record batches must have the same schema.
fn sort_preserving_merge_operator(
session_ctx: Arc,
+ rt: &Runtime,
batches: Vec,
sort: &[&str],
) {
@@ -63,7 +64,6 @@ fn sort_preserving_merge_operator(
.unwrap();
let merge = Arc::new(SortPreservingMergeExec::new(sort, exec));
let task_ctx = session_ctx.task_ctx();
- let rt = Runtime::new().unwrap();
rt.block_on(collect(merge, task_ctx)).unwrap();
}
@@ -166,14 +166,16 @@ fn criterion_benchmark(c: &mut Criterion) {
];
let ctx = Arc::new(SessionContext::new());
+ let rt = Runtime::new().unwrap();
+
for (name, input) in benches {
- let ctx_clone = ctx.clone();
- c.bench_function(name, move |b| {
+ c.bench_function(name, |b| {
b.iter_batched(
|| input.clone(),
|input| {
sort_preserving_merge_operator(
- ctx_clone.clone(),
+ ctx.clone(),
+ &rt,
input,
&["a", "b", "c", "d"],
);
diff --git a/datafusion/core/benches/sort_limit_query_sql.rs b/datafusion/core/benches/sort_limit_query_sql.rs
index cfd4b8bc4bba8..e535a018161f1 100644
--- a/datafusion/core/benches/sort_limit_query_sql.rs
+++ b/datafusion/core/benches/sort_limit_query_sql.rs
@@ -37,9 +37,7 @@ use datafusion::execution::context::SessionContext;
use tokio::runtime::Runtime;
-fn query(ctx: Arc>, sql: &str) {
- let rt = Runtime::new().unwrap();
-
+fn query(ctx: Arc>, rt: &Runtime, sql: &str) {
// execute the query
let df = rt.block_on(ctx.lock().sql(sql)).unwrap();
rt.block_on(df.collect()).unwrap();
@@ -104,11 +102,14 @@ fn create_context() -> Arc> {
}
fn criterion_benchmark(c: &mut Criterion) {
+ let ctx = create_context();
+ let rt = Runtime::new().unwrap();
+
c.bench_function("sort_and_limit_by_int", |b| {
- let ctx = create_context();
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT c1, c13, c6, c10 \
FROM aggregate_test_100 \
ORDER BY c6
@@ -118,10 +119,10 @@ fn criterion_benchmark(c: &mut Criterion) {
});
c.bench_function("sort_and_limit_by_float", |b| {
- let ctx = create_context();
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT c1, c13, c12 \
FROM aggregate_test_100 \
ORDER BY c13
@@ -131,10 +132,10 @@ fn criterion_benchmark(c: &mut Criterion) {
});
c.bench_function("sort_and_limit_lex_by_int", |b| {
- let ctx = create_context();
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT c1, c13, c6, c10 \
FROM aggregate_test_100 \
ORDER BY c6 DESC, c10 DESC
@@ -144,10 +145,10 @@ fn criterion_benchmark(c: &mut Criterion) {
});
c.bench_function("sort_and_limit_lex_by_string", |b| {
- let ctx = create_context();
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT c1, c13, c6, c10 \
FROM aggregate_test_100 \
ORDER BY c1, c13
diff --git a/datafusion/core/benches/sql_planner.rs b/datafusion/core/benches/sql_planner.rs
index 2d79778d4d42f..49cc830d58bc4 100644
--- a/datafusion/core/benches/sql_planner.rs
+++ b/datafusion/core/benches/sql_planner.rs
@@ -45,14 +45,12 @@ const BENCHMARKS_PATH_2: &str = "./benchmarks/";
const CLICKBENCH_DATA_PATH: &str = "data/hits_partitioned/";
/// Create a logical plan from the specified sql
-fn logical_plan(ctx: &SessionContext, sql: &str) {
- let rt = Runtime::new().unwrap();
+fn logical_plan(ctx: &SessionContext, rt: &Runtime, sql: &str) {
criterion::black_box(rt.block_on(ctx.sql(sql)).unwrap());
}
/// Create a physical ExecutionPlan (by way of logical plan)
-fn physical_plan(ctx: &SessionContext, sql: &str) {
- let rt = Runtime::new().unwrap();
+fn physical_plan(ctx: &SessionContext, rt: &Runtime, sql: &str) {
criterion::black_box(rt.block_on(async {
ctx.sql(sql)
.await
@@ -104,9 +102,8 @@ fn register_defs(ctx: SessionContext, defs: Vec) -> SessionContext {
ctx
}
-fn register_clickbench_hits_table() -> SessionContext {
+fn register_clickbench_hits_table(rt: &Runtime) -> SessionContext {
let ctx = SessionContext::new();
- let rt = Runtime::new().unwrap();
// use an external table for clickbench benchmarks
let path =
@@ -128,7 +125,11 @@ fn register_clickbench_hits_table() -> SessionContext {
/// Target of this benchmark: control that placeholders replacing does not get slower,
/// if the query does not contain placeholders at all.
-fn benchmark_with_param_values_many_columns(ctx: &SessionContext, b: &mut Bencher) {
+fn benchmark_with_param_values_many_columns(
+ ctx: &SessionContext,
+ rt: &Runtime,
+ b: &mut Bencher,
+) {
const COLUMNS_NUM: usize = 200;
let mut aggregates = String::new();
for i in 0..COLUMNS_NUM {
@@ -140,7 +141,6 @@ fn benchmark_with_param_values_many_columns(ctx: &SessionContext, b: &mut Benche
// SELECT max(attr0), ..., max(attrN) FROM t1.
let query = format!("SELECT {} FROM t1", aggregates);
let statement = ctx.state().sql_to_statement(&query, "Generic").unwrap();
- let rt = Runtime::new().unwrap();
let plan =
rt.block_on(async { ctx.state().statement_to_plan(statement).await.unwrap() });
b.iter(|| {
@@ -230,33 +230,35 @@ fn criterion_benchmark(c: &mut Criterion) {
}
let ctx = create_context();
+ let rt = Runtime::new().unwrap();
// Test simplest
// https://github.com/apache/datafusion/issues/5157
c.bench_function("logical_select_one_from_700", |b| {
- b.iter(|| logical_plan(&ctx, "SELECT c1 FROM t700"))
+ b.iter(|| logical_plan(&ctx, &rt, "SELECT c1 FROM t700"))
});
// Test simplest
// https://github.com/apache/datafusion/issues/5157
c.bench_function("physical_select_one_from_700", |b| {
- b.iter(|| physical_plan(&ctx, "SELECT c1 FROM t700"))
+ b.iter(|| physical_plan(&ctx, &rt, "SELECT c1 FROM t700"))
});
// Test simplest
c.bench_function("logical_select_all_from_1000", |b| {
- b.iter(|| logical_plan(&ctx, "SELECT * FROM t1000"))
+ b.iter(|| logical_plan(&ctx, &rt, "SELECT * FROM t1000"))
});
// Test simplest
c.bench_function("physical_select_all_from_1000", |b| {
- b.iter(|| physical_plan(&ctx, "SELECT * FROM t1000"))
+ b.iter(|| physical_plan(&ctx, &rt, "SELECT * FROM t1000"))
});
c.bench_function("logical_trivial_join_low_numbered_columns", |b| {
b.iter(|| {
logical_plan(
&ctx,
+ &rt,
"SELECT t1.a2, t2.b2 \
FROM t1, t2 WHERE a1 = b1",
)
@@ -267,6 +269,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
logical_plan(
&ctx,
+ &rt,
"SELECT t1.a99, t2.b99 \
FROM t1, t2 WHERE a199 = b199",
)
@@ -277,6 +280,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
logical_plan(
&ctx,
+ &rt,
"SELECT t1.a99, MIN(t2.b1), MAX(t2.b199), AVG(t2.b123), COUNT(t2.b73) \
FROM t1 JOIN t2 ON t1.a199 = t2.b199 GROUP BY t1.a99",
)
@@ -293,7 +297,7 @@ fn criterion_benchmark(c: &mut Criterion) {
}
let query = format!("SELECT {} FROM t1", aggregates);
b.iter(|| {
- physical_plan(&ctx, &query);
+ physical_plan(&ctx, &rt, &query);
});
});
@@ -302,6 +306,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
physical_plan(
&ctx,
+ &rt,
"SELECT t1.a7, t2.b8 \
FROM t1, t2 WHERE a7 = b7 \
ORDER BY a7",
@@ -313,6 +318,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
physical_plan(
&ctx,
+ &rt,
"SELECT t1.a7, t2.b8 \
FROM t1, t2 WHERE a7 < b7 \
ORDER BY a7",
@@ -324,6 +330,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
physical_plan(
&ctx,
+ &rt,
"SELECT ta.a9, tb.a10, tc.a11, td.a12, te.a13, tf.a14 \
FROM t1 AS ta, t1 AS tb, t1 AS tc, t1 AS td, t1 AS te, t1 AS tf \
WHERE ta.a9 = tb.a10 AND tb.a10 = tc.a11 AND tc.a11 = td.a12 AND \
@@ -336,6 +343,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
physical_plan(
&ctx,
+ &rt,
"SELECT t1.a7 \
FROM t1 WHERE a7 = (SELECT b8 FROM t2)",
);
@@ -346,6 +354,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
physical_plan(
&ctx,
+ &rt,
"SELECT t1.a7 FROM t1 \
INTERSECT SELECT t2.b8 FROM t2",
);
@@ -356,6 +365,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
logical_plan(
&ctx,
+ &rt,
"SELECT DISTINCT t1.a7 \
FROM t1, t2 WHERE t1.a7 = t2.b8",
);
@@ -370,7 +380,7 @@ fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("physical_sorted_union_orderby", |b| {
// SELECT ... UNION ALL ...
let query = union_orderby_query(20);
- b.iter(|| physical_plan(&ctx, &query))
+ b.iter(|| physical_plan(&ctx, &rt, &query))
});
// --- TPC-H ---
@@ -393,7 +403,7 @@ fn criterion_benchmark(c: &mut Criterion) {
let sql =
std::fs::read_to_string(format!("{benchmarks_path}queries/{q}.sql")).unwrap();
c.bench_function(&format!("physical_plan_tpch_{}", q), |b| {
- b.iter(|| physical_plan(&tpch_ctx, &sql))
+ b.iter(|| physical_plan(&tpch_ctx, &rt, &sql))
});
}
@@ -407,7 +417,7 @@ fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("physical_plan_tpch_all", |b| {
b.iter(|| {
for sql in &all_tpch_sql_queries {
- physical_plan(&tpch_ctx, sql)
+ physical_plan(&tpch_ctx, &rt, sql)
}
})
});
@@ -442,7 +452,7 @@ fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("physical_plan_tpcds_all", |b| {
b.iter(|| {
for sql in &all_tpcds_sql_queries {
- physical_plan(&tpcds_ctx, sql)
+ physical_plan(&tpcds_ctx, &rt, sql)
}
})
});
@@ -468,7 +478,7 @@ fn criterion_benchmark(c: &mut Criterion) {
.map(|l| l.expect("Could not parse line"))
.collect_vec();
- let clickbench_ctx = register_clickbench_hits_table();
+ let clickbench_ctx = register_clickbench_hits_table(&rt);
// for (i, sql) in clickbench_queries.iter().enumerate() {
// c.bench_function(&format!("logical_plan_clickbench_q{}", i + 1), |b| {
@@ -478,7 +488,7 @@ fn criterion_benchmark(c: &mut Criterion) {
for (i, sql) in clickbench_queries.iter().enumerate() {
c.bench_function(&format!("physical_plan_clickbench_q{}", i + 1), |b| {
- b.iter(|| physical_plan(&clickbench_ctx, sql))
+ b.iter(|| physical_plan(&clickbench_ctx, &rt, sql))
});
}
@@ -493,13 +503,13 @@ fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("physical_plan_clickbench_all", |b| {
b.iter(|| {
for sql in &clickbench_queries {
- physical_plan(&clickbench_ctx, sql)
+ physical_plan(&clickbench_ctx, &rt, sql)
}
})
});
c.bench_function("with_param_values_many_columns", |b| {
- benchmark_with_param_values_many_columns(&ctx, b);
+ benchmark_with_param_values_many_columns(&ctx, &rt, b);
});
}
diff --git a/datafusion/core/benches/struct_query_sql.rs b/datafusion/core/benches/struct_query_sql.rs
index 3ef7292c66271..f9cc43d1ea2c5 100644
--- a/datafusion/core/benches/struct_query_sql.rs
+++ b/datafusion/core/benches/struct_query_sql.rs
@@ -27,9 +27,7 @@ use futures::executor::block_on;
use std::sync::Arc;
use tokio::runtime::Runtime;
-async fn query(ctx: &SessionContext, sql: &str) {
- let rt = Runtime::new().unwrap();
-
+async fn query(ctx: &SessionContext, rt: &Runtime, sql: &str) {
// execute the query
let df = rt.block_on(ctx.sql(sql)).unwrap();
criterion::black_box(rt.block_on(df.collect()).unwrap());
@@ -68,10 +66,11 @@ fn create_context(array_len: usize, batch_size: usize) -> Result
fn criterion_benchmark(c: &mut Criterion) {
let array_len = 524_288; // 2^19
let batch_size = 4096; // 2^12
+ let ctx = create_context(array_len, batch_size).unwrap();
+ let rt = Runtime::new().unwrap();
c.bench_function("struct", |b| {
- let ctx = create_context(array_len, batch_size).unwrap();
- b.iter(|| block_on(query(&ctx, "select struct(f32, f64) from t")))
+ b.iter(|| block_on(query(&ctx, &rt, "select struct(f32, f64) from t")))
});
}
diff --git a/datafusion/core/benches/topk_aggregate.rs b/datafusion/core/benches/topk_aggregate.rs
index 922cbd2b42292..cf3c7fa2e26fe 100644
--- a/datafusion/core/benches/topk_aggregate.rs
+++ b/datafusion/core/benches/topk_aggregate.rs
@@ -33,8 +33,9 @@ async fn create_context(
sample_cnt: i32,
asc: bool,
use_topk: bool,
+ use_view: bool,
) -> Result<(Arc, Arc)> {
- let (schema, parts) = make_data(partition_cnt, sample_cnt, asc).unwrap();
+ let (schema, parts) = make_data(partition_cnt, sample_cnt, asc, use_view).unwrap();
let mem_table = Arc::new(MemTable::try_new(schema, parts).unwrap());
// Create the DataFrame
@@ -55,8 +56,7 @@ async fn create_context(
Ok((physical_plan, ctx.task_ctx()))
}
-fn run(plan: Arc, ctx: Arc, asc: bool) {
- let rt = Runtime::new().unwrap();
+fn run(rt: &Runtime, plan: Arc, ctx: Arc, asc: bool) {
criterion::black_box(
rt.block_on(async { aggregate(plan.clone(), ctx.clone(), asc).await }),
)
@@ -99,40 +99,37 @@ async fn aggregate(
}
fn criterion_benchmark(c: &mut Criterion) {
+ let rt = Runtime::new().unwrap();
let limit = 10;
let partitions = 10;
let samples = 1_000_000;
- let rt = Runtime::new().unwrap();
- let topk_real = rt.block_on(async {
- create_context(limit, partitions, samples, false, true)
- .await
- .unwrap()
- });
- let topk_asc = rt.block_on(async {
- create_context(limit, partitions, samples, true, true)
- .await
- .unwrap()
- });
- let real = rt.block_on(async {
- create_context(limit, partitions, samples, false, false)
- .await
- .unwrap()
- });
- let asc = rt.block_on(async {
- create_context(limit, partitions, samples, true, false)
- .await
- .unwrap()
- });
-
c.bench_function(
format!("aggregate {} time-series rows", partitions * samples).as_str(),
- |b| b.iter(|| run(real.0.clone(), real.1.clone(), false)),
+ |b| {
+ b.iter(|| {
+ let real = rt.block_on(async {
+ create_context(limit, partitions, samples, false, false, false)
+ .await
+ .unwrap()
+ });
+ run(&rt, real.0.clone(), real.1.clone(), false)
+ })
+ },
);
c.bench_function(
format!("aggregate {} worst-case rows", partitions * samples).as_str(),
- |b| b.iter(|| run(asc.0.clone(), asc.1.clone(), true)),
+ |b| {
+ b.iter(|| {
+ let asc = rt.block_on(async {
+ create_context(limit, partitions, samples, true, false, false)
+ .await
+ .unwrap()
+ });
+ run(&rt, asc.0.clone(), asc.1.clone(), true)
+ })
+ },
);
c.bench_function(
@@ -141,7 +138,16 @@ fn criterion_benchmark(c: &mut Criterion) {
partitions * samples
)
.as_str(),
- |b| b.iter(|| run(topk_real.0.clone(), topk_real.1.clone(), false)),
+ |b| {
+ b.iter(|| {
+ let topk_real = rt.block_on(async {
+ create_context(limit, partitions, samples, false, true, false)
+ .await
+ .unwrap()
+ });
+ run(&rt, topk_real.0.clone(), topk_real.1.clone(), false)
+ })
+ },
);
c.bench_function(
@@ -150,7 +156,54 @@ fn criterion_benchmark(c: &mut Criterion) {
partitions * samples
)
.as_str(),
- |b| b.iter(|| run(topk_asc.0.clone(), topk_asc.1.clone(), true)),
+ |b| {
+ b.iter(|| {
+ let topk_asc = rt.block_on(async {
+ create_context(limit, partitions, samples, true, true, false)
+ .await
+ .unwrap()
+ });
+ run(&rt, topk_asc.0.clone(), topk_asc.1.clone(), true)
+ })
+ },
+ );
+
+ // Utf8View schema,time-series rows
+ c.bench_function(
+ format!(
+ "top k={limit} aggregate {} time-series rows [Utf8View]",
+ partitions * samples
+ )
+ .as_str(),
+ |b| {
+ b.iter(|| {
+ let topk_real = rt.block_on(async {
+ create_context(limit, partitions, samples, false, true, true)
+ .await
+ .unwrap()
+ });
+ run(&rt, topk_real.0.clone(), topk_real.1.clone(), false)
+ })
+ },
+ );
+
+ // Utf8View schema,worst-case rows
+ c.bench_function(
+ format!(
+ "top k={limit} aggregate {} worst-case rows [Utf8View]",
+ partitions * samples
+ )
+ .as_str(),
+ |b| {
+ b.iter(|| {
+ let topk_asc = rt.block_on(async {
+ create_context(limit, partitions, samples, true, true, true)
+ .await
+ .unwrap()
+ });
+ run(&rt, topk_asc.0.clone(), topk_asc.1.clone(), true)
+ })
+ },
);
}
diff --git a/datafusion/core/benches/window_query_sql.rs b/datafusion/core/benches/window_query_sql.rs
index 42a1e51be361a..a55d17a7c5dcf 100644
--- a/datafusion/core/benches/window_query_sql.rs
+++ b/datafusion/core/benches/window_query_sql.rs
@@ -29,8 +29,7 @@ use parking_lot::Mutex;
use std::sync::Arc;
use tokio::runtime::Runtime;
-fn query(ctx: Arc>, sql: &str) {
- let rt = Runtime::new().unwrap();
+fn query(ctx: Arc>, rt: &Runtime, sql: &str) {
let df = rt.block_on(ctx.lock().sql(sql)).unwrap();
criterion::black_box(rt.block_on(df.collect()).unwrap());
}
@@ -51,11 +50,13 @@ fn criterion_benchmark(c: &mut Criterion) {
let array_len = 1024 * 1024;
let batch_size = 8 * 1024;
let ctx = create_context(partitions_len, array_len, batch_size).unwrap();
+ let rt = Runtime::new().unwrap();
c.bench_function("window empty over, aggregate functions", |b| {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT \
MAX(f64) OVER (), \
MIN(f32) OVER (), \
@@ -69,6 +70,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT \
FIRST_VALUE(f64) OVER (), \
LAST_VALUE(f32) OVER (), \
@@ -82,6 +84,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT \
MAX(f64) OVER (ORDER BY u64_narrow), \
MIN(f32) OVER (ORDER BY u64_narrow DESC), \
@@ -95,6 +98,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT \
FIRST_VALUE(f64) OVER (ORDER BY u64_narrow), \
LAST_VALUE(f32) OVER (ORDER BY u64_narrow DESC), \
@@ -108,6 +112,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT \
MAX(f64) OVER (PARTITION BY u64_wide), \
MIN(f32) OVER (PARTITION BY u64_wide), \
@@ -123,6 +128,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT \
MAX(f64) OVER (PARTITION BY u64_narrow), \
MIN(f32) OVER (PARTITION BY u64_narrow), \
@@ -137,6 +143,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT \
FIRST_VALUE(f64) OVER (PARTITION BY u64_wide), \
LAST_VALUE(f32) OVER (PARTITION BY u64_wide), \
@@ -150,6 +157,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT \
FIRST_VALUE(f64) OVER (PARTITION BY u64_narrow), \
LAST_VALUE(f32) OVER (PARTITION BY u64_narrow), \
@@ -165,6 +173,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT \
MAX(f64) OVER (PARTITION BY u64_wide ORDER by f64), \
MIN(f32) OVER (PARTITION BY u64_wide ORDER by f64), \
@@ -181,6 +190,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT \
MAX(f64) OVER (PARTITION BY u64_narrow ORDER by f64), \
MIN(f32) OVER (PARTITION BY u64_narrow ORDER by f64), \
@@ -197,6 +207,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT \
FIRST_VALUE(f64) OVER (PARTITION BY u64_wide ORDER by f64), \
LAST_VALUE(f32) OVER (PARTITION BY u64_wide ORDER by f64), \
@@ -213,6 +224,7 @@ fn criterion_benchmark(c: &mut Criterion) {
b.iter(|| {
query(
ctx.clone(),
+ &rt,
"SELECT \
FIRST_VALUE(f64) OVER (PARTITION BY u64_narrow ORDER by f64), \
LAST_VALUE(f32) OVER (PARTITION BY u64_narrow ORDER by f64), \
diff --git a/datafusion/core/src/bin/print_runtime_config_docs.rs b/datafusion/core/src/bin/print_runtime_config_docs.rs
new file mode 100644
index 0000000000000..f374a5acb78a0
--- /dev/null
+++ b/datafusion/core/src/bin/print_runtime_config_docs.rs
@@ -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.
+
+use datafusion_execution::runtime_env::RuntimeEnvBuilder;
+
+fn main() {
+ let docs = RuntimeEnvBuilder::generate_config_markdown();
+ println!("{}", docs);
+}
diff --git a/datafusion/core/src/dataframe/mod.rs b/datafusion/core/src/dataframe/mod.rs
index 9c27c7c5d3076..9a70f8f43fb61 100644
--- a/datafusion/core/src/dataframe/mod.rs
+++ b/datafusion/core/src/dataframe/mod.rs
@@ -685,6 +685,46 @@ impl DataFrame {
})
}
+ /// Calculate the union of two [`DataFrame`]s using column names, preserving duplicate rows.
+ ///
+ /// The two [`DataFrame`]s are combined using column names rather than position,
+ /// filling missing columns with null.
+ ///
+ ///
+ /// # Example
+ /// ```
+ /// # use datafusion::prelude::*;
+ /// # use datafusion::error::Result;
+ /// # use datafusion_common::assert_batches_sorted_eq;
+ /// # #[tokio::main]
+ /// # async fn main() -> Result<()> {
+ /// let ctx = SessionContext::new();
+ /// let df = ctx.read_csv("tests/data/example.csv", CsvReadOptions::new()).await?;
+ /// let d2 = df.clone().select_columns(&["b", "c", "a"])?.with_column("d", lit("77"))?;
+ /// let df = df.union_by_name(d2)?;
+ /// let expected = vec![
+ /// "+---+---+---+----+",
+ /// "| a | b | c | d |",
+ /// "+---+---+---+----+",
+ /// "| 1 | 2 | 3 | |",
+ /// "| 1 | 2 | 3 | 77 |",
+ /// "+---+---+---+----+"
+ /// ];
+ /// # assert_batches_sorted_eq!(expected, &df.collect().await?);
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn union_by_name(self, dataframe: DataFrame) -> Result {
+ let plan = LogicalPlanBuilder::from(self.plan)
+ .union_by_name(dataframe.plan)?
+ .build()?;
+ Ok(DataFrame {
+ session_state: self.session_state,
+ plan,
+ projection_requires_validation: true,
+ })
+ }
+
/// Calculate the distinct union of two [`DataFrame`]s.
///
/// The two [`DataFrame`]s must have exactly the same schema. Any duplicate
@@ -724,6 +764,45 @@ impl DataFrame {
})
}
+ /// Calculate the union of two [`DataFrame`]s using column names with all duplicated rows removed.
+ ///
+ /// The two [`DataFrame`]s are combined using column names rather than position,
+ /// filling missing columns with null.
+ ///
+ ///
+ /// # Example
+ /// ```
+ /// # use datafusion::prelude::*;
+ /// # use datafusion::error::Result;
+ /// # use datafusion_common::assert_batches_sorted_eq;
+ /// # #[tokio::main]
+ /// # async fn main() -> Result<()> {
+ /// let ctx = SessionContext::new();
+ /// let df = ctx.read_csv("tests/data/example.csv", CsvReadOptions::new()).await?;
+ /// let d2 = df.clone().select_columns(&["b", "c", "a"])?;
+ /// let df = df.union_by_name_distinct(d2)?;
+ /// let expected = vec![
+ /// "+---+---+---+",
+ /// "| a | b | c |",
+ /// "+---+---+---+",
+ /// "| 1 | 2 | 3 |",
+ /// "+---+---+---+"
+ /// ];
+ /// # assert_batches_sorted_eq!(expected, &df.collect().await?);
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn union_by_name_distinct(self, dataframe: DataFrame) -> Result {
+ let plan = LogicalPlanBuilder::from(self.plan)
+ .union_by_name_distinct(dataframe.plan)?
+ .build()?;
+ Ok(DataFrame {
+ session_state: self.session_state,
+ plan,
+ projection_requires_validation: true,
+ })
+ }
+
/// Return a new `DataFrame` with all duplicated rows removed.
///
/// # Example
diff --git a/datafusion/core/src/datasource/file_format/arrow.rs b/datafusion/core/src/datasource/file_format/arrow.rs
index 6c7c9463cf3b7..7fc27453d1ad5 100644
--- a/datafusion/core/src/datasource/file_format/arrow.rs
+++ b/datafusion/core/src/datasource/file_format/arrow.rs
@@ -144,6 +144,7 @@ impl FileFormat for ArrowFormat {
for object in objects {
let r = store.as_ref().get(&object.location).await?;
let schema = match r.payload {
+ #[cfg(not(target_arch = "wasm32"))]
GetResultPayload::File(mut file, _) => {
let reader = FileReader::try_new(&mut file, None)?;
reader.schema()
@@ -442,7 +443,7 @@ mod tests {
let object_meta = ObjectMeta {
location,
last_modified: DateTime::default(),
- size: usize::MAX,
+ size: u64::MAX,
e_tag: None,
version: None,
};
@@ -485,7 +486,7 @@ mod tests {
let object_meta = ObjectMeta {
location,
last_modified: DateTime::default(),
- size: usize::MAX,
+ size: u64::MAX,
e_tag: None,
version: None,
};
diff --git a/datafusion/core/src/datasource/file_format/avro.rs b/datafusion/core/src/datasource/file_format/avro.rs
index a9516aad9e22d..3428d08a6ae52 100644
--- a/datafusion/core/src/datasource/file_format/avro.rs
+++ b/datafusion/core/src/datasource/file_format/avro.rs
@@ -382,6 +382,15 @@ mod tests {
let testdata = test_util::arrow_test_data();
let store_root = format!("{testdata}/avro");
let format = AvroFormat {};
- scan_format(state, &format, &store_root, file_name, projection, limit).await
+ scan_format(
+ state,
+ &format,
+ None,
+ &store_root,
+ file_name,
+ projection,
+ limit,
+ )
+ .await
}
}
diff --git a/datafusion/core/src/datasource/file_format/csv.rs b/datafusion/core/src/datasource/file_format/csv.rs
index 309458975ab6c..323bc28057d43 100644
--- a/datafusion/core/src/datasource/file_format/csv.rs
+++ b/datafusion/core/src/datasource/file_format/csv.rs
@@ -72,7 +72,7 @@ mod tests {
#[derive(Debug)]
struct VariableStream {
bytes_to_repeat: Bytes,
- max_iterations: usize,
+ max_iterations: u64,
iterations_detected: Arc>,
}
@@ -103,14 +103,15 @@ mod tests {
async fn get(&self, location: &Path) -> object_store::Result {
let bytes = self.bytes_to_repeat.clone();
- let range = 0..bytes.len() * self.max_iterations;
+ let len = bytes.len() as u64;
+ let range = 0..len * self.max_iterations;
let arc = self.iterations_detected.clone();
let stream = futures::stream::repeat_with(move || {
let arc_inner = arc.clone();
*arc_inner.lock().unwrap() += 1;
Ok(bytes.clone())
})
- .take(self.max_iterations)
+ .take(self.max_iterations as usize)
.boxed();
Ok(GetResult {
@@ -138,7 +139,7 @@ mod tests {
async fn get_ranges(
&self,
_location: &Path,
- _ranges: &[Range],
+ _ranges: &[Range],
) -> object_store::Result> {
unimplemented!()
}
@@ -154,7 +155,7 @@ mod tests {
fn list(
&self,
_prefix: Option<&Path>,
- ) -> BoxStream<'_, object_store::Result> {
+ ) -> BoxStream<'static, object_store::Result> {
unimplemented!()
}
@@ -179,7 +180,7 @@ mod tests {
}
impl VariableStream {
- pub fn new(bytes_to_repeat: Bytes, max_iterations: usize) -> Self {
+ pub fn new(bytes_to_repeat: Bytes, max_iterations: u64) -> Self {
Self {
bytes_to_repeat,
max_iterations,
@@ -249,6 +250,7 @@ mod tests {
let exec = scan_format(
&state,
&format,
+ None,
root,
"aggregate_test_100_with_nulls.csv",
projection,
@@ -299,6 +301,7 @@ mod tests {
let exec = scan_format(
&state,
&format,
+ None,
root,
"aggregate_test_100_with_nulls.csv",
projection,
@@ -371,7 +374,7 @@ mod tests {
let object_meta = ObjectMeta {
location: Path::parse("/")?,
last_modified: DateTime::default(),
- size: usize::MAX,
+ size: u64::MAX,
e_tag: None,
version: None,
};
@@ -429,7 +432,7 @@ mod tests {
let object_meta = ObjectMeta {
location: Path::parse("/")?,
last_modified: DateTime::default(),
- size: usize::MAX,
+ size: u64::MAX,
e_tag: None,
version: None,
};
@@ -581,7 +584,7 @@ mod tests {
) -> Result> {
let root = format!("{}/csv", arrow_test_data());
let format = CsvFormat::default().with_has_header(has_header);
- scan_format(state, &format, &root, file_name, projection, limit).await
+ scan_format(state, &format, None, &root, file_name, projection, limit).await
}
#[tokio::test]
diff --git a/datafusion/core/src/datasource/file_format/json.rs b/datafusion/core/src/datasource/file_format/json.rs
index d533dcf7646da..a70a0f51d3307 100644
--- a/datafusion/core/src/datasource/file_format/json.rs
+++ b/datafusion/core/src/datasource/file_format/json.rs
@@ -149,7 +149,7 @@ mod tests {
) -> Result> {
let filename = "tests/data/2.json";
let format = JsonFormat::default();
- scan_format(state, &format, ".", filename, projection, limit).await
+ scan_format(state, &format, None, ".", filename, projection, limit).await
}
#[tokio::test]
diff --git a/datafusion/core/src/datasource/file_format/mod.rs b/datafusion/core/src/datasource/file_format/mod.rs
index e921f0158e540..3a098301f14e3 100644
--- a/datafusion/core/src/datasource/file_format/mod.rs
+++ b/datafusion/core/src/datasource/file_format/mod.rs
@@ -36,19 +36,20 @@ pub use datafusion_datasource::write;
#[cfg(test)]
pub(crate) mod test_util {
- use std::sync::Arc;
-
+ use arrow_schema::SchemaRef;
use datafusion_catalog::Session;
use datafusion_common::Result;
use datafusion_datasource::file_scan_config::FileScanConfigBuilder;
use datafusion_datasource::{file_format::FileFormat, PartitionedFile};
use datafusion_execution::object_store::ObjectStoreUrl;
+ use std::sync::Arc;
use crate::test::object_store::local_unpartitioned_file;
pub async fn scan_format(
state: &dyn Session,
format: &dyn FileFormat,
+ schema: Option,
store_root: &str,
file_name: &str,
projection: Option>,
@@ -57,9 +58,13 @@ pub(crate) mod test_util {
let store = Arc::new(object_store::local::LocalFileSystem::new()) as _;
let meta = local_unpartitioned_file(format!("{store_root}/{file_name}"));
- let file_schema = format
- .infer_schema(state, &store, std::slice::from_ref(&meta))
- .await?;
+ let file_schema = if let Some(file_schema) = schema {
+ file_schema
+ } else {
+ format
+ .infer_schema(state, &store, std::slice::from_ref(&meta))
+ .await?
+ };
let statistics = format
.infer_stats(state, &store, file_schema.clone(), &meta)
@@ -127,7 +132,7 @@ mod tests {
.write_parquet(out_dir_url, DataFrameWriteOptions::new(), None)
.await
.expect_err("should fail because input file does not match inferred schema");
- assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value d for column 0 at line 4");
+ assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value 'd' as type 'Int64' for column 0 at line 4. Row data: '[d,4]'");
Ok(())
}
}
diff --git a/datafusion/core/src/datasource/file_format/parquet.rs b/datafusion/core/src/datasource/file_format/parquet.rs
index 27a7e7ae3c061..7b8b99273f4ea 100644
--- a/datafusion/core/src/datasource/file_format/parquet.rs
+++ b/datafusion/core/src/datasource/file_format/parquet.rs
@@ -67,13 +67,13 @@ pub(crate) mod test_util {
.into_iter()
.zip(tmp_files.into_iter())
.map(|(batch, mut output)| {
- let builder = parquet::file::properties::WriterProperties::builder();
- let props = if multi_page {
- builder.set_data_page_row_count_limit(ROWS_PER_PAGE)
- } else {
- builder
+ let mut builder = parquet::file::properties::WriterProperties::builder();
+ if multi_page {
+ builder = builder.set_data_page_row_count_limit(ROWS_PER_PAGE)
}
- .build();
+ builder = builder.set_bloom_filter_enabled(true);
+
+ let props = builder.build();
let mut writer = parquet::arrow::ArrowWriter::try_new(
&mut output,
@@ -331,7 +331,7 @@ mod tests {
fn list(
&self,
_prefix: Option<&Path>,
- ) -> BoxStream<'_, object_store::Result> {
+ ) -> BoxStream<'static, object_store::Result> {
Box::pin(futures::stream::once(async {
Err(object_store::Error::NotImplemented)
}))
@@ -408,7 +408,7 @@ mod tests {
)));
// Use the file size as the hint so we can get the full metadata from the first fetch
- let size_hint = meta[0].size;
+ let size_hint = meta[0].size as usize;
fetch_parquet_metadata(store.upcast().as_ref(), &meta[0], Some(size_hint))
.await
@@ -443,7 +443,7 @@ mod tests {
)));
// Use the a size hint larger than the file size to make sure we don't panic
- let size_hint = meta[0].size + 100;
+ let size_hint = (meta[0].size + 100) as usize;
fetch_parquet_metadata(store.upcast().as_ref(), &meta[0], Some(size_hint))
.await
@@ -1075,7 +1075,10 @@ mod tests {
.map(|factory| factory.create(state, &Default::default()).unwrap())
.unwrap_or(Arc::new(ParquetFormat::new()));
- scan_format(state, &*format, &testdata, file_name, projection, limit).await
+ scan_format(
+ state, &*format, None, &testdata, file_name, projection, limit,
+ )
+ .await
}
/// Test that 0-byte files don't break while reading
diff --git a/datafusion/core/src/datasource/listing/table.rs b/datafusion/core/src/datasource/listing/table.rs
index 61eeb419a4800..a9834da92e5a4 100644
--- a/datafusion/core/src/datasource/listing/table.rs
+++ b/datafusion/core/src/datasource/listing/table.rs
@@ -17,18 +17,16 @@
//! The table implementation.
-use std::collections::HashMap;
-use std::{any::Any, str::FromStr, sync::Arc};
-
use super::helpers::{expr_applicable_for_cols, pruned_partition_list};
use super::{ListingTableUrl, PartitionedFile};
+use std::collections::HashMap;
+use std::{any::Any, str::FromStr, sync::Arc};
use crate::datasource::{
create_ordering,
file_format::{
file_compression_type::FileCompressionType, FileFormat, FilePushdownSupport,
},
- get_statistics_with_limit,
physical_plan::FileSinkConfig,
};
use crate::execution::context::SessionState;
@@ -55,9 +53,11 @@ use datafusion_physical_expr::{
use async_trait::async_trait;
use datafusion_catalog::Session;
+use datafusion_common::stats::Precision;
+use datafusion_datasource::compute_all_files_statistics;
use datafusion_datasource::file_groups::FileGroup;
use datafusion_physical_expr_common::sort_expr::LexRequirement;
-use futures::{future, stream, StreamExt, TryStreamExt};
+use futures::{future, stream, Stream, StreamExt, TryStreamExt};
use itertools::Itertools;
use object_store::ObjectStore;
@@ -715,9 +715,13 @@ impl ListingOptions {
#[derive(Debug)]
pub struct ListingTable {
table_paths: Vec,
- /// File fields only
+ /// `file_schema` contains only the columns physically stored in the data files themselves.
+ /// - Represents the actual fields found in files like Parquet, CSV, etc.
+ /// - Used when reading the raw data from files
file_schema: SchemaRef,
- /// File fields + partition columns
+ /// `table_schema` combines `file_schema` + partition columns
+ /// - Partition columns are derived from directory paths (not stored in files)
+ /// - These are columns like "year=2022/month=01" in paths like `/data/year=2022/month=01/file.parquet`
table_schema: SchemaRef,
options: ListingOptions,
definition: Option,
@@ -795,7 +799,7 @@ impl ListingTable {
/// If `None`, creates a new [`DefaultFileStatisticsCache`] scoped to this query.
pub fn with_cache(mut self, cache: Option) -> Self {
self.collected_statistics =
- cache.unwrap_or(Arc::new(DefaultFileStatisticsCache::default()));
+ cache.unwrap_or_else(|| Arc::new(DefaultFileStatisticsCache::default()));
self
}
@@ -874,15 +878,13 @@ impl TableProvider for ListingTable {
filters.iter().cloned().partition(|filter| {
can_be_evaluted_for_partition_pruning(&table_partition_col_names, filter)
});
- // TODO (https://github.com/apache/datafusion/issues/11600) remove downcast_ref from here?
- let session_state = state.as_any().downcast_ref::().unwrap();
// We should not limit the number of partitioned files to scan if there are filters and limit
// at the same time. This is because the limit should be applied after the filters are applied.
let statistic_file_limit = if filters.is_empty() { limit } else { None };
let (mut partitioned_file_lists, statistics) = self
- .list_files_for_scan(session_state, &partition_filters, statistic_file_limit)
+ .list_files_for_scan(state, &partition_filters, statistic_file_limit)
.await?;
// if no files need to be read, return an `EmptyExec`
@@ -898,10 +900,11 @@ impl TableProvider for ListingTable {
.split_file_groups_by_statistics
.then(|| {
output_ordering.first().map(|output_ordering| {
- FileScanConfig::split_groups_by_statistics(
+ FileScanConfig::split_groups_by_statistics_with_target_partitions(
&self.table_schema,
&partitioned_file_lists,
output_ordering,
+ self.options.target_partitions,
)
})
})
@@ -941,7 +944,7 @@ impl TableProvider for ListingTable {
self.options
.format
.create_physical_plan(
- session_state,
+ state,
FileScanConfigBuilder::new(
object_store_url,
Arc::clone(&self.file_schema),
@@ -1021,10 +1024,8 @@ impl TableProvider for ListingTable {
// Get the object store for the table path.
let store = state.runtime_env().object_store(table_path)?;
- // TODO (https://github.com/apache/datafusion/issues/11600) remove downcast_ref from here?
- let session_state = state.as_any().downcast_ref::().unwrap();
let file_list_stream = pruned_partition_list(
- session_state,
+ state,
store.as_ref(),
table_path,
&[],
@@ -1072,7 +1073,7 @@ impl TableProvider for ListingTable {
self.options()
.format
- .create_writer_physical_plan(input, session_state, config, order_requirements)
+ .create_writer_physical_plan(input, state, config, order_requirements)
.await
}
@@ -1115,32 +1116,26 @@ impl ListingTable {
let files = file_list
.map(|part_file| async {
let part_file = part_file?;
- if self.options.collect_stat {
- let statistics =
- self.do_collect_statistics(ctx, &store, &part_file).await?;
- Ok((part_file, statistics))
+ let statistics = if self.options.collect_stat {
+ self.do_collect_statistics(ctx, &store, &part_file).await?
} else {
- Ok((
- part_file,
- Arc::new(Statistics::new_unknown(&self.file_schema)),
- ))
- }
+ Arc::new(Statistics::new_unknown(&self.file_schema))
+ };
+ Ok(part_file.with_statistics(statistics))
})
.boxed()
.buffer_unordered(ctx.config_options().execution.meta_fetch_concurrency);
- let (files, statistics) = get_statistics_with_limit(
- files,
+ let (file_group, inexact_stats) =
+ get_files_with_limit(files, limit, self.options.collect_stat).await?;
+
+ let file_groups = file_group.split_files(self.options.target_partitions);
+ compute_all_files_statistics(
+ file_groups,
self.schema(),
- limit,
self.options.collect_stat,
+ inexact_stats,
)
- .await?;
-
- Ok((
- files.split_files(self.options.target_partitions),
- statistics,
- ))
}
/// Collects statistics for a given partitioned file.
@@ -1182,6 +1177,82 @@ impl ListingTable {
}
}
+/// Processes a stream of partitioned files and returns a `FileGroup` containing the files.
+///
+/// This function collects files from the provided stream until either:
+/// 1. The stream is exhausted
+/// 2. The accumulated number of rows exceeds the provided `limit` (if specified)
+///
+/// # Arguments
+/// * `files` - A stream of `Result` items to process
+/// * `limit` - An optional row count limit. If provided, the function will stop collecting files
+/// once the accumulated number of rows exceeds this limit
+/// * `collect_stats` - Whether to collect and accumulate statistics from the files
+///
+/// # Returns
+/// A `Result` containing a `FileGroup` with the collected files
+/// and a boolean indicating whether the statistics are inexact.
+///
+/// # Note
+/// The function will continue processing files if statistics are not available or if the
+/// limit is not provided. If `collect_stats` is false, statistics won't be accumulated
+/// but files will still be collected.
+async fn get_files_with_limit(
+ files: impl Stream
- >,
+ limit: Option,
+ collect_stats: bool,
+) -> Result<(FileGroup, bool)> {
+ let mut file_group = FileGroup::default();
+ // Fusing the stream allows us to call next safely even once it is finished.
+ let mut all_files = Box::pin(files.fuse());
+ enum ProcessingState {
+ ReadingFiles,
+ ReachedLimit,
+ }
+
+ let mut state = ProcessingState::ReadingFiles;
+ let mut num_rows = Precision::Absent;
+
+ while let Some(file_result) = all_files.next().await {
+ // Early exit if we've already reached our limit
+ if matches!(state, ProcessingState::ReachedLimit) {
+ break;
+ }
+
+ let file = file_result?;
+
+ // Update file statistics regardless of state
+ if collect_stats {
+ if let Some(file_stats) = &file.statistics {
+ num_rows = if file_group.is_empty() {
+ // For the first file, just take its row count
+ file_stats.num_rows
+ } else {
+ // For subsequent files, accumulate the counts
+ num_rows.add(&file_stats.num_rows)
+ };
+ }
+ }
+
+ // Always add the file to our group
+ file_group.push(file);
+
+ // Check if we've hit the limit (if one was specified)
+ if let Some(limit) = limit {
+ if let Precision::Exact(row_count) = num_rows {
+ if row_count > limit {
+ state = ProcessingState::ReachedLimit;
+ }
+ }
+ }
+ }
+ // If we still have files in the stream, it means that the limit kicked
+ // in, and the statistic could have been different had we processed the
+ // files in a different order.
+ let inexact_stats = all_files.next().await.is_some();
+ Ok((file_group, inexact_stats))
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/datafusion/core/src/datasource/memory.rs b/datafusion/core/src/datasource/memory_test.rs
similarity index 58%
rename from datafusion/core/src/datasource/memory.rs
rename to datafusion/core/src/datasource/memory_test.rs
index 0288cd3e8bc7d..381000ab8ee1e 100644
--- a/datafusion/core/src/datasource/memory.rs
+++ b/datafusion/core/src/datasource/memory_test.rs
@@ -15,378 +15,25 @@
// specific language governing permissions and limitations
// under the License.
-//! [`MemTable`] for querying `Vec` by DataFusion.
-
-use std::any::Any;
-use std::collections::HashMap;
-use std::fmt::{self, Debug};
-use std::sync::Arc;
-
-use crate::datasource::{TableProvider, TableType};
-use crate::error::Result;
-use crate::logical_expr::Expr;
-use crate::physical_plan::repartition::RepartitionExec;
-use crate::physical_plan::{
- common, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
- Partitioning, SendableRecordBatchStream,
-};
-use crate::physical_planner::create_physical_sort_exprs;
-
-use arrow::datatypes::SchemaRef;
-use arrow::record_batch::RecordBatch;
-use datafusion_catalog::Session;
-use datafusion_common::{not_impl_err, plan_err, Constraints, DFSchema, SchemaExt};
-use datafusion_common_runtime::JoinSet;
-pub use datafusion_datasource::memory::MemorySourceConfig;
-use datafusion_datasource::sink::{DataSink, DataSinkExec};
-pub use datafusion_datasource::source::DataSourceExec;
-use datafusion_execution::TaskContext;
-use datafusion_expr::dml::InsertOp;
-use datafusion_expr::SortExpr;
-
-use async_trait::async_trait;
-use futures::StreamExt;
-use log::debug;
-use parking_lot::Mutex;
-use tokio::sync::RwLock;
-
-/// Type alias for partition data
-pub type PartitionData = Arc>>;
-
-/// In-memory data source for presenting a `Vec` as a
-/// data source that can be queried by DataFusion. This allows data to
-/// be pre-loaded into memory and then repeatedly queried without
-/// incurring additional file I/O overhead.
-#[derive(Debug)]
-pub struct MemTable {
- schema: SchemaRef,
- pub(crate) batches: Vec,
- constraints: Constraints,
- column_defaults: HashMap,
- /// Optional pre-known sort order(s). Must be `SortExpr`s.
- /// inserting data into this table removes the order
- pub sort_order: Arc>>>,
-}
-
-impl MemTable {
- /// Create a new in-memory table from the provided schema and record batches
- pub fn try_new(schema: SchemaRef, partitions: Vec>) -> Result {
- for batches in partitions.iter().flatten() {
- let batches_schema = batches.schema();
- if !schema.contains(&batches_schema) {
- debug!(
- "mem table schema does not contain batches schema. \
- Target_schema: {schema:?}. Batches Schema: {batches_schema:?}"
- );
- return plan_err!("Mismatch between schema and batches");
- }
- }
-
- Ok(Self {
- schema,
- batches: partitions
- .into_iter()
- .map(|e| Arc::new(RwLock::new(e)))
- .collect::>(),
- constraints: Constraints::empty(),
- column_defaults: HashMap::new(),
- sort_order: Arc::new(Mutex::new(vec![])),
- })
- }
-
- /// Assign constraints
- pub fn with_constraints(mut self, constraints: Constraints) -> Self {
- self.constraints = constraints;
- self
- }
-
- /// Assign column defaults
- pub fn with_column_defaults(
- mut self,
- column_defaults: HashMap,
- ) -> Self {
- self.column_defaults = column_defaults;
- self
- }
-
- /// Specify an optional pre-known sort order(s). Must be `SortExpr`s.
- ///
- /// If the data is not sorted by this order, DataFusion may produce
- /// incorrect results.
- ///
- /// DataFusion may take advantage of this ordering to omit sorts
- /// or use more efficient algorithms.
- ///
- /// Note that multiple sort orders are supported, if some are known to be
- /// equivalent,
- pub fn with_sort_order(self, mut sort_order: Vec>) -> Self {
- std::mem::swap(self.sort_order.lock().as_mut(), &mut sort_order);
- self
- }
-
- /// Create a mem table by reading from another data source
- pub async fn load(
- t: Arc,
- output_partitions: Option,
- state: &dyn Session,
- ) -> Result {
- let schema = t.schema();
- let constraints = t.constraints();
- let exec = t.scan(state, None, &[], None).await?;
- let partition_count = exec.output_partitioning().partition_count();
-
- let mut join_set = JoinSet::new();
-
- for part_idx in 0..partition_count {
- let task = state.task_ctx();
- let exec = Arc::clone(&exec);
- join_set.spawn(async move {
- let stream = exec.execute(part_idx, task)?;
- common::collect(stream).await
- });
- }
-
- let mut data: Vec> =
- Vec::with_capacity(exec.output_partitioning().partition_count());
-
- while let Some(result) = join_set.join_next().await {
- match result {
- Ok(res) => data.push(res?),
- Err(e) => {
- if e.is_panic() {
- std::panic::resume_unwind(e.into_panic());
- } else {
- unreachable!();
- }
- }
- }
- }
-
- let mut exec = DataSourceExec::new(Arc::new(MemorySourceConfig::try_new(
- &data,
- Arc::clone(&schema),
- None,
- )?));
- if let Some(cons) = constraints {
- exec = exec.with_constraints(cons.clone());
- }
-
- if let Some(num_partitions) = output_partitions {
- let exec = RepartitionExec::try_new(
- Arc::new(exec),
- Partitioning::RoundRobinBatch(num_partitions),
- )?;
-
- // execute and collect results
- let mut output_partitions = vec![];
- for i in 0..exec.properties().output_partitioning().partition_count() {
- // execute this *output* partition and collect all batches
- let task_ctx = state.task_ctx();
- let mut stream = exec.execute(i, task_ctx)?;
- let mut batches = vec![];
- while let Some(result) = stream.next().await {
- batches.push(result?);
- }
- output_partitions.push(batches);
- }
-
- return MemTable::try_new(Arc::clone(&schema), output_partitions);
- }
- MemTable::try_new(Arc::clone(&schema), data)
- }
-}
-
-#[async_trait]
-impl TableProvider for MemTable {
- fn as_any(&self) -> &dyn Any {
- self
- }
-
- fn schema(&self) -> SchemaRef {
- Arc::clone(&self.schema)
- }
-
- fn constraints(&self) -> Option<&Constraints> {
- Some(&self.constraints)
- }
-
- fn table_type(&self) -> TableType {
- TableType::Base
- }
-
- async fn scan(
- &self,
- state: &dyn Session,
- projection: Option<&Vec>,
- _filters: &[Expr],
- _limit: Option,
- ) -> Result> {
- let mut partitions = vec![];
- for arc_inner_vec in self.batches.iter() {
- let inner_vec = arc_inner_vec.read().await;
- partitions.push(inner_vec.clone())
- }
-
- let mut source =
- MemorySourceConfig::try_new(&partitions, self.schema(), projection.cloned())?;
-
- let show_sizes = state.config_options().explain.show_sizes;
- source = source.with_show_sizes(show_sizes);
-
- // add sort information if present
- let sort_order = self.sort_order.lock();
- if !sort_order.is_empty() {
- let df_schema = DFSchema::try_from(self.schema.as_ref().clone())?;
-
- let file_sort_order = sort_order
- .iter()
- .map(|sort_exprs| {
- create_physical_sort_exprs(
- sort_exprs,
- &df_schema,
- state.execution_props(),
- )
- })
- .collect::>>()?;
- source = source.try_with_sort_information(file_sort_order)?;
- }
-
- Ok(DataSourceExec::from_data_source(source))
- }
-
- /// Returns an ExecutionPlan that inserts the execution results of a given [`ExecutionPlan`] into this [`MemTable`].
- ///
- /// The [`ExecutionPlan`] must have the same schema as this [`MemTable`].
- ///
- /// # Arguments
- ///
- /// * `state` - The [`SessionState`] containing the context for executing the plan.
- /// * `input` - The [`ExecutionPlan`] to execute and insert.
- ///
- /// # Returns
- ///
- /// * A plan that returns the number of rows written.
- ///
- /// [`SessionState`]: crate::execution::context::SessionState
- async fn insert_into(
- &self,
- _state: &dyn Session,
- input: Arc,
- insert_op: InsertOp,
- ) -> Result> {
- // If we are inserting into the table, any sort order may be messed up so reset it here
- *self.sort_order.lock() = vec![];
-
- // Create a physical plan from the logical plan.
- // Check that the schema of the plan matches the schema of this table.
- self.schema()
- .logically_equivalent_names_and_types(&input.schema())?;
-
- if insert_op != InsertOp::Append {
- return not_impl_err!("{insert_op} not implemented for MemoryTable yet");
- }
- let sink = MemSink::try_new(self.batches.clone(), Arc::clone(&self.schema))?;
- Ok(Arc::new(DataSinkExec::new(input, Arc::new(sink), None)))
- }
-
- fn get_column_default(&self, column: &str) -> Option<&Expr> {
- self.column_defaults.get(column)
- }
-}
-
-/// Implements for writing to a [`MemTable`]
-struct MemSink {
- /// Target locations for writing data
- batches: Vec,
- schema: SchemaRef,
-}
-
-impl Debug for MemSink {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("MemSink")
- .field("num_partitions", &self.batches.len())
- .finish()
- }
-}
-
-impl DisplayAs for MemSink {
- fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match t {
- DisplayFormatType::Default | DisplayFormatType::Verbose => {
- let partition_count = self.batches.len();
- write!(f, "MemoryTable (partitions={partition_count})")
- }
- DisplayFormatType::TreeRender => {
- // TODO: collect info
- write!(f, "")
- }
- }
- }
-}
-
-impl MemSink {
- /// Creates a new [`MemSink`].
- ///
- /// The caller is responsible for ensuring that there is at least one partition to insert into.
- fn try_new(batches: Vec, schema: SchemaRef) -> Result {
- if batches.is_empty() {
- return plan_err!("Cannot insert into MemTable with zero partitions");
- }
- Ok(Self { batches, schema })
- }
-}
-
-#[async_trait]
-impl DataSink for MemSink {
- fn as_any(&self) -> &dyn Any {
- self
- }
-
- fn schema(&self) -> &SchemaRef {
- &self.schema
- }
-
- async fn write_all(
- &self,
- mut data: SendableRecordBatchStream,
- _context: &Arc,
- ) -> Result {
- let num_partitions = self.batches.len();
-
- // buffer up the data round robin style into num_partitions
-
- let mut new_batches = vec![vec![]; num_partitions];
- let mut i = 0;
- let mut row_count = 0;
- while let Some(batch) = data.next().await.transpose()? {
- row_count += batch.num_rows();
- new_batches[i].push(batch);
- i = (i + 1) % num_partitions;
- }
-
- // write the outputs into the batches
- for (target, mut batches) in self.batches.iter().zip(new_batches.into_iter()) {
- // Append all the new batches in one go to minimize locking overhead
- target.write().await.append(&mut batches);
- }
-
- Ok(row_count as u64)
- }
-}
-
#[cfg(test)]
mod tests {
- use super::*;
+ use crate::datasource::MemTable;
use crate::datasource::{provider_as_source, DefaultTableSource};
use crate::physical_plan::collect;
use crate::prelude::SessionContext;
-
use arrow::array::{AsArray, Int32Array};
use arrow::datatypes::{DataType, Field, Schema, UInt64Type};
use arrow::error::ArrowError;
- use datafusion_common::DataFusionError;
+ use arrow::record_batch::RecordBatch;
+ use arrow_schema::SchemaRef;
+ use datafusion_catalog::TableProvider;
+ use datafusion_common::{DataFusionError, Result};
+ use datafusion_expr::dml::InsertOp;
use datafusion_expr::LogicalPlanBuilder;
+ use futures::StreamExt;
+ use std::collections::HashMap;
+ use std::sync::Arc;
#[tokio::test]
async fn test_with_projection() -> Result<()> {
diff --git a/datafusion/core/src/datasource/mod.rs b/datafusion/core/src/datasource/mod.rs
index 35a451cbc803a..25a89644cd2a4 100644
--- a/datafusion/core/src/datasource/mod.rs
+++ b/datafusion/core/src/datasource/mod.rs
@@ -24,10 +24,9 @@ pub mod empty;
pub mod file_format;
pub mod listing;
pub mod listing_table_factory;
-pub mod memory;
+mod memory_test;
pub mod physical_plan;
pub mod provider;
-mod statistics;
mod view_test;
// backwards compatibility
@@ -40,6 +39,7 @@ pub use crate::catalog::TableProvider;
pub use crate::logical_expr::TableType;
pub use datafusion_catalog::cte_worktable;
pub use datafusion_catalog::default_table_source;
+pub use datafusion_catalog::memory;
pub use datafusion_catalog::stream;
pub use datafusion_catalog::view;
pub use datafusion_datasource::schema_adapter;
@@ -47,7 +47,6 @@ pub use datafusion_datasource::sink;
pub use datafusion_datasource::source;
pub use datafusion_execution::object_store;
pub use datafusion_physical_expr::create_ordering;
-pub use statistics::get_statistics_with_limit;
#[cfg(all(test, feature = "parquet"))]
mod tests {
@@ -107,7 +106,7 @@ mod tests {
let meta = ObjectMeta {
location,
last_modified: metadata.modified().map(chrono::DateTime::from).unwrap(),
- size: metadata.len() as usize,
+ size: metadata.len(),
e_tag: None,
version: None,
};
diff --git a/datafusion/core/src/datasource/physical_plan/arrow_file.rs b/datafusion/core/src/datasource/physical_plan/arrow_file.rs
index 5dcf4df73f57a..f0a1f94d87e1f 100644
--- a/datafusion/core/src/datasource/physical_plan/arrow_file.rs
+++ b/datafusion/core/src/datasource/physical_plan/arrow_file.rs
@@ -273,6 +273,7 @@ impl FileOpener for ArrowOpener {
None => {
let r = object_store.get(file_meta.location()).await?;
match r.payload {
+ #[cfg(not(target_arch = "wasm32"))]
GetResultPayload::File(file, _) => {
let arrow_reader = arrow::ipc::reader::FileReader::try_new(
file, projection,
@@ -305,7 +306,7 @@ impl FileOpener for ArrowOpener {
)?;
// read footer according to footer_len
let get_option = GetOptions {
- range: Some(GetRange::Suffix(10 + footer_len)),
+ range: Some(GetRange::Suffix(10 + (footer_len as u64))),
..Default::default()
};
let get_result = object_store
@@ -332,9 +333,9 @@ impl FileOpener for ArrowOpener {
.iter()
.flatten()
.map(|block| {
- let block_len = block.bodyLength() as usize
- + block.metaDataLength() as usize;
- let block_offset = block.offset() as usize;
+ let block_len =
+ block.bodyLength() as u64 + block.metaDataLength() as u64;
+ let block_offset = block.offset() as u64;
block_offset..block_offset + block_len
})
.collect_vec();
@@ -354,9 +355,9 @@ impl FileOpener for ArrowOpener {
.iter()
.flatten()
.filter(|block| {
- let block_offset = block.offset() as usize;
- block_offset >= range.start as usize
- && block_offset < range.end as usize
+ let block_offset = block.offset() as u64;
+ block_offset >= range.start as u64
+ && block_offset < range.end as u64
})
.copied()
.collect_vec();
@@ -364,9 +365,9 @@ impl FileOpener for ArrowOpener {
let recordbatch_ranges = recordbatches
.iter()
.map(|block| {
- let block_len = block.bodyLength() as usize
- + block.metaDataLength() as usize;
- let block_offset = block.offset() as usize;
+ let block_len =
+ block.bodyLength() as u64 + block.metaDataLength() as u64;
+ let block_offset = block.offset() as u64;
block_offset..block_offset + block_len
})
.collect_vec();
diff --git a/datafusion/core/src/datasource/physical_plan/csv.rs b/datafusion/core/src/datasource/physical_plan/csv.rs
index 5914924797dce..3ef4030134520 100644
--- a/datafusion/core/src/datasource/physical_plan/csv.rs
+++ b/datafusion/core/src/datasource/physical_plan/csv.rs
@@ -658,7 +658,7 @@ mod tests {
)
.await
.expect_err("should fail because input file does not match inferred schema");
- assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value d for column 0 at line 4");
+ assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value 'd' as type 'Int64' for column 0 at line 4. Row data: '[d,4]'");
Ok(())
}
diff --git a/datafusion/core/src/datasource/physical_plan/json.rs b/datafusion/core/src/datasource/physical_plan/json.rs
index 910c4316d9734..736248fbd95df 100644
--- a/datafusion/core/src/datasource/physical_plan/json.rs
+++ b/datafusion/core/src/datasource/physical_plan/json.rs
@@ -495,7 +495,7 @@ mod tests {
.write_json(out_dir_url, DataFrameWriteOptions::new(), None)
.await
.expect_err("should fail because input file does not match inferred schema");
- assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value d for column 0 at line 4");
+ assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value 'd' as type 'Int64' for column 0 at line 4. Row data: '[d,4]'");
Ok(())
}
diff --git a/datafusion/core/src/datasource/physical_plan/parquet.rs b/datafusion/core/src/datasource/physical_plan/parquet.rs
index 9e1b2822e8540..e9bb8b0db3682 100644
--- a/datafusion/core/src/datasource/physical_plan/parquet.rs
+++ b/datafusion/core/src/datasource/physical_plan/parquet.rs
@@ -38,11 +38,12 @@ mod tests {
use crate::prelude::{ParquetReadOptions, SessionConfig, SessionContext};
use crate::test::object_store::local_unpartitioned_file;
use arrow::array::{
- ArrayRef, Date64Array, Int32Array, Int64Array, Int8Array, StringArray,
+ ArrayRef, AsArray, Date64Array, Int32Array, Int64Array, Int8Array, StringArray,
StructArray,
};
use arrow::datatypes::{DataType, Field, Fields, Schema, SchemaBuilder};
use arrow::record_batch::RecordBatch;
+ use arrow::util::pretty::pretty_format_batches;
use arrow_schema::SchemaRef;
use bytes::{BufMut, BytesMut};
use datafusion_common::config::TableParquetOptions;
@@ -61,8 +62,9 @@ mod tests {
use datafusion_execution::object_store::ObjectStoreUrl;
use datafusion_expr::{col, lit, when, Expr};
use datafusion_physical_expr::planner::logical2physical;
+ use datafusion_physical_plan::analyze::AnalyzeExec;
+ use datafusion_physical_plan::collect;
use datafusion_physical_plan::metrics::{ExecutionPlanMetricsSet, MetricsSet};
- use datafusion_physical_plan::{collect, displayable};
use datafusion_physical_plan::{ExecutionPlan, ExecutionPlanProperties};
use chrono::{TimeZone, Utc};
@@ -81,10 +83,10 @@ mod tests {
struct RoundTripResult {
/// Data that was read back from ParquetFiles
batches: Result>,
+ /// The EXPLAIN ANALYZE output
+ explain: Result,
/// The physical plan that was created (that has statistics, etc)
parquet_exec: Arc,
- /// The ParquetSource that is used in plan
- parquet_source: ParquetSource,
}
/// round-trip record batches by writing each individual RecordBatch to
@@ -137,71 +139,109 @@ mod tests {
self.round_trip(batches).await.batches
}
- /// run the test, returning the `RoundTripResult`
- async fn round_trip(self, batches: Vec) -> RoundTripResult {
- let Self {
- projection,
- schema,
- predicate,
- pushdown_predicate,
- page_index_predicate,
- } = self;
-
- let file_schema = match schema {
- Some(schema) => schema,
- None => Arc::new(
- Schema::try_merge(
- batches.iter().map(|b| b.schema().as_ref().clone()),
- )
- .unwrap(),
- ),
- };
- // If testing with page_index_predicate, write parquet
- // files with multiple pages
- let multi_page = page_index_predicate;
- let (meta, _files) = store_parquet(batches, multi_page).await.unwrap();
- let file_group = meta.into_iter().map(Into::into).collect();
-
+ fn build_file_source(&self, file_schema: SchemaRef) -> Arc {
// set up predicate (this is normally done by a layer higher up)
- let predicate = predicate.map(|p| logical2physical(&p, &file_schema));
+ let predicate = self
+ .predicate
+ .as_ref()
+ .map(|p| logical2physical(p, &file_schema));
let mut source = ParquetSource::default();
if let Some(predicate) = predicate {
source = source.with_predicate(Arc::clone(&file_schema), predicate);
}
- if pushdown_predicate {
+ if self.pushdown_predicate {
source = source
.with_pushdown_filters(true)
.with_reorder_filters(true);
}
- if page_index_predicate {
+ if self.page_index_predicate {
source = source.with_enable_page_index(true);
}
+ Arc::new(source)
+ }
+
+ fn build_parquet_exec(
+ &self,
+ file_schema: SchemaRef,
+ file_group: FileGroup,
+ source: Arc,
+ ) -> Arc {
let base_config = FileScanConfigBuilder::new(
ObjectStoreUrl::local_filesystem(),
file_schema,
- Arc::new(source.clone()),
+ source,
)
.with_file_group(file_group)
- .with_projection(projection)
+ .with_projection(self.projection.clone())
.build();
+ DataSourceExec::from_data_source(base_config)
+ }
+
+ /// run the test, returning the `RoundTripResult`
+ async fn round_trip(&self, batches: Vec) -> RoundTripResult {
+ let file_schema = match &self.schema {
+ Some(schema) => schema,
+ None => &Arc::new(
+ Schema::try_merge(
+ batches.iter().map(|b| b.schema().as_ref().clone()),
+ )
+ .unwrap(),
+ ),
+ };
+ let file_schema = Arc::clone(file_schema);
+ // If testing with page_index_predicate, write parquet
+ // files with multiple pages
+ let multi_page = self.page_index_predicate;
+ let (meta, _files) = store_parquet(batches, multi_page).await.unwrap();
+ let file_group: FileGroup = meta.into_iter().map(Into::into).collect();
+
+ // build a ParquetExec to return the results
+ let parquet_source = self.build_file_source(file_schema.clone());
+ let parquet_exec = self.build_parquet_exec(
+ file_schema.clone(),
+ file_group.clone(),
+ Arc::clone(&parquet_source),
+ );
+
+ let analyze_exec = Arc::new(AnalyzeExec::new(
+ false,
+ false,
+ // use a new ParquetSource to avoid sharing execution metrics
+ self.build_parquet_exec(
+ file_schema.clone(),
+ file_group.clone(),
+ self.build_file_source(file_schema.clone()),
+ ),
+ Arc::new(Schema::new(vec![
+ Field::new("plan_type", DataType::Utf8, true),
+ Field::new("plan", DataType::Utf8, true),
+ ])),
+ ));
let session_ctx = SessionContext::new();
let task_ctx = session_ctx.task_ctx();
- let parquet_exec = DataSourceExec::from_data_source(base_config.clone());
+ let batches = collect(
+ Arc::clone(&parquet_exec) as Arc,
+ task_ctx.clone(),
+ )
+ .await;
+
+ let explain = collect(analyze_exec, task_ctx.clone())
+ .await
+ .map(|batches| {
+ let batches = pretty_format_batches(&batches).unwrap();
+ format!("{batches}")
+ });
+
RoundTripResult {
- batches: collect(parquet_exec.clone(), task_ctx).await,
+ batches,
+ explain,
parquet_exec,
- parquet_source: base_config
- .file_source()
- .as_any()
- .downcast_ref::()
- .unwrap()
- .clone(),
}
}
}
@@ -1069,6 +1109,7 @@ mod tests {
let parquet_exec = scan_format(
&state,
&ParquetFormat::default(),
+ None,
&testdata,
filename,
Some(vec![0, 1, 2]),
@@ -1101,6 +1142,92 @@ mod tests {
Ok(())
}
+ #[tokio::test]
+ async fn parquet_exec_with_int96_from_spark() -> Result<()> {
+ // arrow-rs relies on the chrono library to convert between timestamps and strings, so
+ // instead compare as Int64. The underlying type should be a PrimitiveArray of Int64
+ // anyway, so this should be a zero-copy non-modifying cast at the SchemaAdapter.
+
+ let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, true)]));
+ let testdata = datafusion_common::test_util::parquet_test_data();
+ let filename = "int96_from_spark.parquet";
+ let session_ctx = SessionContext::new();
+ let state = session_ctx.state();
+ let task_ctx = state.task_ctx();
+
+ let time_units_and_expected = vec![
+ (
+ None, // Same as "ns" time_unit
+ Arc::new(Int64Array::from(vec![
+ Some(1704141296123456000), // Reads as nanosecond fine (note 3 extra 0s)
+ Some(1704070800000000000), // Reads as nanosecond fine (note 3 extra 0s)
+ Some(-4852191831933722624), // Cannot be represented with nanos timestamp (year 9999)
+ Some(1735599600000000000), // Reads as nanosecond fine (note 3 extra 0s)
+ None,
+ Some(-4864435138808946688), // Cannot be represented with nanos timestamp (year 290000)
+ ])),
+ ),
+ (
+ Some("ns".to_string()),
+ Arc::new(Int64Array::from(vec![
+ Some(1704141296123456000),
+ Some(1704070800000000000),
+ Some(-4852191831933722624),
+ Some(1735599600000000000),
+ None,
+ Some(-4864435138808946688),
+ ])),
+ ),
+ (
+ Some("us".to_string()),
+ Arc::new(Int64Array::from(vec![
+ Some(1704141296123456),
+ Some(1704070800000000),
+ Some(253402225200000000),
+ Some(1735599600000000),
+ None,
+ Some(9089380393200000000),
+ ])),
+ ),
+ ];
+
+ for (time_unit, expected) in time_units_and_expected {
+ let parquet_exec = scan_format(
+ &state,
+ &ParquetFormat::default().with_coerce_int96(time_unit.clone()),
+ Some(schema.clone()),
+ &testdata,
+ filename,
+ Some(vec![0]),
+ None,
+ )
+ .await
+ .unwrap();
+ assert_eq!(parquet_exec.output_partitioning().partition_count(), 1);
+
+ let mut results = parquet_exec.execute(0, task_ctx.clone())?;
+ let batch = results.next().await.unwrap()?;
+
+ assert_eq!(6, batch.num_rows());
+ assert_eq!(1, batch.num_columns());
+
+ assert_eq!(batch.num_columns(), 1);
+ let column = batch.column(0);
+
+ assert_eq!(column.len(), expected.len());
+
+ column
+ .as_primitive::()
+ .iter()
+ .zip(expected.iter())
+ .for_each(|(lhs, rhs)| {
+ assert_eq!(lhs, rhs);
+ });
+ }
+
+ Ok(())
+ }
+
#[tokio::test]
async fn parquet_exec_with_range() -> Result<()> {
fn file_range(meta: &ObjectMeta, start: i64, end: i64) -> PartitionedFile {
@@ -1375,26 +1502,6 @@ mod tests {
create_batch(vec![("c1", c1.clone())])
}
- /// Returns a int64 array with contents:
- /// "[-1, 1, null, 2, 3, null, null]"
- fn int64_batch() -> RecordBatch {
- let contents: ArrayRef = Arc::new(Int64Array::from(vec![
- Some(-1),
- Some(1),
- None,
- Some(2),
- Some(3),
- None,
- None,
- ]));
-
- create_batch(vec![
- ("a", contents.clone()),
- ("b", contents.clone()),
- ("c", contents.clone()),
- ])
- }
-
#[tokio::test]
async fn parquet_exec_metrics() {
// batch1: c1(string)
@@ -1454,110 +1561,17 @@ mod tests {
.round_trip(vec![batch1])
.await;
- // should have a pruning predicate
- let pruning_predicate = rt.parquet_source.pruning_predicate();
- assert!(pruning_predicate.is_some());
-
- // convert to explain plan form
- let display = displayable(rt.parquet_exec.as_ref())
- .indent(true)
- .to_string();
+ let explain = rt.explain.unwrap();
- assert_contains!(
- &display,
- "pruning_predicate=c1_null_count@2 != row_count@3 AND (c1_min@0 != bar OR bar != c1_max@1)"
- );
+ // check that there was a pruning predicate -> row groups got pruned
+ assert_contains!(&explain, "predicate=c1@0 != bar");
- assert_contains!(&display, r#"predicate=c1@0 != bar"#);
+ // there's a single row group, but we can check that it matched
+ // if no pruning was done this would be 0 instead of 1
+ assert_contains!(&explain, "row_groups_matched_statistics=1");
- assert_contains!(&display, "projection=[c1]");
- }
-
- #[tokio::test]
- async fn parquet_exec_display_deterministic() {
- // batches: a(int64), b(int64), c(int64)
- let batches = int64_batch();
-
- fn extract_required_guarantees(s: &str) -> Option<&str> {
- s.split("required_guarantees=").nth(1)
- }
-
- // Ensuring that the required_guarantees remain consistent across every display plan of the filter conditions
- for _ in 0..100 {
- // c = 1 AND b = 1 AND a = 1
- let filter0 = col("c")
- .eq(lit(1))
- .and(col("b").eq(lit(1)))
- .and(col("a").eq(lit(1)));
-
- let rt0 = RoundTrip::new()
- .with_predicate(filter0)
- .with_pushdown_predicate()
- .round_trip(vec![batches.clone()])
- .await;
-
- let pruning_predicate = rt0.parquet_source.pruning_predicate();
- assert!(pruning_predicate.is_some());
-
- let display0 = displayable(rt0.parquet_exec.as_ref())
- .indent(true)
- .to_string();
-
- let guarantees0: &str = extract_required_guarantees(&display0)
- .expect("Failed to extract required_guarantees");
- // Compare only the required_guarantees part (Because the file_groups part will not be the same)
- assert_eq!(
- guarantees0.trim(),
- "[a in (1), b in (1), c in (1)]",
- "required_guarantees don't match"
- );
- }
-
- // c = 1 AND a = 1 AND b = 1
- let filter1 = col("c")
- .eq(lit(1))
- .and(col("a").eq(lit(1)))
- .and(col("b").eq(lit(1)));
-
- let rt1 = RoundTrip::new()
- .with_predicate(filter1)
- .with_pushdown_predicate()
- .round_trip(vec![batches.clone()])
- .await;
-
- // b = 1 AND a = 1 AND c = 1
- let filter2 = col("b")
- .eq(lit(1))
- .and(col("a").eq(lit(1)))
- .and(col("c").eq(lit(1)));
-
- let rt2 = RoundTrip::new()
- .with_predicate(filter2)
- .with_pushdown_predicate()
- .round_trip(vec![batches])
- .await;
-
- // should have a pruning predicate
- let pruning_predicate = rt1.parquet_source.pruning_predicate();
- assert!(pruning_predicate.is_some());
- let pruning_predicate = rt2.parquet_source.predicate();
- assert!(pruning_predicate.is_some());
-
- // convert to explain plan form
- let display1 = displayable(rt1.parquet_exec.as_ref())
- .indent(true)
- .to_string();
- let display2 = displayable(rt2.parquet_exec.as_ref())
- .indent(true)
- .to_string();
-
- let guarantees1 = extract_required_guarantees(&display1)
- .expect("Failed to extract required_guarantees");
- let guarantees2 = extract_required_guarantees(&display2)
- .expect("Failed to extract required_guarantees");
-
- // Compare only the required_guarantees part (Because the predicate part will not be the same)
- assert_eq!(guarantees1, guarantees2, "required_guarantees don't match");
+ // check the projection
+ assert_contains!(&explain, "projection=[c1]");
}
#[tokio::test]
@@ -1581,16 +1595,19 @@ mod tests {
.await;
// Should not contain a pruning predicate (since nothing can be pruned)
- let pruning_predicate = rt.parquet_source.pruning_predicate();
- assert!(
- pruning_predicate.is_none(),
- "Still had pruning predicate: {pruning_predicate:?}"
- );
+ let explain = rt.explain.unwrap();
- // but does still has a pushdown down predicate
- let predicate = rt.parquet_source.predicate();
- let filter_phys = logical2physical(&filter, rt.parquet_exec.schema().as_ref());
- assert_eq!(predicate.unwrap().to_string(), filter_phys.to_string());
+ // When both matched and pruned are 0, it means that the pruning predicate
+ // was not used at all.
+ assert_contains!(&explain, "row_groups_matched_statistics=0");
+ assert_contains!(&explain, "row_groups_pruned_statistics=0");
+
+ // But pushdown predicate should be present
+ assert_contains!(
+ &explain,
+ "predicate=CASE WHEN c1@0 != bar THEN true ELSE false END"
+ );
+ assert_contains!(&explain, "pushdown_rows_pruned=5");
}
#[tokio::test]
@@ -1616,8 +1633,14 @@ mod tests {
.await;
// Should have a pruning predicate
- let pruning_predicate = rt.parquet_source.pruning_predicate();
- assert!(pruning_predicate.is_some());
+ let explain = rt.explain.unwrap();
+ assert_contains!(
+ &explain,
+ "predicate=c1@0 = foo AND CASE WHEN c1@0 != bar THEN true ELSE false END"
+ );
+
+ // And bloom filters should have been evaluated
+ assert_contains!(&explain, "row_groups_pruned_bloom_filter=1");
}
/// Returns the sum of all the metrics with the specified name
@@ -1850,13 +1873,13 @@ mod tests {
path: &str,
store: Arc,
batch: RecordBatch,
- ) -> usize {
+ ) -> u64 {
let mut writer =
ArrowWriter::try_new(BytesMut::new().writer(), batch.schema(), None).unwrap();
writer.write(&batch).unwrap();
writer.flush().unwrap();
let bytes = writer.into_inner().unwrap().into_inner().freeze();
- let total_size = bytes.len();
+ let total_size = bytes.len() as u64;
let path = Path::from(path);
let payload = object_store::PutPayload::from_bytes(bytes);
store
diff --git a/datafusion/core/src/datasource/statistics.rs b/datafusion/core/src/datasource/statistics.rs
deleted file mode 100644
index cf283ecee0bf7..0000000000000
--- a/datafusion/core/src/datasource/statistics.rs
+++ /dev/null
@@ -1,219 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use std::mem;
-use std::sync::Arc;
-
-use futures::{Stream, StreamExt};
-
-use crate::arrow::datatypes::SchemaRef;
-use crate::error::Result;
-use crate::physical_plan::{ColumnStatistics, Statistics};
-use datafusion_common::stats::Precision;
-use datafusion_common::ScalarValue;
-use datafusion_datasource::file_groups::FileGroup;
-
-use super::listing::PartitionedFile;
-
-/// Get all files as well as the file level summary statistics (no statistic for partition columns).
-/// If the optional `limit` is provided, includes only sufficient files. Needed to read up to
-/// `limit` number of rows. `collect_stats` is passed down from the configuration parameter on
-/// `ListingTable`. If it is false we only construct bare statistics and skip a potentially expensive
-/// call to `multiunzip` for constructing file level summary statistics.
-pub async fn get_statistics_with_limit(
- all_files: impl Stream
- )>>,
- file_schema: SchemaRef,
- limit: Option,
- collect_stats: bool,
-) -> Result<(FileGroup, Statistics)> {
- let mut result_files = FileGroup::default();
- // These statistics can be calculated as long as at least one file provides
- // useful information. If none of the files provides any information, then
- // they will end up having `Precision::Absent` values. Throughout calculations,
- // missing values will be imputed as:
- // - zero for summations, and
- // - neutral element for extreme points.
- let size = file_schema.fields().len();
- let mut col_stats_set = vec![ColumnStatistics::default(); size];
- let mut num_rows = Precision::::Absent;
- let mut total_byte_size = Precision::::Absent;
-
- // Fusing the stream allows us to call next safely even once it is finished.
- let mut all_files = Box::pin(all_files.fuse());
-
- if let Some(first_file) = all_files.next().await {
- let (mut file, file_stats) = first_file?;
- file.statistics = Some(file_stats.as_ref().clone());
- result_files.push(file);
-
- // First file, we set them directly from the file statistics.
- num_rows = file_stats.num_rows;
- total_byte_size = file_stats.total_byte_size;
- for (index, file_column) in
- file_stats.column_statistics.clone().into_iter().enumerate()
- {
- col_stats_set[index].null_count = file_column.null_count;
- col_stats_set[index].max_value = file_column.max_value;
- col_stats_set[index].min_value = file_column.min_value;
- col_stats_set[index].sum_value = file_column.sum_value;
- }
-
- // If the number of rows exceeds the limit, we can stop processing
- // files. This only applies when we know the number of rows. It also
- // currently ignores tables that have no statistics regarding the
- // number of rows.
- let conservative_num_rows = match num_rows {
- Precision::Exact(nr) => nr,
- _ => usize::MIN,
- };
- if conservative_num_rows <= limit.unwrap_or(usize::MAX) {
- while let Some(current) = all_files.next().await {
- let (mut file, file_stats) = current?;
- file.statistics = Some(file_stats.as_ref().clone());
- result_files.push(file);
- if !collect_stats {
- continue;
- }
-
- // We accumulate the number of rows, total byte size and null
- // counts across all the files in question. If any file does not
- // provide any information or provides an inexact value, we demote
- // the statistic precision to inexact.
- num_rows = add_row_stats(file_stats.num_rows, num_rows);
-
- total_byte_size =
- add_row_stats(file_stats.total_byte_size, total_byte_size);
-
- for (file_col_stats, col_stats) in file_stats
- .column_statistics
- .iter()
- .zip(col_stats_set.iter_mut())
- {
- let ColumnStatistics {
- null_count: file_nc,
- max_value: file_max,
- min_value: file_min,
- sum_value: file_sum,
- distinct_count: _,
- } = file_col_stats;
-
- col_stats.null_count = add_row_stats(*file_nc, col_stats.null_count);
- set_max_if_greater(file_max, &mut col_stats.max_value);
- set_min_if_lesser(file_min, &mut col_stats.min_value);
- col_stats.sum_value = file_sum.add(&col_stats.sum_value);
- }
-
- // If the number of rows exceeds the limit, we can stop processing
- // files. This only applies when we know the number of rows. It also
- // currently ignores tables that have no statistics regarding the
- // number of rows.
- if num_rows.get_value().unwrap_or(&usize::MIN)
- > &limit.unwrap_or(usize::MAX)
- {
- break;
- }
- }
- }
- };
-
- let mut statistics = Statistics {
- num_rows,
- total_byte_size,
- column_statistics: col_stats_set,
- };
- if all_files.next().await.is_some() {
- // If we still have files in the stream, it means that the limit kicked
- // in, and the statistic could have been different had we processed the
- // files in a different order.
- statistics = statistics.to_inexact()
- }
-
- Ok((result_files, statistics))
-}
-
-fn add_row_stats(
- file_num_rows: Precision,
- num_rows: Precision,
-) -> Precision {
- match (file_num_rows, &num_rows) {
- (Precision::Absent, _) => num_rows.to_inexact(),
- (lhs, Precision::Absent) => lhs.to_inexact(),
- (lhs, rhs) => lhs.add(rhs),
- }
-}
-
-/// If the given value is numerically greater than the original maximum value,
-/// return the new maximum value with appropriate exactness information.
-fn set_max_if_greater(
- max_nominee: &Precision,
- max_value: &mut Precision,
-) {
- match (&max_value, max_nominee) {
- (Precision::Exact(val1), Precision::Exact(val2)) if val1 < val2 => {
- *max_value = max_nominee.clone();
- }
- (Precision::Exact(val1), Precision::Inexact(val2))
- | (Precision::Inexact(val1), Precision::Inexact(val2))
- | (Precision::Inexact(val1), Precision::Exact(val2))
- if val1 < val2 =>
- {
- *max_value = max_nominee.clone().to_inexact();
- }
- (Precision::Exact(_), Precision::Absent) => {
- let exact_max = mem::take(max_value);
- *max_value = exact_max.to_inexact();
- }
- (Precision::Absent, Precision::Exact(_)) => {
- *max_value = max_nominee.clone().to_inexact();
- }
- (Precision::Absent, Precision::Inexact(_)) => {
- *max_value = max_nominee.clone();
- }
- _ => {}
- }
-}
-
-/// If the given value is numerically lesser than the original minimum value,
-/// return the new minimum value with appropriate exactness information.
-fn set_min_if_lesser(
- min_nominee: &Precision,
- min_value: &mut Precision,
-) {
- match (&min_value, min_nominee) {
- (Precision::Exact(val1), Precision::Exact(val2)) if val1 > val2 => {
- *min_value = min_nominee.clone();
- }
- (Precision::Exact(val1), Precision::Inexact(val2))
- | (Precision::Inexact(val1), Precision::Inexact(val2))
- | (Precision::Inexact(val1), Precision::Exact(val2))
- if val1 > val2 =>
- {
- *min_value = min_nominee.clone().to_inexact();
- }
- (Precision::Exact(_), Precision::Absent) => {
- let exact_min = mem::take(min_value);
- *min_value = exact_min.to_inexact();
- }
- (Precision::Absent, Precision::Exact(_)) => {
- *min_value = min_nominee.clone().to_inexact();
- }
- (Precision::Absent, Precision::Inexact(_)) => {
- *min_value = min_nominee.clone();
- }
- _ => {}
- }
-}
diff --git a/datafusion/core/src/execution/context/mod.rs b/datafusion/core/src/execution/context/mod.rs
index fc110a0699df2..0bb91536da3ca 100644
--- a/datafusion/core/src/execution/context/mod.rs
+++ b/datafusion/core/src/execution/context/mod.rs
@@ -35,7 +35,11 @@ use crate::{
},
datasource::{provider_as_source, MemTable, ViewTable},
error::{DataFusionError, Result},
- execution::{options::ArrowReadOptions, runtime_env::RuntimeEnv, FunctionRegistry},
+ execution::{
+ options::ArrowReadOptions,
+ runtime_env::{RuntimeEnv, RuntimeEnvBuilder},
+ FunctionRegistry,
+ },
logical_expr::AggregateUDF,
logical_expr::ScalarUDF,
logical_expr::{
@@ -1036,13 +1040,73 @@ impl SessionContext {
variable, value, ..
} = stmt;
- let mut state = self.state.write();
- state.config_mut().options_mut().set(&variable, &value)?;
- drop(state);
+ // Check if this is a runtime configuration
+ if variable.starts_with("datafusion.runtime.") {
+ self.set_runtime_variable(&variable, &value)?;
+ } else {
+ let mut state = self.state.write();
+ state.config_mut().options_mut().set(&variable, &value)?;
+ drop(state);
+ }
self.return_empty_dataframe()
}
+ fn set_runtime_variable(&self, variable: &str, value: &str) -> Result<()> {
+ let key = variable.strip_prefix("datafusion.runtime.").unwrap();
+
+ match key {
+ "memory_limit" => {
+ let memory_limit = Self::parse_memory_limit(value)?;
+
+ let mut state = self.state.write();
+ let mut builder =
+ RuntimeEnvBuilder::from_runtime_env(state.runtime_env());
+ builder = builder.with_memory_limit(memory_limit, 1.0);
+ *state = SessionStateBuilder::from(state.clone())
+ .with_runtime_env(Arc::new(builder.build()?))
+ .build();
+ }
+ _ => {
+ return Err(DataFusionError::Plan(format!(
+ "Unknown runtime configuration: {}",
+ variable
+ )))
+ }
+ }
+ Ok(())
+ }
+
+ /// Parse memory limit from string to number of bytes
+ /// Supports formats like '1.5G', '100M', '512K'
+ ///
+ /// # Examples
+ /// ```
+ /// use datafusion::execution::context::SessionContext;
+ ///
+ /// assert_eq!(SessionContext::parse_memory_limit("1M").unwrap(), 1024 * 1024);
+ /// assert_eq!(SessionContext::parse_memory_limit("1.5G").unwrap(), (1.5 * 1024.0 * 1024.0 * 1024.0) as usize);
+ /// ```
+ pub fn parse_memory_limit(limit: &str) -> Result {
+ let (number, unit) = limit.split_at(limit.len() - 1);
+ let number: f64 = number.parse().map_err(|_| {
+ DataFusionError::Plan(format!(
+ "Failed to parse number from memory limit '{}'",
+ limit
+ ))
+ })?;
+
+ match unit {
+ "K" => Ok((number * 1024.0) as usize),
+ "M" => Ok((number * 1024.0 * 1024.0) as usize),
+ "G" => Ok((number * 1024.0 * 1024.0 * 1024.0) as usize),
+ _ => Err(DataFusionError::Plan(format!(
+ "Unsupported unit '{}' in memory limit '{}'",
+ unit, limit
+ ))),
+ }
+ }
+
async fn create_custom_table(
&self,
cmd: &CreateExternalTable,
@@ -1833,7 +1897,6 @@ mod tests {
use crate::test;
use crate::test_util::{plan_and_collect, populate_csv_partitions};
use arrow::datatypes::{DataType, TimeUnit};
- use std::env;
use std::error::Error;
use std::path::PathBuf;
diff --git a/datafusion/core/src/execution/session_state.rs b/datafusion/core/src/execution/session_state.rs
index 28f599304f8c8..597700bf8be3d 100644
--- a/datafusion/core/src/execution/session_state.rs
+++ b/datafusion/core/src/execution/session_state.rs
@@ -1348,28 +1348,30 @@ impl SessionStateBuilder {
} = self;
let config = config.unwrap_or_default();
- let runtime_env = runtime_env.unwrap_or(Arc::new(RuntimeEnv::default()));
+ let runtime_env = runtime_env.unwrap_or_else(|| Arc::new(RuntimeEnv::default()));
let mut state = SessionState {
- session_id: session_id.unwrap_or(Uuid::new_v4().to_string()),
+ session_id: session_id.unwrap_or_else(|| Uuid::new_v4().to_string()),
analyzer: analyzer.unwrap_or_default(),
expr_planners: expr_planners.unwrap_or_default(),
type_planner,
optimizer: optimizer.unwrap_or_default(),
physical_optimizers: physical_optimizers.unwrap_or_default(),
- query_planner: query_planner.unwrap_or(Arc::new(DefaultQueryPlanner {})),
- catalog_list: catalog_list
- .unwrap_or(Arc::new(MemoryCatalogProviderList::new())
- as Arc),
+ query_planner: query_planner
+ .unwrap_or_else(|| Arc::new(DefaultQueryPlanner {})),
+ catalog_list: catalog_list.unwrap_or_else(|| {
+ Arc::new(MemoryCatalogProviderList::new()) as Arc
+ }),
table_functions: table_functions.unwrap_or_default(),
scalar_functions: HashMap::new(),
aggregate_functions: HashMap::new(),
window_functions: HashMap::new(),
serializer_registry: serializer_registry
- .unwrap_or(Arc::new(EmptySerializerRegistry)),
+ .unwrap_or_else(|| Arc::new(EmptySerializerRegistry)),
file_formats: HashMap::new(),
- table_options: table_options
- .unwrap_or(TableOptions::default_from_session_config(config.options())),
+ table_options: table_options.unwrap_or_else(|| {
+ TableOptions::default_from_session_config(config.options())
+ }),
config,
execution_props: execution_props.unwrap_or_default(),
table_factories: table_factories.unwrap_or_default(),
diff --git a/datafusion/core/src/lib.rs b/datafusion/core/src/lib.rs
index cc510bc81f1a8..928efd533ca44 100644
--- a/datafusion/core/src/lib.rs
+++ b/datafusion/core/src/lib.rs
@@ -22,7 +22,18 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
// Make sure fast / cheap clones on Arc are explicit:
// https://github.com/apache/datafusion/issues/11143
-#![cfg_attr(not(test), deny(clippy::clone_on_ref_ptr))]
+//
+// Eliminate unnecessary function calls(some may be not cheap) due to `xxx_or`
+// for performance. Also avoid abusing `xxx_or_else` for readability:
+// https://github.com/apache/datafusion/issues/15802
+#![cfg_attr(
+ not(test),
+ deny(
+ clippy::clone_on_ref_ptr,
+ clippy::or_fun_call,
+ clippy::unnecessary_lazy_evaluations
+ )
+)]
#![warn(missing_docs, clippy::needless_borrow)]
//! [DataFusion] is an extensible query engine written in Rust that
@@ -872,6 +883,12 @@ doc_comment::doctest!(
user_guide_configs
);
+#[cfg(doctest)]
+doc_comment::doctest!(
+ "../../../docs/source/user-guide/runtime_configs.md",
+ user_guide_runtime_configs
+);
+
#[cfg(doctest)]
doc_comment::doctest!(
"../../../docs/source/user-guide/crate-configuration.md",
@@ -1021,8 +1038,8 @@ doc_comment::doctest!(
#[cfg(doctest)]
doc_comment::doctest!(
- "../../../docs/source/user-guide/sql/write_options.md",
- user_guide_sql_write_options
+ "../../../docs/source/user-guide/sql/format_options.md",
+ user_guide_sql_format_options
);
#[cfg(doctest)]
diff --git a/datafusion/core/src/physical_planner.rs b/datafusion/core/src/physical_planner.rs
index f1a99a7714ac4..be24206c676c6 100644
--- a/datafusion/core/src/physical_planner.rs
+++ b/datafusion/core/src/physical_planner.rs
@@ -81,7 +81,7 @@ use datafusion_expr::{
WindowFrameBound, WriteOp,
};
use datafusion_physical_expr::aggregate::{AggregateExprBuilder, AggregateFunctionExpr};
-use datafusion_physical_expr::expressions::Literal;
+use datafusion_physical_expr::expressions::{Column, Literal};
use datafusion_physical_expr::LexOrdering;
use datafusion_physical_optimizer::PhysicalOptimizerRule;
use datafusion_physical_plan::execution_plan::InvariantLevel;
@@ -1023,18 +1023,12 @@ impl DefaultPhysicalPlanner {
// Collect left & right field indices, the field indices are sorted in ascending order
let left_field_indices = cols
.iter()
- .filter_map(|c| match left_df_schema.index_of_column(c) {
- Ok(idx) => Some(idx),
- _ => None,
- })
+ .filter_map(|c| left_df_schema.index_of_column(c).ok())
.sorted()
.collect::>();
let right_field_indices = cols
.iter()
- .filter_map(|c| match right_df_schema.index_of_column(c) {
- Ok(idx) => Some(idx),
- _ => None,
- })
+ .filter_map(|c| right_df_schema.index_of_column(c).ok())
.sorted()
.collect::>();
@@ -2006,7 +2000,8 @@ impl DefaultPhysicalPlanner {
input: &Arc,
expr: &[Expr],
) -> Result> {
- let input_schema = input.as_ref().schema();
+ let input_logical_schema = input.as_ref().schema();
+ let input_physical_schema = input_exec.schema();
let physical_exprs = expr
.iter()
.map(|e| {
@@ -2025,7 +2020,7 @@ impl DefaultPhysicalPlanner {
// This depends on the invariant that logical schema field index MUST match
// with physical schema field index.
let physical_name = if let Expr::Column(col) = e {
- match input_schema.index_of_column(col) {
+ match input_logical_schema.index_of_column(col) {
Ok(idx) => {
// index physical field using logical field index
Ok(input_exec.schema().field(idx).name().to_string())
@@ -2038,10 +2033,14 @@ impl DefaultPhysicalPlanner {
physical_name(e)
};
- tuple_err((
- self.create_physical_expr(e, input_schema, session_state),
- physical_name,
- ))
+ let physical_expr =
+ self.create_physical_expr(e, input_logical_schema, session_state);
+
+ // Check for possible column name mismatches
+ let final_physical_expr =
+ maybe_fix_physical_column_name(physical_expr, &input_physical_schema);
+
+ tuple_err((final_physical_expr, physical_name))
})
.collect::>>()?;
@@ -2061,6 +2060,40 @@ fn tuple_err(value: (Result, Result)) -> Result<(T, R)> {
}
}
+// Handle the case where the name of a physical column expression does not match the corresponding physical input fields names.
+// Physical column names are derived from the physical schema, whereas physical column expressions are derived from the logical column names.
+//
+// This is a special case that applies only to column expressions. Logical plans may slightly modify column names by appending a suffix (e.g., using ':'),
+// to avoid duplicates—since DFSchemas do not allow duplicate names. For example: `count(Int64(1)):1`.
+fn maybe_fix_physical_column_name(
+ expr: Result>,
+ input_physical_schema: &SchemaRef,
+) -> Result> {
+ if let Ok(e) = &expr {
+ if let Some(column) = e.as_any().downcast_ref::() {
+ let physical_field = input_physical_schema.field(column.index());
+ let expr_col_name = column.name();
+ let physical_name = physical_field.name();
+
+ if physical_name != expr_col_name {
+ // handle edge cases where the physical_name contains ':'.
+ let colon_count = physical_name.matches(':').count();
+ let mut splits = expr_col_name.match_indices(':');
+ let split_pos = splits.nth(colon_count);
+
+ if let Some((idx, _)) = split_pos {
+ let base_name = &expr_col_name[..idx];
+ if base_name == physical_name {
+ let updated_column = Column::new(physical_name, column.index());
+ return Ok(Arc::new(updated_column));
+ }
+ }
+ }
+ }
+ }
+ expr
+}
+
struct OptimizationInvariantChecker<'a> {
rule: &'a Arc,
}
@@ -2656,6 +2689,30 @@ mod tests {
}
}
+ #[tokio::test]
+ async fn test_maybe_fix_colon_in_physical_name() {
+ // The physical schema has a field name with a colon
+ let schema = Schema::new(vec![Field::new("metric:avg", DataType::Int32, false)]);
+ let schema_ref: SchemaRef = Arc::new(schema);
+
+ // What might happen after deduplication
+ let logical_col_name = "metric:avg:1";
+ let expr_with_suffix =
+ Arc::new(Column::new(logical_col_name, 0)) as Arc;
+ let expr_result = Ok(expr_with_suffix);
+
+ // Call function under test
+ let fixed_expr =
+ maybe_fix_physical_column_name(expr_result, &schema_ref).unwrap();
+
+ // Downcast back to Column so we can check the name
+ let col = fixed_expr
+ .as_any()
+ .downcast_ref::()
+ .expect("Column");
+
+ assert_eq!(col.name(), "metric:avg");
+ }
struct ErrorExtensionPlanner {}
#[async_trait]
diff --git a/datafusion/core/src/test/object_store.rs b/datafusion/core/src/test/object_store.rs
index e1328770cabdd..8b19658bb1473 100644
--- a/datafusion/core/src/test/object_store.rs
+++ b/datafusion/core/src/test/object_store.rs
@@ -66,7 +66,7 @@ pub fn local_unpartitioned_file(path: impl AsRef) -> ObjectMeta
ObjectMeta {
location,
last_modified: metadata.modified().map(chrono::DateTime::from).unwrap(),
- size: metadata.len() as usize,
+ size: metadata.len(),
e_tag: None,
version: None,
}
@@ -166,7 +166,7 @@ impl ObjectStore for BlockingObjectStore {
fn list(
&self,
prefix: Option<&Path>,
- ) -> BoxStream<'_, object_store::Result> {
+ ) -> BoxStream<'static, object_store::Result> {
self.inner.list(prefix)
}
diff --git a/datafusion/core/src/test_util/parquet.rs b/datafusion/core/src/test_util/parquet.rs
index 084554eecbdb0..f5753af64d93f 100644
--- a/datafusion/core/src/test_util/parquet.rs
+++ b/datafusion/core/src/test_util/parquet.rs
@@ -102,7 +102,7 @@ impl TestParquetFile {
println!("Generated test dataset with {num_rows} rows");
- let size = std::fs::metadata(&path)?.len() as usize;
+ let size = std::fs::metadata(&path)?.len();
let mut canonical_path = path.canonicalize()?;
diff --git a/datafusion/core/tests/core_integration.rs b/datafusion/core/tests/core_integration.rs
index 9bcb9e41f86a9..250538b133703 100644
--- a/datafusion/core/tests/core_integration.rs
+++ b/datafusion/core/tests/core_integration.rs
@@ -51,6 +51,9 @@ mod serde;
/// Run all tests that are found in the `catalog` directory
mod catalog;
+/// Run all tests that are found in the `tracing` directory
+mod tracing;
+
#[cfg(test)]
#[ctor::ctor]
fn init() {
diff --git a/datafusion/core/tests/dataframe/dataframe_functions.rs b/datafusion/core/tests/dataframe/dataframe_functions.rs
index c763d4c8de2d6..40590d74ad910 100644
--- a/datafusion/core/tests/dataframe/dataframe_functions.rs
+++ b/datafusion/core/tests/dataframe/dataframe_functions.rs
@@ -384,7 +384,7 @@ async fn test_fn_approx_median() -> Result<()> {
#[tokio::test]
async fn test_fn_approx_percentile_cont() -> Result<()> {
- let expr = approx_percentile_cont(col("b"), lit(0.5), None);
+ let expr = approx_percentile_cont(col("b").sort(true, false), lit(0.5), None);
let df = create_test_table().await?;
let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?;
@@ -392,11 +392,26 @@ async fn test_fn_approx_percentile_cont() -> Result<()> {
assert_snapshot!(
batches_to_string(&batches),
@r"
- +---------------------------------------------+
- | approx_percentile_cont(test.b,Float64(0.5)) |
- +---------------------------------------------+
- | 10 |
- +---------------------------------------------+
+ +---------------------------------------------------------------------------+
+ | approx_percentile_cont(Float64(0.5)) WITHIN GROUP [test.b ASC NULLS LAST] |
+ +---------------------------------------------------------------------------+
+ | 10 |
+ +---------------------------------------------------------------------------+
+ ");
+
+ let expr = approx_percentile_cont(col("b").sort(false, false), lit(0.1), None);
+
+ let df = create_test_table().await?;
+ let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?;
+
+ assert_snapshot!(
+ batches_to_string(&batches),
+ @r"
+ +----------------------------------------------------------------------------+
+ | approx_percentile_cont(Float64(0.1)) WITHIN GROUP [test.b DESC NULLS LAST] |
+ +----------------------------------------------------------------------------+
+ | 100 |
+ +----------------------------------------------------------------------------+
");
// the arg2 parameter is a complex expr, but it can be evaluated to the literal value
@@ -405,23 +420,59 @@ async fn test_fn_approx_percentile_cont() -> Result<()> {
None::<&str>,
"arg_2".to_string(),
));
- let expr = approx_percentile_cont(col("b"), alias_expr, None);
+ let expr = approx_percentile_cont(col("b").sort(true, false), alias_expr, None);
let df = create_test_table().await?;
let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?;
assert_snapshot!(
batches_to_string(&batches),
@r"
- +--------------------------------------+
- | approx_percentile_cont(test.b,arg_2) |
- +--------------------------------------+
- | 10 |
- +--------------------------------------+
+ +--------------------------------------------------------------------+
+ | approx_percentile_cont(arg_2) WITHIN GROUP [test.b ASC NULLS LAST] |
+ +--------------------------------------------------------------------+
+ | 10 |
+ +--------------------------------------------------------------------+
+ "
+ );
+
+ let alias_expr = Expr::Alias(Alias::new(
+ cast(lit(0.1), DataType::Float32),
+ None::<&str>,
+ "arg_2".to_string(),
+ ));
+ let expr = approx_percentile_cont(col("b").sort(false, false), alias_expr, None);
+ let df = create_test_table().await?;
+ let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?;
+
+ assert_snapshot!(
+ batches_to_string(&batches),
+ @r"
+ +---------------------------------------------------------------------+
+ | approx_percentile_cont(arg_2) WITHIN GROUP [test.b DESC NULLS LAST] |
+ +---------------------------------------------------------------------+
+ | 100 |
+ +---------------------------------------------------------------------+
"
);
// with number of centroids set
- let expr = approx_percentile_cont(col("b"), lit(0.5), Some(lit(2)));
+ let expr = approx_percentile_cont(col("b").sort(true, false), lit(0.5), Some(lit(2)));
+
+ let df = create_test_table().await?;
+ let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?;
+
+ assert_snapshot!(
+ batches_to_string(&batches),
+ @r"
+ +------------------------------------------------------------------------------------+
+ | approx_percentile_cont(Float64(0.5),Int32(2)) WITHIN GROUP [test.b ASC NULLS LAST] |
+ +------------------------------------------------------------------------------------+
+ | 30 |
+ +------------------------------------------------------------------------------------+
+ ");
+
+ let expr =
+ approx_percentile_cont(col("b").sort(false, false), lit(0.1), Some(lit(2)));
let df = create_test_table().await?;
let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?;
@@ -429,11 +480,11 @@ async fn test_fn_approx_percentile_cont() -> Result<()> {
assert_snapshot!(
batches_to_string(&batches),
@r"
- +------------------------------------------------------+
- | approx_percentile_cont(test.b,Float64(0.5),Int32(2)) |
- +------------------------------------------------------+
- | 30 |
- +------------------------------------------------------+
+ +-------------------------------------------------------------------------------------+
+ | approx_percentile_cont(Float64(0.1),Int32(2)) WITHIN GROUP [test.b DESC NULLS LAST] |
+ +-------------------------------------------------------------------------------------+
+ | 69 |
+ +-------------------------------------------------------------------------------------+
");
Ok(())
diff --git a/datafusion/core/tests/dataframe/mod.rs b/datafusion/core/tests/dataframe/mod.rs
index b5923269ab8ba..1855a512048d6 100644
--- a/datafusion/core/tests/dataframe/mod.rs
+++ b/datafusion/core/tests/dataframe/mod.rs
@@ -5206,6 +5206,40 @@ fn union_fields() -> UnionFields {
.collect()
}
+#[tokio::test]
+async fn union_literal_is_null_and_not_null() -> Result<()> {
+ let str_array_1 = StringArray::from(vec![None::]);
+ let str_array_2 = StringArray::from(vec![Some("a")]);
+
+ let batch_1 =
+ RecordBatch::try_from_iter(vec![("arr", Arc::new(str_array_1) as ArrayRef)])?;
+ let batch_2 =
+ RecordBatch::try_from_iter(vec![("arr", Arc::new(str_array_2) as ArrayRef)])?;
+
+ let ctx = SessionContext::new();
+ ctx.register_batch("union_batch_1", batch_1)?;
+ ctx.register_batch("union_batch_2", batch_2)?;
+
+ let df1 = ctx.table("union_batch_1").await?;
+ let df2 = ctx.table("union_batch_2").await?;
+
+ let batches = df1.union(df2)?.collect().await?;
+ let schema = batches[0].schema();
+
+ for batch in batches {
+ // Verify schema is the same for all batches
+ if !schema.contains(&batch.schema()) {
+ return Err(DataFusionError::Internal(format!(
+ "Schema mismatch. Previously had\n{:#?}\n\nGot:\n{:#?}",
+ &schema,
+ batch.schema()
+ )));
+ }
+ }
+
+ Ok(())
+}
+
#[tokio::test]
async fn sparse_union_is_null() {
// union of [{A=1}, {A=}, {B=3.2}, {B=}, {C="a"}, {C=}]
@@ -5477,6 +5511,64 @@ async fn boolean_dictionary_as_filter() {
);
}
+#[tokio::test]
+async fn test_union_by_name() -> Result<()> {
+ let df = create_test_table("test")
+ .await?
+ .select(vec![col("a"), col("b"), lit(1).alias("c")])?
+ .alias("table_alias")?;
+
+ let df2 = df.clone().select_columns(&["c", "b", "a"])?;
+ let result = df.union_by_name(df2)?.sort_by(vec![col("a"), col("b")])?;
+
+ assert_snapshot!(
+ batches_to_sort_string(&result.collect().await?),
+ @r"
+ +-----------+-----+---+
+ | a | b | c |
+ +-----------+-----+---+
+ | 123AbcDef | 100 | 1 |
+ | 123AbcDef | 100 | 1 |
+ | CBAdef | 10 | 1 |
+ | CBAdef | 10 | 1 |
+ | abc123 | 10 | 1 |
+ | abc123 | 10 | 1 |
+ | abcDEF | 1 | 1 |
+ | abcDEF | 1 | 1 |
+ +-----------+-----+---+
+ "
+ );
+ Ok(())
+}
+
+#[tokio::test]
+async fn test_union_by_name_distinct() -> Result<()> {
+ let df = create_test_table("test")
+ .await?
+ .select(vec![col("a"), col("b"), lit(1).alias("c")])?
+ .alias("table_alias")?;
+
+ let df2 = df.clone().select_columns(&["c", "b", "a"])?;
+ let result = df
+ .union_by_name_distinct(df2)?
+ .sort_by(vec![col("a"), col("b")])?;
+
+ assert_snapshot!(
+ batches_to_sort_string(&result.collect().await?),
+ @r"
+ +-----------+-----+---+
+ | a | b | c |
+ +-----------+-----+---+
+ | 123AbcDef | 100 | 1 |
+ | CBAdef | 10 | 1 |
+ | abc123 | 10 | 1 |
+ | abcDEF | 1 | 1 |
+ +-----------+-----+---+
+ "
+ );
+ Ok(())
+}
+
#[tokio::test]
async fn test_alias() -> Result<()> {
let df = create_test_table("test")
diff --git a/datafusion/core/tests/execution/logical_plan.rs b/datafusion/core/tests/execution/logical_plan.rs
index b30636ddf6a81..fdee6fd5dbbce 100644
--- a/datafusion/core/tests/execution/logical_plan.rs
+++ b/datafusion/core/tests/execution/logical_plan.rs
@@ -19,15 +19,19 @@
//! create them and depend on them. Test executable semantics of logical plans.
use arrow::array::Int64Array;
-use arrow::datatypes::{DataType, Field};
+use arrow::datatypes::{DataType, Field, Schema};
+use datafusion::datasource::{provider_as_source, ViewTable};
use datafusion::execution::session_state::SessionStateBuilder;
-use datafusion_common::{Column, DFSchema, Result, ScalarValue, Spans};
+use datafusion_common::{Column, DFSchema, DFSchemaRef, Result, ScalarValue, Spans};
use datafusion_execution::TaskContext;
use datafusion_expr::expr::{AggregateFunction, AggregateFunctionParams};
use datafusion_expr::logical_plan::{LogicalPlan, Values};
-use datafusion_expr::{Aggregate, AggregateUDF, Expr};
+use datafusion_expr::{
+ Aggregate, AggregateUDF, EmptyRelation, Expr, LogicalPlanBuilder, UNNAMED_TABLE,
+};
use datafusion_functions_aggregate::count::Count;
use datafusion_physical_plan::collect;
+use insta::assert_snapshot;
use std::collections::HashMap;
use std::fmt::Debug;
use std::ops::Deref;
@@ -96,3 +100,37 @@ where
};
element
}
+
+#[test]
+fn inline_scan_projection_test() -> Result<()> {
+ let name = UNNAMED_TABLE;
+ let column = "a";
+
+ let schema = Schema::new(vec![
+ Field::new("a", DataType::Int32, false),
+ Field::new("b", DataType::Int32, false),
+ ]);
+ let projection = vec![schema.index_of(column)?];
+
+ let provider = ViewTable::new(
+ LogicalPlan::EmptyRelation(EmptyRelation {
+ produce_one_row: false,
+ schema: DFSchemaRef::new(DFSchema::try_from(schema)?),
+ }),
+ None,
+ );
+ let source = provider_as_source(Arc::new(provider));
+
+ let plan = LogicalPlanBuilder::scan(name, source, Some(projection))?.build()?;
+
+ assert_snapshot!(
+ format!("{plan}"),
+ @r"
+ SubqueryAlias: ?table?
+ Projection: a
+ EmptyRelation
+ "
+ );
+
+ Ok(())
+}
diff --git a/datafusion/core/tests/expr_api/simplification.rs b/datafusion/core/tests/expr_api/simplification.rs
index 7bb21725ef401..34e0487f312fb 100644
--- a/datafusion/core/tests/expr_api/simplification.rs
+++ b/datafusion/core/tests/expr_api/simplification.rs
@@ -547,9 +547,9 @@ fn test_simplify_with_cycle_count(
};
let simplifier = ExprSimplifier::new(info);
let (simplified_expr, count) = simplifier
- .simplify_with_cycle_count(input_expr.clone())
+ .simplify_with_cycle_count_transformed(input_expr.clone())
.expect("successfully evaluated");
-
+ let simplified_expr = simplified_expr.data;
assert_eq!(
simplified_expr, expected_expr,
"Mismatch evaluating {input_expr}\n Expected:{expected_expr}\n Got:{simplified_expr}"
diff --git a/datafusion/core/tests/fuzz_cases/aggregate_fuzz.rs b/datafusion/core/tests/fuzz_cases/aggregate_fuzz.rs
index dcf477135a377..ff3b66986ced9 100644
--- a/datafusion/core/tests/fuzz_cases/aggregate_fuzz.rs
+++ b/datafusion/core/tests/fuzz_cases/aggregate_fuzz.rs
@@ -18,16 +18,17 @@
use std::sync::Arc;
use crate::fuzz_cases::aggregation_fuzzer::{
- AggregationFuzzerBuilder, ColumnDescr, DatasetGeneratorConfig, QueryBuilder,
+ AggregationFuzzerBuilder, DatasetGeneratorConfig, QueryBuilder,
};
-use arrow::array::{types::Int64Type, Array, ArrayRef, AsArray, Int64Array, RecordBatch};
-use arrow::compute::{concat_batches, SortOptions};
-use arrow::datatypes::{
- DataType, IntervalUnit, TimeUnit, DECIMAL128_MAX_PRECISION, DECIMAL128_MAX_SCALE,
- DECIMAL256_MAX_PRECISION, DECIMAL256_MAX_SCALE,
+use arrow::array::{
+ types::Int64Type, Array, ArrayRef, AsArray, Int32Array, Int64Array, RecordBatch,
+ StringArray,
};
+use arrow::compute::{concat_batches, SortOptions};
+use arrow::datatypes::DataType;
use arrow::util::pretty::pretty_format_batches;
+use arrow_schema::{Field, Schema, SchemaRef};
use datafusion::common::Result;
use datafusion::datasource::memory::MemorySourceConfig;
use datafusion::datasource::source::DataSourceExec;
@@ -42,14 +43,20 @@ use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion, TreeNodeVisitor}
use datafusion_common::HashMap;
use datafusion_common_runtime::JoinSet;
use datafusion_functions_aggregate::sum::sum_udaf;
-use datafusion_physical_expr::expressions::col;
+use datafusion_physical_expr::expressions::{col, lit, Column};
use datafusion_physical_expr::PhysicalSortExpr;
use datafusion_physical_expr_common::sort_expr::LexOrdering;
use datafusion_physical_plan::InputOrderMode;
use test_utils::{add_empty_batches, StringBatchGenerator};
+use datafusion_execution::memory_pool::FairSpillPool;
+use datafusion_execution::runtime_env::RuntimeEnvBuilder;
+use datafusion_execution::TaskContext;
+use datafusion_physical_plan::metrics::MetricValue;
use rand::rngs::StdRng;
-use rand::{thread_rng, Rng, SeedableRng};
+use rand::{random, thread_rng, Rng, SeedableRng};
+
+use super::record_batch_generator::get_supported_types_columns;
// ========================================================================
// The new aggregation fuzz tests based on [`AggregationFuzzer`]
@@ -113,6 +120,32 @@ async fn test_first_val() {
.await;
}
+#[tokio::test(flavor = "multi_thread")]
+async fn test_last_val() {
+ let mut data_gen_config = baseline_config();
+
+ for i in 0..data_gen_config.columns.len() {
+ if data_gen_config.columns[i].get_max_num_distinct().is_none() {
+ data_gen_config.columns[i] = data_gen_config.columns[i]
+ .clone()
+ // Minimize the chance of identical values in the order by columns to make the test more stable
+ .with_max_num_distinct(usize::MAX);
+ }
+ }
+
+ let query_builder = QueryBuilder::new()
+ .with_table_name("fuzz_table")
+ .with_aggregate_function("last_value")
+ .with_aggregate_arguments(data_gen_config.all_columns())
+ .set_group_by_columns(data_gen_config.all_columns());
+
+ AggregationFuzzerBuilder::from(data_gen_config)
+ .add_query_builder(query_builder)
+ .build()
+ .run()
+ .await;
+}
+
#[tokio::test(flavor = "multi_thread")]
async fn test_max() {
let data_gen_config = baseline_config();
@@ -201,81 +234,7 @@ async fn test_median() {
/// 1. structured types
fn baseline_config() -> DatasetGeneratorConfig {
let mut rng = thread_rng();
- let columns = vec![
- ColumnDescr::new("i8", DataType::Int8),
- ColumnDescr::new("i16", DataType::Int16),
- ColumnDescr::new("i32", DataType::Int32),
- ColumnDescr::new("i64", DataType::Int64),
- ColumnDescr::new("u8", DataType::UInt8),
- ColumnDescr::new("u16", DataType::UInt16),
- ColumnDescr::new("u32", DataType::UInt32),
- ColumnDescr::new("u64", DataType::UInt64),
- ColumnDescr::new("date32", DataType::Date32),
- ColumnDescr::new("date64", DataType::Date64),
- ColumnDescr::new("time32_s", DataType::Time32(TimeUnit::Second)),
- ColumnDescr::new("time32_ms", DataType::Time32(TimeUnit::Millisecond)),
- ColumnDescr::new("time64_us", DataType::Time64(TimeUnit::Microsecond)),
- ColumnDescr::new("time64_ns", DataType::Time64(TimeUnit::Nanosecond)),
- // `None` is passed in here however when generating the array, it will generate
- // random timezones.
- ColumnDescr::new("timestamp_s", DataType::Timestamp(TimeUnit::Second, None)),
- ColumnDescr::new(
- "timestamp_ms",
- DataType::Timestamp(TimeUnit::Millisecond, None),
- ),
- ColumnDescr::new(
- "timestamp_us",
- DataType::Timestamp(TimeUnit::Microsecond, None),
- ),
- ColumnDescr::new(
- "timestamp_ns",
- DataType::Timestamp(TimeUnit::Nanosecond, None),
- ),
- ColumnDescr::new("float32", DataType::Float32),
- ColumnDescr::new("float64", DataType::Float64),
- ColumnDescr::new(
- "interval_year_month",
- DataType::Interval(IntervalUnit::YearMonth),
- ),
- ColumnDescr::new(
- "interval_day_time",
- DataType::Interval(IntervalUnit::DayTime),
- ),
- ColumnDescr::new(
- "interval_month_day_nano",
- DataType::Interval(IntervalUnit::MonthDayNano),
- ),
- // begin decimal columns
- ColumnDescr::new("decimal128", {
- // Generate valid precision and scale for Decimal128 randomly.
- let precision: u8 = rng.gen_range(1..=DECIMAL128_MAX_PRECISION);
- // It's safe to cast `precision` to i8 type directly.
- let scale: i8 = rng.gen_range(
- i8::MIN..=std::cmp::min(precision as i8, DECIMAL128_MAX_SCALE),
- );
- DataType::Decimal128(precision, scale)
- }),
- ColumnDescr::new("decimal256", {
- // Generate valid precision and scale for Decimal256 randomly.
- let precision: u8 = rng.gen_range(1..=DECIMAL256_MAX_PRECISION);
- // It's safe to cast `precision` to i8 type directly.
- let scale: i8 = rng.gen_range(
- i8::MIN..=std::cmp::min(precision as i8, DECIMAL256_MAX_SCALE),
- );
- DataType::Decimal256(precision, scale)
- }),
- // begin string columns
- ColumnDescr::new("utf8", DataType::Utf8),
- ColumnDescr::new("largeutf8", DataType::LargeUtf8),
- ColumnDescr::new("utf8view", DataType::Utf8View),
- // low cardinality columns
- ColumnDescr::new("u8_low", DataType::UInt8).with_max_num_distinct(10),
- ColumnDescr::new("utf8_low", DataType::Utf8).with_max_num_distinct(10),
- ColumnDescr::new("bool", DataType::Boolean),
- ColumnDescr::new("binary", DataType::Binary),
- ColumnDescr::new("large_binary", DataType::LargeBinary),
- ColumnDescr::new("binaryview", DataType::BinaryView),
- ];
+ let columns = get_supported_types_columns(rng.gen());
let min_num_rows = 512;
let max_num_rows = 1024;
@@ -663,3 +622,134 @@ fn extract_result_counts(results: Vec) -> HashMap