diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0aabda5..11e509b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,30 +4,496 @@ on: push: branches: - main - - dev pull_request: branches: - "*" env: CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + RUSTFLAGS: "-C debuginfo=line-tables-only -C incremental=false" jobs: check: - name: Format, clippy and compile check + name: Basic check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: ./.github/actions/free-disk-space - uses: dtolnay/rust-toolchain@stable with: components: clippy, rustfmt - uses: Swatinem/rust-cache@v2 + with: + # Share one cache across all jobs on the same OS. + # Prevent PR runs from consuming cache quota by saving only on main. + shared-key: ci-${{ runner.os }} + save-if: ${{ github.ref == 'refs/heads/main' }} + + - name: tailwindcss + run: | + cd dev/dev-tools + mkdir -p vendor + npm install tailwindcss @tailwindcss/cli + wget https://github.com/saadeghi/daisyui/releases/download/v5.5.19/daisyui.mjs -O vendor/daisyui.mjs + wget https://github.com/saadeghi/daisyui/releases/download/v5.5.19/daisyui-theme.mjs -O vendor/daisyui-theme.mjs + npx @tailwindcss/cli -i tailwind.css -o assets/tailwind.css - name: Check formatting run: cargo fmt --all -- --check + - name: Check documentation + run: cargo doc --no-deps --document-private-items + env: + RUSTDOCFLAGS: -D warnings + - name: Run clippy - run: cargo clippy --workspace --exclude dev-tools --all-targets --all-features -- -D warnings -A clippy::uninlined-format-args -A clippy::useless-conversion + run: cargo clippy --all-targets --all-features -- -D warnings + + - name: Install cargo-shear + run: cargo install cargo-shear --locked + + - name: Check for unused dependencies + run: cargo shear + + dev_tools: + name: Dev Tools + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/free-disk-space + - uses: cachix/install-nix-action@v31 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-${{ runner.os }} + save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Build dev tools + run: nix develop -c bash -lc 'cd dev/dev-tools && dx build' + + unit_test: + name: Unit Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/free-disk-space + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-${{ runner.os }} + save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - name: tailwindcss + run: | + cd dev/dev-tools + mkdir -p vendor + npm install tailwindcss @tailwindcss/cli + wget https://github.com/saadeghi/daisyui/releases/download/v5.5.19/daisyui.mjs -O vendor/daisyui.mjs + wget https://github.com/saadeghi/daisyui/releases/download/v5.5.19/daisyui-theme.mjs -O vendor/daisyui-theme.mjs + npx @tailwindcss/cli -i tailwind.css -o assets/tailwind.css + - name: Generate code coverage + run: cargo llvm-cov --workspace --codecov --output-path codecov.json + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: codecov.json + fail_ci_if_error: true + + shuttle_test: + name: Shuttle Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/free-disk-space + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-${{ runner.os }} + save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Run shuttle test + run: | + cd src/core + cargo test --features "shuttle" --release -- --test-threads=1 shuttle + + address_san: + name: Address Sanitizer + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/free-disk-space + # Sanitizers can only run on nightly + - uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + + # Address sanitizers can't be cached: https://github.com/Swatinem/rust-cache/issues/161 + - run: sudo apt-get update && sudo apt-get install -y llvm-dev + - name: Run address sanitizer + run: > + env RUSTFLAGS="-Z sanitizer=address" cargo test -Zbuild-std --target x86_64-unknown-linux-gnu --tests -p liquid-cache-datafusion + + clickbench: + name: ClickBench + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/free-disk-space + - uses: dtolnay/rust-toolchain@stable + - run: sudo apt-get update && sudo apt-get install -y wget + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-${{ runner.os }} + save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Download ClickBench partition 0 + run: | + mkdir -p benchmark/data + wget https://datasets.clickhouse.com/hits_compatible/athena_partitioned/hits_0.parquet -O benchmark/data/hits_0.parquet + - name: Update manifest for partitioned data + run: | + # Update the manifest to point to the partitioned data directory + sed 's|"benchmark/clickbench/data/hits.parquet"|"benchmark/data/hits_0.parquet"|' \ + benchmark/clickbench/manifest.json > benchmark/clickbench/benchmark_manifest.json + + - name: Run ClickBench + run: | + source <(cargo llvm-cov show-env --export-prefix) + cargo llvm-cov clean --workspace + cargo build --bin bench_server + cargo build --bin clickbench_client + env RUST_LOG=info nohup cargo run --bin bench_server -- --abort-on-panic --cache-mode liquid --max-memory-mb 256 &> server.log & + sleep 2 # Wait for server to start up + env RUST_LOG=info cargo run --bin clickbench_client -- --manifest benchmark/clickbench/benchmark_manifest.json + echo "=== Server logs ===" + cat server.log || echo "No server log found" + curl http://localhost:53703/shutdown + env RUST_LOG=info cargo run --bin in_process -- --manifest benchmark/clickbench/benchmark_manifest.json --bench-mode liquid --max-memory-mb 256 + cargo llvm-cov report --codecov --output-path codecov_clickbench.json + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: codecov_clickbench.json + fail_ci_if_error: true + + tpch: + name: TPC-H + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/free-disk-space + - uses: dtolnay/rust-toolchain@stable + - run: sudo apt-get update && sudo apt-get install -y wget + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-${{ runner.os }} + save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Setup TPC-H data + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + cd benchmark/tpch + uvx --from duckdb python tpch_gen.py --scale 0.1 + - name: Run TPC-H + run: | + source <(cargo llvm-cov show-env --export-prefix) + cargo llvm-cov clean --workspace + cargo build --bin bench_server + cargo build --bin tpch_client + env RUST_LOG=info nohup cargo run --bin bench_server -- --abort-on-panic --cache-mode liquid --max-memory-mb 256 &> server.log & + sleep 2 # Wait for server to start up + env RUST_LOG=info cargo run --bin tpch_client -- --manifest benchmark/tpch/manifest.json --answer-dir benchmark/tpch/answers/sf0.1 + echo "=== Server logs ===" + cat server.log || echo "No server log found" + curl http://localhost:53703/shutdown + env RUST_LOG=info cargo run --bin in_process -- --manifest benchmark/tpch/manifest.json --bench-mode liquid --max-memory-mb 256 + cargo llvm-cov report --codecov --output-path codecov_tpch.json + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: codecov_tpch.json + fail_ci_if_error: true + + tpcds: + name: TPC-DS + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/free-disk-space + - uses: dtolnay/rust-toolchain@stable + - run: sudo apt-get update && sudo apt-get install -y wget + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-${{ runner.os }} + save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Setup TPC-DS data + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + cd benchmark/tpcds + uvx --from duckdb python tpcds_gen.py --scale 0.1 --answers-dir answers --data-dir data --queries-dir queries + - name: Run TPC-DS + run: | + source <(cargo llvm-cov show-env --export-prefix) + cargo llvm-cov clean --workspace + cargo build --bin bench_server + cargo build --bin tpcds_client + env RUST_LOG=info nohup cargo run --bin bench_server -- --abort-on-panic --cache-mode liquid --max-memory-mb 256 &> server.log & + sleep 2 # Wait for server to start up + env RUST_LOG=info cargo run --bin tpcds_client -- --manifest benchmark/tpcds/manifest.json --answer-dir benchmark/tpcds/answers/sf0.1 + echo "=== Server logs ===" + cat server.log || echo "No server log found" + curl http://localhost:53703/shutdown + env RUST_LOG=info cargo run --bin in_process -- --manifest benchmark/tpcds/manifest.json --bench-mode liquid --max-memory-mb 256 + cargo llvm-cov report --codecov --output-path codecov_tpcds.json + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: codecov_tpcds.json + fail_ci_if_error: true + + stackoverflow: + name: StackOverflow + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/free-disk-space + - uses: dtolnay/rust-toolchain@stable + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y wget + - name: Prepare dataset directories + run: | + mkdir -p benchmark/stackoverflow/data/dba + mkdir -p benchmark/stackoverflow/downloads + - name: Cache StackOverflow dataset + uses: actions/cache@v4 + with: + path: | + benchmark/stackoverflow/data/dba + benchmark/stackoverflow/downloads + key: stackoverflow-${{ runner.os }}-dba-v1 + restore-keys: | + stackoverflow-${{ runner.os }}-dba- + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-${{ runner.os }} + save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Prepare StackOverflow data + env: + UV_CACHE_DIR: ${{ runner.temp }}/uv-cache + UV_PYTHON: python3 + run: | + if [ ! -f benchmark/stackoverflow/data/dba/Posts.parquet ]; then + curl -LsSf https://astral.sh/uv/install.sh | sh + uv run --with duckdb python benchmark/stackoverflow/setup_stackoverflow.py --mode dba + else + echo "StackOverflow dataset already prepared, skipping rebuild" + fi + - name: Run StackOverflow + run: | + source <(cargo llvm-cov show-env --export-prefix) + cargo llvm-cov clean --workspace + cargo build --bin in_process + env RUST_LOG=info cargo run --bin in_process -- --manifest benchmark/stackoverflow/manifest.dba.json --bench-mode liquid --max-memory-mb 10 + cargo llvm-cov report --codecov --output-path codecov_stackoverflow.json + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: codecov_stackoverflow.json + fail_ci_if_error: true + + benchmark: + name: Performance Benchmark + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + - uses: dtolnay/rust-toolchain@stable + - name: Setup ClickBench partitioned data download + run: | + mkdir -p benchmark/clickbench/data + for partition in 0 1 2 3; do + echo "Downloading partition ${partition}..." + wget "https://datasets.clickhouse.com/hits_compatible/athena_partitioned/hits_${partition}.parquet" \ + -O "benchmark/clickbench/data/hits_${partition}.parquet" + done + + - name: Update manifest for partitioned data + run: | + # Update the manifest to point to the partitioned data directory + sed 's|"benchmark/clickbench/data/hits.parquet"|"benchmark/clickbench/data"|' \ + benchmark/clickbench/manifest.json > benchmark/clickbench/benchmark_manifest.json + + - name: Build benchmark binary + run: cargo build --release --bin in_process + + - name: Run LiquidCache benchmark (in-process) + run: | + mkdir -p benchmark_results + env RUST_LOG=info cargo run --release --bin in_process -- \ + --manifest benchmark/clickbench/benchmark_manifest.json \ + --output benchmark_results/liquid.json \ + --iteration 5 \ + --reset-cache \ + --bench-mode liquid \ + --max-memory-mb 64 + + - name: Run DataFusion benchmark (plain parquet) + run: | + env RUST_LOG=info cargo run --release --bin in_process -- \ + --manifest benchmark/clickbench/benchmark_manifest.json \ + --output benchmark_results/parquet.json \ + --iteration 5 \ + --bench-mode parquet + + - name: Run DataFusion benchmark (default config) + run: | + env RUST_LOG=info cargo run --release --bin in_process -- \ + --manifest benchmark/clickbench/benchmark_manifest.json \ + --output benchmark_results/df_default.json \ + --iteration 5 \ + --bench-mode datafusion-default + + - name: Annotate results with commit/timestamp + run: | + jq --arg timestamp "$(date -Iminutes)" --arg commit "${{ github.sha }}" \ + '. + {"timestamp": $timestamp, "commit": $commit}' \ + benchmark_results/liquid.json > benchmark_results/liquid_final.json + jq --arg timestamp "$(date -Iminutes)" --arg commit "${{ github.sha }}" \ + '. + {"timestamp": $timestamp, "commit": $commit}' \ + benchmark_results/parquet.json > benchmark_results/parquet_final.json + jq --arg timestamp "$(date -Iminutes)" --arg commit "${{ github.sha }}" \ + '. + {"timestamp": $timestamp, "commit": $commit}' \ + benchmark_results/df_default.json > benchmark_results/df_default_final.json + + - name: Compare LiquidCache vs DataFusion (same runner) + id: compare + run: | + python3 .github/compare_benchmarks.py \ + benchmark_results/liquid_final.json \ + benchmark_results/df_default_final.json \ + --output comparison.md + echo "COMPARISON_AVAILABLE=true" >> $GITHUB_OUTPUT + + - name: Comment PR with benchmark results + if: steps.compare.outputs.COMPARISON_AVAILABLE == 'true' && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + let comment = ''; + try { + comment = fs.readFileSync('comparison.md', 'utf8'); + } catch (error) { + comment = 'Error reading benchmark comparison results'; + } + + // Check if this is an external PR (from a fork) + const isExternalPR = context.payload.pull_request.head.repo.full_name !== context.payload.pull_request.base.repo.full_name; + + if (isExternalPR) { + console.log('Skipping comment for external PR due to permission restrictions'); + console.log('Benchmark results:'); + console.log(comment); + return; + } + + try { + // Find existing benchmark comment + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.data.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('## 📊 Benchmark Comparison') + ); + + if (botComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: comment + }); + } else { + // Create new comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + } + } catch (error) { + console.log('Failed to post comment, likely due to permissions:', error.message); + console.log('Benchmark results:'); + console.log(comment); + } + + examples: + name: Run client/server/inprocess examples + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-${{ runner.os }} + save-if: ${{ github.ref == 'refs/heads/main' }} + + - name: Build datafusion-local example + run: cargo build --bin datafusion-local + - name: Build datafusion-client-server example + run: cargo build --bin datafusion-client-server + - name: Build core example + run: cargo build --bin core + + - name: Start LiquidCache server + run: | + env RUST_LOG=info nohup cargo run --bin datafusion-client-server -- --mode server &> server.log & + echo $! > server.pid # Save PID for later cleanup + sleep 2 # Wait for server to start up + + - name: Run datafusion client + run: | + # First run to populate the cache + env RUST_LOG=info cargo run --bin datafusion-client-server -- --mode client + # Run twice to test the cache + env RUST_LOG=info cargo run --bin datafusion-client-server -- --mode client + + - name: Kill LiquidCache server and show logs + if: always() + run: | + echo "=== Server logs ===" + cat server.log || echo "No server log found" + pkill -F server.pid || true + rm -f server.pid + + - name: Run datafusion-local example + run: env RUST_LOG=info cargo run --bin datafusion-local - - name: Run tests - run: cargo test --workspace --exclude dev-tools + - name: Run core example + run: env RUST_LOG=info cargo run --bin core diff --git a/AGENTS.md b/AGENTS.md index 3bd9b5e9..2e8d8c22 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,15 +13,6 @@ - `src/datafusion-client` and `src/datafusion-server`, Client/Server library, this enables distributed LiquidCache. - `src/datafusion-local`, this is a in-process LiquidCache, used for local DataFusion instances. -## Study guide - -This repo has many studies, they are not for production use, but only for research purposes to understand how the system behaves. -The coding guidelines for study are different: - -1. Focus on concise, minimal intrusive, easy to understand code. -2. No error handling, no robust edge case handling, just one shot code. -3. Ok to hard code if it can simplify the implementation. - ### Lineage-based cache expression 1. The lineage_opt.rs analyze the input query's column usage, and passes it down to LiquidCache. diff --git a/Cargo.lock b/Cargo.lock index 164d2b88..20b7f2ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,9 +96,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -167,9 +167,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "arrow" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d441fdda254b65f3e9025910eb2c2066b6295d9c8ed409522b8d2ace1ff8574c" +checksum = "378530e55cd479eda3c14eb345310799717e6f76d0c332041e8487022166b471" dependencies = [ "arrow-arith", "arrow-array", @@ -188,9 +188,9 @@ dependencies = [ [[package]] name = "arrow-arith" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced5406f8b720cc0bc3aa9cf5758f93e8593cda5490677aa194e4b4b383f9a59" +checksum = "a0ab212d2c1886e802f51c5212d78ebbcbb0bec980fff9dadc1eb8d45cd0b738" dependencies = [ "arrow-array", "arrow-buffer", @@ -202,9 +202,9 @@ dependencies = [ [[package]] name = "arrow-array" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772bd34cacdda8baec9418d80d23d0fb4d50ef0735685bd45158b83dfeb6e62d" +checksum = "cfd33d3e92f207444098c75b42de99d329562be0cf686b307b097cc52b4e999e" dependencies = [ "ahash", "arrow-buffer", @@ -213,7 +213,7 @@ dependencies = [ "chrono", "chrono-tz", "half", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "num-complex", "num-integer", "num-traits", @@ -221,9 +221,9 @@ dependencies = [ [[package]] name = "arrow-buffer" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "898f4cf1e9598fdb77f356fdf2134feedfd0ee8d5a4e0a5f573e7d0aec16baa4" +checksum = "0c6cd424c2693bcdbc150d843dc9d4d137dd2de4782ce6df491ad11a3a0416c0" dependencies = [ "bytes", "half", @@ -233,9 +233,9 @@ dependencies = [ [[package]] name = "arrow-cast" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0127816c96533d20fc938729f48c52d3e48f99717e7a0b5ade77d742510736d" +checksum = "4c5aefb56a2c02e9e2b30746241058b85f8983f0fcff2ba0c6d09006e1cded7f" dependencies = [ "arrow-array", "arrow-buffer", @@ -255,9 +255,9 @@ dependencies = [ [[package]] name = "arrow-csv" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca025bd0f38eeecb57c2153c0123b960494138e6a957bbda10da2b25415209fe" +checksum = "e94e8cf7e517657a52b91ea1263acf38c4ca62a84655d72458a3359b12ab97de" dependencies = [ "arrow-array", "arrow-cast", @@ -270,9 +270,9 @@ dependencies = [ [[package]] name = "arrow-data" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d10beeab2b1c3bb0b53a00f7c944a178b622173a5c7bcabc3cb45d90238df4" +checksum = "3c88210023a2bfee1896af366309a3028fc3bcbd6515fa29a7990ee1baa08ee0" dependencies = [ "arrow-buffer", "arrow-schema", @@ -283,9 +283,9 @@ dependencies = [ [[package]] name = "arrow-flight" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302b2e036335f3f04d65dad3f74ff1f2aae6dc671d6aa04dc6b61193761e16fb" +checksum = "28abfe8bf9f124e5fc83b334af4fa58f8d0323ad25312ccb2d1da50178415704" dependencies = [ "arrow-arith", "arrow-array", @@ -311,9 +311,9 @@ dependencies = [ [[package]] name = "arrow-ipc" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "609a441080e338147a84e8e6904b6da482cefb957c5cdc0f3398872f69a315d0" +checksum = "238438f0834483703d88896db6fe5a7138b2230debc31b34c0336c2996e3c64f" dependencies = [ "arrow-array", "arrow-buffer", @@ -327,18 +327,19 @@ dependencies = [ [[package]] name = "arrow-json" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ead0914e4861a531be48fe05858265cf854a4880b9ed12618b1d08cba9bebc8" +checksum = "205ca2119e6d679d5c133c6f30e68f027738d95ed948cf77677ea69c7800036b" dependencies = [ "arrow-array", "arrow-buffer", "arrow-cast", - "arrow-data", + "arrow-ord", "arrow-schema", + "arrow-select", "chrono", "half", - "indexmap 2.13.0", + "indexmap 2.14.0", "itoa", "lexical-core", "memchr", @@ -351,9 +352,9 @@ dependencies = [ [[package]] name = "arrow-ord" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763a7ba279b20b52dad300e68cfc37c17efa65e68623169076855b3a9e941ca5" +checksum = "1bffd8fd2579286a5d63bac898159873e5094a79009940bcb42bbfce4f19f1d0" dependencies = [ "arrow-array", "arrow-buffer", @@ -364,9 +365,9 @@ dependencies = [ [[package]] name = "arrow-row" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14fe367802f16d7668163ff647830258e6e0aeea9a4d79aaedf273af3bdcd3e" +checksum = "bab5994731204603c73ba69267616c50f80780774c6bb0476f1f830625115e0c" dependencies = [ "arrow-array", "arrow-buffer", @@ -377,9 +378,9 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c30a1365d7a7dc50cc847e54154e6af49e4c4b0fddc9f607b687f29212082743" +checksum = "f633dbfdf39c039ada1bf9e34c694816eb71fbb7dc78f613993b7245e078a1ed" dependencies = [ "serde", "serde_core", @@ -388,9 +389,9 @@ dependencies = [ [[package]] name = "arrow-select" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78694888660a9e8ac949853db393af2a8b8fc82c19ce333132dfa2e72cc1a7fe" +checksum = "8cd065c54172ac787cf3f2f8d4107e0d3fdc26edba76fdf4f4cc170258942222" dependencies = [ "ahash", "arrow-array", @@ -402,9 +403,9 @@ dependencies = [ [[package]] name = "arrow-string" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e04a01f8bb73ce54437514c5fd3ee2aa3e8abe4c777ee5cc55853b1652f79e" +checksum = "29dd7cda3ab9692f43a2e4acc444d760cc17b12bb6d8232ddf64e9bab7c06b42" dependencies = [ "arrow-array", "arrow-buffer", @@ -431,9 +432,9 @@ checksum = "bfdc70193dadb9d7287fa4b633f15f90c876915b31f6af17da307fc59c9859a8" [[package]] name = "async-compression" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" dependencies = [ "compression-codecs", "compression-core", @@ -513,9 +514,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "axum-macros", @@ -542,7 +543,7 @@ dependencies = [ "sha1", "sync_wrapper", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.29.0", "tower", "tower-layer", "tower-service", @@ -593,9 +594,9 @@ dependencies = [ [[package]] name = "axum-macros" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" dependencies = [ "proc-macro2", "quote", @@ -656,9 +657,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ "serde_core", ] @@ -681,21 +682,21 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] name = "blake3" -version = "1.8.3" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", - "cpufeatures 0.2.17", + "cpufeatures 0.3.0", ] [[package]] @@ -708,12 +709,12 @@ dependencies = [ ] [[package]] -name = "block2" -version = "0.6.2" +name = "block-buffer" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" dependencies = [ - "objc2", + "hybrid-array", ] [[package]] @@ -781,9 +782,9 @@ checksum = "cd17eb909a8c6a894926bfcc3400a4bb0e732f5a57d37b1f14e8b29e329bace8" [[package]] name = "cc" -version = "1.2.57" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -828,7 +829,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -843,9 +844,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -894,9 +895,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -917,9 +918,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -929,15 +930,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -961,9 +962,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "bzip2", "compression-core", @@ -976,9 +977,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "condtype" @@ -1006,6 +1007,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-random" version = "0.1.18" @@ -1068,6 +1075,12 @@ dependencies = [ "syn", ] +[[package]] +name = "const-siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03efed02df0504d71e44bfd51d3329f401b8303a2fd14254acf05c95a0af0153" + [[package]] name = "const-str" version = "0.7.1" @@ -1082,11 +1095,12 @@ checksum = "d9c50fcfdf972929aff202c16b80086aa3cfc6a3a820af714096c58c7c1d0582" [[package]] name = "const_format" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" dependencies = [ "const_format_proc_macros", + "konst", ] [[package]] @@ -1188,11 +1202,30 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "corosensei" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c54787b605c7df106ceccf798df23da4f2e09918defad66705d1cedf3bb914f" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "scopeguard", + "windows-sys 0.59.0", +] + [[package]] name = "cpp_demangle" -version = "0.5.1" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" dependencies = [ "cfg-if", ] @@ -1264,6 +1297,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "csv" version = "1.4.0" @@ -1335,20 +1377,19 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "datafusion" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9f8117889ba9503440f1dd79ebab32ba52ccf1720bb83cd718a29d4edc0d16" +checksum = "997a31e15872606a49478e670c58302094c97cb96abb0a7d60720f8e92170040" dependencies = [ "arrow", "arrow-schema", "async-trait", - "bytes", "bzip2", "chrono", "datafusion-catalog", @@ -1378,14 +1419,13 @@ dependencies = [ "datafusion-sql", "flate2", "futures", + "indexmap 2.14.0", "itertools", "liblzma", "log", "object_store", "parking_lot", "parquet", - "rand 0.9.2", - "regex", "sqlparser", "tempfile", "tokio", @@ -1396,9 +1436,9 @@ dependencies = [ [[package]] name = "datafusion-catalog" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be893b73a13671f310ffcc8da2c546b81efcc54c22e0382c0a28aa3537017137" +checksum = "f7dd61161508f8f5fa1107774ea687bd753c22d83a32eebf963549f89de14139" dependencies = [ "arrow", "async-trait", @@ -1421,9 +1461,9 @@ dependencies = [ [[package]] name = "datafusion-catalog-listing" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830487b51ed83807d6b32d6325f349c3144ae0c9bf772cf2a712db180c31d5e6" +checksum = "897c70f871277f9ce99aa38347be0d679bbe3e617156c4d2a8378cec8a2a0891" dependencies = [ "arrow", "async-trait", @@ -1444,34 +1484,35 @@ dependencies = [ [[package]] name = "datafusion-common" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d7663f3af955292f8004e74bcaf8f7ea3d66cc38438749615bb84815b61a293" +checksum = "121c9ded5d87d9172319e006f2afdb9928d72dbacd6a90a458d8acb1e3b43a65" dependencies = [ - "ahash", "arrow", "arrow-ipc", + "arrow-schema", "chrono", + "foldhash 0.2.0", "half", - "hashbrown 0.16.1", - "indexmap 2.13.0", + "hashbrown 0.17.1", + "indexmap 2.14.0", "itertools", "libc", "log", "object_store", "parquet", - "paste", "recursive", "sqlparser", "tokio", + "uuid", "web-time", ] [[package]] name = "datafusion-common-runtime" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f590205c7e32fe1fea48dd53ffb406e56ae0e7a062213a3ac848db8771641bd" +checksum = "981b9dae74f78ee3d9f714fb49b01919eab975461b56149510c3ba9ea11287d1" dependencies = [ "futures", "log", @@ -1480,9 +1521,9 @@ dependencies = [ [[package]] name = "datafusion-datasource" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde1e030a9dc87b743c806fbd631f5ecfa2ccaa4ffb61fa19144a07fea406b79" +checksum = "ffd7d295b2ec7c00d8a56562f41ed41062cf0af75549ed891c12a0a09eddfefe" dependencies = [ "arrow", "async-compression", @@ -1506,7 +1547,8 @@ dependencies = [ "liblzma", "log", "object_store", - "rand 0.9.2", + "parking_lot", + "rand 0.9.4", "tokio", "tokio-util", "url", @@ -1515,9 +1557,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-arrow" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331ebae7055dc108f9b54994b93dff91f3a17445539efe5b74e89264f7b36e15" +checksum = "552b0b3f342f7ec41b3fbd70f6339dc82a30cfd0349e7f280e7852528085349f" dependencies = [ "arrow", "arrow-ipc", @@ -1539,9 +1581,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-csv" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e0d475088325e2986876aa27bb30d0574f72a22955a527d202f454681d55c5c" +checksum = "68850aa426b897e879c8b87e512ea8124f1d0a2869a4e51808ddaaddf1bc0ada" dependencies = [ "arrow", "async-trait", @@ -1562,9 +1604,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-json" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea1520d81f31770f3ad6ee98b391e75e87a68a5bb90de70064ace5e0a7182fe8" +checksum = "402f93242ae08ef99139ee2c528a49d087efe88d5c7b2c3ff5480855a40ce54f" dependencies = [ "arrow", "async-trait", @@ -1579,16 +1621,15 @@ dependencies = [ "datafusion-session", "futures", "object_store", - "serde_json", "tokio", "tokio-stream", ] [[package]] name = "datafusion-datasource-parquet" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95be805d0742ab129720f4c51ad9242cd872599cdb076098b03f061fcdc7f946" +checksum = "ffd2499c1bee0eeccf6a57156105700eeeb17bc701899ac719183c4e74231450" dependencies = [ "arrow", "async-trait", @@ -1598,6 +1639,7 @@ dependencies = [ "datafusion-datasource", "datafusion-execution", "datafusion-expr", + "datafusion-functions", "datafusion-functions-aggregate-common", "datafusion-physical-expr", "datafusion-physical-expr-adapter", @@ -1616,20 +1658,19 @@ dependencies = [ [[package]] name = "datafusion-doc" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c93ad9e37730d2c7196e68616f3f2dd3b04c892e03acd3a8eeca6e177f3c06a" +checksum = "cb9e7e5d11130c48c8bd4e80c79a9772dd28ce6dc330baca9246205d245b9e2e" [[package]] name = "datafusion-execution" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9437d3cd5d363f9319f8122182d4d233427de79c7eb748f23054c9aaa0fdd8df" +checksum = "37a8643ab852eb68864e1b72ae789e8066282dce48eea6347ffb0aee33d1ccc0" dependencies = [ "arrow", "arrow-buffer", "async-trait", - "chrono", "dashmap", "datafusion-common", "datafusion-expr", @@ -1638,18 +1679,19 @@ dependencies = [ "log", "object_store", "parking_lot", - "rand 0.9.2", + "rand 0.9.4", "tempfile", "url", ] [[package]] name = "datafusion-expr" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67164333342b86521d6d93fa54081ee39839894fb10f7a700c099af96d7552cf" +checksum = "6932f4d71eed9c8d9341476a2b845aadfabde5495d08dbcd8fc23881f49fa7a0" dependencies = [ "arrow", + "arrow-schema", "async-trait", "chrono", "datafusion-common", @@ -1658,9 +1700,8 @@ dependencies = [ "datafusion-functions-aggregate-common", "datafusion-functions-window-common", "datafusion-physical-expr-common", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools", - "paste", "recursive", "serde_json", "sqlparser", @@ -1668,22 +1709,21 @@ dependencies = [ [[package]] name = "datafusion-expr-common" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab05fdd00e05d5a6ee362882546d29d6d3df43a6c55355164a7fbee12d163bc9" +checksum = "0225491839a31b1f7d2cb8092c2d50792e2fe1c1724e4e6d08e011f5feaf4ed2" dependencies = [ "arrow", "datafusion-common", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools", - "paste", ] [[package]] name = "datafusion-functions" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04fb863482d987cf938db2079e07ab0d3bb64595f28907a6c2f8671ad71cca7e" +checksum = "14872c47bfc3d21e53ec82f57074e6987a15941c1e2f43cde4ac6ae2746634e3" dependencies = [ "arrow", "arrow-buffer", @@ -1698,26 +1738,25 @@ dependencies = [ "datafusion-expr", "datafusion-expr-common", "datafusion-macros", + "datafusion-physical-expr-common", "hex", "itertools", "log", - "md-5", + "md-5 0.11.0", "memchr", "num-traits", - "rand 0.9.2", + "rand 0.9.4", "regex", - "sha2", - "unicode-segmentation", + "sha2 0.11.0", "uuid", ] [[package]] name = "datafusion-functions-aggregate" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829856f4e14275fb376c104f27cbf3c3b57a9cfe24885d98677525f5e43ce8d6" +checksum = "75a2ca14e1b609be21e657e2d3130b2f446456b08393b377bb721a33952d2e09" dependencies = [ - "ahash", "arrow", "datafusion-common", "datafusion-doc", @@ -1727,19 +1766,18 @@ dependencies = [ "datafusion-macros", "datafusion-physical-expr", "datafusion-physical-expr-common", + "foldhash 0.2.0", "half", "log", "num-traits", - "paste", ] [[package]] name = "datafusion-functions-aggregate-common" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08af79cc3d2aa874a362fb97decfcbd73d687190cb096f16a6c85a7780cce311" +checksum = "1ece74ba09092d2ef9c9b54a38445450aea292a1f8b04faf531936b723a24b3c" dependencies = [ - "ahash", "arrow", "datafusion-common", "datafusion-expr-common", @@ -1748,9 +1786,9 @@ dependencies = [ [[package]] name = "datafusion-functions-nested" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465ae3368146d49c2eda3e2c0ef114424c87e8a6b509ab34c1026ace6497e790" +checksum = "3f3e3f9ee8ca59bf70518802107de6f1b88a9509efdc629fadc5de9d6b2d5ef5" dependencies = [ "arrow", "arrow-ord", @@ -1764,34 +1802,34 @@ dependencies = [ "datafusion-functions-aggregate-common", "datafusion-macros", "datafusion-physical-expr-common", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "itertools", "itoa", "log", - "paste", + "memchr", ] [[package]] name = "datafusion-functions-table" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6156e6b22fcf1784112fc0173f3ae6e78c8fdb4d3ed0eace9543873b437e2af6" +checksum = "89161dffc22cf2b50f9f4b1bee83b5221d3b4ed7c2e37fd7aa2b22a5297b3a26" dependencies = [ "arrow", "async-trait", "datafusion-catalog", "datafusion-common", "datafusion-expr", + "datafusion-physical-expr", "datafusion-physical-plan", "parking_lot", - "paste", ] [[package]] name = "datafusion-functions-window" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca7baec14f866729012efb89011a6973f3a346dc8090c567bfcd328deff551c1" +checksum = "d7339345b226b3874037708bf5023ba1c2de705128f8457a095aae5ae9cb9c78" dependencies = [ "arrow", "datafusion-common", @@ -1802,14 +1840,13 @@ dependencies = [ "datafusion-physical-expr", "datafusion-physical-expr-common", "log", - "paste", ] [[package]] name = "datafusion-functions-window-common" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "159228c3280d342658466bb556dc24de30047fe1d7e559dc5d16ccc5324166f9" +checksum = "fa84836dc2392df6f43d6a29d37fb56a8ebdc8b3f4e10ae8dc15861fd20278fb" dependencies = [ "datafusion-common", "datafusion-physical-expr-common", @@ -1817,9 +1854,9 @@ dependencies = [ [[package]] name = "datafusion-macros" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5427e5da5edca4d21ea1c7f50e1c9421775fe33d7d5726e5641a833566e7578" +checksum = "587164e03ad68732aa9e7bfe5686e3f25970d4c64fd4bd80790749840892dae5" dependencies = [ "datafusion-doc", "quote", @@ -1828,9 +1865,9 @@ dependencies = [ [[package]] name = "datafusion-optimizer" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89099eefcd5b223ec685c36a41d35c69239236310d71d339f2af0fa4383f3f46" +checksum = "77f20e8cf9e8654d92f4c16b24c487353ee5bf153ffc12d5772cd399ab8cd281" dependencies = [ "arrow", "chrono", @@ -1838,7 +1875,7 @@ dependencies = [ "datafusion-expr", "datafusion-expr-common", "datafusion-physical-expr", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools", "log", "recursive", @@ -1848,11 +1885,10 @@ dependencies = [ [[package]] name = "datafusion-physical-expr" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f222df5195d605d79098ef37bdd5323bff0131c9d877a24da6ec98dfca9fe36" +checksum = "f015a4a82f6f7ff7e1d8d4bf3870a936752fa38b17705dfcc14adef95aa8922c" dependencies = [ - "ahash", "arrow", "datafusion-common", "datafusion-expr", @@ -1860,11 +1896,10 @@ dependencies = [ "datafusion-functions-aggregate-common", "datafusion-physical-expr-common", "half", - "hashbrown 0.16.1", - "indexmap 2.13.0", + "hashbrown 0.17.1", + "indexmap 2.14.0", "itertools", "parking_lot", - "paste", "petgraph", "recursive", "tokio", @@ -1872,9 +1907,9 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-adapter" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40838625d63d9c12549d81979db3dd675d159055eb9135009ba272ab0e8d0f64" +checksum = "51e6ffff8acdfe54e0ea15ccf38115c4a9184433b0439f42907637928d00a235" dependencies = [ "arrow", "datafusion-common", @@ -1887,26 +1922,26 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-common" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eacbcc4cfd502558184ed58fa3c72e775ec65bf077eef5fd2b3453db676f893c" +checksum = "7967a3e171c6a4bf09474b3f7a14f1a3db13ed1714ba12156f33fcce2bba54e8" dependencies = [ - "ahash", "arrow", "chrono", "datafusion-common", "datafusion-expr-common", - "hashbrown 0.16.1", - "indexmap 2.13.0", + "hashbrown 0.17.1", + "indexmap 2.14.0", "itertools", "parking_lot", + "pin-project", ] [[package]] name = "datafusion-physical-optimizer" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d501d0e1d0910f015677121601ac177ec59272ef5c9324d1147b394988f40941" +checksum = "59ff803e2a96054cb6d83f35f9e60fd4f42eac515e1932bd1b2dbc91d5fcbf36" dependencies = [ "arrow", "datafusion-common", @@ -1923,12 +1958,13 @@ dependencies = [ [[package]] name = "datafusion-physical-plan" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463c88ad6f1ecab1810f4c9f046898bee035b370137eb79b2b2db925e270631d" +checksum = "776ee54d47d15bdb126452f9ca17b03761e3b004682914beaedd3f86eb507fbc" dependencies = [ - "ahash", "arrow", + "arrow-data", + "arrow-ipc", "arrow-ord", "arrow-schema", "async-trait", @@ -1943,8 +1979,8 @@ dependencies = [ "datafusion-physical-expr-common", "futures", "half", - "hashbrown 0.16.1", - "indexmap 2.13.0", + "hashbrown 0.17.1", + "indexmap 2.14.0", "itertools", "log", "num-traits", @@ -1955,9 +1991,9 @@ dependencies = [ [[package]] name = "datafusion-proto" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677ee4448a010ed5faeff8d73ff78972c2ace59eff3cd7bd15833a1dafa00492" +checksum = "9dd15a1ba5d3af93808241065c6c44dbca8296a189845e8a587c45c07bf0ffae" dependencies = [ "arrow", "chrono", @@ -1978,14 +2014,13 @@ dependencies = [ "datafusion-proto-common", "object_store", "prost", - "rand 0.9.2", ] [[package]] name = "datafusion-proto-common" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965eca01edc8259edbbd95883a00b6d81e329fd44a019cfac3a03b026a83eade" +checksum = "90042982cf9462eb06a0b81f92efa4188dae871e7ea3ab8dc61aa9c9349b2530" dependencies = [ "arrow", "datafusion-common", @@ -1994,9 +2029,9 @@ dependencies = [ [[package]] name = "datafusion-pruning" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2857618a0ecbd8cd0cf29826889edd3a25774ec26b2995fc3862095c95d88fc6" +checksum = "d5fb9e5774660aa69c3ba93c610f175f75b65cb8c3776edb3626de8f3a4f4ee3" dependencies = [ "arrow", "datafusion-common", @@ -2005,15 +2040,14 @@ dependencies = [ "datafusion-physical-expr", "datafusion-physical-expr-common", "datafusion-physical-plan", - "itertools", "log", ] [[package]] name = "datafusion-session" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8637e35022c5c775003b3ab1debc6b4a8f0eb41b069bdd5475dd3aa93f6eba" +checksum = "15ce715fa2a61f4623cc234bcc14a3ef6a91f189128d5b14b468a6a17cdfc417" dependencies = [ "async-trait", "datafusion-common", @@ -2025,9 +2059,9 @@ dependencies = [ [[package]] name = "datafusion-sql" -version = "53.0.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12d9e9f16a1692a11c94bcc418191fa15fd2b4d72a0c1a0c607db93c0b84dd81" +checksum = "6094ad36a3ed6d7ac87b20b479b2d0b118250f66cf997603829fdc65b44a7099" dependencies = [ "arrow", "bigdecimal", @@ -2035,7 +2069,7 @@ dependencies = [ "datafusion-common", "datafusion-expr", "datafusion-functions-nested", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "recursive", "regex", @@ -2096,7 +2130,7 @@ dependencies = [ [[package]] name = "dev-tools" -version = "0.1.2" +version = "0.1.3" dependencies = [ "dioxus", ] @@ -2107,16 +2141,27 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.2", +] + [[package]] name = "dioxus" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d5b0aec58753daee127a5fe2d1a40b0db8cebc0b8a7f97b34df2492cb90d78e" +checksum = "a44c550c06b6785e16258ad620d5b559f5bbcbcc50e3c18c08aa6af2604a4c32" dependencies = [ "dioxus-asset-resolver", "dioxus-cli-config", @@ -2147,9 +2192,9 @@ dependencies = [ [[package]] name = "dioxus-asset-resolver" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c240c4f092024b26e200ecd64723009173cf5bc2e5083c9feb778c077eb5741b" +checksum = "a8b546050ecfc7fcd310be344b2f3a2a79f21554c5a9e8df28e7a07e9b36009b" dependencies = [ "dioxus-cli-config", "http", @@ -2168,18 +2213,18 @@ dependencies = [ [[package]] name = "dioxus-cli-config" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86a13d42c5defcea333bdbae1dc5d64d078acd0fda1d8a1441c37e06be5146e3" +checksum = "d4ad73f0ff638cd27466d389cd57f0975f909b66130dc1c25d5212d4041e5352" dependencies = [ "wasm-bindgen", ] [[package]] name = "dioxus-config-macro" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba1d68a05a8a15293ba65d45c7a3263356f3eedf1a3e599440683f3eb014637" +checksum = "e004bc8b958031117d373db2b5e0ab9d7e763751de129dd36f00ef7e318333cd" dependencies = [ "proc-macro2", "quote", @@ -2187,15 +2232,15 @@ dependencies = [ [[package]] name = "dioxus-config-macros" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43f2d511d3c3c439a2fb7f863668b84caf8e0d2440cbfbcbb28521e26ba7f44" +checksum = "808a9994a9a2623e6b6890b6cc68def24bd669177ec4713684447fb46418c256" [[package]] name = "dioxus-core" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3dd61889e6a09daec93d44db86047fb8e6603beedcf9351b8528582254e075" +checksum = "247ed8d679a13232641f1c84ba22246623fae01320b4c22db225c0b4f2fa7398" dependencies = [ "anyhow", "const_format", @@ -2204,7 +2249,7 @@ dependencies = [ "futures-util", "generational-box", "longest-increasing-subsequence", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustversion", "serde", "slab", @@ -2215,9 +2260,9 @@ dependencies = [ [[package]] name = "dioxus-core-macro" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8577c4d9a8cc23423c4d2137319044b03ab940e4b2790dd25f4f06601bd32d9a" +checksum = "75fbe64029b90144041f8521300c7b3508e6c48caea3244de2ff5d1ade15390c" dependencies = [ "convert_case 0.8.0", "dioxus-rsx", @@ -2228,15 +2273,15 @@ dependencies = [ [[package]] name = "dioxus-core-types" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b99d7d199aad72431b549759550002e7d72c8a257eba500dca9fbdb2122de103" +checksum = "cbfea5c8946e0745b254b5c33c515d81b3ba638f33c2a532ed06730392394d4d" [[package]] name = "dioxus-devtools" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27e7212436a581ce058d7554f1383916bd18a68ebd6015b0b4c2e9ecb0d5535" +checksum = "08d30370fa78266aed3f3d9119dea2de9e92a0348941dbfa777f7a669f2ea375" dependencies = [ "dioxus-cli-config", "dioxus-core", @@ -2254,9 +2299,9 @@ dependencies = [ [[package]] name = "dioxus-devtools-types" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aa24ed651b97e0b423270bf07a0f1b7dc0e0fa1f1dc26407cd2a118d6bf9de5" +checksum = "3907a2b61cf56039f047da6a37317a03d9e15411753bc40e5e34a27e405ac320" dependencies = [ "dioxus-core", "serde", @@ -2265,9 +2310,9 @@ dependencies = [ [[package]] name = "dioxus-document" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24685cb51cc6227ea606c49dfe531836f362c49183d3007241afcd8827498401" +checksum = "e3b75a1809af7c13546ae4487c8b02ab80cc4d059e7db5a5d090374ceaa71d5b" dependencies = [ "dioxus-core", "dioxus-core-macro", @@ -2284,9 +2329,9 @@ dependencies = [ [[package]] name = "dioxus-fullstack" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5940c870751b6273a23b7c0e16d80039f45604d68d9b86c91e27b09edeabeb9e" +checksum = "c8ee56dd65fbf1222fa6a2749c3f821df28facf1832854b43d480b547a096f6d" dependencies = [ "anyhow", "async-stream", @@ -2332,7 +2377,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tokio-stream", - "tokio-tungstenite", + "tokio-tungstenite 0.28.0", "tokio-util", "tower", "tower-http", @@ -2349,9 +2394,9 @@ dependencies = [ [[package]] name = "dioxus-fullstack-core" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28333274cfc8e5fe547ab04258c2511350c4930a07af9616d365dc4ba7b22d8f" +checksum = "d40d33a447cb158acdb61787b2ff52dd8a0f9a9f20e95e5c5fe9873f01c2b55b" dependencies = [ "anyhow", "axum-core", @@ -2377,9 +2422,9 @@ dependencies = [ [[package]] name = "dioxus-fullstack-macro" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53f7e5a9fa7f657aa519a07aced8b8936f3ae8a246d94855d497d8cce59b9533" +checksum = "7c06eb66bce50d5f47b793e6af5fc2e0a511bf2b4fa2423cf86e35023d8f17e6" dependencies = [ "const_format", "convert_case 0.8.0", @@ -2391,9 +2436,9 @@ dependencies = [ [[package]] name = "dioxus-history" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "010b446322b3f9176476579fa61c7552f0430abbeec418cab543482da6ca4363" +checksum = "c1d8024afd482956eadae2c43d0b1e73e584adb724ac09be87f268d52002387b" dependencies = [ "dioxus-core", "tracing", @@ -2401,9 +2446,9 @@ dependencies = [ [[package]] name = "dioxus-hooks" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e7a6ba279050cc161e1215c6db0bd15915c9314ec2916d7b22c113a3039536" +checksum = "233b5e168a7c38c4bf96d0f390221a72a5a28bb31ce2dcf6d2af879dc561ef42" dependencies = [ "dioxus-core", "dioxus-signals", @@ -2417,9 +2462,9 @@ dependencies = [ [[package]] name = "dioxus-html" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0715e38cc6537aef5b79d0ddc1f4d7a56c2f4debe46b127eee24d8aa5dafd2d" +checksum = "4abf4ad27eee650d1ab8ebe13591e8b0ee595fa5a5dd236be13a5b7b3fab678d" dependencies = [ "async-trait", "bytes", @@ -2444,9 +2489,9 @@ dependencies = [ [[package]] name = "dioxus-html-internal-macro" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6b7918b0908c8719a6165b4e3c362da4fd311fc7cb48720eddd8a45b2ddfc6" +checksum = "025e107e677f790f4ed648a189e36f51ae014e5341902b97313e4eae626cffa2" dependencies = [ "convert_case 0.8.0", "proc-macro2", @@ -2456,16 +2501,16 @@ dependencies = [ [[package]] name = "dioxus-interpreter-js" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8ce1cf487007f90d0ec4ec87dff111d74ac04fca0918f9dcc4e80dc3b0531b2" +checksum = "57caa76427d8ec4105ccca44ff8c511055688af732a094b9fe9ef3547d2a11b2" dependencies = [ "dioxus-core", "dioxus-core-types", "dioxus-html", "js-sys", "lazy-js-bundle", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "sledgehammer_bindgen", "sledgehammer_utils", "wasm-bindgen", @@ -2475,9 +2520,9 @@ dependencies = [ [[package]] name = "dioxus-liveview" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9407df2eec82681fa2195282762dddacc40563445df36b3ad1df9d69d4eaa073" +checksum = "64e64d86ad604897c796fdcc1f1f42cab838258647dd47664ab637a8eb97f08e" dependencies = [ "axum", "dioxus-cli-config", @@ -2490,7 +2535,7 @@ dependencies = [ "futures-channel", "futures-util", "generational-box", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "serde_json", "slab", @@ -2503,9 +2548,9 @@ dependencies = [ [[package]] name = "dioxus-logger" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4742b16791a71eb4db2d0747f15c50b278b27369b3d93e5a4d6ec2570bcb9bc" +checksum = "0cbbee192b1b12fccb444a5b04d809710cfce4d27b792129fea6c845fae7f329" dependencies = [ "dioxus-cli-config", "tracing", @@ -2515,9 +2560,9 @@ dependencies = [ [[package]] name = "dioxus-router" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae50f5efa8d6f936c0c3bb85d7a55f6f19290f106290e331d1136d964e832fe6" +checksum = "ecdb19d7a1489ba252be9b3a07f9db92814020eb4ba8c1306f04242a44d17e66" dependencies = [ "dioxus-cli-config", "dioxus-core", @@ -2536,24 +2581,24 @@ dependencies = [ [[package]] name = "dioxus-router-macro" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae9beca02f6baca4b223256805536dc92e77a1541bb2331723100f66aae79332" +checksum = "b525ab775585f1dc4850178de9d0cb6bb37f7b9c1a1a0c121eab7449d7021480" dependencies = [ "base16", - "digest", + "digest 0.10.7", "proc-macro2", "quote", - "sha2", + "sha2 0.10.9", "slab", "syn", ] [[package]] name = "dioxus-rsx" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "344621f6dc435e76fbe272da09988d0118cf35cc2aa88ebb5ae7c1317a36e57c" +checksum = "37fb07e40e9734946511659668ea3675ed214b60889e7aa7f0a5a85271518475" dependencies = [ "proc-macro2", "proc-macro2-diagnostics", @@ -2564,9 +2609,9 @@ dependencies = [ [[package]] name = "dioxus-server" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04f3e741d9b866f38c20f368fdf84226b27ca341fa0108cf2e0bf6cdb40c7e7" +checksum = "5aa22ad381073c68a70cdb28152485abb5f2dcf20dabc68c631e26ce7ac046dd" dependencies = [ "anyhow", "async-trait", @@ -2603,14 +2648,14 @@ dependencies = [ "lru", "parking_lot", "pin-project", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "serde_json", "serde_qs", "subsecond", "thiserror 2.0.18", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.28.0", "tokio-util", "tower", "tower-http", @@ -2622,37 +2667,37 @@ dependencies = [ [[package]] name = "dioxus-signals" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "409bf65d243443416650945f22cd6caf2a6bb13ae0347a50ec5852adb1961072" +checksum = "5393fd579f42c6547bf47ec0a2dedf2a366cd541d31deedc1059a096e6c35798" dependencies = [ "dioxus-core", "futures-channel", "futures-util", "generational-box", "parking_lot", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "tracing", "warnings", ] [[package]] name = "dioxus-ssr" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f16c0c648d1a650be65a16bc24a719519352ab94e6205cceaa300d9c9c5f88" +checksum = "d17c75a43e218012b63a97f55cf585747bd0ca37840ac2a0cf40add06ffcf9fe" dependencies = [ "askama_escape", "dioxus-core", "dioxus-core-types", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", ] [[package]] name = "dioxus-stores" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245ec4f84348e5be77451bd204181998b8bc0995b48ff3adb2db0e0ec430dab4" +checksum = "f1c59f52e8194439604dd35f8c540456c22b4a4b076930e424d0289f98ea3cb4" dependencies = [ "dioxus-core", "dioxus-signals", @@ -2662,9 +2707,9 @@ dependencies = [ [[package]] name = "dioxus-stores-macro" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd9da8e9a1cc2d8bff387e0b99f09f2590b71f67d5d73ab343b2cc9d17990d92" +checksum = "737600865572cecf60ff934f88252bd4144f4ca189e0e570b7a780e8f0e01a1f" dependencies = [ "convert_case 0.8.0", "proc-macro2", @@ -2674,9 +2719,9 @@ dependencies = [ [[package]] name = "dioxus-web" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac92ef863bc5333440021e8ec3e538a39598c9c960daeaab66ab10ba940b5e0" +checksum = "176eb0a5ee8251203a816b413a64fdc014b43e53d4245f769b1b0c2035b88ac3" dependencies = [ "dioxus-cli-config", "dioxus-core", @@ -2694,7 +2739,7 @@ dependencies = [ "gloo-timers", "js-sys", "lazy-js-bundle", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "send_wrapper", "serde", "serde-wasm-bindgen", @@ -2706,16 +2751,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.11.0", - "objc2", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -2878,14 +2913,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "euclid" -version = "0.22.13" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" dependencies = [ "num-traits", "serde", @@ -2895,9 +2930,11 @@ dependencies = [ name = "examples" version = "0.0.0" dependencies = [ + "arrow", "arrow-flight", "clap", "datafusion", + "liquid-cache", "liquid-cache-datafusion-client", "liquid-cache-datafusion-local", "liquid-cache-datafusion-server", @@ -2920,12 +2957,13 @@ dependencies = [ [[package]] name = "fastlanes" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414cb755aee48ff7b0907995d2949c68c8c17900970076dff6a808e18e592d71" +checksum = "20c597e23b8ec8506f589d18bc701ca83a3def6086748f628ad23092e1dfe577" dependencies = [ "arrayref", "const_for", + "core_detect", "num-traits", "paste", "seq-macro", @@ -2941,7 +2979,7 @@ dependencies = [ "fastrace-macro", "parking_lot", "pin-project", - "rand 0.9.2", + "rand 0.9.4", "rtrb", "serde", ] @@ -2985,9 +3023,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "find-msvc-tools" @@ -3019,7 +3057,7 @@ version = "25.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "rustc_version", ] @@ -3063,11 +3101,11 @@ dependencies = [ [[package]] name = "fsst-rs" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf65e16e100438be0030d113042e07a62bed67203998640ca6fae0404eed71e" +checksum = "9b13ac798afc0d9194eb4efefef8b9332efbd80b43f302a968cb8cb23b9d5360" dependencies = [ - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", ] [[package]] @@ -3166,29 +3204,14 @@ dependencies = [ [[package]] name = "generational-box" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ede46ff252793f9b6ef752c506ba8600c69d73cad2ef9bbf2e6dee85019a3bc" +checksum = "c68d74be1fbe3bba37604bdfd61403f26af9f6324cf325053abd89d60c22e799" dependencies = [ "parking_lot", "tracing", ] -[[package]] -name = "generator" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows-link", - "windows-result", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -3235,7 +3258,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "rand_core 0.10.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -3321,7 +3344,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.13.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -3372,6 +3395,17 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + [[package]] name = "hdrhistogram" version = "7.5.4" @@ -3428,15 +3462,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "http" version = "1.4.0" @@ -3494,11 +3519,20 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -3511,7 +3545,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -3519,16 +3552,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", "rustls-native-certs", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -3600,12 +3632,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -3613,9 +3646,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -3626,9 +3659,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -3640,15 +3673,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -3660,15 +3693,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -3704,9 +3737,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -3724,12 +3757,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -3750,7 +3783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "232929e1d75fe899576a3d5c7416ad0d88dbfbb3c3d6aa00873a7408a50ddb88" dependencies = [ "ahash", - "indexmap 2.13.0", + "indexmap 2.14.0", "is-terminal", "itoa", "log", @@ -3781,20 +3814,20 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "inventory" -version = "0.3.22" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" dependencies = [ "rustversion", ] [[package]] name = "io-uring" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd7bddefd0a8833b88a4b68f90dae22c7450d11b354198baee3874fd811b344" +checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "libc", ] @@ -3805,16 +3838,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-terminal" version = "0.4.17" @@ -3823,7 +3846,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3843,15 +3866,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -3859,14 +3882,14 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", @@ -3897,7 +3920,7 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", @@ -3906,9 +3929,31 @@ dependencies = [ [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] [[package]] name = "jobserver" @@ -3922,40 +3967,46 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] [[package]] -name = "kani-verifier" -version = "0.67.0" +name = "keyboard-types" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6225a7ec13037e984f6bebdda33d34390e643a1cd2a4522cb5d5e8e6ed85e2b" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "anyhow", - "home", - "os_info", + "bitflags 2.11.1", + "serde", ] [[package]] -name = "keyboard-types" -version = "0.7.0" +name = "konst" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" dependencies = [ - "bitflags 2.11.0", - "serde", + "konst_macro_rules", ] +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "lazy-js-bundle" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d7adc10cb9440d17fa67e467febdfc98931338773d11bfee81809af54d0697" +checksum = "ebbde2c5796719fbd82d6b8ec0be3dacf1f70c2876dee0f2c001632794d6641f" [[package]] name = "lazy_static" @@ -4028,15 +4079,15 @@ dependencies = [ [[package]] name = "libbz2-rs-sys" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" +checksum = "b3a6a8c165077efc8f3a971534c50ea6a1a18b329ef4a66e897a7e3a1494565f" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libfuzzer-sys" @@ -4069,9 +4120,9 @@ dependencies = [ [[package]] name = "liblzma-sys" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f2db66f3268487b5033077f266da6777d057949b8f93c8ad82e441df25e6186" +checksum = "1a60851d15cd8c5346eca4ab8babff585be2ae4bc8097c067291d3ffe2add3b6" dependencies = [ "cc", "libc", @@ -4086,12 +4137,11 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libmimalloc-sys" -version = "0.1.44" +version = "0.1.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" dependencies = [ "cc", - "libc", ] [[package]] @@ -4102,7 +4152,7 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "liquid-cache" -version = "0.1.12" +version = "0.1.13" dependencies = [ "ahash", "arrow", @@ -4123,7 +4173,6 @@ dependencies = [ "fsst-rs", "futures", "insta", - "kani-verifier", "liquid-cache-common", "log", "mimalloc", @@ -4131,10 +4180,11 @@ dependencies = [ "object_store", "parquet", "parquet-variant-compute", - "rand 0.10.0", + "rand 0.10.1", "serde", "serde_json", "shuttle", + "sysinfo", "t4", "tempfile", "tokio", @@ -4171,7 +4221,7 @@ dependencies = [ "perf-event2", "pprof", "regex", - "reqwest 0.13.2", + "reqwest 0.13.4", "serde", "serde_json", "sysinfo", @@ -4184,7 +4234,7 @@ dependencies = [ [[package]] name = "liquid-cache-common" -version = "0.1.12" +version = "0.1.13" dependencies = [ "arrow-flight", "async-trait", @@ -4202,12 +4252,11 @@ dependencies = [ [[package]] name = "liquid-cache-datafusion" -version = "0.1.12" +version = "0.1.13" dependencies = [ "ahash", "arrow", "arrow-schema", - "async-trait", "bytes", "datafusion", "divan", @@ -4218,10 +4267,8 @@ dependencies = [ "log", "object_store", "parquet", - "parquet-variant-compute", "parquet-variant-json", - "rand 0.10.0", - "serde", + "rand 0.10.1", "serde_json", "shuttle", "t4", @@ -4232,7 +4279,7 @@ dependencies = [ [[package]] name = "liquid-cache-datafusion-client" -version = "0.1.12" +version = "0.1.13" dependencies = [ "arrow", "arrow-flight", @@ -4244,6 +4291,7 @@ dependencies = [ "futures", "insta", "liquid-cache-common", + "liquid-cache-datafusion", "object_store", "tokio", "tonic", @@ -4253,7 +4301,7 @@ dependencies = [ [[package]] name = "liquid-cache-datafusion-local" -version = "0.1.12" +version = "0.1.13" dependencies = [ "arrow", "arrow-schema", @@ -4271,7 +4319,7 @@ dependencies = [ [[package]] name = "liquid-cache-datafusion-server" -version = "0.1.12" +version = "0.1.13" dependencies = [ "anyhow", "arrow", @@ -4304,7 +4352,7 @@ dependencies = [ [[package]] name = "liquid-cache-fuzz" -version = "0.1.12" +version = "0.1.13" dependencies = [ "arbitrary", "arrow", @@ -4314,9 +4362,9 @@ dependencies = [ [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litrs" @@ -4335,9 +4383,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" dependencies = [ "sval", "sval_ref", @@ -4408,9 +4456,9 @@ checksum = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86" [[package]] name = "lru" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" dependencies = [ "hashbrown 0.16.1", ] @@ -4443,9 +4491,9 @@ dependencies = [ [[package]] name = "manganis" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "492da8d77990281eabe6ded633e7b0cf805c5cf7a023a99abed8811edc872d6f" +checksum = "2e06225f29a781d86afdfafa562de09621f2ace377136ae2ae9ca9e72a29b920" dependencies = [ "const-serialize 0.7.2", "const-serialize 0.8.0-alpha.0", @@ -4459,9 +4507,9 @@ dependencies = [ [[package]] name = "manganis-core" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b84cc2951f3b119702fab499b9b1aec3f454929c62feca55b895b82c628308" +checksum = "774ddc382b4fb30f3fdcf2418131cbac5e6111f00e6c7ddf9e598ef3b2b4cd91" dependencies = [ "const-serialize 0.7.2", "const-serialize 0.8.0-alpha.0", @@ -4473,9 +4521,9 @@ dependencies = [ [[package]] name = "manganis-macro" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d2e60d36758b201b6ebb8a31aff6b013e58924eeb6d3cbf19aea764f51d69e4" +checksum = "731c83c89d831f341fb46eba0aefdfb3433f77a3f78a8b08d9d88746613a8f8b" dependencies = [ "dunce", "macro-string", @@ -4507,7 +4555,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", ] [[package]] @@ -4545,9 +4603,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.48" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" dependencies = [ "libmimalloc-sys", ] @@ -4586,9 +4644,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -4618,8 +4676,8 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.11.0", - "jni-sys", + "bitflags 2.11.1", + "jni-sys 0.3.1", "log", "ndk-sys", "num_enum", @@ -4639,7 +4697,7 @@ version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", + "jni-sys 0.3.1", ] [[package]] @@ -4653,18 +4711,6 @@ dependencies = [ "libc", ] -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "nom" version = "7.1.3" @@ -4690,7 +4736,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4714,9 +4760,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-format" @@ -4749,9 +4795,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -4759,9 +4805,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -4778,69 +4824,13 @@ dependencies = [ "objc2-encode", ] -[[package]] -name = "objc2-cloud-kit" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" -dependencies = [ - "objc2", - "objc2-foundation", -] - [[package]] name = "objc2-core-foundation" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.0", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" -dependencies = [ - "bitflags 2.11.0", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-location" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac0f75792558aa9d618443bbb5db7426a7a0b6fddf96903f86ef9ad02e135740" -dependencies = [ - "objc2", - "objc2-foundation", + "bitflags 2.11.1", ] [[package]] @@ -4849,19 +4839,6 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" -[[package]] -name = "objc2-foundation" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" -dependencies = [ - "bitflags 2.11.0", - "block2", - "libc", - "objc2", - "objc2-core-foundation", -] - [[package]] name = "objc2-io-kit" version = "0.3.2" @@ -4872,59 +4849,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "objc2-io-surface" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" -dependencies = [ - "bitflags 2.11.0", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-location", - "objc2-foundation", - "objc2-quartz-core", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a3f5ec77a81d9e0c5a0b32159b0cb143d7086165e79708351e02bf37dfc65cd" -dependencies = [ - "objc2", - "objc2-foundation", -] - [[package]] name = "object" version = "0.37.3" @@ -4953,11 +4877,11 @@ dependencies = [ "humantime", "hyper", "itertools", - "md-5", + "md-5 0.10.6", "parking_lot", "percent-encoding", "quick-xml 0.39.2", - "rand 0.10.0", + "rand 0.10.1", "reqwest 0.12.28", "ring", "serde", @@ -5060,7 +4984,7 @@ dependencies = [ "futures-util", "opentelemetry", "percent-encoding", - "rand 0.9.2", + "rand 0.9.4", "thiserror 2.0.18", ] @@ -5073,21 +4997,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "os_info" -version = "3.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" -dependencies = [ - "android_system_properties", - "log", - "nix 0.30.1", - "objc2", - "objc2-foundation", - "objc2-ui-kit", - "windows-sys 0.61.2", -] - [[package]] name = "owo-colors" version = "3.5.0" @@ -5119,9 +5028,9 @@ dependencies = [ [[package]] name = "parquet" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3f9f2205199603564127932b89695f52b62322f541d0fc7179d57c2e1c9877" +checksum = "5dafa7d01085b62a47dd0c1829550a0a36710ea9c4fe358a05a85477cec8a908" dependencies = [ "ahash", "arrow-array", @@ -5137,7 +5046,7 @@ dependencies = [ "flate2", "futures", "half", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "lz4_flex", "num-bigint", "num-integer", @@ -5158,29 +5067,31 @@ dependencies = [ [[package]] name = "parquet-variant" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf493f3c9ddd984d0efb019f67343e4aa4bab893931f6a14b82083065dc3d28" +checksum = "74c8db065291f088a2aad8ab831853eae1871c0d311c8d0b83bbc3b7e735d0fc" dependencies = [ + "arrow", "arrow-schema", "chrono", "half", - "indexmap 2.13.0", + "indexmap 2.14.0", + "num-traits", "simdutf8", "uuid", ] [[package]] name = "parquet-variant-compute" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac038d46a503a7d563b4f5df5802c4315d5343d009feab195d15ac512b4cb27" +checksum = "a530e8d5b5e14efcb39c9a6ec55432ad11f6afb7dc4455a79be0dc615fe3cc31" dependencies = [ "arrow", "arrow-schema", "chrono", "half", - "indexmap 2.13.0", + "indexmap 2.14.0", "parquet-variant", "parquet-variant-json", "serde_json", @@ -5189,9 +5100,9 @@ dependencies = [ [[package]] name = "parquet-variant-json" -version = "58.1.0" +version = "58.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "015a09c2ffe5108766c7c1235c307b8a3c2ea64eca38455ba1a7f3a7f32f16e2" +checksum = "00ed89908289f67caa2ca078f9ff9aacd6229a313ec92b12bf4f48f613dc2b97" dependencies = [ "arrow-schema", "base64 0.22.1", @@ -5219,7 +5130,7 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "575828d9d7d205188048eb1508560607a03d21eafdbba47b8cade1736c1c28e1" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "c-enum", "perf-event-open-sys2", ] @@ -5240,7 +5151,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0939b8fad77dfaeb29ebbd35faaeaadbf833167f30975f1b8993bbba09ea0a0f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "c-enum", "libc", "memmap2", @@ -5288,7 +5199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -5299,7 +5210,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap 2.14.0", "serde", ] @@ -5347,17 +5258,11 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -5379,18 +5284,18 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -5414,7 +5319,7 @@ dependencies = [ "inferno", "libc", "log", - "nix 0.26.4", + "nix", "once_cell", "smallvec", "spin 0.10.0", @@ -5434,9 +5339,9 @@ dependencies = [ [[package]] name = "pretty-hex" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc83ee4a840062f368f9096d80077a9841ec117e17e7f700df81958f1451254" +checksum = "9a65843dfefbafd3c879c683306959a6de478443ffe9c9adf02f5976432402d7" [[package]] name = "prettyplease" @@ -5502,9 +5407,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -5512,9 +5417,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools", @@ -5540,9 +5445,9 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" [[package]] name = "psm" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" dependencies = [ "ar_archive_writer", "cc", @@ -5588,7 +5493,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustls", "socket2", "thiserror 2.0.18", @@ -5606,9 +5511,9 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustls", "rustls-pki-types", "slab", @@ -5661,9 +5566,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -5672,9 +5577,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -5682,13 +5587,13 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", "getrandom 0.4.2", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -5731,9 +5636,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rand_pcg" @@ -5776,14 +5681,14 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -5810,9 +5715,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -5863,9 +5768,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", @@ -5917,9 +5822,9 @@ dependencies = [ [[package]] name = "rtrb" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7204ed6420f698836b76d4d5c2ec5dec7585fd5c3a788fd1cde855d1de598239" +checksum = "4ade083ccbb4bf536df69d1f6432cc23deb7acccff86b183f3923a6fd56a1153" [[package]] name = "rustc-demangle" @@ -5935,9 +5840,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -5954,18 +5859,18 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "once_cell", "ring", @@ -5989,9 +5894,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -5999,9 +5904,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -6031,9 +5936,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -6076,7 +5981,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -6095,9 +6000,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "send_wrapper" @@ -6251,7 +6156,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -6262,7 +6167,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -6282,17 +6198,18 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "shuttle" -version = "0.8.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab17edba38d63047f46780cf7360acf7467fec2c048928689a5c1dd1c2b4e31" +checksum = "ba93071c1b720be2505f4c8ce2863502cb9a26a3819e268df1932458a755152c" dependencies = [ "assoc", "bitvec", "cfg-if", - "generator", + "const-siphasher", + "corosensei", "hex", "owo-colors", - "rand 0.8.5", + "rand 0.8.6", "rand_core 0.6.4", "rand_pcg", "scoped-tls", @@ -6302,9 +6219,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simdutf8" @@ -6414,9 +6331,9 @@ dependencies = [ [[package]] name = "sqlparser" -version = "0.61.0" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf5ea8d4d7c808e1af1cbabebca9a2abe603bcefc22294c5b95018d53200cb7" +checksum = "13c6d1b651dc4edf07eead2a0c6c78016ce971bc2c10da5266861b13f25e7cec" dependencies = [ "log", "recursive", @@ -6442,15 +6359,15 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stacker" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" dependencies = [ "cc", "cfg-if", "libc", "psm", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6467,9 +6384,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subsecond" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dbb9f2928b6654ccc28d4ddfef5213e97ed66afed4907774d049b376c62a838" +checksum = "feae81a4a7ca6d0bcf70c385a43b7dbacbff527f0805cb0a4043ce2c2c559a2c" dependencies = [ "js-sys", "libc", @@ -6486,9 +6403,9 @@ dependencies = [ [[package]] name = "subsecond-types" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388bb28e6ddbee717745963b8932d9a6e24a5d3c93350655f733e938de04d81f" +checksum = "85256ee192cbdf00473e48e6133863b125dd4f772fddfbc97287ec7a61458c25" dependencies = [ "serde", ] @@ -6501,15 +6418,15 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sval" -version = "2.17.0" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1aaf178a50bbdd86043fce9bf0a5867007d9b382db89d1c96ccae4601ff1ff9" +checksum = "2eb9318255ebd817902d7e279d8f8e39b35b1b9954decd5eb9ea0e30e5fd2b6a" [[package]] name = "sval_buffer" -version = "2.17.0" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89273e48f03807ebf51c4d81c52f28d35ffa18a593edf97e041b52de143df89" +checksum = "12571299185e653fdb0fbfe36cd7f6529d39d4e747a60b15a3f34574b7b97c61" dependencies = [ "sval", "sval_ref", @@ -6517,18 +6434,18 @@ dependencies = [ [[package]] name = "sval_dynamic" -version = "2.17.0" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0430f4e18e7eba21a49d10d25a8dec3ce0e044af40b162347e99a8e3c3ced864" +checksum = "39526f24e997706c0de7f03fb7371f7f5638b66a504ded508e20ad173d0a3677" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.17.0" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835f51b9d7331b9d7fc48fc716c02306fa88c4a076b1573531910c91a525882d" +checksum = "933dd3bb26965d682280fcc49400ac2a05036f4ee1e6dbd61bf8402d5a5c3a54" dependencies = [ "itoa", "ryu", @@ -6537,9 +6454,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.17.0" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13cbfe3ef406ee2366e7e8ab3678426362085fa9eaedf28cb878a967159dced3" +checksum = "a0cda08f6d5c9948024a6551077557b1fdcc3880ff2f20ae839667d2ec2d87ed" dependencies = [ "itoa", "ryu", @@ -6548,9 +6465,9 @@ dependencies = [ [[package]] name = "sval_nested" -version = "2.17.0" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b20358af4af787c34321a86618c3cae12eabdd0e9df22cd9dd2c6834214c518" +checksum = "88d49d5e6c1f9fd0e53515819b03a97ca4eb1bff5c8ee097c43391c09ecfb19f" dependencies = [ "sval", "sval_buffer", @@ -6559,18 +6476,18 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.17.0" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5e500f8eb2efa84f75e7090f7fc43f621b9f8b6cde571c635b3855f97b332a" +checksum = "14f876c5a78405375b4e19cbb9554407513b59c93dea12dc6a4af4e1d30899ca" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.17.0" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2032ae39b11dcc6c18d5fbc50a661ea191cac96484c59ccf49b002261ca2c1" +checksum = "5f9ccd3b7f7200239a655e517dd3fd48d960b9111ad24bd6a5e055bef17607c7" dependencies = [ "serde_core", "sval", @@ -6579,9 +6496,9 @@ dependencies = [ [[package]] name = "symbolic-common" -version = "12.17.2" +version = "12.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "751a2823d606b5d0a7616499e4130a516ebd01a44f39811be2b9600936509c23" +checksum = "332615d90111d8eeaf86a84dc9bbe9f65d0d8c5cf11b4caccedc37754eb0dcfd" dependencies = [ "debugid", "memmap2", @@ -6591,9 +6508,9 @@ dependencies = [ [[package]] name = "symbolic-demangle" -version = "12.17.2" +version = "12.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b237cfbe320601dd24b4ac817a5b68bb28f5508e33f08d42be0682cadc8ac9" +checksum = "912017718eb4d21930546245af9a3475c9dccf15675a5c215664e76621afc471" dependencies = [ "cpp_demangle", "rustc-demangle", @@ -6651,7 +6568,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6668,21 +6585,25 @@ dependencies = [ [[package]] name = "t4" -version = "0.1.3" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586231b4196aa1217287f36db73b82afaf2e428d82824b395a806e0e7ae2b76" +checksum = "d3105026c5fb7e1dcf8864c2193387439aa650b690e1a5513ab27ae7de7999a9" dependencies = [ + "cfg-if", + "crossbeam-channel", + "futures-util", "io-uring", "libc", "shuttle", "t4-verified", + "vstd", ] [[package]] name = "t4-verified" -version = "0.1.3" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27bc679002d08d84ba2ee9e644ee2da132db2540e454b7a1f5b024e3e668089" +checksum = "494c2f2c4cfb21784ef9b380c378fbfdbbe2f19f1ad606e6729816ea2a0f4297" dependencies = [ "vstd", ] @@ -6703,17 +6624,17 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6828,9 +6749,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -6853,9 +6774,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -6868,9 +6789,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -6922,6 +6843,18 @@ dependencies = [ "tungstenite 0.28.0", ] +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.29.0", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -6939,39 +6872,39 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.1+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.25.5+spec-1.1.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "toml_datetime", "toml_parser", - "winnow 1.0.0", + "winnow 1.0.2", ] [[package]] name = "toml_parser" -version = "1.0.10+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.0", + "winnow 1.0.2", ] [[package]] name = "tonic" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "axum", @@ -7015,7 +6948,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.13.0", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -7028,11 +6961,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", "futures-core", "futures-util", @@ -7041,7 +6974,6 @@ dependencies = [ "http-body-util", "http-range-header", "httpdate", - "iri-string", "mime", "mime_guess", "percent-encoding", @@ -7051,7 +6983,7 @@ dependencies = [ "tower", "tower-layer", "tower-service", - "tracing", + "url", ] [[package]] @@ -7166,7 +7098,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.2", + "rand 0.9.4", "sha1", "thiserror 2.0.18", "utf-8", @@ -7183,12 +7115,28 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.2", + "rand 0.9.4", "sha1", "thiserror 2.0.18", "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.4", + "sha1", + "thiserror 2.0.18", +] + [[package]] name = "twox-hash" version = "2.1.2" @@ -7203,9 +7151,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "ucd-trie" @@ -7227,9 +7175,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" @@ -7343,9 +7291,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -7403,15 +7351,15 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "verus_builtin" -version = "0.0.0-2026-02-08-0120" +version = "0.0.0-2026-05-06-1803" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1bd13abb198fff161cf4be16cf8c1723f09941d03e323af71146fa4cfb0de0f" +checksum = "650bcc71ef90cbc79bc2a2494c54dd159edad63a72677be791954edc993883fe" [[package]] name = "verus_builtin_macros" -version = "0.0.0-2026-02-22-0103" +version = "0.0.0-2026-05-10-0145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe750a656777699bfdcfee3894cc24f04b6f276e1240b2c9dbdeccdae4e5d94e" +checksum = "7ef946d78c84f284991d59035ca252f29cd8071526003e794be80f0beee18f2d" dependencies = [ "proc-macro2", "quote", @@ -7423,9 +7371,9 @@ dependencies = [ [[package]] name = "verus_prettyplease" -version = "0.0.0-2026-02-15-0106" +version = "0.0.0-2026-05-10-0145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa1c9a8d5635aac38528868312f72ed3f0e76f58ca25ac5c407d1064bdc33430" +checksum = "6a227e7eaa03f51ac4d659641192bc1b9fe4eda0a509a6734752cf72f5d7daaf" dependencies = [ "proc-macro2", "verus_syn", @@ -7433,9 +7381,9 @@ dependencies = [ [[package]] name = "verus_state_machines_macros" -version = "0.0.0-2026-02-15-0106" +version = "0.0.0-2026-05-10-0145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28cb0ec7d66fc27adccbd5aa9d96acf3ab5508cd97f1ca8930fed4f272f6ba8" +checksum = "3ff46046bffd2d55757503feef0b72a74a60ead2b58aa211861267485b955136" dependencies = [ "indexmap 1.9.3", "proc-macro2", @@ -7445,9 +7393,9 @@ dependencies = [ [[package]] name = "verus_syn" -version = "0.0.0-2026-02-15-0106" +version = "0.0.0-2026-05-10-0145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fbe9986fe3ffe05a07a87834a6a2400792e52969813fa9660e313e1c9eda6b7" +checksum = "a82174e474e1f06418dd304f714785ae1c3e4d2fbff415a0d0c05a02cd995819" dependencies = [ "proc-macro2", "quote", @@ -7456,9 +7404,9 @@ dependencies = [ [[package]] name = "vstd" -version = "0.0.0-2026-02-22-0103" +version = "0.0.0-2026-05-10-0145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b74abf6d04d21b8898894960d11dbe2489ab48be1f24eca92821b865616910c" +checksum = "6958222eaf7bbe565c93890676bb1f96c4f5e25d90a81bb9993fdc19fc0d3f72" dependencies = [ "verus_builtin", "verus_builtin_macros", @@ -7514,11 +7462,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -7527,14 +7475,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -7545,23 +7493,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7569,9 +7513,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -7582,9 +7526,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -7606,7 +7550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.13.0", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -7630,17 +7574,17 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap 2.14.0", "semver", ] [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -7658,9 +7602,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -7687,7 +7631,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7835,15 +7779,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -7877,30 +7812,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows-threading" version = "0.2.1" @@ -7922,12 +7840,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -7940,12 +7852,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -7958,24 +7864,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -7988,12 +7882,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -8006,12 +7894,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -8024,12 +7906,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -8042,12 +7918,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.7.15" @@ -8059,9 +7929,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" dependencies = [ "memchr", ] @@ -8075,6 +7945,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -8094,7 +7970,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.13.0", + "indexmap 2.14.0", "prettyplease", "syn", "wasm-metadata", @@ -8124,8 +8000,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", - "indexmap 2.13.0", + "bitflags 2.11.1", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -8144,7 +8020,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "semver", "serde", @@ -8156,9 +8032,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wyz" @@ -8177,9 +8053,9 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -8188,9 +8064,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -8200,18 +8076,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -8220,18 +8096,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -8247,9 +8123,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -8258,9 +8134,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -8269,9 +8145,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index f5aea123..9fcc379c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.1.12" +version = "0.1.13" edition = "2024" repository = "https://github.com/XiangpengHao/liquid-cache" authors = ["XiangpengHao "] @@ -24,50 +24,50 @@ members = [ resolver = "3" [workspace.dependencies] -liquid-cache-datafusion-server = { path = "src/datafusion-server", version = "0.1.12" } -liquid-cache-datafusion-client = { path = "src/datafusion-client", version = "0.1.12" } -liquid-cache-datafusion = { path = "src/datafusion", version = "0.1.12" } -liquid-cache-common = { path = "src/common", version = "0.1.12" } -liquid-cache = { path = "src/core", version = "0.1.12" } -liquid-cache-datafusion-local = { path = "src/datafusion-local", version = "0.1.12" } -arrow = { version = "58.1.0", default-features = false, features = [ +liquid-cache-datafusion-server = { path = "src/datafusion-server", version = "0.1.13" } +liquid-cache-datafusion-client = { path = "src/datafusion-client", version = "0.1.13" } +liquid-cache-datafusion = { path = "src/datafusion", version = "0.1.13" } +liquid-cache-common = { path = "src/common", version = "0.1.13" } +liquid-cache = { path = "src/core", version = "0.1.13" } +liquid-cache-datafusion-local = { path = "src/datafusion-local", version = "0.1.13" } +arrow = { version = "58.3.0", default-features = false, features = [ "prettyprint", "ipc", ] } -arrow-flight = { version = "58.1.0", features = ["flight-sql-experimental"] } -arrow-schema = { version = "58.1.0", features = ["serde"] } -parquet = { version = "58.1.0", features = [ +arrow-flight = { version = "58.3.0", features = ["flight-sql-experimental"] } +arrow-schema = { version = "58.3.0", features = ["serde"] } +parquet = { version = "58.3.0", features = [ "async", "experimental", "variant_experimental", ] } -parquet-variant-json = { version = "58.1.0" } -parquet-variant-compute = { version = "58.1.0" } -datafusion = { version = "53.0.0" } -datafusion-common = { version = "53.0.0" } -datafusion-expr-common = { version = "53.0.0" } -datafusion-physical-expr = { version = "53.0.0" } -datafusion-physical-expr-common = { version = "53.0.0" } -datafusion-proto = { version = "53.0.0" } +parquet-variant-json = { version = "58.3.0" } +parquet-variant-compute = { version = "58.3.0" } +datafusion = { version = "54.0.0" } +datafusion-common = { version = "54.0.0" } +datafusion-expr-common = { version = "54.0.0" } +datafusion-physical-expr = { version = "54.0.0" } +datafusion-physical-expr-common = { version = "54.0.0" } +datafusion-proto = { version = "54.0.0" } async-trait = "0.1.89" futures = { version = "0.3.32", default-features = false, features = ["std"] } -tokio = { version = "1.50.0", features = ["rt-multi-thread"] } -log = "0.4.29" -tonic = { version = "0.14.5" } +tokio = { version = "1.52.3", features = ["rt-multi-thread"] } +log = "0.4.32" +tonic = { version = "0.14.6" } url = "2.5.8" bytes = { version = "1.11.1", default-features = false } ahash = "0.8.12" -prost = "0.14.3" +prost = "0.14.4" object_store = { version = "0.13.2", default-features = false } serde = { version = "1.0", default-features = false, features = ["derive"] } serde_json = { version = "1.0", default-features = false, features = ["std"] } tempfile = "3.27.0" -uuid = { version = "1.23.0", features = ["v4"] } +uuid = { version = "1.23.3", features = ["v4"] } fastrace = "0.7" fastrace-tonic = "0.2" congee = "0.4.1" insta = "1.47.2" -t4 = "0.1.3" +t4 = "0.1.7" [profile.dev.package] insta.opt-level = 3 diff --git a/README.md b/README.md index 3c53b70f..98dcfa52 100644 --- a/README.md +++ b/README.md @@ -88,15 +88,15 @@ tokio_test::block_on(async { }); ``` -## Development -See [dev/README.md](./dev/README.md) +## Performance troubleshooting -## Benchmark +### LiquidCache uses DIRECT I/O -See [benchmark/README.md](./benchmark/README.md) +By default, LiquidCache uses [DIRECT I/O](https://man7.org/linux/man-pages/man2/open.2.html#:~:text=O_DIRECT). This means that it bypasses the OS page cache, this avoids double-caching and bound memory usage. + +This also means LiquidCache can *appear slower* than other caches when most data fits in OS page cache, which is common in dev environments but unrealistic in production. -## Performance troubleshooting ### Use LiquidCache with DataFusion @@ -137,6 +137,14 @@ let (ctx, _) = LiquidCacheLocalBuilder::new() LiquidCache is optimized for x86-64 with specific [instructions](https://github.com/XiangpengHao/liquid-cache/blob/f8d5b77829fa7996a56c031eb25503f7b0b0428d/src/liquid_parquet/src/utils.rs#L229-L327). On ARM (e.g., Apple Silicon), fallback implementations are used. Contributions are welcome. +## Development + +See [dev/README.md](./dev/README.md) + +## Benchmark + +See [benchmark/README.md](./benchmark/README.md) + ## FAQ #### Can I use LiquidCache in production today? diff --git a/benchmark/Cargo.toml b/benchmark/Cargo.toml index 6180a248..25eb33de 100644 --- a/benchmark/Cargo.toml +++ b/benchmark/Cargo.toml @@ -16,9 +16,9 @@ tokio = { workspace = true } log = { workspace = true } arrow-flight = { workspace = true } tonic = { workspace = true } -clap = { version = "4.6.0", features = ["derive"] } +clap = { version = "4.6.1", features = ["derive"] } url = { workspace = true } -mimalloc = "0.1.48" +mimalloc = "0.1.52" serde_json.workspace = true serde.workspace = true sysinfo = { version = "0.38.4", default-features = false, features = [ @@ -35,12 +35,12 @@ opentelemetry = "0.31.0" opentelemetry_sdk = "0.31.0" opentelemetry-otlp = { version = "0.31.1", features = ["trace", "grpc-tonic"] } logforth = { version = "0.29.1", features = ["append-opentelemetry", "bridge-log"] } -reqwest = { version = "0.13.2", default-features = false, features = ["json"] } -uuid = { version = "1.23.0", features = ["v4"] } +reqwest = { version = "0.13.4", default-features = false, features = ["json"] } +uuid = { version = "1.23.3", features = ["v4"] } pprof = { version = "0.15.0", features = ["flamegraph"] } anyhow = "1.0" usdt = "0.6" -regex = "1.12.3" +regex = "1.12.4" perf-event2 = "0.7.4" [features] diff --git a/benchmark/README.md b/benchmark/README.md index 5225d0b2..056ec543 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -112,7 +112,7 @@ cargo run --release --bin clickbench_client -- --manifest benchmark/stackoverflo cargo run --release --bin in_process -- \ --manifest benchmark/stackoverflow/manifest.json \ --bench-mode liquid \ - --max-cache-mb 256 + --max-memory-mb 256 ``` ## In process mode diff --git a/benchmark/bench_server.rs b/benchmark/bench_server.rs index 3f772f37..fbccae3c 100644 --- a/benchmark/bench_server.rs +++ b/benchmark/bench_server.rs @@ -27,9 +27,9 @@ struct CliArgs { #[arg(long = "abort-on-panic")] abort_on_panic: bool, - /// Maximum cache size in MB - #[arg(long = "max-cache-mb")] - max_cache_mb: Option, + /// Maximum memory size in MB + #[arg(long = "max-memory-mb")] + max_memory_mb: Option, /// Path to disk cache directory #[arg(long = "disk-cache-dir")] @@ -56,7 +56,7 @@ async fn main() -> Result<(), Box> { args.jaeger_endpoint.as_deref(), ); - let max_cache_bytes = args.max_cache_mb.map(|size| size * 1024 * 1024); + let max_memory_bytes = args.max_memory_mb.map(|size| size * 1024 * 1024); if args.abort_on_panic { // Be loud and crash loudly if any thread panics. @@ -73,7 +73,7 @@ async fn main() -> Result<(), Box> { let ctx = LiquidCacheService::context()?; let liquid_cache_datafusion_server = LiquidCacheService::new( ctx, - max_cache_bytes, + max_memory_bytes, args.disk_cache_dir.clone(), Box::new(LiquidPolicy::new()), squeeze_policy, diff --git a/benchmark/in_process.rs b/benchmark/in_process.rs index b85f80a6..4f123a76 100644 --- a/benchmark/in_process.rs +++ b/benchmark/in_process.rs @@ -46,9 +46,9 @@ struct InProcessBenchmark { #[arg(long)] pub partitions: Option, - /// Maximum cache size in bytes - #[arg(long = "max-cache-mb")] - pub max_cache_mb: Option, + /// Maximum memory size in MB + #[arg(long = "max-memory-mb")] + pub max_memory_mb: Option, /// Directory to write flamegraph SVG files to #[arg(long = "flamegraph-dir")] @@ -78,7 +78,7 @@ impl InProcessBenchmark { .with_reset_cache(self.reset_cache) .with_perf_events(self.perf_events) .with_partitions(self.partitions) - .with_max_cache_mb(self.max_cache_mb) + .with_max_memory_mb(self.max_memory_mb) .with_flamegraph_dir(self.flamegraph_dir.clone()) .with_cache_dir(self.cache_dir.clone()) .with_query_filter(self.query_index) diff --git a/benchmark/src/inprocess_runner.rs b/benchmark/src/inprocess_runner.rs index 31191a52..4ef6f57d 100644 --- a/benchmark/src/inprocess_runner.rs +++ b/benchmark/src/inprocess_runner.rs @@ -194,7 +194,7 @@ pub struct InProcessBenchmarkRunner { pub iteration: u32, pub reset_cache: bool, pub partitions: Option, - pub max_cache_mb: Option, + pub max_memory_mb: Option, pub flamegraph_dir: Option, pub query_filter: Option, pub cache_dir: Option, @@ -215,7 +215,7 @@ impl InProcessBenchmarkRunner { iteration: 3, reset_cache: false, partitions: None, - max_cache_mb: None, + max_memory_mb: None, flamegraph_dir: None, query_filter: None, cache_dir: None, @@ -249,8 +249,8 @@ impl InProcessBenchmarkRunner { self } - pub fn with_max_cache_mb(mut self, max_cache_mb: Option) -> Self { - self.max_cache_mb = max_cache_mb; + pub fn with_max_memory_mb(mut self, max_memory_mb: Option) -> Self { + self.max_memory_mb = max_memory_mb; self } @@ -294,7 +294,7 @@ impl InProcessBenchmarkRunner { } let cache_size = self - .max_cache_mb + .max_memory_mb .map(|size| size * 1024 * 1024) .unwrap_or(usize::MAX); @@ -323,7 +323,7 @@ impl InProcessBenchmarkRunner { } InProcessBenchmarkMode::Arrow => { let v = LiquidCacheLocalBuilder::new() - .with_max_cache_bytes(cache_size) + .with_max_memory_bytes(cache_size) .with_cache_dir(cache_dir) .with_cache_policy(Box::new(LiquidPolicy::new())) .with_hydration_policy(Box::new(NoHydration::new())) @@ -334,19 +334,18 @@ impl InProcessBenchmarkRunner { } InProcessBenchmarkMode::Liquid => { let v = LiquidCacheLocalBuilder::new() - .with_max_cache_bytes(cache_size) + .with_max_memory_bytes(cache_size) .with_cache_dir(cache_dir) .with_cache_policy(Box::new(LiquidPolicy::new())) .with_hydration_policy(Box::new(NoHydration::new())) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) - .with_eager_shredding(true) .build(session_config) .await?; (v.0, Some(v.1)) } InProcessBenchmarkMode::LiquidNoSqueeze => { let v = LiquidCacheLocalBuilder::new() - .with_max_cache_bytes(cache_size) + .with_max_memory_bytes(cache_size) .with_cache_dir(cache_dir) .with_cache_policy(Box::new(LiquidPolicy::new())) .with_hydration_policy(Box::new(NoHydration::new())) diff --git a/benchmark/src/lib.rs b/benchmark/src/lib.rs index d4e4440b..88f28a97 100644 --- a/benchmark/src/lib.rs +++ b/benchmark/src/lib.rs @@ -229,8 +229,16 @@ impl ClientBenchmarkArgs { .send() .await .unwrap(); - let v = response.json::().await.unwrap(); - metrics.push(v); + // The endpoint returns `null` when a registered plan recorded no + // metrics (e.g. a sub-plan that scanned no data), so decode as an + // Option and skip the empty ones rather than failing to deserialize. + let v = response + .json::>() + .await + .unwrap(); + if let Some(v) = v { + metrics.push(v); + } } let metric = metrics .iter() diff --git a/benchmark/src/observability.rs b/benchmark/src/observability.rs index 500bf176..d33d3d45 100644 --- a/benchmark/src/observability.rs +++ b/benchmark/src/observability.rs @@ -22,18 +22,16 @@ pub fn instrument_liquid_source_with_span( ) -> Arc { let rewritten = plan .transform_up(|node| { - let Some(data_source) = node.as_any().downcast_ref::() else { + let Some(data_source) = node.downcast_ref::() else { return Ok(Transformed::no(node)); }; let file_scan_config = data_source .data_source() - .as_any() .downcast_ref::() .expect("FileScanConfig not found"); let mut new_config = file_scan_config.clone(); let Some(liquid_source) = file_scan_config .file_source() - .as_any() .downcast_ref::() else { return Ok(Transformed::no(node)); diff --git a/benchmark/src/utils.rs b/benchmark/src/utils.rs index e047b9db..feb6d2b8 100644 --- a/benchmark/src/utils.rs +++ b/benchmark/src/utils.rs @@ -20,7 +20,7 @@ use uuid::Uuid; pub(crate) fn get_plan_uuids(plan: &Arc) -> Vec { let mut uuids = Vec::new(); plan.apply(|plan| { - if let Some(plan) = plan.as_any().downcast_ref::() { + if let Some(plan) = plan.downcast_ref::() { uuids.push(plan.get_uuid()); } Ok(datafusion::common::tree_node::TreeNodeRecursion::Continue) diff --git a/dev/README.md b/dev/README.md index 956007c1..72aa3c03 100644 --- a/dev/README.md +++ b/dev/README.md @@ -59,7 +59,7 @@ sudo bpftrace -e ' delete(@t[args->user_data]); } ' \ --c 'target/release/in_process --manifest benchmark/clickbench/manifest.json --bench-mode liquid-no-squeeze --max-cache-mb 128 --query-index 20 --io-mode uring' +-c 'target/release/in_process --manifest benchmark/clickbench/manifest.json --bench-mode liquid-no-squeeze --max-memory-mb 128 --query-index 20 --io-mode uring' ``` This will trace the execution of `iteration = 2` (`arg1 == 2`) and print the `io_uring` latency in us (from submission to completion) histogram: ``` @@ -90,7 +90,7 @@ tracepoint:syscalls:sys_exit_read /@go==1 && @s[tid]/ { @r = hist((nsecs-@s[ tracepoint:syscalls:sys_exit_pread64 /@go==1 && @s[tid]/ { @r = hist((nsecs-@s[tid])/1000); delete(@s[tid]); } tracepoint:syscalls:sys_exit_write /@go==1 && @s[tid]/ { @w = hist((nsecs-@s[tid])/1000); delete(@s[tid]); } tracepoint:syscalls:sys_exit_pwrite64 /@go==1 && @s[tid]/ { @w = hist((nsecs-@s[tid])/1000); delete(@s[tid]); } -' -c 'target/release/in_process --manifest benchmark/clickbench/manifest.json --bench-mode liquid-no-squeeze --max-cache-mb 128 --query-index 20 --io-mode std-blocking' +' -c 'target/release/in_process --manifest benchmark/clickbench/manifest.json --bench-mode liquid-no-squeeze --max-memory-mb 128 --query-index 20 --io-mode std-blocking' ``` It will generate: diff --git a/dev/dev-tools/Cargo.toml b/dev/dev-tools/Cargo.toml index 75502557..879412d3 100644 --- a/dev/dev-tools/Cargo.toml +++ b/dev/dev-tools/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "dev-tools" -version = "0.1.2" +version = "0.1.3" authors = ["XiangpengHao "] edition = "2024" [dependencies] -dioxus = { version = "0.7.4", features = ["router", "fullstack"] } +dioxus = { version = "=0.7.5", features = ["router", "fullstack"] } [features] default = ["web"] diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 7903fd7a..36b1704e 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -4,35 +4,21 @@ edition = { workspace = true } publish = false [[bin]] -name = "example_server" -path = "example_server.rs" +name = "datafusion-local" +path = "datafusion-local.rs" [[bin]] -name = "example_client" -path = "example_client.rs" +name = "datafusion-client-server" +path = "datafusion-client-server.rs" [[bin]] -name = "example_local" -path = "example_local.rs" - -[[bin]] -name = "example_projection_pushdown" -path = "example_projection_pushdown.rs" - -[[bin]] -name = "example_inprocess_insertion" -path = "example_inprocess_insertion.rs" - -[[bin]] -name = "example_inprocess_cache_eviction" -path = "example_inprocess_cache_eviction.rs" - -[[bin]] -name = "example_inprocess_read" -path = "example_inprocess_read.rs" +name = "core" +path = "core.rs" [dependencies] +arrow = { workspace = true } datafusion = { workspace = true } +liquid-cache = { workspace = true } liquid-cache-datafusion-server = { workspace = true } liquid-cache-datafusion-client = { workspace = true } liquid-cache-datafusion-local = { workspace = true } @@ -41,5 +27,5 @@ arrow-flight = { workspace = true } tonic = { workspace = true } url = { workspace = true } tempfile = "3.27.0" -clap = { version = "4.6.0", features = ["derive"] } +clap = { version = "4.6.1", features = ["derive"] } object_store = { workspace = true, features = ["http"] } diff --git a/examples/README.md b/examples/README.md index 3c895f8b..2de328c5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,13 +1,35 @@ -DataFusion Cache examples +LiquidCache examples -## Server +## DataFusion (in-process) + +Query a local parquet file through `LiquidCacheLocalBuilder`. + +```bash +cargo run --bin datafusion-local +``` + +## DataFusion (client/server) + +A single binary that runs as either the Flight cache server or a DataFusion client. + +Start the server: ```bash -cargo run --bin example_server +cargo run --bin datafusion-client-server -- --mode server ``` -## Client +In another shell, run the client: + +```bash +cargo run --bin datafusion-client-server -- --mode client +``` + +Use `--query` and `--file` to customize the SQL and remote parquet URL. + +## Core (no DataFusion) + +Use the raw `LiquidCacheBuilder` storage API to insert, flush, and read an Arrow array. ```bash -cargo run --bin example_client +cargo run --bin core ``` diff --git a/examples/core.rs b/examples/core.rs new file mode 100644 index 00000000..d172f601 --- /dev/null +++ b/examples/core.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use arrow::array::UInt64Array; +use liquid_cache::cache::{ + AlwaysHydrate, EntryID, LiquidCacheBuilder, LiquidPolicy, TranscodeSqueezeEvict, +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let storage = LiquidCacheBuilder::new() + .with_max_memory_bytes(1024 * 1024 * 1024) // 1GB + .with_max_disk_bytes(1024 * 1024 * 1024 * 10) // 10GB + .with_batch_size(8192) + .with_cache_policy(Box::new(LiquidPolicy::new())) + .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) + .with_hydration_policy(Box::new(AlwaysHydrate::new())) + .build() + .await; + + let entry_id = EntryID::from(7); + let arrow_array = Arc::new(UInt64Array::from_iter_values(0..16)); + storage.insert(entry_id, arrow_array.clone()).await.unwrap(); + + // Move data to disk so the read demonstrates async I/O + storage.flush_all_to_disk().await.unwrap(); + + let retrieved = storage.get(&entry_id).await.unwrap(); + assert_eq!(retrieved.as_ref(), arrow_array.as_ref()); + + Ok(()) +} diff --git a/examples/datafusion-client-server.rs b/examples/datafusion-client-server.rs new file mode 100644 index 00000000..03b5f54b --- /dev/null +++ b/examples/datafusion-client-server.rs @@ -0,0 +1,101 @@ +use arrow_flight::flight_service_server::FlightServiceServer; +use clap::{Parser, ValueEnum}; +use datafusion::{error::Result, execution::object_store::ObjectStoreUrl, prelude::*}; +use liquid_cache_datafusion_client::LiquidCacheClientBuilder; +use liquid_cache_datafusion_local::storage::cache::squeeze_policies::TranscodeSqueezeEvict; +use liquid_cache_datafusion_local::storage::cache::{AlwaysHydrate, LiquidPolicy}; +use liquid_cache_datafusion_server::LiquidCacheService; +use std::path::Path; +use std::sync::Arc; +use tonic::transport::Server; +use url::Url; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] +enum Mode { + Server, + Client, +} + +#[derive(Parser, Clone)] +#[command(name = "Example Client/Server")] +struct CliArgs { + /// Run as server or client + #[arg(long, value_enum, default_value_t = Mode::Server)] + mode: Mode, + + /// SQL query to execute (client mode) + #[arg( + long, + default_value = "SELECT COUNT(*) FROM \"aws-edge-locations\" WHERE \"countryCode\" = 'US';" + )] + query: String, + + /// URL of the table to query (client mode) + #[arg( + long, + default_value = "https://raw.githubusercontent.com/tobilg/aws-edge-locations/main/data/aws-edge-locations.parquet" + )] + file: String, + + /// Server address (host:port for server mode, URL for client mode) + #[arg(long, default_value = "http://localhost:15214")] + cache_server: String, +} + +#[tokio::main] +async fn main() -> std::result::Result<(), Box> { + let args = CliArgs::parse(); + match args.mode { + Mode::Server => run_server().await, + Mode::Client => run_client(args).await.map_err(Into::into), + } +} + +async fn run_server() -> std::result::Result<(), Box> { + let liquid_cache = LiquidCacheService::new( + SessionContext::new(), + Some(1024 * 1024 * 1024), // max memory size 1GB + Some(tempfile::tempdir()?.keep()), // disk cache dir + Box::new(LiquidPolicy::new()), + Box::new(TranscodeSqueezeEvict), + Box::new(AlwaysHydrate::new()), + ) + .await?; + + let flight = FlightServiceServer::new(liquid_cache); + + Server::builder() + .add_service(flight) + .serve("0.0.0.0:15214".parse()?) + .await?; + + Ok(()) +} + +async fn run_client(args: CliArgs) -> Result<()> { + let url = Url::parse(&args.file).unwrap(); + let object_store_url = format!("{}://{}", url.scheme(), url.host_str().unwrap_or_default()); + + let ctx = LiquidCacheClientBuilder::new(args.cache_server.clone()) + .with_object_store(ObjectStoreUrl::parse(object_store_url.as_str())?, None) + .build(SessionConfig::from_env()?)?; + let ctx = Arc::new(ctx); + + let table_name = Path::new(url.path()) + .file_stem() + .unwrap_or_default() + .to_str() + .unwrap_or("default"); + let object_store = object_store::http::HttpBuilder::new() + .with_url(object_store_url.as_str()) + .build() + .unwrap(); + let object_store_url = ObjectStoreUrl::parse(object_store_url.as_str()).unwrap(); + ctx.register_object_store(object_store_url.as_ref(), Arc::new(object_store)); + ctx.register_parquet(table_name, url.as_ref(), Default::default()) + .await?; + + ctx.sql(&args.query).await?.show().await?; + + Ok(()) +} diff --git a/examples/example_local.rs b/examples/datafusion-local.rs similarity index 87% rename from examples/example_local.rs rename to examples/datafusion-local.rs index d060703c..1908ad81 100644 --- a/examples/example_local.rs +++ b/examples/datafusion-local.rs @@ -9,7 +9,8 @@ async fn main() -> Result<(), Box> { let temp_dir = TempDir::new().unwrap(); let (ctx, _) = LiquidCacheLocalBuilder::new() - .with_max_cache_bytes(1024 * 1024 * 1024) // 1GB + .with_max_memory_bytes(1024 * 1024 * 1024) // 1GB + .with_max_disk_bytes(1024 * 1024 * 1024 * 10) // 10GB .with_cache_dir(temp_dir.path().to_path_buf()) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) .with_cache_policy(Box::new(LiquidPolicy::new())) diff --git a/examples/example_client.rs b/examples/example_client.rs deleted file mode 100644 index 554a9c09..00000000 --- a/examples/example_client.rs +++ /dev/null @@ -1,59 +0,0 @@ -use clap::Parser; -use datafusion::{error::Result, execution::object_store::ObjectStoreUrl, prelude::*}; -use liquid_cache_datafusion_client::LiquidCacheClientBuilder; -use std::path::Path; -use std::sync::Arc; -use url::Url; - -#[derive(Parser, Clone)] -#[command(name = "Example Client")] -struct CliArgs { - /// SQL query to execute - #[arg( - long, - default_value = "SELECT COUNT(*) FROM \"aws-edge-locations\" WHERE \"countryCode\" = 'US';" - )] - query: String, - - /// URL of the table to query - #[arg( - long, - default_value = "https://raw.githubusercontent.com/tobilg/aws-edge-locations/main/data/aws-edge-locations.parquet" - )] - file: String, - - /// Server URL - #[arg(long, default_value = "http://localhost:15214")] - cache_server: String, -} - -#[tokio::main] -pub async fn main() -> Result<()> { - let args = CliArgs::parse(); - let url = Url::parse(&args.file).unwrap(); - let object_store_url = format!("{}://{}", url.scheme(), url.host_str().unwrap_or_default()); - - let ctx = LiquidCacheClientBuilder::new(args.cache_server.clone()) - .with_object_store(ObjectStoreUrl::parse(object_store_url.as_str())?, None) - .build(SessionConfig::from_env()?)?; - let ctx = Arc::new(ctx); - - let table_name = Path::new(url.path()) - .file_stem() - .unwrap_or_default() - .to_str() - .unwrap_or("default"); - let sql = args.query; - let object_store = object_store::http::HttpBuilder::new() - .with_url(object_store_url.as_str()) - .build() - .unwrap(); - let object_store_url = ObjectStoreUrl::parse(object_store_url.as_str()).unwrap(); - ctx.register_object_store(object_store_url.as_ref(), Arc::new(object_store)); - ctx.register_parquet(table_name, url.as_ref(), Default::default()) - .await?; - - ctx.sql(&sql).await?.show().await?; - - Ok(()) -} diff --git a/examples/example_inprocess_cache_eviction.rs b/examples/example_inprocess_cache_eviction.rs deleted file mode 100644 index 8f3c493a..00000000 --- a/examples/example_inprocess_cache_eviction.rs +++ /dev/null @@ -1,39 +0,0 @@ -use datafusion::{arrow::array::UInt64Array, prelude::SessionConfig}; -use liquid_cache_datafusion_local::{ - LiquidCacheLocalBuilder, - storage::{ - cache::{EntryID, squeeze_policies::TranscodeSqueezeEvict}, - cache_policies::LiquidPolicy, - }, -}; -use std::sync::Arc; -use tempfile::TempDir; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let temp_dir = TempDir::new().unwrap(); - - println!("{:?}", temp_dir); - - let (_ctx, storage) = LiquidCacheLocalBuilder::new() - .with_max_cache_bytes(1024 * 1024) // 1MB - .with_cache_dir(temp_dir.path().to_path_buf()) - .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) - .with_cache_policy(Box::new(LiquidPolicy::new())) - .build(SessionConfig::new()) - .await?; - - for i in 1..725 { - let entry_id = EntryID::from(i); - let arrow_array = Arc::new(UInt64Array::from_iter_values(0..1000)); - // Insert once; replacement/placement is handled by the cache policy - storage - .storage() - .insert(entry_id, arrow_array.clone()) - .await; - let _ = storage.storage().get(&entry_id).await.unwrap(); - } - println!("{:?}", storage.storage().stats()); - - Ok(()) -} diff --git a/examples/example_inprocess_insertion.rs b/examples/example_inprocess_insertion.rs deleted file mode 100644 index 7605d50f..00000000 --- a/examples/example_inprocess_insertion.rs +++ /dev/null @@ -1,36 +0,0 @@ -use datafusion::{arrow::array::UInt64Array, prelude::SessionConfig}; -use liquid_cache_datafusion_local::{ - LiquidCacheLocalBuilder, - storage::{ - cache::{EntryID, squeeze_policies::TranscodeSqueezeEvict}, - cache_policies::LiquidPolicy, - }, -}; -use std::sync::Arc; -use tempfile::TempDir; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let temp_dir = TempDir::new().unwrap(); - - let (_ctx, storage) = LiquidCacheLocalBuilder::new() - .with_max_cache_bytes(1024 * 1024) // 1MB - .with_cache_dir(temp_dir.path().to_path_buf()) - .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) - .with_cache_policy(Box::new(LiquidPolicy::new())) - .build(SessionConfig::new()) - .await?; - - let entry_id = EntryID::from(42); - let arrow_array = Arc::new(UInt64Array::from_iter_values(0..1000)); - - // Insert once; replacement/placement is handled by the cache policy - storage - .storage() - .insert(entry_id, arrow_array.clone()) - .await; - - assert!(storage.storage().is_cached(&entry_id)); - - Ok(()) -} diff --git a/examples/example_inprocess_read.rs b/examples/example_inprocess_read.rs deleted file mode 100644 index f2b874ed..00000000 --- a/examples/example_inprocess_read.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::sync::Arc; - -use datafusion::arrow::array::UInt64Array; -use liquid_cache_datafusion_local::storage::cache::{EntryID, LiquidCacheBuilder}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let storage = LiquidCacheBuilder::new().build().await; - - let entry_id = EntryID::from(7); - let arrow_array = Arc::new(UInt64Array::from_iter_values(0..16)); - storage.insert(entry_id, arrow_array.clone()).await; - - // Move data to disk so the read will demonstrate async I/O - storage.flush_all_to_disk().await; - - // Read asynchronously - let retrieved = storage.get(&entry_id).await.unwrap(); - assert_eq!(retrieved.as_ref(), arrow_array.as_ref()); - - Ok(()) -} diff --git a/examples/example_projection_pushdown.rs b/examples/example_projection_pushdown.rs deleted file mode 100644 index bd66c315..00000000 --- a/examples/example_projection_pushdown.rs +++ /dev/null @@ -1,59 +0,0 @@ -use clap::Parser; -use datafusion::{error::Result, execution::object_store::ObjectStoreUrl, prelude::*}; -use liquid_cache_datafusion_client::LiquidCacheClientBuilder; -use std::path::Path; -use std::sync::Arc; -use url::Url; - -#[derive(Parser, Clone)] -#[command(name = "Example Selection PushDown")] -struct CliArgs { - /// SQL query to execute - #[arg( - long, - default_value = "SELECT city, country FROM \"aws-edge-locations\" WHERE \"countryCode\" = 'US';" - )] - query: String, - - /// URL of the table to query - #[arg( - long, - default_value = "https://raw.githubusercontent.com/tobilg/aws-edge-locations/main/data/aws-edge-locations.parquet" - )] - file: String, - - /// Server URL - #[arg(long, default_value = "http://localhost:15214")] - cache_server: String, -} - -#[tokio::main] -pub async fn main() -> Result<()> { - let args = CliArgs::parse(); - let url = Url::parse(&args.file).unwrap(); - let object_store_url = format!("{}://{}", url.scheme(), url.host_str().unwrap_or_default()); - - let ctx = LiquidCacheClientBuilder::new(args.cache_server.clone()) - .with_object_store(ObjectStoreUrl::parse(object_store_url.as_str())?, None) - .build(SessionConfig::from_env()?)?; - let ctx = Arc::new(ctx); - - let table_name = Path::new(url.path()) - .file_stem() - .unwrap_or_default() - .to_str() - .unwrap_or("default"); - let sql = args.query; - let object_store = object_store::http::HttpBuilder::new() - .with_url(object_store_url.as_str()) - .build() - .unwrap(); - let object_store_url = ObjectStoreUrl::parse(object_store_url.as_str()).unwrap(); - ctx.register_object_store(object_store_url.as_ref(), Arc::new(object_store)); - ctx.register_parquet(table_name, url.as_ref(), Default::default()) - .await?; - - ctx.sql(&sql).await?.show().await?; - - Ok(()) -} diff --git a/examples/example_server.rs b/examples/example_server.rs deleted file mode 100644 index f6b15303..00000000 --- a/examples/example_server.rs +++ /dev/null @@ -1,29 +0,0 @@ -use arrow_flight::flight_service_server::FlightServiceServer; -use datafusion::prelude::SessionContext; -use liquid_cache_datafusion_local::storage::cache::AlwaysHydrate; -use liquid_cache_datafusion_local::storage::cache::squeeze_policies::TranscodeSqueezeEvict; -use liquid_cache_datafusion_server::LiquidCacheService; -use liquid_cache_datafusion_server::storage::cache_policies::LruPolicy; -use tonic::transport::Server; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let liquid_cache = LiquidCacheService::new( - SessionContext::new(), - Some(1024 * 1024 * 1024), // max memory cache size 1GB - Some(tempfile::tempdir()?.keep()), // disk cache dir - Box::new(LruPolicy::new()), - Box::new(TranscodeSqueezeEvict), - Box::new(AlwaysHydrate::new()), - ) - .await?; - - let flight = FlightServiceServer::new(liquid_cache); - - Server::builder() - .add_service(flight) - .serve("0.0.0.0:15214".parse()?) - .await?; - - Ok(()) -} diff --git a/flake.lock b/flake.lock index 6aedd304..91d09a14 100644 --- a/flake.lock +++ b/flake.lock @@ -1,20 +1,5 @@ { "nodes": { - "crane": { - "locked": { - "lastModified": 1774313767, - "narHash": "sha256-hy0XTQND6avzGEUFrJtYBBpFa/POiiaGBr2vpU6Y9tY=", - "owner": "ipetkov", - "repo": "crane", - "rev": "3d9df76e29656c679c744968b17fbaf28f0e923d", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, "flake-utils": { "inputs": { "systems": "systems" @@ -35,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1775036866, - "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", + "lastModified": 1777268161, + "narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", + "rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76", "type": "github" }, "original": { @@ -67,7 +52,6 @@ }, "root": { "inputs": { - "crane": "crane", "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay" @@ -78,11 +62,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1775099554, - "narHash": "sha256-3xBsGnGDLOFtnPZ1D3j2LU19wpAlYefRKTlkv648rU0=", + "lastModified": 1777432579, + "narHash": "sha256-Ce11TStDsqCge2vAAfLKe2+4lDI5cSX5ZYZOuKJBKKQ=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "8d6387ed6d8e6e6672fd3ed4b61b59d44b124d99", + "rev": "3ecb5e6ab380ced3272ef7fcfe398bffbcc0f152", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 327fbe8f..ef8360ae 100644 --- a/flake.nix +++ b/flake.nix @@ -5,14 +5,12 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; rust-overlay.url = "github:oxalica/rust-overlay"; flake-utils.url = "github:numtide/flake-utils"; - crane.url = "github:ipetkov/crane"; }; outputs = { nixpkgs , rust-overlay , flake-utils - , crane , ... }: flake-utils.lib.eachDefaultSystem ( @@ -22,26 +20,14 @@ pkgs = import nixpkgs { inherit system overlays; }; - craneLib = crane.mkLib pkgs; - kaniVerifier = craneLib.buildPackage { - pname = "kani-verifier"; - version = "0.67.0"; - src = craneLib.downloadCargoPackage { - name = "kani-verifier"; - version = "0.67.0"; - source = "registry+https://github.com/rust-lang/crates.io-index"; - checksum = "sha256-1iJafsEwN+mE9r692jPTQ5DmQ6HNKkUiy11ejm7YXis="; - }; - doCheck = false; - }; # Fetch daisyUI bundle files daisyui-bundle = pkgs.fetchurl { - url = "https://github.com/saadeghi/daisyui/releases/latest/download/daisyui.mjs"; - sha256 = "sha256-dH6epo+aSV+eeh3uQbxd7MkWlG+6hCaGaknQ4Bnljj4="; + url = "https://github.com/saadeghi/daisyui/releases/download/v5.5.19/daisyui.mjs"; + sha256 = "sha256-X+Q/9eg8XPUZzMMtdqoagu1r/FDuPm9dxgB+6mI5rx8="; }; daisyui-theme-bundle = pkgs.fetchurl { - url = "https://github.com/saadeghi/daisyui/releases/latest/download/daisyui-theme.mjs"; - sha256 = "sha256-iiUODarjHRxAD+tyOPh95xhHJELC40oczt+dsDo86yE="; + url = "https://github.com/saadeghi/daisyui/releases/download/v5.5.19/daisyui-theme.mjs"; + sha256 = "sha256-tAcb7y5ZvYNQllnB5ybMGXBKH9FP8uVtR5vBampT8m0="; }; in { @@ -52,7 +38,6 @@ pkg-config eza fd - kaniVerifier llvmPackages.bintools lldb cargo-fuzz @@ -64,7 +49,7 @@ nodejs tailwindcss_4 dioxus-cli - wasm-bindgen-cli_0_2_108 + wasm-bindgen-cli_0_2_118 binaryen (rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override { extensions = [ "rust-src" "llvm-tools-preview" ]; diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 2082772f..1ae1a25c 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "liquid-cache-fuzz" -version = "0.1.12" +version = "0.1.13" publish = false edition = "2024" diff --git a/src/common/Cargo.toml b/src/common/Cargo.toml index d69c270a..582f4bc9 100644 --- a/src/common/Cargo.toml +++ b/src/common/Cargo.toml @@ -11,7 +11,7 @@ repository = { workspace = true } arrow-flight = { workspace = true } async-trait = { workspace = true } bytes = { workspace = true } -chrono = "0.4.44" +chrono = "0.4.45" futures = { workspace = true } object_store = { workspace = true } prost = { workspace = true } diff --git a/src/common/src/rpc.rs b/src/common/src/rpc.rs index f9a9929b..943c4d4a 100644 --- a/src/common/src/rpc.rs +++ b/src/common/src/rpc.rs @@ -65,6 +65,24 @@ impl From for LiquidCacheActions { } } +/// A typed squeeze hint for one file-schema column, shipped alongside a plan. +/// +/// The cache server cannot re-derive squeeze hints for lineage that lives only +/// in the client-side part of the plan (e.g. a `date_part` projection above the +/// pushed-down scan), so the client derives them from the full physical plan and +/// ships them here. `hint` is the canonical encoding produced by +/// `CacheExpression::to_metadata_value`. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ColumnSqueezeHint { + /// File-schema column name the hint applies to. + #[prost(string, tag = "1")] + pub column: ::prost::alloc::string::String, + + /// Canonical `CacheExpression` encoding. + #[prost(string, tag = "2")] + pub hint: ::prost::alloc::string::String, +} + #[derive(Clone, PartialEq, ::prost::Message)] pub struct RegisterPlanRequest { #[prost(bytes, tag = "1")] @@ -72,6 +90,11 @@ pub struct RegisterPlanRequest { #[prost(bytes, tag = "2")] pub handle: Bytes, + + /// Typed squeeze hints for the (single) scan in this plan fragment, derived + /// by the client from the full physical plan. + #[prost(message, repeated, tag = "3")] + pub squeeze_hints: ::prost::alloc::vec::Vec, } impl ProstMessageExt for RegisterPlanRequest { diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml index c7a6cca3..d8e2b9f4 100644 --- a/src/core/Cargo.toml +++ b/src/core/Cargo.toml @@ -7,10 +7,6 @@ readme = "README.md" description = { workspace = true } repository = { workspace = true } -[package.metadata.cargo-shear] -ignored = ["kani-verifier"] - - [dependencies] async-stream = "0.3.6" async-trait = { workspace = true } @@ -25,9 +21,9 @@ datafusion-physical-expr = { workspace = true } datafusion-physical-expr-common = { workspace = true } arrow = { workspace = true } arrow-schema = { workspace = true } -fastlanes = "0.5.0" +fastlanes = "0.5.1" num-traits = "0.2.19" -fsst-rs = "0.5.9" +fsst-rs = "0.5.11" ahash = { workspace = true } tempfile = { workspace = true } congee = { workspace = true } @@ -37,29 +33,23 @@ parquet = { workspace = true } parquet-variant-compute = { workspace = true } fastrace = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } +sysinfo = { version = "0.38.4", default-features = false, features = ["system"] } [dev-dependencies] tempfile = { workspace = true } -shuttle = "0.8.1" +shuttle = "0.9.1" tokio-test = "0.4" tracing-subscriber = "0.3.23" -rand = "0.10.0" +rand = "0.10.1" serde.workspace = true serde_json = { workspace = true } -mimalloc = "0.1.48" -clap = { version = "4.6.0", features = ["derive"] } +mimalloc = "0.1.52" +clap = { version = "4.6.1", features = ["derive"] } divan = "0.1.21" -kani-verifier = "0.67.0" insta = { workspace = true } datafusion = { workspace = true } -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = [ - 'cfg(rust_analyzer)', - 'cfg(kani)', -] } - - [features] shuttle = ["t4/shuttle"] diff --git a/src/core/src/cache/budget.rs b/src/core/src/cache/budget.rs index 921b388f..2b7bc5d0 100644 --- a/src/core/src/cache/budget.rs +++ b/src/core/src/cache/budget.rs @@ -1,18 +1,30 @@ -use crate::sync::atomic::{AtomicUsize, Ordering}; +use super::observer::Observer; +use crate::sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, +}; #[derive(Debug)] pub struct BudgetAccounting { max_memory_bytes: usize, + max_disk_bytes: usize, used_memory_bytes: AtomicUsize, used_disk_bytes: AtomicUsize, + observer: Arc, } impl BudgetAccounting { - pub(super) fn new(max_memory_bytes: usize) -> Self { + pub(super) fn new( + max_memory_bytes: usize, + max_disk_bytes: usize, + observer: Arc, + ) -> Self { Self { max_memory_bytes, + max_disk_bytes, used_memory_bytes: AtomicUsize::new(0), used_disk_bytes: AtomicUsize::new(0), + observer, } } @@ -21,8 +33,8 @@ impl BudgetAccounting { self.used_disk_bytes.store(0, Ordering::Relaxed); } - /// Try to reserve space in the cache. - /// Returns ok if the space was reserved, err if the cache is full. + /// Try to reserve memory in the cache. + /// Returns ok if the memory was reserved, err if the memory budget is full. pub(super) fn try_reserve_memory(&self, request_bytes: usize) -> Result<(), ()> { let used = self.used_memory_bytes.load(Ordering::Relaxed); if used + request_bytes > self.max_memory_bytes { @@ -40,8 +52,8 @@ impl BudgetAccounting { } } - /// Adjust the cache size after transcoding. - /// Returns true if the size was adjusted, false if the cache is full, when new_size is larger than old_size. + /// Adjust memory usage after transcoding. + /// Returns ok if the usage was adjusted, err if the memory budget is full when new_size is larger than old_size. pub(super) fn try_update_memory_usage( &self, old_size: usize, @@ -66,8 +78,26 @@ impl BudgetAccounting { self.used_disk_bytes.load(Ordering::Relaxed) } - pub fn add_used_disk_bytes(&self, bytes: usize) { - self.used_disk_bytes.fetch_add(bytes, Ordering::Relaxed); + pub(super) fn try_reserve_disk(&self, request_bytes: usize) -> Result<(), ()> { + let used = self.used_disk_bytes.load(Ordering::Relaxed); + if used + request_bytes > self.max_disk_bytes { + self.observer.on_disk_reservation_failure(); + return Err(()); + } + + match self.used_disk_bytes.compare_exchange( + used, + used + request_bytes, + Ordering::Relaxed, + Ordering::Relaxed, + ) { + Ok(_) => Ok(()), + Err(_) => self.try_reserve_disk(request_bytes), + } + } + + pub(super) fn release_disk(&self, bytes: usize) { + self.used_disk_bytes.fetch_sub(bytes, Ordering::Relaxed); } } @@ -76,9 +106,13 @@ mod tests { use super::*; use crate::sync::{Arc, Barrier, thread}; + fn test_budget(max_memory_bytes: usize, max_disk_bytes: usize) -> BudgetAccounting { + BudgetAccounting::new(max_memory_bytes, max_disk_bytes, Arc::new(Observer::new())) + } + #[test] fn test_memory_reservation_and_accounting() { - let config = BudgetAccounting::new(1000); + let config = test_budget(1000, usize::MAX); assert_eq!(config.memory_usage_bytes(), 0); @@ -111,7 +145,7 @@ mod tests { let max_memory = 10000; let operations_per_thread = 100; - let budget = Arc::new(BudgetAccounting::new(max_memory)); + let budget = Arc::new(test_budget(max_memory, usize::MAX)); let barrier = Arc::new(Barrier::new(num_threads)); let mut thread_handles = vec![]; @@ -165,4 +199,22 @@ mod tests { assert_eq!(budget.memory_usage_bytes(), expected_memory_usage); assert!(budget.memory_usage_bytes() <= max_memory); } + + #[test] + fn disk_reservation_and_release() { + let budget = test_budget(usize::MAX, 1000); + + assert_eq!(budget.disk_usage_bytes(), 0); + assert!(budget.try_reserve_disk(400).is_ok()); + assert_eq!(budget.disk_usage_bytes(), 400); + assert!(budget.try_reserve_disk(600).is_ok()); + assert_eq!(budget.disk_usage_bytes(), 1000); + assert!(budget.try_reserve_disk(1).is_err()); + assert_eq!(budget.disk_usage_bytes(), 1000); + + budget.release_disk(250); + assert_eq!(budget.disk_usage_bytes(), 750); + assert!(budget.try_reserve_disk(250).is_ok()); + assert_eq!(budget.disk_usage_bytes(), 1000); + } } diff --git a/src/core/src/cache/builders.rs b/src/core/src/cache/builders.rs index f3d35862..18db151f 100644 --- a/src/core/src/cache/builders.rs +++ b/src/core/src/cache/builders.rs @@ -8,9 +8,9 @@ use arrow::buffer::BooleanBuffer; use super::cached_batch::CacheEntry; use super::core::LiquidCache; -use super::io_context::{DefaultIoContext, IoContext}; +use super::io_context::{DefaultCacheMetadata, EntryMetadata}; use super::policies::{CachePolicy, HydrationPolicy, SqueezePolicy, TranscodeSqueezeEvict}; -use super::{CacheExpression, EntryID, LiquidExpr, LiquidPolicy}; +use super::{CacheExpression, CacheFull, EntryID, LiquidExpr, LiquidPolicy}; use crate::sync::Arc; /// Builder for [LiquidCache]. @@ -23,7 +23,7 @@ use crate::sync::Arc; /// tokio_test::block_on(async { /// let _storage = LiquidCacheBuilder::new() /// .with_batch_size(8192) -/// .with_max_cache_bytes(1024 * 1024 * 1024) +/// .with_max_memory_bytes(1024 * 1024 * 1024) /// .with_cache_policy(Box::new(LiquidPolicy::new())) /// .build() /// .await; @@ -31,11 +31,14 @@ use crate::sync::Arc; /// ``` pub struct LiquidCacheBuilder { batch_size: usize, - max_cache_bytes: usize, + max_memory_bytes: usize, + max_disk_bytes: usize, cache_policy: Box, hydration_policy: Box, squeeze_policy: Box, - io_context: Option>, + metadata: Option>, + store: Option, + squeeze_victims_concurrently: bool, } impl Default for LiquidCacheBuilder { @@ -47,13 +50,18 @@ impl Default for LiquidCacheBuilder { impl LiquidCacheBuilder { /// Create a new instance of [LiquidCacheBuilder]. pub fn new() -> Self { + let max_memory_bytes = default_max_memory_bytes(); + let max_disk_bytes = max_memory_bytes.saturating_mul(10); Self { batch_size: 8192, - max_cache_bytes: 1024 * 1024 * 1024, + max_memory_bytes, + max_disk_bytes, cache_policy: Box::new(LiquidPolicy::new()), hydration_policy: Box::new(super::AlwaysHydrate::new()), squeeze_policy: Box::new(TranscodeSqueezeEvict), - io_context: None, + metadata: None, + store: None, + squeeze_victims_concurrently: !cfg!(test), } } @@ -64,10 +72,17 @@ impl LiquidCacheBuilder { self } - /// Set the max cache bytes for the cache. - /// Default is 1GB. - pub fn with_max_cache_bytes(mut self, max_cache_bytes: usize) -> Self { - self.max_cache_bytes = max_cache_bytes; + /// Set the max memory bytes for the cache. + /// Default is half of available system memory. + pub fn with_max_memory_bytes(mut self, max_memory_bytes: usize) -> Self { + self.max_memory_bytes = max_memory_bytes; + self + } + + /// Set the max disk bytes for the cache. + /// Default is 10x the default memory size. + pub fn with_max_disk_bytes(mut self, max_disk_bytes: usize) -> Self { + self.max_disk_bytes = max_disk_bytes; self } @@ -92,41 +107,69 @@ impl LiquidCacheBuilder { self } - /// Set the [IoContext] for the cache. - /// Default is [DefaultIoContext]. - pub fn with_io_context(mut self, io_context: Arc) -> Self { - self.io_context = Some(io_context); + /// Set the [EntryMetadata] for the cache. + /// Default is [DefaultCacheMetadata]. + pub fn with_metadata(mut self, metadata: Arc) -> Self { + self.metadata = Some(metadata); + self + } + + /// Set the [`t4::Store`] used for on-disk IO. + /// If not provided, the builder mounts a fresh store at a temporary directory. + pub fn with_store(mut self, store: t4::Store) -> Self { + self.store = Some(store); + self + } + + /// Set whether cache victims are squeezed concurrently. + pub fn with_squeeze_victims_concurrently(mut self, enabled: bool) -> Self { + self.squeeze_victims_concurrently = enabled; self } /// Build the cache storage. /// /// The cache storage is wrapped in an [Arc] to allow for concurrent access. - /// When no custom [IoContext] is provided, a [`t4::Store`] is mounted at a - /// temporary directory. + /// When no [`t4::Store`] is provided, one is mounted at a temporary directory. pub async fn build(self) -> Arc { - let io_worker = match self.io_context { - Some(io_context) => io_context, + let store = match self.store { + Some(store) => store, None => { let cache_dir = tempfile::tempdir().unwrap().keep(); let store_path = cache_dir.join("liquid_cache.t4"); - let store = t4::mount(&store_path) + t4::mount(&store_path) .await - .expect("failed to mount t4 store"); - Arc::new(DefaultIoContext::new(store)) + .expect("failed to mount t4 store") } }; + let metadata = self + .metadata + .unwrap_or_else(|| Arc::new(DefaultCacheMetadata::new())); Arc::new(LiquidCache::new( self.batch_size, - self.max_cache_bytes, + self.max_memory_bytes, + self.max_disk_bytes, self.squeeze_policy, self.cache_policy, self.hydration_policy, - io_worker, + metadata, + store, + self.squeeze_victims_concurrently, )) } } +/// Returns half of the total system memory in bytes. +/// +/// Used as the default value for [`LiquidCacheBuilder::with_max_memory_bytes`]. +pub fn default_max_memory_bytes() -> usize { + let mut sys = sysinfo::System::new(); + sys.refresh_memory(); + let total = sys.total_memory(); + let half = total / 2; + usize::try_from(half).unwrap_or(usize::MAX) +} + /// Builder returned by [`LiquidCache::insert`] for configuring cache writes. #[derive(Debug)] pub struct Insert<'a> { @@ -160,7 +203,7 @@ impl<'a> Insert<'a> { self } - async fn run(self) { + async fn run(self) -> Result<(), CacheFull> { let batch = if self.skip_gc { self.batch.clone() } else { @@ -170,13 +213,13 @@ impl<'a> Insert<'a> { self.storage.add_squeeze_hint(&self.entry_id, squeeze_hint); } let batch = CacheEntry::memory_arrow(batch); - self.storage.insert_inner(self.entry_id, batch).await; + self.storage.insert_inner(self.entry_id, batch).await } } impl<'a> IntoFuture for Insert<'a> { - type Output = (); - type IntoFuture = Pin + Send + 'a>>; + type Output = Result<(), CacheFull>; + type IntoFuture = Pin> + Send + 'a>>; fn into_future(self) -> Self::IntoFuture { Box::pin(async move { self.run().await }) @@ -409,7 +452,7 @@ mod tests { let cache = LiquidCacheBuilder::new().build().await; let entry_id = EntryID::from(123usize); - cache.insert(entry_id, root.clone()).await; + cache.insert(entry_id, root.clone()).await.unwrap(); let stored = cache.get(&entry_id).await.expect("array present"); let post_size = stored.get_array_memory_size(); diff --git a/src/core/src/cache/cached_batch.rs b/src/core/src/cache/cached_batch.rs index 62b5b811..fda9950a 100644 --- a/src/core/src/cache/cached_batch.rs +++ b/src/core/src/cache/cached_batch.rs @@ -17,9 +17,19 @@ pub enum CacheEntry { /// Cached batch in memory as squeezed liquid array. MemorySqueezedLiquid(LiquidSqueezedArrayRef), /// Cached batch on disk as liquid array. - DiskLiquid(DataType), + DiskLiquid { + /// Original Arrow data type. + data_type: DataType, + /// Byte length of the persisted backing data. + disk_bytes: usize, + }, /// Cached batch on disk as Arrow array. - DiskArrow(DataType), + DiskArrow { + /// Original Arrow data type. + data_type: DataType, + /// Byte length of the persisted backing data. + disk_bytes: usize, + }, } impl CacheEntry { @@ -39,13 +49,19 @@ impl CacheEntry { } /// Construct a cached batch stored on disk as Liquid bytes. - pub fn disk_liquid(data_type: DataType) -> Self { - Self::DiskLiquid(data_type) + pub fn disk_liquid(data_type: DataType, disk_bytes: usize) -> Self { + Self::DiskLiquid { + data_type, + disk_bytes, + } } /// Construct a cached batch stored on disk as Arrow bytes. - pub fn disk_arrow(data_type: DataType) -> Self { - Self::DiskArrow(data_type) + pub fn disk_arrow(data_type: DataType, disk_bytes: usize) -> Self { + Self::DiskArrow { + data_type, + disk_bytes, + } } /// Memory usage reported by the underlying representation. @@ -54,7 +70,7 @@ impl CacheEntry { Self::MemoryArrow(array) => array.get_array_memory_size(), Self::MemoryLiquid(array) => array.get_array_memory_size(), Self::MemorySqueezedLiquid(array) => array.get_array_memory_size(), - Self::DiskLiquid(_) | Self::DiskArrow(_) => 0, + Self::DiskLiquid { .. } | Self::DiskArrow { .. } => 0, } } @@ -64,7 +80,7 @@ impl CacheEntry { Self::MemoryArrow(array) => Arc::strong_count(array), Self::MemoryLiquid(array) => Arc::strong_count(array), Self::MemorySqueezedLiquid(array) => Arc::strong_count(array), - Self::DiskLiquid(_) | Self::DiskArrow(_) => 0, + Self::DiskLiquid { .. } | Self::DiskArrow { .. } => 0, } } } @@ -75,8 +91,8 @@ impl Display for CacheEntry { Self::MemoryArrow(_) => write!(f, "MemoryArrow"), Self::MemoryLiquid(_) => write!(f, "MemoryLiquid"), Self::MemorySqueezedLiquid(_) => write!(f, "MemorySqueezedLiquid"), - Self::DiskLiquid(_) => write!(f, "DiskLiquid"), - Self::DiskArrow(_) => write!(f, "DiskArrow"), + Self::DiskLiquid { .. } => write!(f, "DiskLiquid"), + Self::DiskArrow { .. } => write!(f, "DiskArrow"), } } } @@ -102,8 +118,8 @@ impl From<&CacheEntry> for CachedBatchType { CacheEntry::MemoryArrow(_) => Self::MemoryArrow, CacheEntry::MemoryLiquid(_) => Self::MemoryLiquid, CacheEntry::MemorySqueezedLiquid(_) => Self::MemorySqueezedLiquid, - CacheEntry::DiskLiquid(_) => Self::DiskLiquid, - CacheEntry::DiskArrow(_) => Self::DiskArrow, + CacheEntry::DiskLiquid { .. } => Self::DiskLiquid, + CacheEntry::DiskArrow { .. } => Self::DiskArrow, } } } diff --git a/src/core/src/cache/core.rs b/src/core/src/cache/core.rs index 7a5d76df..f26f2877 100644 --- a/src/core/src/cache/core.rs +++ b/src/core/src/cache/core.rs @@ -2,7 +2,7 @@ use arrow::array::cast::AsArray; use arrow::array::{ArrayRef, BooleanArray}; use arrow::buffer::BooleanBuffer; use arrow::record_batch::RecordBatch; -use arrow_schema::{DataType, Field, Schema}; +use arrow_schema::{Field, Schema}; use bytes::Bytes; use futures::StreamExt; @@ -10,16 +10,16 @@ use super::{ budget::BudgetAccounting, builders::{EvaluatePredicate, Get, Insert}, cached_batch::{CacheEntry, CachedBatchType}, - io_context::IoContext, + io_context::{EntryMetadata, entry_id_to_key}, observer::{CacheTracer, InternalEvent, Observer}, policies::{CachePolicy, HydrationPolicy, HydrationRequest, MaterializedEntry}, utils::CacheConfig, }; use crate::cache::DefaultSqueezeIo; -use crate::cache::policies::SqueezePolicy; +use crate::cache::policies::{SqueezeOutcome, SqueezePolicy}; use crate::cache::utils::{LiquidCompressorStates, arrow_to_bytes}; use crate::cache::{CacheExpression, LiquidExpr, index::ArtIndex, utils::EntryID}; -use crate::cache::{CacheStats, EventTrace}; +use crate::cache::{CacheFull, CacheStats, EventTrace}; use crate::liquid_array::{ LiquidSqueezedArrayRef, SqueezeIoHandler, SqueezedBacking, SqueezedDate32Array, VariantStructSqueezedArray, @@ -57,7 +57,9 @@ pub struct LiquidCache { hydration_policy: Box, squeeze_policy: Box, observer: Arc, - io_context: Arc, + metadata: Arc, + store: t4::Store, + squeeze_victims_concurrently: bool, } /// Builder returned by [`LiquidCache::insert`] for configuring cache writes. @@ -90,8 +92,8 @@ impl LiquidCache { memory_squeezed_liquid_entries += 1; memory_squeezed_liquid_bytes += array.get_array_memory_size(); } - CacheEntry::DiskLiquid(_) => disk_liquid_entries += 1, - CacheEntry::DiskArrow(_) => disk_arrow_entries += 1, + CacheEntry::DiskLiquid { .. } => disk_liquid_entries += 1, + CacheEntry::DiskArrow { .. } => disk_arrow_entries += 1, }); let memory_usage_bytes = self.budget.memory_usage_bytes(); @@ -110,7 +112,8 @@ impl LiquidCache { memory_squeezed_liquid_bytes, memory_usage_bytes, disk_usage_bytes, - max_cache_bytes: self.config.max_cache_bytes(), + max_memory_bytes: self.config.max_memory_bytes(), + max_disk_bytes: self.config.max_disk_bytes(), runtime, } } @@ -152,20 +155,20 @@ impl LiquidCache { match batch.as_ref() { CacheEntry::MemoryLiquid(array) => Some(array.clone()), - entry @ CacheEntry::DiskLiquid(_) => { + entry @ CacheEntry::DiskLiquid { .. } => { let liquid = self.read_disk_liquid_array(entry_id).await; self.maybe_hydrate(entry_id, entry, MaterializedEntry::Liquid(&liquid), None) .await; Some(liquid) } CacheEntry::MemorySqueezedLiquid(array) => match array.disk_backing() { - SqueezedBacking::Liquid => { + SqueezedBacking::Liquid(_) => { let liquid = self.read_disk_liquid_array(entry_id).await; Some(liquid) } - SqueezedBacking::Arrow => None, + SqueezedBacking::Arrow(_) => None, }, - CacheEntry::DiskArrow(_) | CacheEntry::MemoryArrow(_) => None, + CacheEntry::DiskArrow { .. } | CacheEntry::MemoryArrow(_) => None, } } @@ -209,16 +212,16 @@ impl LiquidCache { /// Get the compressor states of the cache. pub fn compressor_states(&self, entry_id: &EntryID) -> Arc { - self.io_context.get_compressor(entry_id) + self.metadata.get_compressor(entry_id) } /// Add a squeeze hint for an entry. pub fn add_squeeze_hint(&self, entry_id: &EntryID, expression: Arc) { - self.io_context.add_squeeze_hint(entry_id, expression); + self.metadata.add_squeeze_hint(entry_id, expression); } /// Flush all entries to disk. - pub async fn flush_all_to_disk(&self) { + pub async fn flush_all_to_disk(&self) -> Result<(), CacheFull> { let mut entires = Vec::new(); self.for_each_entry(|entry_id, batch| { entires.push((*entry_id, batch.clone())); @@ -227,19 +230,37 @@ impl LiquidCache { match &batch { CacheEntry::MemoryArrow(array) => { let bytes = arrow_to_bytes(array).expect("failed to convert arrow to bytes"); - self.write_batch_to_disk(entry_id, &batch, bytes).await; - self.try_insert(entry_id, CacheEntry::disk_arrow(array.data_type().clone())) - .expect("failed to insert disk arrow entry"); + let disk_bytes = bytes.len(); + match self.write_batch_to_disk(entry_id, &batch, bytes).await { + Ok(()) => { + self.try_insert( + entry_id, + CacheEntry::disk_arrow(array.data_type().clone(), disk_bytes), + ) + .expect("failed to insert disk arrow entry"); + } + Err(CacheFull) => self.drop_memory_entry(entry_id, &batch), + } } CacheEntry::MemoryLiquid(liquid_array) => { let liquid_bytes = liquid_array.to_bytes(); - self.write_batch_to_disk(entry_id, &batch, Bytes::from(liquid_bytes)) - .await; - self.try_insert( - entry_id, - CacheEntry::disk_liquid(liquid_array.original_arrow_data_type()), - ) - .expect("failed to insert disk liquid entry"); + let disk_bytes = liquid_bytes.len(); + match self + .write_batch_to_disk(entry_id, &batch, Bytes::from(liquid_bytes)) + .await + { + Ok(()) => { + self.try_insert( + entry_id, + CacheEntry::disk_liquid( + liquid_array.original_arrow_data_type(), + disk_bytes, + ), + ) + .expect("failed to insert disk liquid entry"); + } + Err(CacheFull) => self.drop_memory_entry(entry_id, &batch), + } } CacheEntry::MemorySqueezedLiquid(array) => { // We don't have to do anything, because it's already on disk @@ -247,11 +268,12 @@ impl LiquidCache { self.try_insert(entry_id, disk_entry) .expect("failed to insert disk entry"); } - CacheEntry::DiskArrow(_) | CacheEntry::DiskLiquid(_) => { + CacheEntry::DiskArrow { .. } | CacheEntry::DiskLiquid { .. } => { // Already on disk, skip } } } + Ok(()) } } @@ -261,70 +283,85 @@ impl LiquidCache { &self, entry_id: EntryID, batch: CacheEntry, - ) -> CacheEntry { + ) -> Result { match &batch { batch @ CacheEntry::MemoryArrow(_) => { let squeeze_io: Arc = Arc::new(DefaultSqueezeIo::new( - self.io_context.clone(), + self.store.clone(), entry_id, self.observer.clone(), )); - let (new_batch, bytes_to_write) = self.squeeze_policy.squeeze( + let outcome = self.squeeze_policy.squeeze( batch, - self.io_context.get_compressor(&entry_id).as_ref(), + self.metadata.get_compressor(&entry_id).as_ref(), None, &squeeze_io, ); + let SqueezeOutcome::Replace { + entry: new_batch, + bytes_to_write, + } = outcome + else { + unreachable!("memory arrow squeeze cannot remove entry"); + }; if let Some(bytes_to_write) = bytes_to_write { self.write_batch_to_disk(entry_id, &new_batch, bytes_to_write) - .await; + .await?; } - new_batch + Ok(new_batch) } CacheEntry::MemoryLiquid(liquid_array) => { let liquid_bytes = Bytes::from(liquid_array.to_bytes()); + let disk_bytes = liquid_bytes.len(); self.write_batch_to_disk(entry_id, &batch, liquid_bytes) - .await; - CacheEntry::disk_liquid(liquid_array.original_arrow_data_type()) + .await?; + Ok(CacheEntry::disk_liquid( + liquid_array.original_arrow_data_type(), + disk_bytes, + )) } CacheEntry::MemorySqueezedLiquid(squeezed_array) => { // The full data is already on disk, so we just need to mark ourself as disk entry - let backing = squeezed_array.disk_backing(); - if backing == SqueezedBacking::Liquid { - CacheEntry::disk_liquid(squeezed_array.original_arrow_data_type()) - } else { - CacheEntry::disk_arrow(squeezed_array.original_arrow_data_type()) - } + let data_type = squeezed_array.original_arrow_data_type(); + let entry = match squeezed_array.disk_backing() { + SqueezedBacking::Liquid(n) => CacheEntry::disk_liquid(data_type, n), + SqueezedBacking::Arrow(n) => CacheEntry::disk_arrow(data_type, n), + }; + Ok(entry) } - CacheEntry::DiskLiquid(_) | CacheEntry::DiskArrow(_) => { + CacheEntry::DiskLiquid { .. } | CacheEntry::DiskArrow { .. } => { unreachable!("Unexpected batch in write_in_memory_batch_to_disk") } } } /// Insert a batch into the cache, it will run cache replacement policy until the batch is inserted. - pub(crate) async fn insert_inner(&self, entry_id: EntryID, mut batch_to_cache: CacheEntry) { + pub(crate) async fn insert_inner( + &self, + entry_id: EntryID, + mut batch_to_cache: CacheEntry, + ) -> Result<(), CacheFull> { loop { let Err(not_inserted) = self.try_insert(entry_id, batch_to_cache) else { - return; + return Ok(()); }; self.trace(InternalEvent::InsertFailed { entry: entry_id, kind: CachedBatchType::from(¬_inserted), }); - let victims = self.cache_policy.find_victim(8); + let victims = self.cache_policy.find_memory_victim(8); if victims.is_empty() { // no advice, because the cache is already empty // this can happen if the entry to be inserted is too large, in that case, // we write it to disk let on_disk_batch = self .write_in_memory_batch_to_disk(entry_id, not_inserted) - .await; + .await?; batch_to_cache = on_disk_batch; continue; } - self.squeeze_victims(victims).await; + self.squeeze_victims(victims).await?; batch_to_cache = not_inserted; crate::utils::yield_now_if_shuttle(); @@ -332,24 +369,35 @@ impl LiquidCache { } /// Create a new instance of CacheStorage. + #[allow(clippy::too_many_arguments)] pub(crate) fn new( batch_size: usize, - max_cache_bytes: usize, + max_memory_bytes: usize, + max_disk_bytes: usize, squeeze_policy: Box, cache_policy: Box, hydration_policy: Box, - io_worker: Arc, + metadata: Arc, + store: t4::Store, + squeeze_victims_concurrently: bool, ) -> Self { - let config = CacheConfig::new(batch_size, max_cache_bytes); + let config = CacheConfig::new(batch_size, max_memory_bytes, max_disk_bytes); + let observer = Arc::new(Observer::new()); Self { index: ArtIndex::new(), - budget: BudgetAccounting::new(config.max_cache_bytes()), + budget: BudgetAccounting::new( + config.max_memory_bytes(), + config.max_disk_bytes(), + observer.clone(), + ), config, cache_policy, hydration_policy, squeeze_policy, - observer: Arc::new(Observer::new()), - io_context: io_worker, + observer, + metadata, + store, + squeeze_victims_concurrently, } } @@ -386,6 +434,46 @@ impl LiquidCache { Ok(()) } + fn drop_memory_entry(&self, entry_id: EntryID, _expected: &CacheEntry) { + let Some(removed) = self.index.remove(&entry_id) else { + return; + }; + assert!( + matches!( + removed.as_ref(), + CacheEntry::MemoryArrow(_) + | CacheEntry::MemoryLiquid(_) + | CacheEntry::MemorySqueezedLiquid(_) + ), + "flush should only drop memory entries" + ); + self.budget + .try_update_memory_usage(removed.memory_usage_bytes(), 0) + .expect("memory release cannot fail"); + self.cache_policy.notify_remove(&entry_id); + } + + async fn remove_disk_entry(&self, entry_id: EntryID) { + let Some(removed) = self.index.remove(&entry_id) else { + return; + }; + let disk_bytes = match removed.as_ref() { + CacheEntry::DiskLiquid { disk_bytes, .. } + | CacheEntry::DiskArrow { disk_bytes, .. } => *disk_bytes, + _ => panic!("remove_disk_entry called for non-disk entry"), + }; + self.store + .remove(&entry_id_to_key(&entry_id)) + .await + .expect("disk remove failed"); + self.budget.release_disk(disk_bytes); + self.cache_policy.notify_remove(&entry_id); + self.trace(InternalEvent::DiskEvict { + entry: entry_id, + bytes: disk_bytes, + }); + } + /// Consume the trace of the cache, for testing only. pub fn consume_event_trace(&self) -> EventTrace { self.observer.consume_event_trace() @@ -402,61 +490,80 @@ impl LiquidCache { } #[fastrace::trace] - async fn squeeze_victims(&self, victims: Vec) { - // Run squeeze operations sequentially using async I/O + async fn squeeze_victims(&self, victims: Vec) -> Result<(), CacheFull> { self.trace(InternalEvent::SqueezeBegin { victims: victims.clone(), }); - futures::stream::iter(victims) - .for_each_concurrent(None, |victim| async move { - self.squeeze_victim_inner(victim).await; - }) - .await; + if self.squeeze_victims_concurrently { + let results = futures::stream::iter(victims) + .map(|victim| self.squeeze_victim_inner(victim)) + .buffer_unordered(usize::MAX) + .collect::>() + .await; + results.into_iter().collect::, _>>()?; + } else { + for victim in victims { + self.squeeze_victim_inner(victim).await?; + } + } + Ok(()) } - async fn squeeze_victim_inner(&self, to_squeeze: EntryID) { + async fn squeeze_victim_inner(&self, to_squeeze: EntryID) -> Result<(), CacheFull> { let Some(mut to_squeeze_batch) = self.index.get(&to_squeeze) else { - return; + return Ok(()); }; self.trace(InternalEvent::SqueezeVictim { entry: to_squeeze }); - let compressor = self.io_context.get_compressor(&to_squeeze); - let squeeze_hint_arc = self.io_context.squeeze_hint(&to_squeeze); + let compressor = self.metadata.get_compressor(&to_squeeze); + let squeeze_hint_arc = self.metadata.squeeze_hint(&to_squeeze); let squeeze_hint = squeeze_hint_arc.as_deref(); let squeeze_io: Arc = Arc::new(DefaultSqueezeIo::new( - self.io_context.clone(), + self.store.clone(), to_squeeze, self.observer.clone(), )); loop { - let (new_batch, bytes_to_write) = self.squeeze_policy.squeeze( + let outcome = self.squeeze_policy.squeeze( to_squeeze_batch.as_ref(), compressor.as_ref(), squeeze_hint, &squeeze_io, ); - if let Some(bytes_to_write) = bytes_to_write { - self.write_batch_to_disk(to_squeeze, &new_batch, bytes_to_write) - .await; - } - match self.try_insert(to_squeeze, new_batch) { - Ok(()) => { - break; + match outcome { + SqueezeOutcome::Replace { + entry: new_batch, + bytes_to_write, + } => { + if let Some(bytes_to_write) = bytes_to_write { + self.write_batch_to_disk(to_squeeze, &new_batch, bytes_to_write) + .await?; + } + match self.try_insert(to_squeeze, new_batch) { + Ok(()) => { + break; + } + Err(batch) => { + to_squeeze_batch = Arc::new(batch); + } + } } - Err(batch) => { - to_squeeze_batch = Arc::new(batch); + SqueezeOutcome::Remove => { + self.remove_disk_entry(to_squeeze).await; + break; } } } + Ok(()) } fn disk_entry_from_squeezed(array: &LiquidSqueezedArrayRef) -> CacheEntry { - let constructor: fn(DataType) -> CacheEntry = match array.disk_backing() { - SqueezedBacking::Liquid => CacheEntry::disk_liquid, - SqueezedBacking::Arrow => CacheEntry::disk_arrow, - }; - constructor(array.original_arrow_data_type()) + let data_type = array.original_arrow_data_type(); + match array.disk_backing() { + SqueezedBacking::Liquid(n) => CacheEntry::disk_liquid(data_type, n), + SqueezedBacking::Arrow(n) => CacheEntry::disk_arrow(data_type, n), + } } async fn maybe_hydrate( @@ -466,7 +573,7 @@ impl LiquidCache { materialized: MaterializedEntry<'_>, expression: Option<&CacheExpression>, ) { - let compressor = self.io_context.get_compressor(entry_id); + let compressor = self.metadata.get_compressor(entry_id); if let Some(new_entry) = self.hydration_policy.hydrate(&HydrationRequest { entry_id: *entry_id, cached, @@ -481,7 +588,7 @@ impl LiquidCache { cached: cached_type, new: new_type, }); - self.insert_inner(*entry_id, new_entry).await; + let _ = self.insert_inner(*entry_id, new_entry).await; } } @@ -515,7 +622,7 @@ impl LiquidCache { Some(selection) => Some(array.filter(selection)), None => Some(array.to_arrow_array()), }, - CacheEntry::DiskArrow(_) | CacheEntry::DiskLiquid(_) => { + CacheEntry::DiskArrow { .. } | CacheEntry::DiskLiquid { .. } => { self.read_disk_array(batch.as_ref(), entry_id, expression, selection) .await } @@ -534,7 +641,7 @@ impl LiquidCache { selection: Option<&BooleanBuffer>, ) -> Option { match entry { - CacheEntry::DiskArrow(data_type) => { + CacheEntry::DiskArrow { data_type, .. } => { if let Some(selection) = selection && selection.count_set_bits() == 0 { @@ -556,7 +663,7 @@ impl LiquidCache { None => Some(full_array), } } - CacheEntry::DiskLiquid(data_type) => { + CacheEntry::DiskLiquid { data_type, .. } => { if let Some(selection) = selection && selection.count_set_bits() == 0 { @@ -621,9 +728,9 @@ impl LiquidCache { expression: Option<&CacheExpression>, selection: Option<&BooleanBuffer>, ) -> Option { - if let Some(CacheExpression::ExtractDate32 { field }) = expression + if let Some(field) = expression.and_then(CacheExpression::as_date32_field) && let Some(squeezed) = array.as_any().downcast_ref::() - && squeezed.field() == *field + && squeezed.field() == field { let component = squeezed.to_component_array(); self.observer.on_hit_date32_expression(); @@ -680,31 +787,52 @@ impl LiquidCache { } } - async fn write_batch_to_disk(&self, entry_id: EntryID, batch: &CacheEntry, bytes: Bytes) { + async fn write_batch_to_disk( + &self, + entry_id: EntryID, + batch: &CacheEntry, + bytes: Bytes, + ) -> Result<(), CacheFull> { + let len = bytes.len(); + loop { + if self.budget.try_reserve_disk(len).is_ok() { + break; + } + let victims = self.cache_policy.find_disk_victim(8); + if victims.is_empty() { + return Err(CacheFull); + } + for victim in victims { + self.remove_disk_entry(victim).await; + } + } self.trace(InternalEvent::IoWrite { entry: entry_id, kind: CachedBatchType::from(batch), - bytes: bytes.len(), + bytes: len, }); - let len = bytes.len(); - self.io_context.write(&entry_id, bytes).await.unwrap(); - self.budget.add_used_disk_bytes(len); + self.store + .put(entry_id_to_key(&entry_id), bytes.to_vec()) + .await + .expect("write failed"); + Ok(()) } async fn read_disk_arrow_array(&self, entry_id: &EntryID) -> ArrayRef { let bytes = self - .io_context - .read(entry_id, None) + .store + .get(&entry_id_to_key(entry_id)) .await .expect("read failed"); - let cursor = std::io::Cursor::new(bytes.to_vec()); + let bytes_len = bytes.len(); + let cursor = std::io::Cursor::new(bytes); let mut reader = arrow::ipc::reader::StreamReader::try_new(cursor, None).expect("create reader failed"); let batch = reader.next().unwrap().expect("read batch failed"); let array = batch.column(0).clone(); self.trace(InternalEvent::IoReadArrow { entry: *entry_id, - bytes: bytes.len(), + bytes: bytes_len, }); array } @@ -714,19 +842,19 @@ impl LiquidCache { entry_id: &EntryID, ) -> crate::liquid_array::LiquidArrayRef { let bytes = self - .io_context - .read(entry_id, None) + .store + .get(&entry_id_to_key(entry_id)) .await .expect("read failed"); self.trace(InternalEvent::IoReadLiquid { entry: *entry_id, bytes: bytes.len(), }); - let compressor_states = self.io_context.get_compressor(entry_id); + let compressor_states = self.metadata.get_compressor(entry_id); let compressor = compressor_states.fsst_compressor(); (crate::liquid_array::ipc::read_from_bytes( - bytes, + Bytes::from(bytes), &crate::liquid_array::ipc::LiquidIPCContext::new(compressor), )) as _ } @@ -761,7 +889,7 @@ impl LiquidCache { .expect("selection must match array length"); Some(self.eval_predicate_on_array(filtered, predicate)) } - entry @ CacheEntry::DiskArrow(_) => { + entry @ CacheEntry::DiskArrow { .. } => { let array = self.read_disk_arrow_array(entry_id).await; self.maybe_hydrate(entry_id, entry, MaterializedEntry::Arrow(&array), None) .await; @@ -783,7 +911,7 @@ impl LiquidCache { }); Some(array.try_eval_predicate(predicate, selection)) } - entry @ CacheEntry::DiskLiquid(_) => { + entry @ CacheEntry::DiskLiquid { .. } => { let liquid = self.read_disk_liquid_array(entry_id).await; self.maybe_hydrate(entry_id, entry, MaterializedEntry::Liquid(&liquid), None) .await; @@ -838,11 +966,11 @@ impl LiquidCache { mod tests { use super::*; use crate::cache::{ - CacheEntry, CacheExpression, CachePolicy, LiquidCacheBuilder, TranscodeSqueezeEvict, - policies::LruPolicy, - transcode_liquid_inner, + CacheEntry, CacheExpression, CachePolicy, LiquidCacheBuilder, LiquidPolicy, + TranscodeSqueezeEvict, transcode_liquid_inner, utils::{ - LiquidCompressorStates, create_cache_store, create_test_array, create_test_arrow_array, + LiquidCompressorStates, arrow_to_bytes, create_cache_store, create_test_array, + create_test_arrow_array, }, }; use crate::liquid_array::{ @@ -872,7 +1000,7 @@ mod tests { } impl CachePolicy for TestPolicy { - fn find_victim(&self, _cnt: usize) -> Vec { + fn find_memory_victim(&self, _cnt: usize) -> Vec { self.advice_count.fetch_add(1, Ordering::SeqCst); let id_to_use = self.target_id.unwrap(); vec![id_to_use] @@ -883,7 +1011,7 @@ mod tests { async fn test_basic_cache_operations() { // Test basic insert, get, and size tracking in one test let budget_size = 10 * 1024; - let store = create_cache_store(budget_size, Box::new(LruPolicy::new())).await; + let store = create_cache_store(budget_size, Box::new(LiquidPolicy::new())).await; // 1. Initial budget should be empty assert_eq!(store.budget.memory_usage_bytes(), 0); @@ -892,7 +1020,7 @@ mod tests { let entry_id1: EntryID = EntryID::from(1); let array1 = create_test_array(100); let size1 = array1.memory_usage_bytes(); - store.insert_inner(entry_id1, array1).await; + store.insert_inner(entry_id1, array1).await.unwrap(); // Verify budget usage and data correctness assert_eq!(store.budget.memory_usage_bytes(), size1); @@ -905,13 +1033,13 @@ mod tests { let entry_id2: EntryID = EntryID::from(2); let array2 = create_test_array(200); let size2 = array2.memory_usage_bytes(); - store.insert_inner(entry_id2, array2).await; + store.insert_inner(entry_id2, array2).await.unwrap(); assert_eq!(store.budget.memory_usage_bytes(), size1 + size2); let array3 = create_test_array(150); let size3 = array3.memory_usage_bytes(); - store.insert_inner(entry_id1, array3).await; + store.insert_inner(entry_id1, array3).await.unwrap(); assert_eq!(store.budget.memory_usage_bytes(), size3 + size2); assert!(store.index().get(&EntryID::from(999)).is_none()); @@ -919,10 +1047,10 @@ mod tests { #[tokio::test] async fn get_arrow_array_with_expression_extracts_year() { - let store = create_cache_store(1 << 20, Box::new(LruPolicy::new())).await; + let store = create_cache_store(1 << 20, Box::new(LiquidPolicy::new())).await; let entry_id = EntryID::from(42); - let date_values = Date32Array::from(vec![Some(0), Some(365), None, Some(730)]); + let date_values = Date32Array::from(vec![Some(2), Some(365 + 1), None, Some(365 + 100)]); let liquid = LiquidPrimitiveArray::::from_arrow_array(date_values.clone()); let squeezed = SqueezedDate32Array::from_liquid_date32(&liquid, Date32Field::Year); let squeezed: LiquidSqueezedArrayRef = Arc::new(squeezed); @@ -932,7 +1060,8 @@ mod tests { entry_id, CacheEntry::memory_squeezed_liquid(squeezed.clone()), ) - .await; + .await + .unwrap(); let expr = Arc::new(CacheExpression::extract_date32(Date32Field::Year)); let result = store @@ -947,10 +1076,10 @@ mod tests { .downcast_ref::() .expect("date32 result"); assert_eq!(result.len(), 4); - assert_eq!(result.value(0), 1970); - assert_eq!(result.value(1), 1971); + assert_eq!(result.value(0), 0); + assert_eq!(result.value(1), 365); assert!(result.is_null(2)); - assert_eq!(result.value(3), 1972); + assert_eq!(result.value(3), 365); } #[tokio::test] @@ -966,13 +1095,19 @@ mod tests { let advisor = TestPolicy::new(Some(entry_id1)); let store = create_cache_store(8000, Box::new(advisor)).await; // Small budget to force advice - store.insert_inner(entry_id1, create_test_array(800)).await; + store + .insert_inner(entry_id1, create_test_array(800)) + .await + .unwrap(); match store.index().get(&entry_id1).unwrap().as_ref() { CacheEntry::MemoryArrow(_) => {} other => panic!("Expected ArrowMemory, got {other:?}"), } - store.insert_inner(entry_id2, create_test_array(800)).await; + store + .insert_inner(entry_id2, create_test_array(800)) + .await + .unwrap(); match store.index().get(&entry_id1).unwrap().as_ref() { CacheEntry::MemoryLiquid(_) => {} other => panic!("Expected LiquidMemory after eviction, got {other:?}"), @@ -985,13 +1120,13 @@ mod tests { concurrent_cache_operations().await; } - #[cfg(feature = "shuttle")] - #[test] - fn shuttle_cache_operations() { - crate::utils::shuttle_test(|| { - block_on(concurrent_cache_operations()); - }); - } + // #[cfg(feature = "shuttle")] + // #[test] + // fn shuttle_cache_operations() { + // crate::utils::shuttle_test(|| { + // block_on(concurrent_cache_operations()); + // }); + // } pub fn block_on(future: F) -> F::Output { #[cfg(feature = "shuttle")] @@ -1009,7 +1144,7 @@ mod tests { let ops_per_thread = 50; let budget_size = num_threads * ops_per_thread * 100 * 8 / 2; - let store = create_cache_store(budget_size, Box::new(LruPolicy::new())).await; + let store = create_cache_store(budget_size, Box::new(LiquidPolicy::new())).await; let mut handles = vec![]; for thread_id in 0..num_threads { @@ -1020,7 +1155,7 @@ mod tests { let unique_id = thread_id * ops_per_thread + i; let entry_id: EntryID = EntryID::from(unique_id); let array = create_test_arrow_array(100); - store.insert(entry_id, array).await; + store.insert(entry_id, array).await.unwrap(); } }); })); @@ -1046,7 +1181,7 @@ mod tests { async fn test_cache_stats_memory_and_disk_usage() { // Build a small cache in blocking liquid mode to avoid background tasks let storage = LiquidCacheBuilder::new() - .with_max_cache_bytes(10 * 1024 * 1024) + .with_max_memory_bytes(10 * 1024 * 1024) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) .build() .await; @@ -1054,18 +1189,18 @@ mod tests { // Insert two small batches let arr1: ArrayRef = Arc::new(Int32Array::from_iter_values(0..64)); let arr2: ArrayRef = Arc::new(Int32Array::from_iter_values(0..128)); - storage.insert(EntryID::from(1usize), arr1).await; - storage.insert(EntryID::from(2usize), arr2).await; + storage.insert(EntryID::from(1usize), arr1).await.unwrap(); + storage.insert(EntryID::from(2usize), arr2).await.unwrap(); // Stats after insert: 2 entries, memory usage > 0, disk usage == 0 let s = storage.stats(); assert_eq!(s.total_entries, 2); assert!(s.memory_usage_bytes > 0); assert_eq!(s.disk_usage_bytes, 0); - assert_eq!(s.max_cache_bytes, 10 * 1024 * 1024); + assert_eq!(s.max_memory_bytes, 10 * 1024 * 1024); // Flush to disk and verify memory usage drops and disk usage increases - storage.flush_all_to_disk().await; + storage.flush_all_to_disk().await.unwrap(); let s2 = storage.stats(); assert_eq!(s2.total_entries, 2); assert!(s2.disk_usage_bytes > 0); @@ -1075,15 +1210,15 @@ mod tests { #[tokio::test] async fn hydrate_disk_arrow_on_get_promotes_to_memory() { - let store = create_cache_store(1 << 20, Box::new(LruPolicy::new())).await; + let store = create_cache_store(1 << 20, Box::new(LiquidPolicy::new())).await; let entry_id = EntryID::from(321usize); let array = create_test_arrow_array(8); - store.insert(entry_id, array.clone()).await; - store.flush_all_to_disk().await; + store.insert(entry_id, array.clone()).await.unwrap(); + store.flush_all_to_disk().await.unwrap(); { let entry = store.index().get(&entry_id).unwrap(); - assert!(matches!(entry.as_ref(), CacheEntry::DiskArrow(_))); + assert!(matches!(entry.as_ref(), CacheEntry::DiskArrow { .. })); } let result = store.get(&entry_id).await.expect("present"); @@ -1096,7 +1231,7 @@ mod tests { #[tokio::test] async fn hydrate_disk_liquid_on_get_promotes_to_memory_liquid() { - let store = create_cache_store(1 << 20, Box::new(LruPolicy::new())).await; + let store = create_cache_store(1 << 20, Box::new(LiquidPolicy::new())).await; let entry_id = EntryID::from(322usize); let arrow_array: ArrayRef = Arc::new(Int32Array::from(vec![1, 2, 3, 4])); let compressor = LiquidCompressorStates::new(); @@ -1104,11 +1239,12 @@ mod tests { store .insert_inner(entry_id, CacheEntry::memory_liquid(liquid.clone())) - .await; - store.flush_all_to_disk().await; + .await + .unwrap(); + store.flush_all_to_disk().await.unwrap(); { let entry = store.index().get(&entry_id).unwrap(); - assert!(matches!(entry.as_ref(), CacheEntry::DiskLiquid(_))); + assert!(matches!(entry.as_ref(), CacheEntry::DiskLiquid { .. })); } let result = store.get(&entry_id).await.expect("present"); @@ -1118,4 +1254,113 @@ mod tests { assert!(matches!(entry.as_ref(), CacheEntry::MemoryLiquid(_))); } } + + #[tokio::test] + async fn insert_returns_cache_full_when_memory_and_disk_are_saturated() { + let cache = LiquidCacheBuilder::new() + .with_max_memory_bytes(0) + .with_max_disk_bytes(0) + .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) + .build() + .await; + let array: ArrayRef = Arc::new(Int32Array::from_iter_values(0..16)); + + let err = cache.insert(EntryID::from(900usize), array).await; + + assert_eq!(err, Err(CacheFull)); + assert!(!cache.is_cached(&EntryID::from(900usize))); + } + + #[tokio::test] + async fn insert_until_disk_full_then_evicts_oldest_disk_entry() { + let first_array: ArrayRef = Arc::new(Int32Array::from_iter_values(0..16)); + let second_array: ArrayRef = Arc::new(Int32Array::from_iter_values(16..32)); + let first_bytes = arrow_to_bytes(&first_array).unwrap().len(); + let second_bytes = arrow_to_bytes(&second_array).unwrap().len(); + let cache = LiquidCacheBuilder::new() + .with_max_memory_bytes(1 << 20) + .with_max_disk_bytes(first_bytes.max(second_bytes)) + .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) + .with_cache_policy(Box::new(LiquidPolicy::new())) + .build() + .await; + + let first = EntryID::from(910usize); + let second = EntryID::from(911usize); + cache.insert(first, first_array).await.unwrap(); + cache.flush_all_to_disk().await.unwrap(); + assert!(cache.is_cached(&first)); + + cache.insert(second, second_array).await.unwrap(); + cache.flush_all_to_disk().await.unwrap(); + + assert!(!cache.is_cached(&first)); + assert!(matches!( + cache.index().get(&second).unwrap().as_ref(), + CacheEntry::DiskArrow { .. } + )); + } + + #[tokio::test] + async fn flush_all_to_disk_evicts_when_overflow() { + let first_array: ArrayRef = Arc::new(Int32Array::from_iter_values(0..16)); + let second_array: ArrayRef = Arc::new(Int32Array::from_iter_values(16..32)); + let disk_bytes = arrow_to_bytes(&first_array).unwrap().len(); + let cache = LiquidCacheBuilder::new() + .with_max_memory_bytes(1 << 20) + .with_max_disk_bytes(disk_bytes) + .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) + .with_cache_policy(Box::new(LiquidPolicy::new())) + .build() + .await; + let first = EntryID::from(912usize); + let second = EntryID::from(913usize); + cache.insert(first, first_array).await.unwrap(); + cache.flush_all_to_disk().await.unwrap(); + cache.insert(second, second_array).await.unwrap(); + + cache.flush_all_to_disk().await.unwrap(); + + assert!(!cache.is_cached(&first) || !cache.is_cached(&second)); + } + + #[tokio::test] + async fn disk_eviction_releases_budget() { + let array: ArrayRef = Arc::new(Int32Array::from_iter_values(0..16)); + let disk_bytes = arrow_to_bytes(&array).unwrap().len(); + let cache = LiquidCacheBuilder::new() + .with_max_memory_bytes(1 << 20) + .with_max_disk_bytes(disk_bytes) + .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) + .with_cache_policy(Box::new(LiquidPolicy::new())) + .build() + .await; + let entry = EntryID::from(914usize); + cache.insert(entry, array).await.unwrap(); + cache.flush_all_to_disk().await.unwrap(); + let before = cache.stats().disk_usage_bytes; + + cache.remove_disk_entry(entry).await; + + assert_eq!(cache.stats().disk_usage_bytes, before - disk_bytes); + assert!(!cache.is_cached(&entry)); + } + + #[tokio::test] + async fn flush_all_to_disk_drops_entry_on_unrecoverable_overflow() { + let cache = LiquidCacheBuilder::new() + .with_max_memory_bytes(1 << 20) + .with_max_disk_bytes(0) + .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) + .build() + .await; + let entry_id = EntryID::from(901usize); + let array: ArrayRef = Arc::new(Int32Array::from_iter_values(0..16)); + cache.insert(entry_id, array).await.unwrap(); + + let result = cache.flush_all_to_disk().await; + + assert_eq!(result, Ok(())); + assert!(!cache.is_cached(&entry_id)); + } } diff --git a/src/core/src/cache/expressions.rs b/src/core/src/cache/expressions.rs index eacc7107..64e7d781 100644 --- a/src/core/src/cache/expressions.rs +++ b/src/core/src/cache/expressions.rs @@ -1,5 +1,6 @@ //! Definitions for cache-aware expressions that can be applied when materializing arrays. +use std::str::FromStr; use std::sync::Arc; use arrow_schema::DataType; @@ -34,12 +35,22 @@ impl VariantRequest { } /// Experimental expression descriptor for cache lookups. +/// +/// A `CacheExpression` is a *squeeze hint*: it tells the cache how a column is +/// consumed by a query so that, under memory pressure, the cache can keep only +/// the part of the column the query actually needs (e.g. a single date +/// component, or a handful of variant paths) instead of evicting it wholesale. #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)] pub enum CacheExpression { - /// Extract a specific component (YEAR/MONTH/DAY/DOW) from a `Date32` column. + /// Extract one or more components (YEAR/MONTH/DAY/DOW) from a `Date32` or + /// timestamp column. + /// + /// The set is non-empty and de-duplicated. A column used as + /// `EXTRACT(MONTH FROM d)` *and* `EXTRACT(DAY FROM d)` carries both + /// components so neither is silently lost. ExtractDate32 { - /// Component to extract (YEAR/MONTH/DAY). - field: Date32Field, + /// Components requested by the query, sorted and de-duplicated. + fields: Arc<[Date32Field]>, }, /// Extract a field from a variant column via `variant_get`. VariantGet { @@ -64,8 +75,8 @@ impl std::fmt::Display for CacheExpression { write!(f, "{}", requests.join(","))?; write!(f, "]") } - Self::ExtractDate32 { field } => { - write!(f, "ExtractDate32:{:?}", field) + Self::ExtractDate32 { fields } => { + write!(f, "ExtractDate32{:?}", fields) } Self::PredicateColumn => { write!(f, "PredicateColumn") @@ -78,9 +89,28 @@ impl std::fmt::Display for CacheExpression { } impl CacheExpression { - /// Build an extract expression for a `Date32`/timestamp column. + /// Build an extract expression for a single `Date32`/timestamp component. pub fn extract_date32(field: Date32Field) -> Self { - Self::ExtractDate32 { field } + Self::ExtractDate32 { + fields: Arc::from(vec![field].into_boxed_slice()), + } + } + + /// Build an extract expression covering multiple `Date32`/timestamp + /// components. The components are sorted and de-duplicated; returns `None` + /// if `fields` is empty. + pub fn extract_date32_many(fields: I) -> Option + where + I: IntoIterator, + { + let mut fields: Vec = fields.into_iter().collect(); + sort_dedup_fields(&mut fields); + if fields.is_empty() { + return None; + } + Some(Self::ExtractDate32 { + fields: Arc::from(fields.into_boxed_slice()), + }) } /// Build a variant-get expression for the provided dotted path. @@ -114,26 +144,25 @@ impl CacheExpression { Self::SubstringSearch } - /// Attempt to parse a metadata value (e.g. `"YEAR"`) into an expression. + /// Return the requested `Date32` component when this is an extract + /// expression for exactly one component. /// - /// The value is compared case-insensitively against supported components. - pub fn try_from_date_part_str(value: &str) -> Option { - let upper = value.to_ascii_uppercase(); - let field = match upper.as_str() { - "YEAR" => Date32Field::Year, - "MONTH" => Date32Field::Month, - "DAY" => Date32Field::Day, - "DOW" | "DAYOFWEEK" | "DAY_OF_WEEK" => Date32Field::DayOfWeek, - _ => return None, - }; - Some(Self::ExtractDate32 { field }) - } - - /// Return the requested `Date32` component when this is an extract expression. + /// Multi-component extractions return `None`: there is no single-component + /// squeezed representation that satisfies all of them, so the squeeze path + /// keeps the column intact rather than dropping a needed component. pub fn as_date32_field(&self) -> Option { match self { - Self::ExtractDate32 { field } => Some(*field), - Self::VariantGet { .. } | Self::PredicateColumn | Self::SubstringSearch => None, + Self::ExtractDate32 { fields } if fields.len() == 1 => Some(fields[0]), + _ => None, + } + } + + /// Return all requested `Date32` components when this is an extract + /// expression. + pub fn date32_fields(&self) -> Option<&[Date32Field]> { + match self { + Self::ExtractDate32 { fields } => Some(fields.as_ref()), + _ => None, } } @@ -141,7 +170,7 @@ impl CacheExpression { pub fn variant_path(&self) -> Option<&str> { match self { Self::VariantGet { requests } => requests.first().map(|request| request.path()), - Self::ExtractDate32 { .. } | Self::PredicateColumn | Self::SubstringSearch => None, + _ => None, } } @@ -149,7 +178,7 @@ impl CacheExpression { pub fn variant_data_type(&self) -> Option<&DataType> { match self { Self::VariantGet { requests } => requests.first().map(|request| request.data_type()), - Self::ExtractDate32 { .. } | Self::PredicateColumn | Self::SubstringSearch => None, + _ => None, } } @@ -157,7 +186,168 @@ impl CacheExpression { pub fn variant_requests(&self) -> Option<&[VariantRequest]> { match self { Self::VariantGet { requests } => Some(requests.as_ref()), - Self::ExtractDate32 { .. } | Self::PredicateColumn | Self::SubstringSearch => None, + _ => None, } } + + /// Encode this expression into a compact, lossless string suitable for + /// carrying through Arrow schema field metadata (and therefore across the + /// `physical_plan_to_bytes` serialization boundary). + /// + /// The DataFusion layer derives a typed [`CacheExpression`] from the query + /// plan, stamps it onto the scan's file schema via [`Self::to_metadata_value`], + /// and decodes it back here via [`Self::from_metadata_value`] when the cache + /// column is created. The round-trip is exact — including multi-component + /// dates and multi-path variant requests, both of which the previous + /// ad-hoc string format silently dropped. + pub fn to_metadata_value(&self) -> String { + let dto = CacheExprDto::from(self); + serde_json::to_string(&dto).expect("CacheExpression DTO is always serializable") + } + + /// Decode an expression previously produced by [`Self::to_metadata_value`]. + /// + /// Returns `None` if the value is not a recognized encoding. + pub fn from_metadata_value(value: &str) -> Option { + let dto: CacheExprDto = serde_json::from_str(value).ok()?; + Self::try_from(dto) + } +} + +fn sort_dedup_fields(fields: &mut Vec) { + fields.sort_by_key(field_order); + fields.dedup(); +} + +fn field_order(field: &Date32Field) -> u8 { + match field { + Date32Field::Year => 0, + Date32Field::Month => 1, + Date32Field::Day => 2, + Date32Field::DayOfWeek => 3, + } +} + +/// Owned, `Arc`-free mirror of [`CacheExpression`] used purely for the metadata +/// codec, so the encoding does not depend on serde's `rc` feature. Variant data +/// types are carried as their Arrow `Display` form, which round-trips through +/// `DataType::from_str`. +#[derive(serde::Serialize, serde::Deserialize)] +enum CacheExprDto { + Date { fields: Vec }, + Variant { requests: Vec }, + Predicate, + Substring, +} + +#[derive(serde::Serialize, serde::Deserialize)] +struct VariantReqDto { + path: String, + data_type: String, +} + +impl From<&CacheExpression> for CacheExprDto { + fn from(expr: &CacheExpression) -> Self { + match expr { + CacheExpression::ExtractDate32 { fields } => CacheExprDto::Date { + fields: fields.to_vec(), + }, + CacheExpression::VariantGet { requests } => CacheExprDto::Variant { + requests: requests + .iter() + .map(|request| VariantReqDto { + path: request.path().to_string(), + data_type: request.data_type().to_string(), + }) + .collect(), + }, + CacheExpression::PredicateColumn => CacheExprDto::Predicate, + CacheExpression::SubstringSearch => CacheExprDto::Substring, + } + } +} + +impl CacheExpression { + fn try_from(dto: CacheExprDto) -> Option { + match dto { + CacheExprDto::Date { fields } => CacheExpression::extract_date32_many(fields), + CacheExprDto::Variant { requests } => { + let mut parsed = Vec::with_capacity(requests.len()); + for request in requests { + let data_type = DataType::from_str(&request.data_type).ok()?; + parsed.push(VariantRequest::new(request.path, data_type)); + } + if parsed.is_empty() { + return None; + } + Some(CacheExpression::VariantGet { + requests: Arc::from(parsed.into_boxed_slice()), + }) + } + CacheExprDto::Predicate => Some(CacheExpression::PredicateColumn), + CacheExprDto::Substring => Some(CacheExpression::SubstringSearch), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn date_metadata_roundtrip_multi_component() { + let expr = CacheExpression::extract_date32_many([ + Date32Field::Day, + Date32Field::Month, + // duplicate is collapsed + Date32Field::Day, + ]) + .unwrap(); + let encoded = expr.to_metadata_value(); + let decoded = CacheExpression::from_metadata_value(&encoded).unwrap(); + assert_eq!(decoded, expr); + // Multi-component extractions do not collapse to a single squeezable field. + assert_eq!(decoded.as_date32_field(), None); + assert_eq!( + decoded.date32_fields().unwrap(), + &[Date32Field::Month, Date32Field::Day] + ); + } + + #[test] + fn date_metadata_roundtrip_single_component() { + let expr = CacheExpression::extract_date32(Date32Field::Year); + let decoded = CacheExpression::from_metadata_value(&expr.to_metadata_value()).unwrap(); + assert_eq!(decoded, expr); + assert_eq!(decoded.as_date32_field(), Some(Date32Field::Year)); + } + + #[test] + fn variant_metadata_roundtrip_multi_path() { + let expr = CacheExpression::variant_get_many([ + ("name", DataType::Utf8), + ("age", DataType::Int64), + // paths can contain delimiter-like characters; JSON encoding is safe + ("address.zip,code", DataType::Utf8), + ]); + let decoded = CacheExpression::from_metadata_value(&expr.to_metadata_value()).unwrap(); + assert_eq!(decoded, expr); + } + + #[test] + fn predicate_and_substring_roundtrip() { + for expr in [ + CacheExpression::PredicateColumn, + CacheExpression::substring_search(), + ] { + let decoded = CacheExpression::from_metadata_value(&expr.to_metadata_value()).unwrap(); + assert_eq!(decoded, expr); + } + } + + #[test] + fn rejects_unknown_metadata() { + assert_eq!(CacheExpression::from_metadata_value("not json"), None); + assert_eq!(CacheExpression::from_metadata_value("{}"), None); + } } diff --git a/src/core/src/cache/index.rs b/src/core/src/cache/index.rs index b2a84edd..a25fec75 100644 --- a/src/core/src/cache/index.rs +++ b/src/core/src/cache/index.rs @@ -51,6 +51,15 @@ impl ArtIndex { } } + pub(crate) fn remove(&self, entry_id: &EntryID) -> Option> { + let guard = self.art.pin(); + let removed = self.art.remove(*entry_id, &guard); + if removed.is_some() { + self.entry_count.fetch_sub(1, Ordering::Relaxed); + } + removed + } + pub(crate) fn reset(&self) { let guard = self.art.pin(); self.art.keys().into_iter().for_each(|k| { diff --git a/src/core/src/cache/io_context.rs b/src/core/src/cache/io_context.rs index 8f804fb2..a9c8974f 100644 --- a/src/core/src/cache/io_context.rs +++ b/src/core/src/cache/io_context.rs @@ -13,12 +13,12 @@ use crate::{ liquid_array::SqueezeIoHandler, }; -/// A trait for objects that can handle IO operations for the cache. +/// Per-entry metadata used by the cache. /// -/// All IO is key-based: entries are identified by their [`EntryID`] and stored -/// in a [`t4::Store`] rather than as individual files on disk. -#[async_trait::async_trait] -pub trait IoContext: Debug + Send + Sync { +/// This trait covers only the metadata side of the cache: where to find a +/// batch's compressor and squeeze hints. All actual byte IO goes through the +/// [`t4::Store`] held by the cache itself. +pub trait EntryMetadata: Debug + Send + Sync { /// Add a squeeze hint for an entry. fn add_squeeze_hint(&self, _entry_id: &EntryID, _expression: Arc) { // Do nothing by default @@ -27,7 +27,7 @@ pub trait IoContext: Debug + Send + Sync { /// Get the squeeze hint for an entry. /// If None, the entry will be evicted to disk entirely. /// If Some, the entry will be squeezed according to the cache expressions previously recorded for this column. - /// For example, if expression is ExtractDate32 { field: Date32Field::Year }, + /// For example, if expression is `ExtractDate32 { fields: [Date32Field::Year] }`, /// the entry will be squeezed to a [crate::liquid_array::SqueezedDate32Array] with the year /// component (Date32 or Timestamp input). fn squeeze_hint(&self, _entry_id: &EntryID) -> Option> { @@ -36,44 +36,34 @@ pub trait IoContext: Debug + Send + Sync { /// Get the compressor for an entry. fn get_compressor(&self, entry_id: &EntryID) -> Arc; - - /// Read bytes for the given entry, optionally restricted to the provided range. - async fn read( - &self, - entry_id: &EntryID, - range: Option>, - ) -> Result; - - /// Write data for the given entry. - async fn write(&self, entry_id: &EntryID, data: Bytes) -> Result<(), std::io::Error>; } /// Convert an [`EntryID`] to a t4 key (8-byte little-endian representation). -fn entry_id_to_key(entry_id: &EntryID) -> Vec { +pub(crate) fn entry_id_to_key(entry_id: &EntryID) -> Vec { usize::from(*entry_id).to_le_bytes().to_vec() } -/// A default implementation of [`IoContext`] backed by a [`t4::Store`]. -#[derive(Debug)] -pub struct DefaultIoContext { +/// A default implementation of [`EntryMetadata`]. +/// +/// All entries share a single [`LiquidCompressorStates`] and squeeze hints are +/// stored in a flat map keyed by [`EntryID`]. +#[derive(Debug, Default)] +pub struct DefaultCacheMetadata { compressor_state: Arc, squeeze_hints: RwLock>>, - store: t4::Store, } -impl DefaultIoContext { - /// Create a new instance of [`DefaultIoContext`] backed by the given [`t4::Store`]. - pub fn new(store: t4::Store) -> Self { +impl DefaultCacheMetadata { + /// Create a new instance of [`DefaultCacheMetadata`]. + pub fn new() -> Self { Self { compressor_state: Arc::new(LiquidCompressorStates::new()), - store, squeeze_hints: RwLock::new(AHashMap::new()), } } } -#[async_trait::async_trait] -impl IoContext for DefaultIoContext { +impl EntryMetadata for DefaultCacheMetadata { fn add_squeeze_hint(&self, entry_id: &EntryID, expression: Arc) { let mut guard = self.squeeze_hints.write().unwrap(); guard.insert(*entry_id, expression); @@ -87,57 +77,21 @@ impl IoContext for DefaultIoContext { fn get_compressor(&self, _entry_id: &EntryID) -> Arc { self.compressor_state.clone() } - - async fn read( - &self, - entry_id: &EntryID, - range: Option>, - ) -> Result { - let key = entry_id_to_key(entry_id); - match range { - Some(range) => { - let len = range.end - range.start; - let bytes = self - .store - .get_range(&key, range.start, len) - .await - .map_err(|e| std::io::Error::other(e.to_string()))?; - Ok(Bytes::from(bytes)) - } - None => { - let bytes = self - .store - .get(&key) - .await - .map_err(|e| std::io::Error::other(e.to_string()))?; - Ok(Bytes::from(bytes)) - } - } - } - - async fn write(&self, entry_id: &EntryID, data: Bytes) -> Result<(), std::io::Error> { - let key = entry_id_to_key(entry_id); - self.store - .put(key, data.to_vec()) - .await - .map_err(|e| std::io::Error::other(e.to_string()))?; - Ok(()) - } } -/// A default implementation of [SqueezeIoHandler] that uses the default [IoContext]. +/// A default implementation of [SqueezeIoHandler] backed by a [`t4::Store`]. #[derive(Debug)] pub struct DefaultSqueezeIo { - io_context: Arc, + store: t4::Store, entry_id: EntryID, observer: Arc, } impl DefaultSqueezeIo { /// Create a new instance of [DefaultSqueezeIo]. - pub fn new(io_context: Arc, entry_id: EntryID, observer: Arc) -> Self { + pub fn new(store: t4::Store, entry_id: EntryID, observer: Arc) -> Self { Self { - io_context, + store, entry_id, observer, } @@ -147,7 +101,22 @@ impl DefaultSqueezeIo { #[async_trait::async_trait] impl SqueezeIoHandler for DefaultSqueezeIo { async fn read(&self, range: Option>) -> std::io::Result { - let bytes = self.io_context.read(&self.entry_id, range).await?; + let key = entry_id_to_key(&self.entry_id); + let bytes = match range { + Some(range) => { + let len = range.end - range.start; + self.store + .get_range(&key, range.start, len) + .await + .map_err(|e| std::io::Error::other(e.to_string()))? + } + None => self + .store + .get(&key) + .await + .map_err(|e| std::io::Error::other(e.to_string()))?, + }; + let bytes = Bytes::from(bytes); self.observer .record_internal(InternalEvent::IoReadSqueezedBacking { entry: self.entry_id, diff --git a/src/core/src/cache/liquid_expr.rs b/src/core/src/cache/liquid_expr.rs index d7dd42d9..a317181c 100644 --- a/src/core/src/cache/liquid_expr.rs +++ b/src/core/src/cache/liquid_expr.rs @@ -2,8 +2,7 @@ use arrow_schema::DataType; use datafusion_common::ScalarValue; use datafusion_expr_common::operator::Operator; use datafusion_physical_expr::expressions::{ - BinaryExpr, CastColumnExpr, CastExpr, Column, DynamicFilterPhysicalExpr, LikeExpr, Literal, - TryCastExpr, + BinaryExpr, CastExpr, Column, DynamicFilterPhysicalExpr, LikeExpr, Literal, TryCastExpr, }; use datafusion_physical_expr::{PhysicalExpr, ScalarFunctionExpr}; @@ -55,7 +54,7 @@ impl LiquidExpr { } fn unwrap_dynamic_filter(expr: &Arc) -> Option> { - if let Some(dynamic_filter) = expr.as_any().downcast_ref::() { + if let Some(dynamic_filter) = expr.downcast_ref::() { dynamic_filter.current().ok() } else { Some(expr.clone()) @@ -67,15 +66,15 @@ fn supports_expr( data_type: &DataType, expression_hint: Option<&CacheExpression>, ) -> bool { - if let Some(binary) = expr.as_any().downcast_ref::() { + if let Some(binary) = expr.downcast_ref::() { return supports_binary_expr(binary, data_type, expression_hint); } - if let Some(like_expr) = expr.as_any().downcast_ref::() { + if let Some(like_expr) = expr.downcast_ref::() { return supports_like_expr(like_expr, data_type, expression_hint); } - if let Some(literal) = expr.as_any().downcast_ref::() { + if let Some(literal) = expr.downcast_ref::() { return matches!(literal.value(), ScalarValue::Boolean(Some(_))) && is_byte_like(data_type); } @@ -87,7 +86,7 @@ fn supports_binary_expr( data_type: &DataType, expression_hint: Option<&CacheExpression>, ) -> bool { - let Some(literal) = binary.right().as_any().downcast_ref::() else { + let Some(literal) = binary.right().downcast_ref::() else { return false; }; let op = binary.op(); @@ -137,7 +136,6 @@ fn supports_like_expr( } like_expr .pattern() - .as_any() .downcast_ref::() .and_then(|literal| get_bytes_needle(literal.value())) .is_some() @@ -148,23 +146,20 @@ fn is_substring_hint_enabled(expression_hint: Option<&CacheExpression>) -> bool } fn is_column_like(expr: &Arc) -> bool { - if expr.as_any().downcast_ref::().is_some() { + if expr.downcast_ref::().is_some() { return true; } - if let Some(cast_expr) = expr.as_any().downcast_ref::() { + if let Some(cast_expr) = expr.downcast_ref::() { return is_column_like(cast_expr.expr()); } - if let Some(cast_column_expr) = expr.as_any().downcast_ref::() { - return is_column_like(cast_column_expr.expr()); - } - if let Some(try_cast_expr) = expr.as_any().downcast_ref::() { + if let Some(try_cast_expr) = expr.downcast_ref::() { return is_column_like(try_cast_expr.expr()); } false } fn is_to_timestamp_seconds_column(expr: &Arc) -> bool { - if let Some(func) = expr.as_any().downcast_ref::() + if let Some(func) = expr.downcast_ref::() && func.name() == "to_timestamp_seconds" && let [arg] = func.args() { diff --git a/src/core/src/cache/mod.rs b/src/core/src/cache/mod.rs index 663f19fd..4f52abe5 100644 --- a/src/core/src/cache/mod.rs +++ b/src/core/src/cache/mod.rs @@ -13,13 +13,13 @@ pub mod policies; mod transcode; mod utils; -pub use builders::{EvaluatePredicate, Get, Insert, LiquidCacheBuilder}; +pub use builders::{EvaluatePredicate, Get, Insert, LiquidCacheBuilder, default_max_memory_bytes}; pub use cached_batch::{CacheEntry, CachedBatchType}; pub use core::LiquidCache; pub use expressions::{CacheExpression, VariantRequest}; #[cfg(test)] pub(crate) use io_context::TestSqueezeIo; -pub use io_context::{DefaultIoContext, DefaultSqueezeIo, IoContext}; +pub use io_context::{DefaultCacheMetadata, DefaultSqueezeIo, EntryMetadata}; pub use liquid_expr::LiquidExpr; pub use observer::EventTrace; pub use observer::Observer; @@ -31,6 +31,10 @@ pub use policies::{ pub use transcode::{transcode_liquid_inner, transcode_liquid_inner_with_hint}; pub use utils::{EntryID, LiquidCompressorStates}; +/// The cache could not reserve enough disk budget for a write. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CacheFull; + // Backwards-compatible module paths for existing imports. /// Legacy path: re-export cache policy types under `cache::cache_policies`. pub mod cache_policies { diff --git a/src/core/src/cache/observer/internal_tracing.rs b/src/core/src/cache/observer/internal_tracing.rs index b73e8ad6..21c4a534 100644 --- a/src/core/src/cache/observer/internal_tracing.rs +++ b/src/core/src/cache/observer/internal_tracing.rs @@ -24,6 +24,10 @@ pub(crate) enum InternalEvent { kind: CachedBatchType, bytes: usize, }, + DiskEvict { + entry: EntryID, + bytes: usize, + }, IoReadSqueezedBacking { entry: EntryID, bytes: usize, @@ -119,6 +123,14 @@ impl fmt::Display for InternalEvent { bytes ) } + InternalEvent::DiskEvict { entry, bytes } => { + write!( + f, + "event=disk_evict entry={} bytes={}", + usize::from(*entry), + bytes + ) + } InternalEvent::IoReadSqueezedBacking { entry, bytes } => { write!( f, diff --git a/src/core/src/cache/observer/mod.rs b/src/core/src/cache/observer/mod.rs index cba1c300..38de5fc4 100644 --- a/src/core/src/cache/observer/mod.rs +++ b/src/core/src/cache/observer/mod.rs @@ -99,9 +99,15 @@ impl Observer { self.runtime.incr_hit_date32_expression(); } + #[inline] + pub(crate) fn on_disk_reservation_failure(&self) { + self.runtime.incr_disk_reservation_failures(); + } + pub(crate) fn record_internal(&self, event: InternalEvent) { match event { InternalEvent::IoWrite { .. } => self.runtime.incr_write_io_count(), + InternalEvent::DiskEvict { .. } => self.runtime.incr_disk_evictions(), InternalEvent::IoReadArrow { .. } | InternalEvent::IoReadLiquid { .. } => { self.runtime.incr_read_io_count() } diff --git a/src/core/src/cache/observer/stats.rs b/src/core/src/cache/observer/stats.rs index f81d57f5..fa0c3d9a 100644 --- a/src/core/src/cache/observer/stats.rs +++ b/src/core/src/cache/observer/stats.rs @@ -103,6 +103,8 @@ define_runtime_stats! { (hit_date32_expression_calls, "Number of `hit_date32_expression` calls.", incr_hit_date32_expression), (read_io_count, "Number of read IO operations.", incr_read_io_count), (write_io_count, "Number of write IO operations.", incr_write_io_count), + (disk_evictions, "Number of disk cache entries evicted.", incr_disk_evictions), + (disk_reservation_failures, "Number of failed disk budget reservations.", incr_disk_reservation_failures), (eval_predicate_on_liquid_failed, "Number of `eval_predicate` calls that failed on Liquid array.", incr_eval_predicate_on_liquid_failed), (squeezed_decompressed_count, "Number of decompressed Squeezed-Liquid entries.", __incr_squeezed_decompressed_count), (squeezed_total_count, "Total number of Squeezed-Liquid entries.", __incr_squeezed_total_count), @@ -144,8 +146,10 @@ pub struct CacheStats { pub memory_usage_bytes: usize, /// Total disk usage of the cache. pub disk_usage_bytes: usize, - /// Maximum cache size. - pub max_cache_bytes: usize, + /// Maximum memory size. + pub max_memory_bytes: usize, + /// Maximum disk size. + pub max_disk_bytes: usize, /// Runtime counters snapshot. pub runtime: RuntimeStatsSnapshot, } diff --git a/src/core/src/cache/policies/cache/clock.rs b/src/core/src/cache/policies/cache/clock.rs deleted file mode 100644 index c9b40555..00000000 --- a/src/core/src/cache/policies/cache/clock.rs +++ /dev/null @@ -1,361 +0,0 @@ -//! CLOCK (second-chance) cache policy implementation with optional size awareness. - -use std::{collections::HashMap, fmt, ptr::NonNull, sync::Arc}; - -use crate::{ - cache::{cached_batch::CachedBatchType, utils::EntryID}, - sync::Mutex, -}; - -use super::{ - CachePolicy, - doubly_linked_list::{DoublyLinkedList, DoublyLinkedNode, drop_boxed_node}, -}; - -type ClockEntrySizeFn = Option usize + Send + Sync>>; - -/// The CLOCK (second-chance) eviction policy with optional size awareness. -#[derive(Default)] -pub struct ClockPolicy { - state: Mutex, - size_of: ClockEntrySizeFn, -} - -#[derive(Debug)] -struct ClockNode { - entry_id: EntryID, - referenced: bool, -} - -type NodePtr = NonNull>; - -#[derive(Debug, Default)] -struct ClockInternalState { - map: HashMap, - list: DoublyLinkedList, - hand: Option, - total_size: usize, -} - -impl fmt::Debug for ClockPolicy { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ClockPolicy") - .field("state", &self.state) - .finish() - } -} - -impl ClockPolicy { - /// Create a new CLOCK policy. - pub fn new() -> Self { - Self::new_with_size_fn(None) - } - - /// Create a new CLOCK policy with size awareness. - pub fn new_with_size_fn(size_of: ClockEntrySizeFn) -> Self { - ClockPolicy { - state: Mutex::new(ClockInternalState::default()), - size_of, - } - } - - fn entry_size(&self, entry_id: &EntryID) -> usize { - self.size_of.as_ref().map(|f| f(entry_id)).unwrap_or(1) - } -} - -unsafe impl Send for ClockPolicy {} -unsafe impl Sync for ClockPolicy {} - -impl CachePolicy for ClockPolicy { - fn find_victim(&self, cnt: usize) -> Vec { - let mut state = self.state.lock().unwrap(); - if cnt == 0 { - return Vec::new(); - } - - let mut evicted = Vec::with_capacity(cnt); - let mut cursor = match state.hand { - Some(ptr) => Some(ptr), - None => state.list.head(), - }; - - for _ in 0..cnt { - loop { - let Some(handle) = cursor else { - state.hand = None; - break; - }; - - let mut handle_ptr = handle; - if unsafe { handle_ptr.as_ref() }.data.referenced { - unsafe { handle_ptr.as_mut() }.data.referenced = false; - let next = unsafe { handle_ptr.as_ref().next }.or(state.list.head()); - cursor = next; - state.hand = next; - } else { - let victim_id = unsafe { handle_ptr.as_ref().data.entry_id }; - let succ = unsafe { handle_ptr.as_ref().next }; - state - .map - .remove(&victim_id) - .expect("pointer must exist in map"); - unsafe { - state.list.unlink(handle_ptr); - drop_boxed_node(handle_ptr); - } - state.total_size -= self.entry_size(&victim_id); - state.hand = succ.or(state.list.head()); - evicted.push(victim_id); - cursor = state.hand; - break; - } - } - - if state.hand.is_none() { - break; - } - } - - evicted - } - - fn notify_insert(&self, entry_id: &EntryID, _batch_type: CachedBatchType) { - let mut state = self.state.lock().unwrap(); - - if let Some(mut existing) = state.map.get(entry_id).copied() { - unsafe { - existing.as_mut().data.referenced = true; - } - return; - } - - let node = DoublyLinkedNode::new(ClockNode { - entry_id: *entry_id, - referenced: true, - }); - let new_ptr = NonNull::from(Box::leak(node)); - - unsafe { state.list.push_back(new_ptr) }; - if state.hand.is_none() { - state.hand = Some(new_ptr); - } - - state.map.insert(*entry_id, new_ptr); - state.total_size += self.entry_size(entry_id); - } - - fn notify_access(&self, entry_id: &EntryID, _batch_type: CachedBatchType) { - let state = self.state.lock().unwrap(); - if let Some(mut handle) = state.map.get(entry_id).copied() { - unsafe { - handle.as_mut().data.referenced = true; - } - } - } -} - -impl Drop for ClockPolicy { - fn drop(&mut self) { - if let Ok(mut state) = self.state.lock() { - let handles: Vec<_> = state.map.drain().map(|(_, ptr)| ptr).collect(); - for ptr in handles { - unsafe { - state.list.unlink(ptr); - drop_boxed_node(ptr); - } - } - unsafe { - state.list.drop_all(); - } - state.hand = None; - state.total_size = 0; - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::cache::{ - cached_batch::CacheEntry, - utils::{EntryID, create_cache_store, create_test_arrow_array}, - }; - - fn entry(id: usize) -> EntryID { - id.into() - } - - #[test] - fn test_clock_policy_insertion_order() { - let advisor = ClockPolicy::new(); - - let entry_id1 = EntryID::from(1); - let entry_id2 = EntryID::from(2); - let entry_id3 = EntryID::from(3); - - advisor.notify_insert(&entry_id1, CachedBatchType::MemoryArrow); - advisor.notify_insert(&entry_id2, CachedBatchType::MemoryArrow); - advisor.notify_insert(&entry_id3, CachedBatchType::MemoryArrow); - - assert_eq!(advisor.find_victim(1), vec![entry_id1]); - } - - #[test] - fn test_clock_policy_sequential_evictions() { - let advisor = ClockPolicy::new(); - - let entry_id1 = EntryID::from(1); - let entry_id2 = EntryID::from(2); - let entry_id3 = EntryID::from(3); - - advisor.notify_insert(&entry_id1, CachedBatchType::MemoryArrow); - advisor.notify_insert(&entry_id2, CachedBatchType::MemoryArrow); - advisor.notify_insert(&entry_id3, CachedBatchType::MemoryArrow); - - assert_eq!(advisor.find_victim(1), vec![entry_id1]); - assert_eq!(advisor.find_victim(1), vec![entry_id2]); - assert_eq!(advisor.find_victim(1), vec![entry_id3]); - } - - #[test] - fn test_clock_policy_single_item() { - let advisor = ClockPolicy::new(); - - let entry_id1 = EntryID::from(1); - advisor.notify_insert(&entry_id1, CachedBatchType::MemoryArrow); - - assert_eq!(advisor.find_victim(1), vec![entry_id1]); - } - - #[test] - fn test_clock_policy_advise_empty() { - let advisor = ClockPolicy::new(); - - assert_eq!(advisor.find_victim(1), vec![]); - } - - #[tokio::test] - async fn test_clock_policy_integration_with_store() { - let advisor = ClockPolicy::new(); - let store = create_cache_store(3100, Box::new(advisor)).await; - - let entry_id1 = EntryID::from(1); - let entry_id2 = EntryID::from(2); - let entry_id3 = EntryID::from(3); - - store.insert(entry_id1, create_test_arrow_array(100)).await; - store.insert(entry_id2, create_test_arrow_array(100)).await; - store.insert(entry_id3, create_test_arrow_array(100)).await; - - let entry_id4 = EntryID::from(4); - store.insert(entry_id4, create_test_arrow_array(100)).await; - - let data = store.index().get(&entry_id1).unwrap(); - assert!(matches!(data.as_ref(), CacheEntry::DiskLiquid(_))); - assert!(store.index().get(&entry_id2).is_some()); - assert!(store.index().get(&entry_id3).is_some()); - assert!(store.index().get(&entry_id4).is_some()); - } - - #[test] - fn test_clock_policy_size_awareness_with_closure() { - let policy = - ClockPolicy::new_with_size_fn(Some(Arc::new( - |id: &EntryID| { - if id.gt(&entry(10)) { 100 } else { 1 } - }, - ))); - - let e1 = entry(1); - let e2 = entry(2); - let e3 = entry(11); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - policy.notify_insert(&e3, CachedBatchType::MemoryArrow); - - let state = policy.state.lock().unwrap(); - assert_eq!(state.total_size, 102); - } - - #[test] - fn test_clock_policy_size_awareness_without_closure() { - let policy = ClockPolicy::new(); - - let e1 = entry(1); - let e2 = entry(2); - let e3 = entry(11); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - policy.notify_insert(&e3, CachedBatchType::MemoryArrow); - - let state = policy.state.lock().unwrap(); - assert_eq!(state.total_size, 3); - } - - #[test] - fn test_clock_policy_size_tracking_on_eviction() { - let policy = - ClockPolicy::new_with_size_fn(Some(Arc::new( - |id: &EntryID| { - if id.gt(&entry(10)) { 100 } else { 1 } - }, - ))); - - let e1 = entry(1); - let e2 = entry(2); - let e3 = entry(11); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - policy.notify_insert(&e3, CachedBatchType::MemoryArrow); - - { - let state = policy.state.lock().unwrap(); - assert_eq!(state.total_size, 102); - } - - let evicted = policy.find_victim(1); - assert_eq!(evicted, vec![e1]); - - { - let state = policy.state.lock().unwrap(); - assert_eq!(state.total_size, 101); - } - - let evicted = policy.find_victim(1); - assert_eq!(evicted, vec![e2]); - - { - let state = policy.state.lock().unwrap(); - assert_eq!(state.total_size, 100); - } - } - - #[test] - fn test_clock_policy_reinsert_sets_reference_bit() { - let policy = ClockPolicy::new(); - let entry_id = entry(42); - - policy.notify_insert(&entry_id, CachedBatchType::MemoryArrow); - - { - let state = policy.state.lock().unwrap(); - let mut node_ptr = state.map.get(&entry_id).copied().unwrap(); - unsafe { - node_ptr.as_mut().data.referenced = false; - } - } - - policy.notify_insert(&entry_id, CachedBatchType::MemoryArrow); - - let state = policy.state.lock().unwrap(); - let node_ptr = state.map.get(&entry_id).copied().unwrap(); - unsafe { - assert!(node_ptr.as_ref().data.referenced); - } - assert_eq!(state.map.len(), 1); - } -} diff --git a/src/core/src/cache/policies/cache/doubly_linked_list.rs b/src/core/src/cache/policies/cache/doubly_linked_list.rs index e85c6fdc..1a48caaf 100644 --- a/src/core/src/cache/policies/cache/doubly_linked_list.rs +++ b/src/core/src/cache/policies/cache/doubly_linked_list.rs @@ -43,6 +43,7 @@ impl DoublyLinkedList { self.head } + #[allow(dead_code)] pub(crate) fn tail(&self) -> Option>> { self.tail } @@ -78,6 +79,7 @@ impl DoublyLinkedList { } /// Moves an existing node to the front of the list. + #[allow(dead_code)] pub(crate) unsafe fn move_to_front(&mut self, node_ptr: NonNull>) { unsafe { self.unlink(node_ptr); @@ -121,65 +123,3 @@ impl DoublyLinkedList { pub(crate) unsafe fn drop_boxed_node(ptr: NonNull>) { unsafe { drop(Box::from_raw(ptr.as_ptr())) } } - -#[cfg_attr(not(rust_analyzer), cfg(kani))] -mod proofs { - use super::*; - use kani::any; - use std::ptr::NonNull; - - #[cfg_attr(not(rust_analyzer), kani::proof)] - #[cfg_attr(not(rust_analyzer), kani::unwind(12))] - fn kani_linked_list_push_front() { - let mut list = DoublyLinkedList::::new(); - - // Choose n in [0, 3] non-deterministically - let n_raw: u8 = any(); - kani::assume(n_raw < 4); - let n: usize = n_raw as usize; - - // Track first (future tail) and last (future head) inserted nodes - let mut first_inserted: Option>> = None; - let mut last_inserted: Option>> = None; - - for i in 0..n { - let value: u8 = any(); - let boxed = DoublyLinkedNode::new(value); - let ptr = NonNull::from(Box::leak(boxed)); - if i == 0 { - first_inserted = Some(ptr); - } - last_inserted = Some(ptr); - unsafe { list.push_front(ptr) }; - } - - match n { - 0 => { - assert!(list.head().is_none()); - assert!(list.tail().is_none()); - } - _ => { - let head_ptr = list.head().expect("non-empty list must have head"); - let tail_ptr = list.tail().expect("non-empty list must have tail"); - assert_eq!(head_ptr.as_ptr(), last_inserted.unwrap().as_ptr()); - assert_eq!(tail_ptr.as_ptr(), first_inserted.unwrap().as_ptr()); - - let head_ref = unsafe { head_ptr.as_ref() }; - assert!(head_ref.prev.is_none()); - let tail_ref = unsafe { tail_ptr.as_ref() }; - assert!(tail_ref.next.is_none()); - } - } - - // Count entries by traversing from head using next pointers - let mut count: usize = 0; - let mut current = list.head(); - while let Some(node_ptr) = current { - count += 1; - current = unsafe { node_ptr.as_ref().next }; - } - assert_eq!(count, n); - - unsafe { list.drop_all() }; - } -} diff --git a/src/core/src/cache/policies/cache/filo.rs b/src/core/src/cache/policies/cache/filo.rs deleted file mode 100644 index 41269f36..00000000 --- a/src/core/src/cache/policies/cache/filo.rs +++ /dev/null @@ -1,295 +0,0 @@ -//! FILO (First In, Last Out) and FIFO cache policy implementations. - -use std::{collections::HashMap, ptr::NonNull}; - -use crate::{ - cache::{cached_batch::CachedBatchType, utils::EntryID}, - sync::Mutex, -}; - -use super::{ - CachePolicy, - doubly_linked_list::{DoublyLinkedList, DoublyLinkedNode, drop_boxed_node}, -}; - -#[derive(Debug)] -struct QueueNode { - entry_id: EntryID, -} - -type NodePtr = NonNull>; - -#[derive(Debug, Default)] -struct QueueState { - map: HashMap, - list: DoublyLinkedList, -} - -impl QueueState { - fn is_empty(&self) -> bool { - self.list.head().is_none() - } - - fn insert_front(&mut self, entry_id: EntryID) { - if let Some(ptr) = self.map.get(&entry_id).copied() { - unsafe { - self.list.unlink(ptr); - self.list.push_front(ptr); - } - return; - } - - let node = DoublyLinkedNode::new(QueueNode { entry_id }); - let ptr = NonNull::from(Box::leak(node)); - - self.map.insert(entry_id, ptr); - unsafe { - self.list.push_front(ptr); - } - } - - fn insert_back(&mut self, entry_id: EntryID) { - if let Some(ptr) = self.map.get(&entry_id).copied() { - unsafe { - self.list.unlink(ptr); - self.list.push_back(ptr); - } - return; - } - - let node = DoublyLinkedNode::new(QueueNode { entry_id }); - let ptr = NonNull::from(Box::leak(node)); - - self.map.insert(entry_id, ptr); - unsafe { - self.list.push_back(ptr); - } - } - - fn pop_front(&mut self) -> Option { - let head_ptr = self.list.head()?; - let entry_id = unsafe { head_ptr.as_ref().data.entry_id }; - let node_ptr = self - .map - .remove(&entry_id) - .expect("head pointer must have map entry"); - unsafe { - self.list.unlink(node_ptr); - drop_boxed_node(node_ptr); - } - Some(entry_id) - } -} - -impl Drop for QueueState { - fn drop(&mut self) { - let handles: Vec<_> = self.map.drain().map(|(_, ptr)| ptr).collect(); - for ptr in handles { - unsafe { - self.list.unlink(ptr); - drop_boxed_node(ptr); - } - } - unsafe { - self.list.drop_all(); - } - } -} - -/// The policy that implements the FILO (First In, Last Out) algorithm. -/// Newest entries are evicted first. -#[derive(Debug, Default)] -pub struct FiloPolicy { - state: Mutex, -} - -impl FiloPolicy { - /// Create a new [`FiloPolicy`]. - pub fn new() -> Self { - Self { - state: Mutex::new(QueueState::default()), - } - } -} - -// SAFETY: Access to raw pointers is protected by the internal `Mutex`. -unsafe impl Send for FiloPolicy {} -unsafe impl Sync for FiloPolicy {} - -impl CachePolicy for FiloPolicy { - fn find_victim(&self, cnt: usize) -> Vec { - if cnt == 0 { - return vec![]; - } - - let mut state = self.state.lock().unwrap(); - if state.is_empty() { - return vec![]; - } - - let mut victims = Vec::with_capacity(cnt); - for _ in 0..cnt { - let Some(entry) = state.pop_front() else { - break; - }; - victims.push(entry); - } - victims - } - - fn notify_insert(&self, entry_id: &EntryID, _batch_type: CachedBatchType) { - let mut state = self.state.lock().unwrap(); - state.insert_front(*entry_id); - } -} - -/// The policy that implements the FIFO (First In, First Out) algorithm. -/// Oldest entries are evicted first. -#[derive(Debug, Default)] -pub struct FifoPolicy { - state: Mutex, -} - -impl FifoPolicy { - /// Create a new [`FifoPolicy`]. - pub fn new() -> Self { - Self { - state: Mutex::new(QueueState::default()), - } - } -} - -// SAFETY: Access to raw pointers is protected by the internal `Mutex`. -unsafe impl Send for FifoPolicy {} -unsafe impl Sync for FifoPolicy {} - -impl CachePolicy for FifoPolicy { - fn find_victim(&self, cnt: usize) -> Vec { - if cnt == 0 { - return vec![]; - } - - let mut state = self.state.lock().unwrap(); - if state.is_empty() { - return vec![]; - } - - let mut victims = Vec::with_capacity(cnt); - for _ in 0..cnt { - let Some(entry) = state.pop_front() else { - break; - }; - victims.push(entry); - } - victims - } - - fn notify_insert(&self, entry_id: &EntryID, _batch_type: CachedBatchType) { - let mut state = self.state.lock().unwrap(); - state.insert_back(*entry_id); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::cache::cached_batch::{CacheEntry, CachedBatchType}; - use crate::cache::utils::{EntryID, create_cache_store, create_test_arrow_array}; - - fn entry(id: usize) -> EntryID { - id.into() - } - - #[tokio::test] - async fn test_filo_advisor() { - let advisor = FiloPolicy::new(); - let store = create_cache_store(3100, Box::new(advisor)).await; - - let entry_id1 = EntryID::from(1); - let entry_id2 = EntryID::from(2); - let entry_id3 = EntryID::from(3); - - store.insert(entry_id1, create_test_arrow_array(100)).await; - - let data = store.index().get(&entry_id1).unwrap(); - assert!(matches!(data.as_ref(), CacheEntry::MemoryArrow(_))); - store.insert(entry_id2, create_test_arrow_array(100)).await; - store.insert(entry_id3, create_test_arrow_array(100)).await; - - let entry_id4: EntryID = EntryID::from(4); - store.insert(entry_id4, create_test_arrow_array(100)).await; - - assert!(store.index().get(&entry_id1).is_some()); - assert!(store.index().get(&entry_id2).is_some()); - assert!(store.index().get(&entry_id4).is_some()); - - let data = store.index().get(&entry_id3).unwrap(); - assert!(matches!(data.as_ref(), CacheEntry::DiskLiquid(_))); - } - - #[test] - fn test_filo_advise_empty() { - let policy = FiloPolicy::new(); - assert!(policy.find_victim(1).is_empty()); - } - - #[test] - fn test_filo_advise_order() { - let policy = FiloPolicy::new(); - let e1 = entry(1); - let e2 = entry(2); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - - assert_eq!(policy.find_victim(1), vec![e2]); - assert_eq!(policy.find_victim(1), vec![e1]); - } - - #[test] - fn test_filo_reinsert_moves_to_front() { - let policy = FiloPolicy::new(); - let first = entry(1); - let second = entry(2); - - policy.notify_insert(&first, CachedBatchType::MemoryArrow); - policy.notify_insert(&second, CachedBatchType::MemoryArrow); - policy.notify_insert(&first, CachedBatchType::MemoryArrow); - - assert_eq!(policy.find_victim(1), vec![first]); - assert_eq!(policy.find_victim(1), vec![second]); - } - - #[test] - fn test_fifo_advise_empty() { - let policy = FifoPolicy::new(); - assert!(policy.find_victim(1).is_empty()); - } - - #[test] - fn test_fifo_advise_order() { - let policy = FifoPolicy::new(); - let e1 = entry(1); - let e2 = entry(2); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - - assert_eq!(policy.find_victim(1), vec![e1]); - assert_eq!(policy.find_victim(1), vec![e2]); - } - - #[test] - fn test_fifo_reinsert_moves_to_back() { - let policy = FifoPolicy::new(); - let first = entry(1); - let second = entry(2); - - policy.notify_insert(&first, CachedBatchType::MemoryArrow); - policy.notify_insert(&second, CachedBatchType::MemoryArrow); - policy.notify_insert(&first, CachedBatchType::MemoryArrow); - - assert_eq!(policy.find_victim(1), vec![second]); - assert_eq!(policy.find_victim(1), vec![first]); - } -} diff --git a/src/core/src/cache/policies/cache/lru.rs b/src/core/src/cache/policies/cache/lru.rs deleted file mode 100644 index 794e8500..00000000 --- a/src/core/src/cache/policies/cache/lru.rs +++ /dev/null @@ -1,382 +0,0 @@ -//! LRU cache policy implementation using a hash map and doubly linked list. - -use std::{collections::HashMap, ptr::NonNull}; - -use crate::{ - cache::{cached_batch::CachedBatchType, utils::EntryID}, - sync::Mutex, -}; - -use super::{ - CachePolicy, - doubly_linked_list::{DoublyLinkedList, DoublyLinkedNode, drop_boxed_node}, -}; - -#[derive(Debug)] -struct LruNode { - entry_id: EntryID, -} - -type NodePtr = NonNull>; - -#[derive(Debug, Default)] -struct HashList { - map: HashMap, - list: DoublyLinkedList, -} - -impl HashList { - fn tail(&self) -> Option { - self.list.tail() - } - - unsafe fn move_to_front(&mut self, node_ptr: NodePtr) { - unsafe { self.list.move_to_front(node_ptr) }; - } - - unsafe fn push_front(&mut self, node_ptr: NodePtr) { - unsafe { self.list.push_front(node_ptr) }; - } - - unsafe fn remove_and_release(&mut self, node_ptr: NodePtr) { - unsafe { - self.list.unlink(node_ptr); - drop_boxed_node(node_ptr); - } - } -} - -impl Drop for HashList { - fn drop(&mut self) { - for (_, node_ptr) in self.map.drain() { - unsafe { - self.list.unlink(node_ptr); - drop_boxed_node(node_ptr); - } - } - // Any nodes not tracked in the map (shouldn't happen) get cleaned up here. - unsafe { - self.list.drop_all(); - } - } -} - -/// The policy that implement the LRU algorithm using a HashMap and a doubly linked list. -#[derive(Debug, Default)] -pub struct LruPolicy { - state: Mutex, -} - -impl LruPolicy { - /// Create a new [`LruPolicy`]. - pub fn new() -> Self { - Self { - state: Mutex::new(HashList::default()), - } - } -} - -// SAFETY: The Mutex ensures that only one thread accesses the internal state -// (hash map and intrusive list containing NonNull pointers) at a time, making it safe -// to send and share across threads. -unsafe impl Send for LruPolicy {} -unsafe impl Sync for LruPolicy {} - -impl CachePolicy for LruPolicy { - fn find_victim(&self, cnt: usize) -> Vec { - let mut state = self.state.lock().unwrap(); - if cnt == 0 { - return vec![]; - } - - let mut advices = Vec::with_capacity(cnt); - for _ in 0..cnt { - let Some(tail_ptr) = state.tail() else { - break; - }; - let tail_entry_id = unsafe { tail_ptr.as_ref().data.entry_id }; - let node_ptr = state - .map - .remove(&tail_entry_id) - .expect("tail node not found"); - unsafe { - state.remove_and_release(node_ptr); - } - advices.push(tail_entry_id); - } - - advices - } - - fn notify_access(&self, entry_id: &EntryID, _batch_type: CachedBatchType) { - let mut state = self.state.lock().unwrap(); - if let Some(node_ptr) = state.map.get(entry_id).copied() { - unsafe { state.move_to_front(node_ptr) }; - } - } - - fn notify_insert(&self, entry_id: &EntryID, _batch_type: CachedBatchType) { - let mut state = self.state.lock().unwrap(); - - if let Some(existing_node_ptr) = state.map.get(entry_id).copied() { - unsafe { state.move_to_front(existing_node_ptr) }; - return; - } - - let node = DoublyLinkedNode::new(LruNode { - entry_id: *entry_id, - }); - let node_ptr = NonNull::from(Box::leak(node)); - - state.map.insert(*entry_id, node_ptr); - unsafe { - state.push_front(node_ptr); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::cache::utils::{EntryID, create_cache_store, create_test_arrow_array}; - use crate::sync::{Arc, Barrier, thread}; - use std::sync::atomic::{AtomicUsize, Ordering}; - - fn entry(id: usize) -> EntryID { - id.into() - } - - fn assert_evict_advice(policy: &LruPolicy, expect_evict: EntryID) { - let advice = policy.find_victim(1); - assert_eq!(advice, vec![expect_evict]); - } - - #[test] - fn test_lru_policy_insertion_order() { - let policy = LruPolicy::new(); - let e1 = entry(1); - let e2 = entry(2); - let e3 = entry(3); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - policy.notify_insert(&e3, CachedBatchType::MemoryArrow); - - assert_evict_advice(&policy, e1); - } - - #[test] - fn test_lru_policy_access_moves_to_front() { - let policy = LruPolicy::new(); - let e1 = entry(1); - let e2 = entry(2); - let e3 = entry(3); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - policy.notify_insert(&e3, CachedBatchType::MemoryArrow); - - policy.notify_access(&e1, CachedBatchType::MemoryArrow); - assert_evict_advice(&policy, e2); - policy.notify_access(&e2, CachedBatchType::MemoryArrow); - assert_evict_advice(&policy, e3); - } - - #[test] - fn test_lru_policy_reinsert_moves_to_front() { - let policy = LruPolicy::new(); - let e1 = entry(1); - let e2 = entry(2); - let e3 = entry(3); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - policy.notify_insert(&e3, CachedBatchType::MemoryArrow); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - assert_evict_advice(&policy, e2); - } - - #[test] - fn test_lru_policy_advise_empty() { - let policy = LruPolicy::new(); - assert_eq!(policy.find_victim(1), vec![]); - } - - #[test] - fn test_lru_policy_advise_single_item_self() { - let policy = LruPolicy::new(); - let e1 = entry(1); - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - - assert_evict_advice(&policy, e1); - } - - #[test] - fn test_lru_policy_advise_single_item_other() { - let policy = LruPolicy::new(); - let e1 = entry(1); - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - assert_evict_advice(&policy, e1); - } - - #[test] - fn test_lru_policy_access_nonexistent() { - let policy = LruPolicy::new(); - let e1 = entry(1); - let e2 = entry(2); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - - policy.notify_access(&entry(99), CachedBatchType::MemoryArrow); - - assert_evict_advice(&policy, e1); - } - - impl HashList { - fn check_integrity(&self) { - let map_count = self.map.len(); - let forward_count = count_nodes_in_list(self); - let backward_count = count_nodes_reverse(self); - - assert_eq!(map_count, forward_count); - assert_eq!(map_count, backward_count); - } - } - - fn count_nodes_in_list(state: &HashList) -> usize { - let mut count = 0; - let mut current = state.list.head(); - - while let Some(node_ptr) = current { - count += 1; - current = unsafe { node_ptr.as_ref().next }; - } - - count - } - - fn count_nodes_reverse(state: &HashList) -> usize { - let mut count = 0; - let mut current = state.list.tail(); - - while let Some(node_ptr) = current { - count += 1; - current = unsafe { node_ptr.as_ref().prev }; - } - - count - } - - #[test] - fn test_lru_policy_invariants() { - let policy = LruPolicy::new(); - - for i in 0..10 { - policy.notify_insert(&entry(i), CachedBatchType::MemoryArrow); - } - policy.notify_access(&entry(2), CachedBatchType::MemoryArrow); - policy.notify_access(&entry(5), CachedBatchType::MemoryArrow); - policy.find_victim(1); - policy.find_victim(1); - - let state = policy.state.lock().unwrap(); - state.check_integrity(); - - let map_count = state.map.len(); - assert_eq!(map_count, 8); - assert!(!state.map.contains_key(&entry(0))); - assert!(!state.map.contains_key(&entry(1))); - assert!(state.map.contains_key(&entry(2))); - - let head_id = unsafe { state.list.head().unwrap().as_ref().data.entry_id }; - assert_eq!(head_id, entry(5)); - } - - #[test] - fn test_concurrent_lru_operations() { - concurrent_lru_operations(); - } - - #[cfg(feature = "shuttle")] - #[test] - fn shuttle_lru_operations() { - crate::utils::shuttle_test(concurrent_lru_operations); - } - - fn concurrent_lru_operations() { - let policy = Arc::new(LruPolicy::new()); - let num_threads = 4; - let operations_per_thread = 100; - - let total_inserts = Arc::new(AtomicUsize::new(0)); - let total_evictions = Arc::new(AtomicUsize::new(0)); - - let barrier = Arc::new(Barrier::new(num_threads)); - - let mut handles = vec![]; - for thread_id in 0..num_threads { - let policy_clone = policy.clone(); - let total_inserts_clone = total_inserts.clone(); - let total_evictions_clone = total_evictions.clone(); - let barrier_clone = barrier.clone(); - - let handle = thread::spawn(move || { - barrier_clone.wait(); - - for i in 0..operations_per_thread { - let op_type = i % 3; - let entry_id = entry(thread_id * operations_per_thread + i); - - match op_type { - 0 => { - policy_clone.notify_insert(&entry_id, CachedBatchType::MemoryArrow); - total_inserts_clone.fetch_add(1, Ordering::SeqCst); - } - 1 => { - policy_clone.notify_access(&entry_id, CachedBatchType::MemoryArrow); - } - _ => { - let advised = policy_clone.find_victim(1); - if !advised.is_empty() { - total_evictions_clone.fetch_add(1, Ordering::SeqCst); - } - } - } - } - }); - - handles.push(handle); - } - - for handle in handles { - handle.join().unwrap(); - } - - let state = policy.state.lock().unwrap(); - state.check_integrity(); - - let inserts = total_inserts.load(Ordering::SeqCst); - let evictions = total_evictions.load(Ordering::SeqCst); - assert!(inserts >= evictions); - } - - #[tokio::test] - async fn test_lru_integration() { - let policy = LruPolicy::new(); - let store = create_cache_store(3000, Box::new(policy)).await; - - let entry_id1 = EntryID::from(1); - let entry_id2 = EntryID::from(2); - let entry_id3 = EntryID::from(3); - - store.insert(entry_id1, create_test_arrow_array(100)).await; - store.insert(entry_id2, create_test_arrow_array(100)).await; - store.insert(entry_id3, create_test_arrow_array(100)).await; - - assert!(store.index().get(&entry_id1).is_some()); - assert!(store.index().get(&entry_id2).is_some()); - assert!(store.index().get(&entry_id3).is_some()); - } -} diff --git a/src/core/src/cache/policies/cache/mod.rs b/src/core/src/cache/policies/cache/mod.rs index a253d1e3..352f83a0 100644 --- a/src/core/src/cache/policies/cache/mod.rs +++ b/src/core/src/cache/policies/cache/mod.rs @@ -3,32 +3,29 @@ use crate::cache::cached_batch::CachedBatchType; use crate::cache::utils::EntryID; -mod clock; mod doubly_linked_list; -mod filo; -mod lru; -mod s3_fifo; -mod sieve; mod three_queue; -pub use clock::ClockPolicy; -pub use filo::FifoPolicy; -pub use filo::FiloPolicy; -pub use lru::LruPolicy; -pub use s3_fifo::S3FifoPolicy; -pub use sieve::SievePolicy; pub use three_queue::LiquidPolicy; /// The cache policy that guides the replacement of LiquidCache pub trait CachePolicy: std::fmt::Debug + Send + Sync { /// Give cnt amount of entries to evict when cache is full. - fn find_victim(&self, cnt: usize) -> Vec; + fn find_memory_victim(&self, cnt: usize) -> Vec; + + /// Give cnt amount of disk entries to remove when disk is full. + fn find_disk_victim(&self, _cnt: usize) -> Vec { + vec![] + } /// Notify the cache policy that an entry was inserted. fn notify_insert(&self, _entry_id: &EntryID, _batch_type: CachedBatchType) {} /// Notify the cache policy that an entry was accessed. fn notify_access(&self, _entry_id: &EntryID, _batch_type: CachedBatchType) {} + + /// Notify the cache policy that an entry was removed. + fn notify_remove(&self, _entry_id: &EntryID) {} } #[cfg(test)] @@ -56,7 +53,7 @@ mod tests { let advised_entries_clone = advised_entries.clone(); let handle = thread::spawn(move || { - let advice = policy_clone.find_victim(1); + let advice = policy_clone.find_memory_victim(1); if let Some(entry_id) = advice.first() { let mut entries = advised_entries_clone.lock().unwrap(); entries.push(*entry_id); @@ -83,8 +80,7 @@ mod tests { } fn run_concurrent_invariant_tests() { - concurrent_invariant_advice_once(Arc::new(LruPolicy::new())); - concurrent_invariant_advice_once(Arc::new(FiloPolicy::new())); + concurrent_invariant_advice_once(Arc::new(LiquidPolicy::new())); } #[test] diff --git a/src/core/src/cache/policies/cache/s3_fifo.rs b/src/core/src/cache/policies/cache/s3_fifo.rs deleted file mode 100644 index fbd88278..00000000 --- a/src/core/src/cache/policies/cache/s3_fifo.rs +++ /dev/null @@ -1,379 +0,0 @@ -//! S3 FIFO cache policy implementation - -use std::collections::{HashMap, HashSet, VecDeque}; -use std::fmt; -use std::sync::Mutex; - -use crate::cache::CachePolicy; -use crate::cache::{cached_batch::CachedBatchType, utils::EntryID}; - -type EntryFreq = u8; - -#[derive(Debug, Default)] -struct S3FifoInternalState { - small: VecDeque, - main: VecDeque, - ghost: VecDeque, - ghost_set: HashSet, - - frequency: HashMap, - - small_queue_size: usize, - main_queue_size: usize, - total_size: usize, -} - -impl S3FifoInternalState { - fn cap_frequency(freq: u8) -> u8 { - std::cmp::min(freq, 3) - } - - fn inc_frequency(&mut self, entry_id: &EntryID) { - if let Some(freq) = self.frequency.get_mut(entry_id) { - *freq = Self::cap_frequency(*freq + 1); - } - } - - fn dec_frequency(&mut self, entry_id: &EntryID) { - if let Some(freq) = self.frequency.get_mut(entry_id) { - *freq = freq.saturating_sub(1); - } - } - - fn inc_small_queue_size(&mut self, size: usize) { - self.small_queue_size += size; - self.total_size += size; - } - - fn dec_small_queue_size(&mut self, size: usize) { - self.small_queue_size -= size; - self.total_size -= size; - } - - fn inc_main_queue_size(&mut self, size: usize) { - self.main_queue_size += size; - self.total_size += size; - } - - fn dec_main_queue_size(&mut self, size: usize) { - self.main_queue_size -= size; - self.total_size -= size; - } - - fn small_queue_fraction(&self) -> f32 { - if self.total_size == 0 { - 0.0 - } else { - self.small_queue_size as f32 / self.total_size as f32 - } - } - - fn check_if_entry_exists_in_small_or_main(&self, entry_id: &EntryID) -> bool { - self.frequency.contains_key(entry_id) && !self.ghost_set.contains(entry_id) - } -} - -/// The policy that implements object size aware S3Fifo algorithm using Deque. -#[derive(Default)] -pub struct S3FifoPolicy { - state: Mutex, -} - -impl fmt::Debug for S3FifoPolicy { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("S3FifoPolicy") - .field("state", &self.state) - .finish() - } -} - -unsafe impl Send for S3FifoPolicy {} -unsafe impl Sync for S3FifoPolicy {} - -impl S3FifoPolicy { - /// Create a new [`S3FifoPolicy`]. - pub fn new() -> Self { - Self { - state: Mutex::new(S3FifoInternalState::default()), - } - } - - fn entry_size(&self, _entry_id: &EntryID) -> usize { - 1 - } - - fn evict_from_small(&self, state: &mut S3FifoInternalState) -> Option { - let mut is_evicted = false; - let mut victim: Option = None; - - while !is_evicted && !state.small.is_empty() { - let Some(element) = state.small.pop_back() else { - break; - }; - let freq = state.frequency.get(&element).copied().unwrap_or(0); - let entry_size = self.entry_size(&element); - state.dec_small_queue_size(entry_size); - - if freq > 1 { - state.main.push_front(element); - state.inc_main_queue_size(entry_size); - state.frequency.insert(element, 0); - } else { - // Move to ghost queue - state.ghost.push_front(element); - state.ghost_set.insert(element); - state.frequency.remove(&element); - is_evicted = true; - victim = Some(element); - } - } - victim - } - - fn evict_from_main(&self, state: &mut S3FifoInternalState) -> Option { - let mut is_evicted = false; - let mut victim: Option = None; - - while !is_evicted && !state.main.is_empty() { - let Some(element) = state.main.pop_back() else { - break; - }; - - let freq = state.frequency.get(&element).copied().unwrap_or(0); - let entry_size = self.entry_size(&element); - state.dec_main_queue_size(entry_size); - - if freq > 0 { - state.main.push_front(element); - state.dec_frequency(&element); - state.inc_main_queue_size(entry_size); - } else { - state.frequency.remove(&element); - is_evicted = true; - victim = Some(element); - } - } - victim - } -} - -impl CachePolicy for S3FifoPolicy { - fn find_victim(&self, cnt: usize) -> Vec { - let mut state = self.state.lock().unwrap(); - let mut advices = Vec::with_capacity(cnt); - let threshold_for_small_eviction = 0.1; - while advices.len() < cnt && state.total_size > 0 { - let victim = if !state.small.is_empty() - && state.small_queue_fraction() >= threshold_for_small_eviction - { - self.evict_from_small(&mut state) - } else { - self.evict_from_main(&mut state) - }; - - if let Some(v) = victim { - advices.push(v); - } - } - advices - } - - fn notify_insert(&self, entry_id: &EntryID, _batch_type: CachedBatchType) { - let mut state = self.state.lock().unwrap(); - let entry_size = self.entry_size(entry_id); - - if state.check_if_entry_exists_in_small_or_main(entry_id) { - state.inc_frequency(entry_id); - } else if state.ghost_set.contains(entry_id) { - state.ghost_set.remove(entry_id); - state.ghost.retain(|x| *x != *entry_id); - state.main.push_front(*entry_id); - state.inc_main_queue_size(entry_size); - } else { - state.small.push_front(*entry_id); - state.inc_small_queue_size(entry_size); - state.frequency.insert(*entry_id, 0); - } - } - - fn notify_access(&self, entry_id: &EntryID, _batch_type: CachedBatchType) { - let mut state = self.state.lock().unwrap(); - if state.check_if_entry_exists_in_small_or_main(entry_id) { - state.inc_frequency(entry_id); - } - } -} - -impl Drop for S3FifoPolicy { - fn drop(&mut self) { - let mut state = self.state.lock().unwrap(); - state.small.clear(); - state.main.clear(); - state.ghost.clear(); - state.ghost_set.clear(); - state.frequency.clear(); - state.total_size = 0; - state.small_queue_size = 0; - state.main_queue_size = 0; - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::cache::utils::EntryID; - - fn entry(id: usize) -> EntryID { - id.into() - } - - #[test] - fn test_s3fifo_basic_insert_eviction() { - let policy = S3FifoPolicy::new(); - let e1 = entry(1); - let e2 = entry(2); - let e3 = entry(3); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - policy.notify_insert(&e3, CachedBatchType::MemoryArrow); - - let evicted = policy.find_victim(1); - assert_eq!(evicted.len(), 1); - } - - #[test] - fn test_s3fifo_frequency_increase() { - let policy = S3FifoPolicy::new(); - let e1 = entry(1); - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_access(&e1, CachedBatchType::MemoryArrow); - - let state = policy.state.lock().unwrap(); - assert_eq!(*state.frequency.get(&e1).unwrap(), 1); - } - - #[test] - fn test_s3fifo_eviction_order() { - let policy = S3FifoPolicy::new(); - let e1 = entry(1); - let e2 = entry(2); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - - let evicted = policy.find_victim(1); - assert_eq!(evicted[0], e1); - } - - #[test] - fn test_s3fifo_ghost_promote() { - let policy = S3FifoPolicy::new(); - let e1 = entry(1); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - let evicted = policy.find_victim(1); - assert_eq!(evicted[0], e1); - - // Re-insert evicted entry from ghost - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - let state = policy.state.lock().unwrap(); - assert!(state.main.contains(&e1)); - assert!(!state.ghost_set.contains(&e1)); - } - - #[test] - fn test_s3fifo_size_aware_fraction() { - let policy = S3FifoPolicy::new(); - let e1 = entry(20); - let e2 = entry(30); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - - let state = policy.state.lock().unwrap(); - assert_eq!(state.small_queue_size, 2); - assert_eq!(state.small_queue_fraction(), 1.0); - } - - #[test] - fn test_insert_and_access_updates_freq() { - let policy = S3FifoPolicy::new(); - let e1 = entry(1); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_access(&e1, CachedBatchType::MemoryArrow); - policy.notify_access(&e1, CachedBatchType::MemoryArrow); - - let state = policy.state.lock().unwrap(); - assert_eq!(*state.frequency.get(&e1).unwrap(), 2); // capped at 3 - } - - #[test] - fn test_freq_cap_at_three() { - let policy = S3FifoPolicy::new(); - let e1 = entry(1); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - for _ in 0..10 { - policy.notify_access(&e1, CachedBatchType::MemoryArrow); - } - - let state = policy.state.lock().unwrap(); - assert_eq!(*state.frequency.get(&e1).unwrap(), 3); - } - - #[test] - fn test_eviction_from_s_to_ghost() { - let policy = S3FifoPolicy::new(); - let e1 = entry(1); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - let evicted = policy.find_victim(1); - - assert_eq!(evicted[0], e1); - let state = policy.state.lock().unwrap(); - assert!(state.ghost_set.contains(&e1)); - assert!(state.ghost.contains(&e1)); - } - - #[test] - fn test_eviction_from_main_and_reinsertion_logic() { - let policy = S3FifoPolicy::new(); - let e1 = entry(1); - let e2 = entry(2); - let _e3 = entry(3); - - let mut state = policy.state.lock().unwrap(); - state.main.push_front(e1); - state.frequency.insert(e1, 1); - state.main_queue_size += 1; - - state.main.push_front(e2); - state.frequency.insert(e2, 2); - state.main_queue_size += 1; - state.total_size += 2; - drop(state); - - let evicted = policy.find_victim(2); - assert_eq!(evicted.len(), 2); - } - - #[test] - fn test_s3fifo_reinsert_does_not_duplicate_entry() { - let policy = S3FifoPolicy::new(); - let e1 = entry(1); - let e2 = entry(2); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - - let state = policy.state.lock().unwrap(); - let occurrences = state.small.iter().filter(|&&id| id == e1).count() - + state.main.iter().filter(|&&id| id == e1).count(); - assert_eq!(occurrences, 1); - assert_eq!(*state.frequency.get(&e1).unwrap(), 1); - } -} diff --git a/src/core/src/cache/policies/cache/sieve.rs b/src/core/src/cache/policies/cache/sieve.rs deleted file mode 100644 index 6fe54638..00000000 --- a/src/core/src/cache/policies/cache/sieve.rs +++ /dev/null @@ -1,295 +0,0 @@ -//! SIEVE cache policy implementation. - -use std::{collections::HashMap, fmt, ptr::NonNull}; - -use crate::{ - cache::{cached_batch::CachedBatchType, utils::EntryID}, - sync::Mutex, -}; - -use super::{ - CachePolicy, - doubly_linked_list::{DoublyLinkedList, DoublyLinkedNode, drop_boxed_node}, -}; - -#[derive(Debug)] -struct SieveNode { - entry_id: EntryID, - visited: bool, -} - -type NodePtr = NonNull>; - -#[derive(Debug, Default)] -struct SieveInternalState { - map: HashMap, - list: DoublyLinkedList, - hand: Option, - total_size: usize, -} - -/// The policy that implements object size aware SIEVE algorithm using a HashMap and a doubly linked list. -#[derive(Default)] -pub struct SievePolicy { - state: Mutex, -} - -impl fmt::Debug for SievePolicy { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SievePolicy") - .field("state", &self.state) - .finish() - } -} - -impl SievePolicy { - /// Create a new [`SievePolicy`]. - pub fn new() -> Self { - Self { - state: Mutex::new(SieveInternalState::default()), - } - } - - fn entry_size(&self, _entry_id: &EntryID) -> usize { - 1 - } -} - -unsafe impl Send for SievePolicy {} -unsafe impl Sync for SievePolicy {} - -impl CachePolicy for SievePolicy { - fn find_victim(&self, cnt: usize) -> Vec { - let mut state = self.state.lock().unwrap(); - let mut advices = Vec::with_capacity(cnt); - for _ in 0..cnt { - let hand_ptr = match state.hand { - Some(ptr) => Some(ptr), - None => state.list.tail(), - }; - let mut hand_ptr = match hand_ptr { - Some(p) => p, - None => break, - }; - loop { - if unsafe { hand_ptr.as_ref() }.data.visited { - unsafe { hand_ptr.as_mut() }.data.visited = false; - let prev = unsafe { hand_ptr.as_ref().prev }; - let next_hand = prev - .or(state.list.tail()) - .expect("non-empty list must have a tail"); - hand_ptr = next_hand; - state.hand = Some(next_hand); - } else { - let victim_id = unsafe { hand_ptr.as_ref().data.entry_id }; - let prev = unsafe { hand_ptr.as_ref().prev }; - let node_ptr = state.map.remove(&victim_id).unwrap(); - unsafe { - state.list.unlink(node_ptr); - drop_boxed_node(node_ptr); - } - state.total_size -= self.entry_size(&victim_id); - advices.push(victim_id); - state.hand = prev.or(state.list.tail()); - break; - } - } - } - advices - } - - fn notify_insert(&self, entry_id: &EntryID, _batch_type: CachedBatchType) { - let mut state = self.state.lock().unwrap(); - if state.map.contains_key(entry_id) { - if let Some(mut node_ptr) = state.map.get(entry_id).copied() { - unsafe { - node_ptr.as_mut().data.visited = true; - } - } - return; - } - - let was_empty = state.list.head().is_none(); - let node = DoublyLinkedNode::new(SieveNode { - entry_id: *entry_id, - visited: false, - }); - let node_ptr = NonNull::from(Box::leak(node)); - state.map.insert(*entry_id, node_ptr); - unsafe { - state.list.push_front(node_ptr); - } - if was_empty { - state.hand = Some(node_ptr); - } - state.total_size += self.entry_size(entry_id); - } - - fn notify_access(&self, entry_id: &EntryID, _batch_type: CachedBatchType) { - let state = self.state.lock().unwrap(); - if let Some(mut node_ptr) = state.map.get(entry_id).copied() { - unsafe { - node_ptr.as_mut().data.visited = true; - } - } - } -} - -impl Drop for SievePolicy { - fn drop(&mut self) { - let mut state = self.state.lock().unwrap(); - let handles: Vec<_> = state.map.drain().map(|(_, ptr)| ptr).collect(); - for node_ptr in handles { - unsafe { - state.list.unlink(node_ptr); - drop_boxed_node(node_ptr); - } - } - unsafe { - state.list.drop_all(); - } - state.hand = None; - state.total_size = 0; - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::cache::{ - cached_batch::CachedBatchType, - utils::{EntryID, create_cache_store, create_test_arrow_array}, - }; - - fn entry(id: usize) -> EntryID { - id.into() - } - - fn assert_evict_advice(policy: &SievePolicy, expect_evict: EntryID) { - let advice = policy.find_victim(1); - assert_eq!(advice, vec![expect_evict]); - } - - #[test] - fn test_sieve_insert_order() { - let policy = SievePolicy::new(); - let e1 = entry(1); - let e2 = entry(2); - let e3 = entry(3); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - policy.notify_insert(&e3, CachedBatchType::MemoryArrow); - - assert_evict_advice(&policy, e1); - } - - #[test] - fn test_sieve_access_sets_visited() { - let policy = SievePolicy::new(); - let e1 = entry(1); - let e2 = entry(2); - let e3 = entry(3); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - policy.notify_insert(&e3, CachedBatchType::MemoryArrow); - - policy.notify_access(&e1, CachedBatchType::MemoryArrow); - assert_evict_advice(&policy, e2); - } - - #[test] - fn test_sieve_reinsert_marks_visited() { - let policy = SievePolicy::new(); - let e1 = entry(1); - let e2 = entry(2); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - - assert_evict_advice(&policy, e2); - } - - #[test] - fn test_sieve_reinsert_sets_visited_flag() { - let policy = SievePolicy::new(); - let e1 = entry(1); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - - { - let state = policy.state.lock().unwrap(); - let mut node_ptr = state.map.get(&e1).copied().unwrap(); - unsafe { - node_ptr.as_mut().data.visited = false; - } - } - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - - let state = policy.state.lock().unwrap(); - let node_ptr = state.map.get(&e1).copied().unwrap(); - unsafe { - assert!(node_ptr.as_ref().data.visited); - } - assert_eq!(state.map.len(), 1); - } - - #[test] - fn test_sieve_advise_empty() { - let policy = SievePolicy::new(); - assert_eq!(policy.find_victim(1), vec![]); - } - - #[test] - fn test_sieve_with_sizeof_closure_defined() { - let policy = SievePolicy::new(); - - let e1 = entry(1); - let e2 = entry(2); - let e3 = entry(11); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - policy.notify_insert(&e3, CachedBatchType::MemoryArrow); - - let state = policy.state.lock().unwrap(); - assert_eq!(state.total_size, 3); - } - - #[test] - fn test_sieve_sizeof_without_closure() { - let policy = SievePolicy::new(); - - let e1 = entry(1); - let e2 = entry(2); - let e3 = entry(11); - - policy.notify_insert(&e1, CachedBatchType::MemoryArrow); - policy.notify_insert(&e2, CachedBatchType::MemoryArrow); - policy.notify_insert(&e3, CachedBatchType::MemoryArrow); - - let state = policy.state.lock().unwrap(); - assert_eq!(state.total_size, 3); - } - - #[tokio::test] - async fn test_sieve_integration() { - let advisor = SievePolicy::new(); - let store = create_cache_store(3000, Box::new(advisor)).await; - - let entry_id1 = EntryID::from(1); - let entry_id2 = EntryID::from(2); - let entry_id3 = EntryID::from(3); - - store.insert(entry_id1, create_test_arrow_array(100)).await; - store.insert(entry_id2, create_test_arrow_array(100)).await; - store.insert(entry_id3, create_test_arrow_array(100)).await; - assert!(store.index().get(&entry_id1).is_some()); - assert!(store.index().get(&entry_id2).is_some()); - assert!(store.index().get(&entry_id3).is_some()); - } -} diff --git a/src/core/src/cache/policies/cache/three_queue.rs b/src/core/src/cache/policies/cache/three_queue.rs index 57ff6a4b..655c1984 100644 --- a/src/core/src/cache/policies/cache/three_queue.rs +++ b/src/core/src/cache/policies/cache/three_queue.rs @@ -97,6 +97,16 @@ impl LiquidQueueInternalState { } Some(entry_id) } + + fn remove(&mut self, entry_id: &EntryID) -> Option { + let node_ptr = self.map.remove(entry_id)?; + let removed = unsafe { node_ptr.as_ref().data.entry_id }; + unsafe { + self.detach(node_ptr); + drop_boxed_node(node_ptr); + } + Some(removed) + } } impl Drop for LiquidQueueInternalState { @@ -155,7 +165,7 @@ impl CachePolicy for LiquidPolicy { inner.upsert_into_queue(*entry_id, target); } - fn find_victim(&self, cnt: usize) -> Vec { + fn find_memory_victim(&self, cnt: usize) -> Vec { if cnt == 0 { return vec![]; } @@ -185,7 +195,30 @@ impl CachePolicy for LiquidPolicy { victims } + fn find_disk_victim(&self, cnt: usize) -> Vec { + if cnt == 0 { + return vec![]; + } + + let mut inner = self.inner.lock().unwrap(); + let mut victims = Vec::with_capacity(cnt); + + while victims.len() < cnt { + let Some(entry) = inner.pop_front(QueueKind::Disk) else { + break; + }; + victims.push(entry); + } + + victims + } + fn notify_access(&self, _entry_id: &EntryID, _batch_type: CachedBatchType) {} + + fn notify_remove(&self, entry_id: &EntryID) { + let mut inner = self.inner.lock().unwrap(); + inner.remove(entry_id); + } } #[cfg(test)] @@ -211,9 +244,9 @@ mod tests { policy.notify_insert(&liquid_a, CachedBatchType::MemoryLiquid); policy.notify_insert(&liquid_b, CachedBatchType::MemoryLiquid); - assert_eq!(policy.find_victim(1), vec![arrow_a]); - assert_eq!(policy.find_victim(2), vec![arrow_b, liquid_a]); - assert_eq!(policy.find_victim(1), vec![liquid_b]); + assert_eq!(policy.find_memory_victim(1), vec![arrow_a]); + assert_eq!(policy.find_memory_victim(2), vec![arrow_b, liquid_a]); + assert_eq!(policy.find_memory_victim(1), vec![liquid_b]); } #[test] @@ -229,7 +262,7 @@ mod tests { policy.notify_insert(&arrow_entry, CachedBatchType::MemoryArrow); // Request more victims than available to ensure we only get what exists. - let victims = policy.find_victim(5); + let victims = policy.find_memory_victim(5); assert_eq!(victims, vec![arrow_entry, liquid_entry, hybrid_entry]); } @@ -238,7 +271,7 @@ mod tests { let policy = LiquidPolicy::new(); policy.notify_insert(&entry(1), CachedBatchType::MemoryArrow); - assert!(policy.find_victim(0).is_empty()); + assert!(policy.find_memory_victim(0).is_empty()); } #[test] @@ -253,11 +286,25 @@ mod tests { policy.notify_insert(&arrow_entry, CachedBatchType::MemoryArrow); policy.notify_insert(&liquid_entry, CachedBatchType::MemoryLiquid); - let victims = policy.find_victim(5); + let victims = policy.find_memory_victim(5); assert_eq!(victims, vec![arrow_entry, liquid_entry]); // Only the disk entry remains and should still not be evicted. - assert!(policy.find_victim(1).is_empty()); + assert!(policy.find_memory_victim(1).is_empty()); + } + + #[test] + fn test_disk_victims_and_remove() { + let policy = LiquidPolicy::new(); + let disk_old = entry(1); + let disk_new = entry(2); + + policy.notify_insert(&disk_old, CachedBatchType::DiskArrow); + policy.notify_insert(&disk_new, CachedBatchType::DiskLiquid); + + assert_eq!(policy.find_disk_victim(1), vec![disk_old]); + policy.notify_remove(&disk_new); + assert!(policy.find_disk_victim(1).is_empty()); } #[test] @@ -273,8 +320,8 @@ mod tests { // Reinserting should refresh the entry as the newest arrow batch. policy.notify_insert(&first, CachedBatchType::MemoryArrow); - assert_eq!(policy.find_victim(1), vec![second]); - assert_eq!(policy.find_victim(1), vec![first]); + assert_eq!(policy.find_memory_victim(1), vec![second]); + assert_eq!(policy.find_memory_victim(1), vec![first]); } #[test] @@ -286,7 +333,7 @@ mod tests { policy.notify_insert(&entry_id, CachedBatchType::MemoryArrow); policy.notify_insert(&entry_id, CachedBatchType::MemoryLiquid); - let victims = policy.find_victim(2); + let victims = policy.find_memory_victim(2); assert_eq!(victims, vec![entry_id]); } } diff --git a/src/core/src/cache/policies/hydration.rs b/src/core/src/cache/policies/hydration.rs index e2448ebf..ea96175d 100644 --- a/src/core/src/cache/policies/hydration.rs +++ b/src/core/src/cache/policies/hydration.rs @@ -87,6 +87,7 @@ fn hydrate_variant_paths( combined_values, nulls, squeezed.original_arrow_data_type(), + squeezed.disk_backing().disk_bytes(), ); Some(CacheEntry::memory_squeezed_liquid( Arc::new(merged) as LiquidSqueezedArrayRef @@ -96,16 +97,27 @@ fn hydrate_variant_paths( impl HydrationPolicy for AlwaysHydrate { fn hydrate(&self, request: &HydrationRequest<'_>) -> Option { match (request.cached, &request.materialized) { - (CacheEntry::DiskArrow(_), MaterializedEntry::Arrow(arr)) => { + (CacheEntry::DiskArrow { disk_bytes, .. }, MaterializedEntry::Arrow(arr)) => { if let Some(CacheExpression::VariantGet { requests }) = request.expression && let Some((squeezed, _bytes)) = try_variant_squeeze(arr, requests, request.compressor.as_ref()) { - return Some(CacheEntry::memory_squeezed_liquid(squeezed)); + let variant = squeezed + .as_any() + .downcast_ref::()?; + let squeezed = VariantStructSqueezedArray::new( + variant.typed_values(), + variant.nulls(), + variant.original_arrow_data_type(), + *disk_bytes, + ); + return Some(CacheEntry::memory_squeezed_liquid( + Arc::new(squeezed) as LiquidSqueezedArrayRef + )); } Some(CacheEntry::memory_arrow((*arr).clone())) } - (CacheEntry::DiskLiquid(_), MaterializedEntry::Liquid(liq)) => { + (CacheEntry::DiskLiquid { .. }, MaterializedEntry::Liquid(liq)) => { Some(CacheEntry::memory_liquid((*liq).clone())) } (CacheEntry::MemoryLiquid(_), _) => None, @@ -169,7 +181,7 @@ mod tests { let expr = CacheExpression::variant_get("age", DataType::Int64); let policy = AlwaysHydrate::new(); let compressor = Arc::new(LiquidCompressorStates::new()); - let cached_entry = CacheEntry::disk_arrow(arr.data_type().clone()); + let cached_entry = CacheEntry::disk_arrow(arr.data_type().clone(), 1); let hydrated = policy.hydrate(&HydrationRequest { entry_id: EntryID::from(0), diff --git a/src/core/src/cache/policies/squeeze.rs b/src/core/src/cache/policies/squeeze.rs index 21e09eff..53f48913 100644 --- a/src/core/src/cache/policies/squeeze.rs +++ b/src/core/src/cache/policies/squeeze.rs @@ -18,16 +18,29 @@ use crate::liquid_array::{ use crate::utils::VariantSchema; /// What to do when we need to squeeze an entry? +#[derive(Debug, Clone)] +pub enum SqueezeOutcome { + /// Replace the cache entry, optionally writing bytes to disk first. + Replace { + /// Replacement cache entry. + entry: CacheEntry, + /// Bytes that must be written before inserting the replacement. + bytes_to_write: Option, + }, + /// Remove the entry entirely. + Remove, +} + +/// Policy that chooses the next representation for an entry under memory pressure. pub trait SqueezePolicy: std::fmt::Debug + Send + Sync { /// Squeeze the entry. - /// Returns the squeezed entry and the bytes that were used to store the entry on disk. fn squeeze( &self, entry: &CacheEntry, compressor: &LiquidCompressorStates, squeeze_hint: Option<&CacheExpression>, squeeze_io: &Arc, - ) -> (CacheEntry, Option); + ) -> SqueezeOutcome; } /// Squeeze the entry to disk. @@ -41,31 +54,37 @@ impl SqueezePolicy for Evict { _compressor: &LiquidCompressorStates, _squeeze_hint: Option<&CacheExpression>, _squeeze_io: &Arc, - ) -> (CacheEntry, Option) { + ) -> SqueezeOutcome { match entry { CacheEntry::MemoryArrow(array) => { let bytes = arrow_to_bytes(array).expect("failed to convert arrow to bytes"); - ( - CacheEntry::disk_arrow(array.data_type().clone()), - Some(bytes), - ) + SqueezeOutcome::Replace { + entry: CacheEntry::disk_arrow(array.data_type().clone(), bytes.len()), + bytes_to_write: Some(bytes), + } } CacheEntry::MemoryLiquid(liquid_array) => { let disk_data = liquid_array.to_bytes(); - ( - CacheEntry::disk_liquid(liquid_array.original_arrow_data_type()), - Some(Bytes::from(disk_data)), - ) + SqueezeOutcome::Replace { + entry: CacheEntry::disk_liquid( + liquid_array.original_arrow_data_type(), + disk_data.len(), + ), + bytes_to_write: Some(Bytes::from(disk_data)), + } } CacheEntry::MemorySqueezedLiquid(squeezed_array) => { let data_type = squeezed_array.original_arrow_data_type(); let new_entry = match squeezed_array.disk_backing() { - SqueezedBacking::Liquid => CacheEntry::disk_liquid(data_type), - SqueezedBacking::Arrow => CacheEntry::disk_arrow(data_type), + SqueezedBacking::Liquid(n) => CacheEntry::disk_liquid(data_type, n), + SqueezedBacking::Arrow(n) => CacheEntry::disk_arrow(data_type, n), }; - (new_entry, None) + SqueezeOutcome::Replace { + entry: new_entry, + bytes_to_write: None, + } } - CacheEntry::DiskLiquid(_) | CacheEntry::DiskArrow(_) => (entry.clone(), None), + CacheEntry::DiskLiquid { .. } | CacheEntry::DiskArrow { .. } => SqueezeOutcome::Remove, } } } @@ -81,7 +100,7 @@ impl SqueezePolicy for TranscodeSqueezeEvict { compressor: &LiquidCompressorStates, squeeze_hint: Option<&CacheExpression>, squeeze_io: &Arc, - ) -> (CacheEntry, Option) { + ) -> SqueezeOutcome { match entry { CacheEntry::MemoryArrow(array) => { if let Some(requests) = @@ -89,20 +108,23 @@ impl SqueezePolicy for TranscodeSqueezeEvict { && let Some((squeezed_array, bytes)) = try_variant_squeeze(array, requests, compressor) { - return ( - CacheEntry::memory_squeezed_liquid(squeezed_array), - Some(bytes), - ); + return SqueezeOutcome::Replace { + entry: CacheEntry::memory_squeezed_liquid(squeezed_array), + bytes_to_write: Some(bytes), + }; } match transcode_liquid_inner_with_hint(array, compressor, squeeze_hint) { - Ok(liquid_array) => (CacheEntry::memory_liquid(liquid_array), None), + Ok(liquid_array) => SqueezeOutcome::Replace { + entry: CacheEntry::memory_liquid(liquid_array), + bytes_to_write: None, + }, Err(_) => { let bytes = arrow_to_bytes(array).expect("failed to convert arrow to bytes"); - ( - CacheEntry::disk_arrow(array.data_type().clone()), - Some(bytes), - ) + SqueezeOutcome::Replace { + entry: CacheEntry::disk_arrow(array.data_type().clone(), bytes.len()), + bytes_to_write: Some(bytes), + } } } } @@ -112,26 +134,32 @@ impl SqueezePolicy for TranscodeSqueezeEvict { Some(result) => result, None => { let bytes = Bytes::from(liquid_array.to_bytes()); - return ( - CacheEntry::disk_liquid(liquid_array.original_arrow_data_type()), - Some(bytes), - ); + return SqueezeOutcome::Replace { + entry: CacheEntry::disk_liquid( + liquid_array.original_arrow_data_type(), + bytes.len(), + ), + bytes_to_write: Some(bytes), + }; } }; - ( - CacheEntry::memory_squeezed_liquid(squeezed_array), - Some(bytes), - ) + SqueezeOutcome::Replace { + entry: CacheEntry::memory_squeezed_liquid(squeezed_array), + bytes_to_write: Some(bytes), + } } CacheEntry::MemorySqueezedLiquid(squeezed_array) => { let data_type = squeezed_array.original_arrow_data_type(); let new_entry = match squeezed_array.disk_backing() { - SqueezedBacking::Liquid => CacheEntry::disk_liquid(data_type), - SqueezedBacking::Arrow => CacheEntry::disk_arrow(data_type), + SqueezedBacking::Liquid(n) => CacheEntry::disk_liquid(data_type, n), + SqueezedBacking::Arrow(n) => CacheEntry::disk_arrow(data_type, n), }; - (new_entry, None) + SqueezeOutcome::Replace { + entry: new_entry, + bytes_to_write: None, + } } - CacheEntry::DiskLiquid(_) | CacheEntry::DiskArrow(_) => (entry.clone(), None), + CacheEntry::DiskLiquid { .. } | CacheEntry::DiskArrow { .. } => SqueezeOutcome::Remove, } } } @@ -147,37 +175,46 @@ impl SqueezePolicy for TranscodeEvict { compressor: &LiquidCompressorStates, _squeeze_hint: Option<&CacheExpression>, _squeeze_io: &Arc, - ) -> (CacheEntry, Option) { + ) -> SqueezeOutcome { match entry { CacheEntry::MemoryArrow(array) => { match transcode_liquid_inner_with_hint(array, compressor, None) { - Ok(liquid_array) => (CacheEntry::memory_liquid(liquid_array), None), + Ok(liquid_array) => SqueezeOutcome::Replace { + entry: CacheEntry::memory_liquid(liquid_array), + bytes_to_write: None, + }, Err(_) => { let bytes = arrow_to_bytes(array).expect("failed to convert arrow to bytes"); - ( - CacheEntry::disk_arrow(array.data_type().clone()), - Some(bytes), - ) + SqueezeOutcome::Replace { + entry: CacheEntry::disk_arrow(array.data_type().clone(), bytes.len()), + bytes_to_write: Some(bytes), + } } } } CacheEntry::MemoryLiquid(liquid_array) => { let bytes = Bytes::from(liquid_array.to_bytes()); - ( - CacheEntry::disk_liquid(liquid_array.original_arrow_data_type()), - Some(bytes), - ) + SqueezeOutcome::Replace { + entry: CacheEntry::disk_liquid( + liquid_array.original_arrow_data_type(), + bytes.len(), + ), + bytes_to_write: Some(bytes), + } } CacheEntry::MemorySqueezedLiquid(squeezed_array) => { let data_type = squeezed_array.original_arrow_data_type(); let new_entry = match squeezed_array.disk_backing() { - SqueezedBacking::Liquid => CacheEntry::disk_liquid(data_type), - SqueezedBacking::Arrow => CacheEntry::disk_arrow(data_type), + SqueezedBacking::Liquid(n) => CacheEntry::disk_liquid(data_type, n), + SqueezedBacking::Arrow(n) => CacheEntry::disk_arrow(data_type, n), }; - (new_entry, None) + SqueezeOutcome::Replace { + entry: new_entry, + bytes_to_write: None, + } } - CacheEntry::DiskLiquid(_) | CacheEntry::DiskArrow(_) => (entry.clone(), None), + CacheEntry::DiskLiquid { .. } | CacheEntry::DiskArrow { .. } => SqueezeOutcome::Remove, } } } @@ -243,8 +280,12 @@ pub(crate) fn try_variant_squeeze( }; liquid_values.push((path, liquid_array)); } - let squeezed = - VariantStructSqueezedArray::new(liquid_values, nulls, backing_array.data_type().clone()); + let squeezed = VariantStructSqueezedArray::new( + liquid_values, + nulls, + backing_array.data_type().clone(), + bytes.len(), + ); Some((Arc::new(squeezed) as LiquidSqueezedArrayRef, bytes)) } @@ -324,6 +365,16 @@ mod tests { batch.column(0).clone() } + fn into_replace(outcome: SqueezeOutcome) -> (CacheEntry, Option) { + match outcome { + SqueezeOutcome::Replace { + entry, + bytes_to_write, + } => (entry, bytes_to_write), + SqueezeOutcome::Remove => panic!("expected replacement"), + } + } + fn struct_array() -> ArrayRef { let values = Arc::new(Int32Array::from(vec![Some(1), None, Some(3)])) as ArrayRef; let field = Arc::new(Field::new("value", DataType::Int32, true)); @@ -337,16 +388,23 @@ mod tests { let squeeze_io: Arc = Arc::new(TestSqueezeIo::default()); // MemoryArrow -> DiskArrow + bytes (Arrow IPC) let arr = int_array(8); - let (new_batch, bytes) = disk.squeeze( + let (new_batch, bytes) = into_replace(disk.squeeze( &CacheEntry::memory_arrow(arr.clone()), &states, None, &squeeze_io, - ); + )); let data = new_batch; match (data, bytes) { - (CacheEntry::DiskArrow(dt), Some(b)) => { + ( + CacheEntry::DiskArrow { + data_type: dt, + disk_bytes, + }, + Some(b), + ) => { assert_eq!(dt, DataType::Int32); + assert_eq!(disk_bytes, b.len()); let decoded = decode_arrow(&b); assert_eq!(decoded.as_ref(), arr.as_ref()); } @@ -356,15 +414,16 @@ mod tests { // MemoryLiquid (strings) -> MemoryHybridLiquid + bytes let strings = Arc::new(StringArray::from(vec!["a", "b", "a"])) as ArrayRef; let liquid = transcode_liquid_inner(&strings, &states).unwrap(); - let (new_batch, bytes) = disk.squeeze( + let (new_batch, bytes) = into_replace(disk.squeeze( &CacheEntry::memory_liquid(liquid.clone()), &states, None, &squeeze_io, - ); + )); let data = new_batch; match (data, bytes) { - (CacheEntry::DiskLiquid(_), Some(b)) => { + (CacheEntry::DiskLiquid { disk_bytes, .. }, Some(b)) => { + assert_eq!(disk_bytes, b.len()); assert!(!b.is_empty()); } other => panic!("unexpected: {other:?}"), @@ -376,33 +435,39 @@ mod tests { Some((h, _b)) => h, None => panic!("squeeze should succeed for byte-view"), }; - let (new_batch, bytes) = disk.squeeze( + let (new_batch, bytes) = into_replace(disk.squeeze( &CacheEntry::memory_squeezed_liquid(squeezed), &states, expression, &squeeze_io, - ); + )); let data = new_batch; match (data, bytes) { - (CacheEntry::DiskLiquid(_data_type), None) => {} + ( + CacheEntry::DiskLiquid { + data_type: _data_type, + .. + }, + None, + ) => {} other => panic!("unexpected: {other:?}"), } - // Disk* -> unchanged, no bytes - let (b1, w1) = disk.squeeze( - &CacheEntry::disk_arrow(DataType::Utf8), + // Disk* -> remove + let b1 = disk.squeeze( + &CacheEntry::disk_arrow(DataType::Utf8, 1), &states, expression, &squeeze_io, ); - assert!(matches!(b1, CacheEntry::DiskArrow(DataType::Utf8)) && w1.is_none()); - let (b2, w2) = disk.squeeze( - &CacheEntry::disk_liquid(DataType::Utf8), + assert!(matches!(b1, SqueezeOutcome::Remove)); + let b2 = disk.squeeze( + &CacheEntry::disk_liquid(DataType::Utf8, 1), &states, expression, &squeeze_io, ); - assert!(matches!(b2, CacheEntry::DiskLiquid(DataType::Utf8)) && w2.is_none()); + assert!(matches!(b2, SqueezeOutcome::Remove)); } #[test] @@ -413,12 +478,12 @@ mod tests { // MemoryArrow -> MemoryLiquid, no bytes let arr = int_array(8); - let (new_batch, bytes) = to_liquid.squeeze( + let (new_batch, bytes) = into_replace(to_liquid.squeeze( &CacheEntry::memory_arrow(arr.clone()), &states, None, &squeeze_io, - ); + )); assert!(bytes.is_none()); match new_batch { CacheEntry::MemoryLiquid(liq) => { @@ -431,12 +496,12 @@ mod tests { // MemoryLiquid (strings) -> MemorySqueezedLiquid + bytes let strings = Arc::new(StringArray::from(vec!["x", "y", "x"])) as ArrayRef; let liquid = transcode_liquid_inner(&strings, &states).unwrap(); - let (new_batch, bytes) = to_liquid.squeeze( + let (new_batch, bytes) = into_replace(to_liquid.squeeze( &CacheEntry::memory_liquid(liquid), &states, expression, &squeeze_io, - ); + )); match (new_batch, bytes) { (CacheEntry::MemorySqueezedLiquid(_), Some(b)) => assert!(!b.is_empty()), other => panic!("unexpected: {other:?}"), @@ -446,32 +511,38 @@ mod tests { let strings = Arc::new(StringArray::from(vec!["m", "n"])) as ArrayRef; let liquid = transcode_liquid_inner(&strings, &states).unwrap(); let squeezed = liquid.squeeze(squeeze_io.clone(), expression).unwrap().0; - let (new_batch, bytes) = to_liquid.squeeze( + let (new_batch, bytes) = into_replace(to_liquid.squeeze( &CacheEntry::memory_squeezed_liquid(squeezed), &states, expression, &squeeze_io, - ); + )); match (new_batch, bytes) { - (CacheEntry::DiskLiquid(DataType::Utf8), None) => {} + ( + CacheEntry::DiskLiquid { + data_type: DataType::Utf8, + .. + }, + None, + ) => {} other => panic!("unexpected: {other:?}"), } - // Disk* -> unchanged - let (b1, w1) = to_liquid.squeeze( - &CacheEntry::disk_arrow(DataType::Utf8), + // Disk* -> remove + let b1 = to_liquid.squeeze( + &CacheEntry::disk_arrow(DataType::Utf8, 1), &states, expression, &squeeze_io, ); - assert!(matches!(b1, CacheEntry::DiskArrow(DataType::Utf8)) && w1.is_none()); - let (b2, w2) = to_liquid.squeeze( - &CacheEntry::disk_liquid(DataType::Utf8), + assert!(matches!(b1, SqueezeOutcome::Remove)); + let b2 = to_liquid.squeeze( + &CacheEntry::disk_liquid(DataType::Utf8, 1), &states, expression, &squeeze_io, ); - assert!(matches!(b2, CacheEntry::DiskLiquid(DataType::Utf8)) && w2.is_none()); + assert!(matches!(b2, SqueezeOutcome::Remove)); } #[test] @@ -480,15 +551,22 @@ mod tests { let states = LiquidCompressorStates::new(); let squeeze_io: Arc = Arc::new(TestSqueezeIo::default()); let struct_arr = struct_array(); - let (new_batch, bytes) = to_liquid.squeeze( + let (new_batch, bytes) = into_replace(to_liquid.squeeze( &CacheEntry::memory_arrow(struct_arr.clone()), &states, None, &squeeze_io, - ); + )); match (new_batch, bytes) { - (CacheEntry::DiskArrow(dt), Some(b)) => { + ( + CacheEntry::DiskArrow { + data_type: dt, + disk_bytes, + }, + Some(b), + ) => { assert_eq!(&dt, struct_arr.data_type()); + assert_eq!(disk_bytes, b.len()); assert_eq!(decode_arrow(&b).as_ref(), struct_arr.as_ref()); } other => panic!("expected disk arrow fallback, got {other:?}"), @@ -501,15 +579,22 @@ mod tests { let states = LiquidCompressorStates::new(); let squeeze_io: Arc = Arc::new(TestSqueezeIo::default()); let struct_arr = struct_array(); - let (new_batch, bytes) = to_disk.squeeze( + let (new_batch, bytes) = into_replace(to_disk.squeeze( &CacheEntry::memory_arrow(struct_arr.clone()), &states, None, &squeeze_io, - ); + )); match (new_batch, bytes) { - (CacheEntry::DiskArrow(dt), Some(b)) => { + ( + CacheEntry::DiskArrow { + data_type: dt, + disk_bytes, + }, + Some(b), + ) => { assert_eq!(&dt, struct_arr.data_type()); + assert_eq!(disk_bytes, b.len()); assert_eq!(decode_arrow(&b).as_ref(), struct_arr.as_ref()); } other => panic!("expected disk arrow fallback, got {other:?}"), @@ -609,7 +694,7 @@ mod tests { use futures::executor::block_on; assert!(!bytes.is_empty()); - assert_eq!(squeezed.disk_backing(), SqueezedBacking::Arrow); + assert!(matches!(squeezed.disk_backing(), SqueezedBacking::Arrow(_))); let struct_squeezed = squeezed .as_any() .downcast_ref::() @@ -643,12 +728,12 @@ mod tests { let hint = CacheExpression::variant_get("name", DataType::Utf8); let squeeze_io: Arc = Arc::new(TestSqueezeIo::default()); - let (new_batch, bytes) = policy.squeeze( + let (new_batch, bytes) = into_replace(policy.squeeze( &CacheEntry::memory_arrow(variant_arr), &states, Some(&hint), &squeeze_io, - ); + )); match (new_batch, bytes) { (CacheEntry::MemorySqueezedLiquid(squeezed), Some(b)) => { @@ -666,12 +751,12 @@ mod tests { let hint = CacheExpression::variant_get("age", DataType::Int64); let squeeze_io: Arc = Arc::new(TestSqueezeIo::default()); - let (new_batch, bytes) = policy.squeeze( + let (new_batch, bytes) = into_replace(policy.squeeze( &CacheEntry::memory_arrow(variant_arr), &states, Some(&hint), &squeeze_io, - ); + )); match (new_batch, bytes) { (CacheEntry::MemorySqueezedLiquid(squeezed), Some(b)) => { @@ -692,12 +777,12 @@ mod tests { let hint = CacheExpression::variant_get("name", DataType::Utf8); let squeeze_io: Arc = Arc::new(TestSqueezeIo::default()); - let (new_batch, bytes) = policy.squeeze( + let (new_batch, bytes) = into_replace(policy.squeeze( &CacheEntry::memory_arrow(variant_arr), &states, Some(&hint), &squeeze_io, - ); + )); match (new_batch, bytes) { (CacheEntry::MemorySqueezedLiquid(squeezed), Some(b)) => { @@ -728,15 +813,18 @@ mod tests { let variant_arr = enriched_variant_array("name", DataType::Utf8); let squeeze_io: Arc = Arc::new(TestSqueezeIo::default()); - let (new_batch, bytes) = policy.squeeze( + let (new_batch, bytes) = into_replace(policy.squeeze( &CacheEntry::memory_arrow(variant_arr), &states, None, &squeeze_io, - ); + )); match (new_batch, bytes) { - (CacheEntry::DiskArrow(_), Some(b)) => assert!(!b.is_empty()), + (CacheEntry::DiskArrow { disk_bytes, .. }, Some(b)) => { + assert_eq!(disk_bytes, b.len()); + assert!(!b.is_empty()); + } (CacheEntry::MemoryLiquid(_), None) => {} other => panic!("expected DiskArrow with bytes or MemoryLiquid, got {other:?}"), } @@ -750,16 +838,23 @@ mod tests { let variant_arr = enriched_variant_array("name", DataType::Utf8); let hint = CacheExpression::variant_get("age", DataType::Int64); - let (new_batch, bytes) = policy.squeeze( + let (new_batch, bytes) = into_replace(policy.squeeze( &CacheEntry::memory_arrow(variant_arr.clone()), &states, Some(&hint), &squeeze_io, - ); + )); match (new_batch, bytes) { - (CacheEntry::DiskArrow(dt), Some(b)) => { + ( + CacheEntry::DiskArrow { + data_type: dt, + disk_bytes, + }, + Some(b), + ) => { assert_eq!(dt, variant_arr.data_type().clone()); + assert_eq!(disk_bytes, b.len()); assert!(!b.is_empty()); } other => panic!("expected DiskArrow fallback when path missing, got {other:?}"), diff --git a/src/core/src/cache/tests/policies.rs b/src/core/src/cache/tests/policies.rs index b0aba179..29c84990 100644 --- a/src/core/src/cache/tests/policies.rs +++ b/src/core/src/cache/tests/policies.rs @@ -12,13 +12,13 @@ async fn default_policies() { .with_cache_policy(Box::new(LiquidPolicy::new())) .with_hydration_policy(Box::new(AlwaysHydrate::new())) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) - .with_max_cache_bytes(capacity) + .with_max_memory_bytes(capacity) .build() .await; for i in 0..5 { let entry_id = EntryID::from(i); - cache.insert(entry_id, test_array.clone()).await; + cache.insert(entry_id, test_array.clone()).await.unwrap(); } for i in 0..5 { @@ -40,14 +40,20 @@ async fn insert_wont_fit_cache() { .with_cache_policy(Box::new(LiquidPolicy::new())) .with_hydration_policy(Box::new(AlwaysHydrate::new())) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) - .with_max_cache_bytes(capacity) + .with_max_memory_bytes(capacity) .build() .await; - cache.insert(EntryID::from(0), test_array.clone()).await; + cache + .insert(EntryID::from(0), test_array.clone()) + .await + .unwrap(); let array_3x = arrow::compute::concat(&[&test_array, &test_array, &test_array]).unwrap(); let array_9x = arrow::compute::concat(&[&array_3x, &array_3x, &array_3x]).unwrap(); let array_27x = arrow::compute::concat(&[&array_9x, &array_9x, &array_9x]).unwrap(); - cache.insert(EntryID::from(1), array_27x.clone()).await; + cache + .insert(EntryID::from(1), array_27x.clone()) + .await + .unwrap(); cache.get(&EntryID::from(1)).read().await.unwrap(); let trace = cache.consume_event_trace(); diff --git a/src/core/src/cache/tests/snapshots/liquid_cache__cache__tests__policies__default_policies.snap b/src/core/src/cache/tests/snapshots/liquid_cache__cache__tests__policies__default_policies.snap index b53d1069..83ab4e86 100644 --- a/src/core/src/cache/tests/snapshots/liquid_cache__cache__tests__policies__default_policies.snap +++ b/src/core/src/cache/tests/snapshots/liquid_cache__cache__tests__policies__default_policies.snap @@ -18,9 +18,9 @@ event=squeeze_victim entry=2 event=insert_success entry=2 kind=MemoryLiquid event=squeeze_victim entry=0 event=io_write entry=0 kind=DiskLiquid bytes=1320 +event=insert_success entry=0 kind=DiskLiquid event=squeeze_victim entry=1 event=io_write entry=1 kind=DiskLiquid bytes=1320 -event=insert_success entry=0 kind=DiskLiquid event=insert_success entry=1 kind=DiskLiquid event=insert_success entry=3 kind=MemoryArrow event=insert_failed entry=4 kind=MemoryArrow diff --git a/src/core/src/cache/tests/snapshots/liquid_cache__cache__tests__squeezed__read_squeezed_date_time.snap b/src/core/src/cache/tests/snapshots/liquid_cache__cache__tests__squeezed__read_squeezed_date_time.snap index de26da71..dbb4d543 100644 --- a/src/core/src/cache/tests/snapshots/liquid_cache__cache__tests__squeezed__read_squeezed_date_time.snap +++ b/src/core/src/cache/tests/snapshots/liquid_cache__cache__tests__squeezed__read_squeezed_date_time.snap @@ -18,17 +18,17 @@ event=squeeze_victim entry=2 event=insert_success entry=2 kind=MemoryLiquid event=squeeze_victim entry=0 event=io_write entry=0 kind=MemorySqueezedLiquid bytes=6184 +event=insert_success entry=0 kind=MemorySqueezedLiquid event=squeeze_victim entry=1 event=io_write entry=1 kind=MemorySqueezedLiquid bytes=6184 -event=insert_success entry=0 kind=MemorySqueezedLiquid event=insert_success entry=1 kind=MemorySqueezedLiquid event=insert_success entry=3 kind=MemoryArrow -event=read entry=0 selection=false expr=ExtractDate32:Year cached=MemorySqueezedLiquid -event=read_squeezed_data entry=0 expression=ExtractDate32:Year -event=read entry=1 selection=false expr=ExtractDate32:Year cached=MemorySqueezedLiquid -event=read_squeezed_data entry=1 expression=ExtractDate32:Year -event=read entry=2 selection=false expr=ExtractDate32:Year cached=MemoryLiquid -event=read entry=3 selection=false expr=ExtractDate32:Year cached=MemoryArrow -event=read entry=1 selection=false expr=ExtractDate32:Month cached=MemorySqueezedLiquid +event=read entry=0 selection=false expr=ExtractDate32[Year] cached=MemorySqueezedLiquid +event=read_squeezed_data entry=0 expression=ExtractDate32[Year] +event=read entry=1 selection=false expr=ExtractDate32[Year] cached=MemorySqueezedLiquid +event=read_squeezed_data entry=1 expression=ExtractDate32[Year] +event=read entry=2 selection=false expr=ExtractDate32[Year] cached=MemoryLiquid +event=read entry=3 selection=false expr=ExtractDate32[Year] cached=MemoryArrow +event=read entry=1 selection=false expr=ExtractDate32[Month] cached=MemorySqueezedLiquid event=io_read_squeezed_backing entry=1 bytes=6184 ] diff --git a/src/core/src/cache/tests/snapshots/liquid_cache__cache__tests__squeezed__read_squeezed_int64_array.snap b/src/core/src/cache/tests/snapshots/liquid_cache__cache__tests__squeezed__read_squeezed_int64_array.snap index b0f67dbf..d0ae4ddb 100644 --- a/src/core/src/cache/tests/snapshots/liquid_cache__cache__tests__squeezed__read_squeezed_int64_array.snap +++ b/src/core/src/cache/tests/snapshots/liquid_cache__cache__tests__squeezed__read_squeezed_int64_array.snap @@ -18,9 +18,9 @@ event=squeeze_victim entry=2 event=insert_success entry=2 kind=MemoryLiquid event=squeeze_victim entry=0 event=io_write entry=0 kind=MemorySqueezedLiquid bytes=6184 +event=insert_success entry=0 kind=MemorySqueezedLiquid event=squeeze_victim entry=1 event=io_write entry=1 kind=DiskLiquid bytes=6184 -event=insert_success entry=0 kind=MemorySqueezedLiquid event=insert_success entry=1 kind=DiskLiquid event=insert_success entry=3 kind=MemoryArrow event=read entry=0 selection=false expr=PredicateColumn cached=MemorySqueezedLiquid diff --git a/src/core/src/cache/tests/snapshots/liquid_cache__cache__tests__squeezed__read_squeezed_variant_path.snap b/src/core/src/cache/tests/snapshots/liquid_cache__cache__tests__squeezed__read_squeezed_variant_path.snap index 3ce06825..18756a3b 100644 --- a/src/core/src/cache/tests/snapshots/liquid_cache__cache__tests__squeezed__read_squeezed_variant_path.snap +++ b/src/core/src/cache/tests/snapshots/liquid_cache__cache__tests__squeezed__read_squeezed_variant_path.snap @@ -14,9 +14,9 @@ event=insert_failed entry=2 kind=MemoryArrow event=squeeze_begin victims=[1,0] event=squeeze_victim entry=1 event=io_write entry=1 kind=MemorySqueezedLiquid bytes=7816 +event=insert_success entry=1 kind=MemorySqueezedLiquid event=squeeze_victim entry=0 event=insert_success entry=0 kind=DiskArrow -event=insert_success entry=1 kind=MemorySqueezedLiquid event=insert_success entry=2 kind=MemoryArrow event=read entry=0 selection=false expr=VariantGet[name:Utf8] cached=DiskArrow event=io_read_arrow entry=0 bytes=7816 diff --git a/src/core/src/cache/tests/squeezed.rs b/src/core/src/cache/tests/squeezed.rs index b14c6800..ec46a450 100644 --- a/src/core/src/cache/tests/squeezed.rs +++ b/src/core/src/cache/tests/squeezed.rs @@ -6,8 +6,8 @@ use parquet_variant_compute::json_to_variant; use crate::{ cache::{ - AlwaysHydrate, CacheExpression, DefaultIoContext, EntryID, LiquidCacheBuilder, - LiquidPolicy, TranscodeSqueezeEvict, + AlwaysHydrate, CacheExpression, EntryID, LiquidCacheBuilder, LiquidPolicy, + TranscodeSqueezeEvict, }, liquid_array::Date32Field, }; @@ -27,12 +27,12 @@ async fn read_squeezed_date_time() { .with_cache_policy(Box::new(LiquidPolicy::new())) .with_hydration_policy(Box::new(AlwaysHydrate::new())) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) - .with_max_cache_bytes(array_size * 2) - .with_io_context(Arc::new(DefaultIoContext::new( + .with_max_memory_bytes(array_size * 2) + .with_store( t4::mount(temp_dir.path().join("liquid_cache.t4")) .await .unwrap(), - ))) + ) .build() .await; @@ -43,7 +43,8 @@ async fn read_squeezed_date_time() { cache .insert(entry_id, array.clone()) .with_squeeze_hint(expression.clone()) - .await; + .await + .unwrap(); } for i in 0..4 { @@ -92,12 +93,12 @@ async fn read_squeezed_variant_path() { .with_cache_policy(Box::new(LiquidPolicy::new())) .with_hydration_policy(Box::new(AlwaysHydrate::new())) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) - .with_max_cache_bytes(array_size * 3 / 2) - .with_io_context(Arc::new(DefaultIoContext::new( + .with_max_memory_bytes(array_size * 3 / 2) + .with_store( t4::mount(temp_dir.path().join("liquid_cache.t4")) .await .unwrap(), - ))) + ) .build() .await; @@ -112,7 +113,8 @@ async fn read_squeezed_variant_path() { cache .insert(entry_id, variant_array.clone()) .with_squeeze_hint(name_expr.clone()) - .await; + .await + .unwrap(); } let squeezed = cache @@ -154,12 +156,12 @@ async fn read_squeezed_int64_array() { .with_cache_policy(Box::new(LiquidPolicy::new())) .with_hydration_policy(Box::new(AlwaysHydrate::new())) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) - .with_max_cache_bytes(array_size * 2) - .with_io_context(Arc::new(DefaultIoContext::new( + .with_max_memory_bytes(array_size * 2) + .with_store( t4::mount(temp_dir.path().join("liquid_cache.t4")) .await .unwrap(), - ))) + ) .build() .await; @@ -171,9 +173,10 @@ async fn read_squeezed_int64_array() { cache .insert(entry_id, int64_array.clone()) .with_squeeze_hint(expression.clone()) - .await; + .await + .unwrap(); } else { - cache.insert(entry_id, int64_array.clone()).await; + cache.insert(entry_id, int64_array.clone()).await.unwrap(); } } diff --git a/src/core/src/cache/utils.rs b/src/core/src/cache/utils.rs index a430db48..a4d25343 100644 --- a/src/core/src/cache/utils.rs +++ b/src/core/src/cache/utils.rs @@ -8,14 +8,16 @@ use bytes::Bytes; #[derive(Debug)] pub struct CacheConfig { batch_size: usize, - max_cache_bytes: usize, + max_memory_bytes: usize, + max_disk_bytes: usize, } impl CacheConfig { - pub(super) fn new(batch_size: usize, max_cache_bytes: usize) -> Self { + pub(super) fn new(batch_size: usize, max_memory_bytes: usize, max_disk_bytes: usize) -> Self { Self { batch_size, - max_cache_bytes, + max_memory_bytes, + max_disk_bytes, } } @@ -23,8 +25,12 @@ impl CacheConfig { self.batch_size } - pub fn max_cache_bytes(&self) -> usize { - self.max_cache_bytes + pub fn max_memory_bytes(&self) -> usize { + self.max_memory_bytes + } + + pub fn max_disk_bytes(&self) -> usize { + self.max_disk_bytes } } @@ -46,7 +52,7 @@ pub(crate) fn create_test_arrow_array(size: usize) -> ArrayRef { #[cfg(test)] pub(crate) async fn create_cache_store( - max_cache_bytes: usize, + max_memory_bytes: usize, policy: Box, ) -> Arc { use crate::cache::{AlwaysHydrate, LiquidCacheBuilder, TranscodeSqueezeEvict}; @@ -55,7 +61,7 @@ pub(crate) async fn create_cache_store( let builder = LiquidCacheBuilder::new() .with_batch_size(batch_size) - .with_max_cache_bytes(max_cache_bytes) + .with_max_memory_bytes(max_memory_bytes) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) .with_hydration_policy(Box::new(AlwaysHydrate::new())) .with_cache_policy(policy); diff --git a/src/core/src/liquid_array/byte_view_array/mod.rs b/src/core/src/liquid_array/byte_view_array/mod.rs index b5e78b0e..1d484148 100644 --- a/src/core/src/liquid_array/byte_view_array/mod.rs +++ b/src/core/src/liquid_array/byte_view_array/mod.rs @@ -20,7 +20,7 @@ use crate::liquid_array::raw::FsstArray; use crate::liquid_array::raw::fsst_buffer::{DiskBuffer, FsstBacking, PrefixKey}; use crate::liquid_array::{ LiquidArray, LiquidDataType, LiquidSqueezedArray, LiquidSqueezedArrayRef, SqueezeIoHandler, - eval_predicate_on_array, + SqueezedBacking, eval_predicate_on_array, }; mod comparisons; @@ -468,6 +468,10 @@ impl LiquidSqueezedArray for LiquidByteViewArray { self.original_arrow_type.to_arrow_type() } + fn disk_backing(&self) -> SqueezedBacking { + SqueezedBacking::Liquid(self.fsst_buffer.disk_range_len()) + } + /// Filter the Liquid array with a boolean array and return an **arrow array**. async fn filter(&self, selection: &BooleanBuffer) -> ArrayRef { let select_any = selection.count_set_bits() > 0; diff --git a/src/core/src/liquid_array/byte_view_array/operator.rs b/src/core/src/liquid_array/byte_view_array/operator.rs index fe852007..1035e4b6 100644 --- a/src/core/src/liquid_array/byte_view_array/operator.rs +++ b/src/core/src/liquid_array/byte_view_array/operator.rs @@ -134,22 +134,20 @@ impl ByteViewExpression { impl TryFrom<&Arc> for ByteViewExpression { type Error = UnsupportedExpression; fn try_from(expr: &Arc) -> Result { - let expr = if let Some(dynamic_filter) = - expr.as_any().downcast_ref::() - { + let expr = if let Some(dynamic_filter) = expr.downcast_ref::() { dynamic_filter.current().unwrap() } else { expr.clone() }; - if let Some(literal) = expr.as_any().downcast_ref::() + if let Some(literal) = expr.downcast_ref::() && let ScalarValue::Boolean(Some(v)) = literal.value() { return Err(UnsupportedExpression::Constant(*v)); } - if let Some(binary_expr) = expr.as_any().downcast_ref::() { - if let Some(literal) = binary_expr.right().as_any().downcast_ref::() { + if let Some(binary_expr) = expr.downcast_ref::() { + if let Some(literal) = binary_expr.right().downcast_ref::() { let op = binary_expr.op(); let byte_view_operator = ByteViewOperator::try_from(op)?; let literal = @@ -161,8 +159,8 @@ impl TryFrom<&Arc> for ByteViewExpression { } } // Handle like expressions - else if let Some(like_expr) = expr.as_any().downcast_ref::() - && let Some(literal) = like_expr.pattern().as_any().downcast_ref::() + else if let Some(like_expr) = expr.downcast_ref::() + && let Some(literal) = like_expr.pattern().downcast_ref::() { let byte_view_operator = ByteViewOperator::from_like_expr(like_expr)?; let literal = get_bytes_needle(literal.value()).ok_or(UnsupportedExpression::Expr)?; diff --git a/src/core/src/liquid_array/decimal_array.rs b/src/core/src/liquid_array/decimal_array.rs index 354f564e..374ff9cc 100644 --- a/src/core/src/liquid_array/decimal_array.rs +++ b/src/core/src/liquid_array/decimal_array.rs @@ -20,7 +20,7 @@ use num_traits::ToPrimitive; use super::{ LiquidArray, LiquidDataType, LiquidSqueezedArray, LiquidSqueezedArrayRef, NeedsBacking, - Operator, SqueezeIoHandler, SqueezeResult, + Operator, SqueezeIoHandler, SqueezeResult, SqueezedBacking, }; use crate::cache::{CacheExpression, LiquidExpr}; use crate::liquid_array::eval_predicate_on_array; @@ -537,6 +537,10 @@ impl LiquidSqueezedArray for LiquidDecimalQuantizedArray { self.meta.data_type() } + fn disk_backing(&self) -> SqueezedBacking { + SqueezedBacking::Liquid((self.disk_range.end - self.disk_range.start) as usize) + } + async fn try_eval_predicate( &self, liquid_expr: &LiquidExpr, @@ -550,21 +554,16 @@ impl LiquidSqueezedArray for LiquidDecimalQuantizedArray { let fallback = self.filter(filter).await; return eval_predicate_on_array(fallback, liquid_expr); }; - let Some(binary_expr) = expr.as_any().downcast_ref::() else { + let Some(binary_expr) = expr.downcast_ref::() else { let fallback = self.filter(filter).await; return eval_predicate_on_array(fallback, liquid_expr); }; - if binary_expr - .left() - .as_any() - .downcast_ref::() - .is_none() - { + if !binary_expr.left().is::() { let fallback = self.filter(filter).await; return eval_predicate_on_array(fallback, liquid_expr); } - let Some(literal) = binary_expr.right().as_any().downcast_ref::() else { + let Some(literal) = binary_expr.right().downcast_ref::() else { let fallback = self.filter(filter).await; return eval_predicate_on_array(fallback, liquid_expr); }; @@ -617,7 +616,7 @@ impl LiquidSqueezedArray for LiquidDecimalQuantizedArray { } fn unwrap_dynamic_filter(expr: &Arc) -> Option> { - if let Some(dynamic_filter) = expr.as_any().downcast_ref::() { + if let Some(dynamic_filter) = expr.downcast_ref::() { dynamic_filter.current().ok() } else { Some(expr.clone()) diff --git a/src/core/src/liquid_array/float_array.rs b/src/core/src/liquid_array/float_array.rs index b3985aa8..683dece8 100644 --- a/src/core/src/liquid_array/float_array.rs +++ b/src/core/src/liquid_array/float_array.rs @@ -36,7 +36,7 @@ use crate::liquid_array::ipc::{PhysicalTypeMarker, get_physical_type_id}; use crate::liquid_array::raw::BitPackedArray; use crate::liquid_array::{ LiquidSqueezedArray, LiquidSqueezedArrayRef, NeedsBacking, Operator, SqueezeResult, - eval_predicate_on_array, ipc::LiquidIPCHeader, + SqueezedBacking, eval_predicate_on_array, ipc::LiquidIPCHeader, }; use crate::utils::get_bit_width; use crate::{cache::CacheExpression, liquid_array::SqueezeIoHandler}; @@ -985,6 +985,10 @@ where T::DATA_TYPE.clone() } + fn disk_backing(&self) -> SqueezedBacking { + SqueezedBacking::Liquid((self.disk_range.end - self.disk_range.start) as usize) + } + async fn try_eval_predicate( &self, liquid_expr: &LiquidExpr, @@ -994,8 +998,8 @@ where let filtered = self.filter_inner(filter); let expr = liquid_expr.physical_expr(); - if let Some(binary_expr) = expr.as_any().downcast_ref::() - && let Some(literal) = binary_expr.right().as_any().downcast_ref::() + if let Some(binary_expr) = expr.downcast_ref::() + && let Some(literal) = binary_expr.right().downcast_ref::() { let op = binary_expr.op(); let supported_op = Operator::from_datafusion(op); diff --git a/src/core/src/liquid_array/hybrid_primitive_array.rs b/src/core/src/liquid_array/hybrid_primitive_array.rs index 15dcb5cb..f2d0845b 100644 --- a/src/core/src/liquid_array/hybrid_primitive_array.rs +++ b/src/core/src/liquid_array/hybrid_primitive_array.rs @@ -23,7 +23,7 @@ use crate::liquid_array::raw::BitPackedArray; use super::primitive_array::LiquidPrimitiveType; use super::{ LiquidDataType, LiquidSqueezedArray, NeedsBacking, Operator, PrimitiveKind, SqueezeIoHandler, - SqueezeResult, + SqueezeResult, SqueezedBacking, }; #[derive(Clone, Copy)] @@ -33,7 +33,7 @@ enum PredicateLhs { } fn unwrap_dynamic_filter(expr: &Arc) -> Option> { - if let Some(dynamic_filter) = expr.as_any().downcast_ref::() { + if let Some(dynamic_filter) = expr.downcast_ref::() { dynamic_filter.current().ok() } else { Some(expr.clone()) @@ -41,13 +41,13 @@ fn unwrap_dynamic_filter(expr: &Arc) -> Option) -> Option { - if expr.as_any().downcast_ref::().is_some() { + if expr.is::() { return Some(PredicateLhs::Plain); } - if let Some(func) = expr.as_any().downcast_ref::() + if let Some(func) = expr.downcast_ref::() && func.name() == "to_timestamp_seconds" && let [arg] = func.args() - && arg.as_any().downcast_ref::().is_some() + && arg.is::() { return Some(PredicateLhs::ToTimestampSeconds); } @@ -317,6 +317,10 @@ where T::DATA_TYPE.clone() } + fn disk_backing(&self) -> SqueezedBacking { + SqueezedBacking::Liquid((self.disk_range.end - self.disk_range.start) as usize) + } + async fn filter(&self, selection: &BooleanBuffer) -> ArrayRef { if selection.count_set_bits() == 0 { return arrow::array::new_empty_array(&self.original_arrow_data_type()); @@ -344,7 +348,7 @@ where let fallback = self.filter(filter).await; return eval_predicate_on_array(fallback, liquid_expr); }; - let Some(binary_expr) = expr.as_any().downcast_ref::() else { + let Some(binary_expr) = expr.downcast_ref::() else { let fallback = self.filter(filter).await; return eval_predicate_on_array(fallback, liquid_expr); }; @@ -352,7 +356,7 @@ where let fallback = self.filter(filter).await; return eval_predicate_on_array(fallback, liquid_expr); }; - let Some(literal) = binary_expr.right().as_any().downcast_ref::() else { + let Some(literal) = binary_expr.right().downcast_ref::() else { let fallback = self.filter(filter).await; return eval_predicate_on_array(fallback, liquid_expr); }; @@ -689,6 +693,10 @@ where T::DATA_TYPE.clone() } + fn disk_backing(&self) -> SqueezedBacking { + SqueezedBacking::Liquid((self.disk_range.end - self.disk_range.start) as usize) + } + async fn try_eval_predicate( &self, liquid_expr: &LiquidExpr, @@ -703,7 +711,7 @@ where let fallback = self.filter(filter).await; return eval_predicate_on_array(fallback, liquid_expr); }; - let Some(binary_expr) = expr.as_any().downcast_ref::() else { + let Some(binary_expr) = expr.downcast_ref::() else { let fallback = self.filter(filter).await; return eval_predicate_on_array(fallback, liquid_expr); }; @@ -711,7 +719,7 @@ where let fallback = self.filter(filter).await; return eval_predicate_on_array(fallback, liquid_expr); }; - let Some(literal) = binary_expr.right().as_any().downcast_ref::() else { + let Some(literal) = binary_expr.right().downcast_ref::() else { let fallback = self.filter(filter).await; return eval_predicate_on_array(fallback, liquid_expr); }; diff --git a/src/core/src/liquid_array/mod.rs b/src/core/src/liquid_array/mod.rs index 22b07cb0..7776c764 100644 --- a/src/core/src/liquid_array/mod.rs +++ b/src/core/src/liquid_array/mod.rs @@ -148,13 +148,25 @@ pub trait LiquidArray: std::fmt::Debug + Send + Sync { /// A reference to a Liquid array. pub type LiquidArrayRef = Arc; -/// On-disk backing type for a hybrid array. +/// On-disk backing for a squeezed array. +/// +/// Each variant carries the byte length of the persisted backing data, so the +/// cache can release the disk budget when the entry is evicted. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SqueezedBacking { /// Bytes are stored using the Liquid IPC format. - Liquid, + Liquid(usize), /// Bytes are stored using Arrow IPC (or another Arrow-compatible encoding). - Arrow, + Arrow(usize), +} + +impl SqueezedBacking { + /// Byte length of the backing data persisted on disk. + pub fn disk_bytes(&self) -> usize { + match self { + Self::Liquid(n) | Self::Arrow(n) => *n, + } + } } /// A reference to a Liquid squeezed array. @@ -245,10 +257,9 @@ pub trait LiquidSqueezedArray: std::fmt::Debug + Send + Sync { eval_predicate_on_array(filtered, predicate) } - /// Describe how the squeezed array persists its backing bytes on disk. - fn disk_backing(&self) -> SqueezedBacking { - SqueezedBacking::Liquid - } + /// Describe how the squeezed array persists its backing bytes on disk, + /// including the byte length of the persisted data. + fn disk_backing(&self) -> SqueezedBacking; } pub(crate) fn eval_predicate_on_array(array: ArrayRef, predicate: &LiquidExpr) -> BooleanArray { diff --git a/src/core/src/liquid_array/raw/fsst_buffer.rs b/src/core/src/liquid_array/raw/fsst_buffer.rs index f518cdae..4e64b6f4 100644 --- a/src/core/src/liquid_array/raw/fsst_buffer.rs +++ b/src/core/src/liquid_array/raw/fsst_buffer.rs @@ -718,6 +718,10 @@ impl DiskBuffer { self.disk_range.clone() } + pub(crate) fn disk_range_len(&self) -> usize { + (self.disk_range.end - self.disk_range.start) as usize + } + pub(crate) fn compressor_arc(&self) -> Arc { self.compressor.clone() } diff --git a/src/core/src/liquid_array/squeezed_date32_array.rs b/src/core/src/liquid_array/squeezed_date32_array.rs index 67e2b93a..e2497e55 100644 --- a/src/core/src/liquid_array/squeezed_date32_array.rs +++ b/src/core/src/liquid_array/squeezed_date32_array.rs @@ -16,7 +16,7 @@ use std::sync::Arc; use super::LiquidArray; use super::primitive_array::LiquidPrimitiveArray; -use super::{LiquidDataType, LiquidSqueezedArray}; +use super::{LiquidDataType, LiquidSqueezedArray, SqueezedBacking}; use crate::cache::LiquidExpr; use crate::liquid_array::LiquidPrimitiveType; use crate::liquid_array::SqueezeIoHandler; @@ -25,7 +25,7 @@ use crate::liquid_array::raw::BitPackedArray; use crate::utils::get_bit_width; /// Which component to extract from a `Date32`/Timestamp (days since UNIX epoch). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub enum Date32Field { /// Year component Year, @@ -49,11 +49,11 @@ pub struct SqueezedDate32Array { /// The minimum extracted value used as reference for offsetting. reference_value: i32, original_data_type: DataType, - backing: Option, + backing: Option, } #[derive(Debug, Clone)] -struct SqueezedBacking { +struct DiskBacking { io: Arc, disk_range: Range, } @@ -225,7 +225,7 @@ impl SqueezedDate32Array { io: Arc, disk_range: Range, ) -> Self { - self.backing = Some(SqueezedBacking { io, disk_range }); + self.backing = Some(DiskBacking { io, disk_range }); self } @@ -261,12 +261,14 @@ impl SqueezedDate32Array { self.field } - /// Convert to an Arrow array holding the extracted component. + /// Convert to an Arrow array shaped like the original input, encoded so that + /// re-applying `date_part` (or any equivalent extraction) recovers the + /// component value originally squeezed. pub fn to_component_array(&self) -> ArrayRef { match &self.original_data_type { - DataType::Date32 => Arc::new(self.to_component_date32()) as ArrayRef, - DataType::Timestamp(unit, _) => self.to_component_timestamp(*unit), - _ => Arc::new(self.to_component_date32()) as ArrayRef, + DataType::Date32 => Arc::new(self.to_arrow_date32_lossy()) as ArrayRef, + DataType::Timestamp(unit, _) => self.to_arrow_timestamp_lossy(*unit), + _ => Arc::new(self.to_arrow_date32_lossy()) as ArrayRef, } } @@ -281,28 +283,35 @@ impl SqueezedDate32Array { PrimitiveArray::::new(signed_values, nulls) } - fn to_component_timestamp(&self, unit: TimeUnit) -> ArrayRef { - let unsigned: PrimitiveArray = self.bit_packed.to_primitive(); - let (_dt, values, nulls) = unsigned.into_parts(); - let ref_v = self.reference_value; - let signed_values: ScalarBuffer = - ScalarBuffer::from_iter(values.iter().map(|&v| (v as i32 + ref_v) as i64)); - + /// Lossy reconstruction to Arrow Timestamp at the requested unit, using the + /// same date mapping as [`Self::to_arrow_date32_lossy`] (midnight UTC of the + /// reconstructed date). + pub fn to_arrow_timestamp_lossy(&self, unit: TimeUnit) -> ArrayRef { + let date_array = self.to_arrow_date32_lossy(); + let (_dt, day_values, nulls) = date_array.into_parts(); + let ticks_per_day: i64 = match unit { + TimeUnit::Second => 86_400, + TimeUnit::Millisecond => 86_400_000, + TimeUnit::Microsecond => 86_400_000_000, + TimeUnit::Nanosecond => 86_400_000_000_000, + }; + let tick_values: ScalarBuffer = + ScalarBuffer::from_iter(day_values.iter().map(|&d| (d as i64) * ticks_per_day)); match unit { TimeUnit::Second => Arc::new(PrimitiveArray::::new( - signed_values, + tick_values, nulls, )), TimeUnit::Millisecond => Arc::new(PrimitiveArray::::new( - signed_values.clone(), + tick_values, nulls, )), TimeUnit::Microsecond => Arc::new(PrimitiveArray::::new( - signed_values.clone(), + tick_values, nulls, )), TimeUnit::Nanosecond => Arc::new(PrimitiveArray::::new( - signed_values, + tick_values, nulls, )), } @@ -450,6 +459,14 @@ impl LiquidSqueezedArray for SqueezedDate32Array { self.original_data_type.clone() } + fn disk_backing(&self) -> SqueezedBacking { + let backing = self + .backing + .as_ref() + .expect("SqueezedDate32Array backing not set"); + SqueezedBacking::Liquid((backing.disk_range.end - backing.disk_range.start) as usize) + } + async fn filter(&self, selection: &BooleanBuffer) -> ArrayRef { if selection.count_set_bits() == 0 { return arrow::array::new_empty_array(&self.original_arrow_data_type()); @@ -645,8 +662,56 @@ mod tests { } } + /// `to_component_array` is consumed by [`crate::cache::core::LiquidCache::try_read_squeezed_date32_array`] + /// as the SQL fast path. The query plan still runs `date_part` on the returned array, so the + /// values must round-trip through `component_from_days`: feeding a returned Date32 day-value + /// back into `component_from_days(field, days)` must recover the original component. + /// + /// Before the encoding fix, the Year case returned `Date32(year_int)` (e.g. year 1970 became + /// Date32 day-1970 = 1975-05-24), so re-extracting the year gave 1975 instead of 1970. + #[test] + fn to_component_array_date32_round_trips_through_extract() { + let inputs: Vec> = vec![ + Some(ymd_to_epoch_days(1970, 1, 1)), + Some(ymd_to_epoch_days(1971, 7, 15)), + Some(ymd_to_epoch_days(1999, 12, 31)), + Some(ymd_to_epoch_days(2024, 2, 29)), + Some(ymd_to_epoch_days(4709, 11, 24)), + None, + ]; + let expected_components: Vec> = inputs + .iter() + .map(|opt| opt.map(|d| component_from_days(Date32Field::Year, d))) + .collect(); + + let arr = dates(&inputs); + let liquid = LiquidPrimitiveArray::::from_arrow_array(arr); + let squeezed = SqueezedDate32Array::from_liquid_date32(&liquid, Date32Field::Year); + let component = squeezed + .to_component_array() + .as_any() + .downcast_ref::>() + .expect("date32 component array") + .clone(); + + for (idx, expected) in expected_components.iter().enumerate() { + match expected { + Some(year) => { + assert!(!component.is_null(idx), "row {idx} unexpectedly null"); + let recovered = component_from_days(Date32Field::Year, component.value(idx)); + assert_eq!( + recovered, *year, + "row {idx}: extracting Year from to_component_array output recovered {recovered}, expected {year}", + ); + } + None => assert!(component.is_null(idx), "row {idx} should be null"), + } + } + } + #[test] fn test_timestamp_extraction() { + // Two Microsecond timestamps at 2021-01-01 00:00:00 UTC and 2022-01-01 00:00:00 UTC. let input = vec![ Some(1_609_459_200_000_000), Some(1_640_995_200_000_000), @@ -661,8 +726,23 @@ mod tests { .downcast_ref::>() .expect("timestamp array"); - assert_eq!(out.value(0), 2021); - assert_eq!(out.value(1), 2022); + // to_component_array returns Timestamps that round-trip through `date_part`: + // year 2021 maps to (2021,1,1) at midnight UTC. + let micros_per_day: i64 = 86_400_000_000; + assert_eq!( + out.value(0), + ymd_to_epoch_days(2021, 1, 1) as i64 * micros_per_day, + ); + assert_eq!( + out.value(1), + ymd_to_epoch_days(2022, 1, 1) as i64 * micros_per_day, + ); assert!(out.is_null(2)); + + // Direct integer view is still available via `to_component_date32`. + let int_view = squeezed.to_component_date32(); + assert_eq!(int_view.value(0), 2021); + assert_eq!(int_view.value(1), 2022); + assert!(int_view.is_null(2)); } } diff --git a/src/core/src/liquid_array/variant_array.rs b/src/core/src/liquid_array/variant_array.rs index 4b4e122d..aaae6f38 100644 --- a/src/core/src/liquid_array/variant_array.rs +++ b/src/core/src/liquid_array/variant_array.rs @@ -16,6 +16,7 @@ pub struct VariantStructSqueezedArray { len: usize, nulls: Option, original_arrow_type: DataType, + disk_backing_size: usize, } impl VariantStructSqueezedArray { @@ -24,6 +25,7 @@ impl VariantStructSqueezedArray { values: Vec<(Arc, LiquidArrayRef)>, nulls: Option, original_arrow_type: DataType, + disk_backing_size: usize, ) -> Self { let len = values.first().map(|(_, array)| array.len()).unwrap_or(0); let mut map = AHashMap::with_capacity(values.len()); @@ -36,6 +38,7 @@ impl VariantStructSqueezedArray { len, nulls, original_arrow_type, + disk_backing_size, } } @@ -100,6 +103,7 @@ impl VariantStructSqueezedArray { filtered, self.nulls.clone(), self.original_arrow_type.clone(), + self.disk_backing_size, ); Ok(Arc::new(filtered.build_root_struct()) as ArrayRef) } @@ -148,7 +152,7 @@ impl LiquidSqueezedArray for VariantStructSqueezedArray { } fn disk_backing(&self) -> SqueezedBacking { - SqueezedBacking::Arrow + SqueezedBacking::Arrow(self.disk_backing_size) } } @@ -248,6 +252,7 @@ mod tests { ], None, DataType::Struct(Fields::from(Vec::>::new())), + 0, ); // Request only time_us; did should be pruned from typed_value. diff --git a/src/core/study/cache_storage.rs b/src/core/study/cache_storage.rs index d9a819d3..0c69f448 100644 --- a/src/core/study/cache_storage.rs +++ b/src/core/study/cache_storage.rs @@ -10,13 +10,12 @@ use datafusion::logical_expr::Operator; use datafusion::prelude::*; use datafusion::scalar::ScalarValue; use futures::StreamExt; -use liquid_cache::cache::DefaultIoContext; use liquid_cache::cache::EntryID; use liquid_cache::cache::LiquidCache; use liquid_cache::cache::LiquidCacheBuilder; use liquid_cache::cache::LiquidExpr; +use liquid_cache::cache::LiquidPolicy; use liquid_cache::cache::squeeze_policies::TranscodeSqueezeEvict; -use liquid_cache::cache_policies::FiloPolicy; #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; @@ -48,13 +47,12 @@ fn main() { .unwrap_or_else(|| tempfile::tempdir().unwrap().keep()); let store_path = cache_dir.join("liquid_cache.t4"); let store = tokio_test::block_on(t4::mount(&store_path)).expect("failed to mount t4 store"); - let io_context = Arc::new(DefaultIoContext::new(store)); let storage = tokio_test::block_on(async { LiquidCacheBuilder::new() - .with_max_cache_bytes(500 * 1024 * 1024) + .with_max_memory_bytes(500 * 1024 * 1024) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) - .with_cache_policy(Box::new(FiloPolicy::new())) - .with_io_context(io_context) + .with_cache_policy(Box::new(LiquidPolicy::new())) + .with_store(store) .build() .await }); @@ -146,7 +144,7 @@ fn load_and_insert_referer( let id = EntryID::from(idx); ids.push(id); total_size += array.get_array_memory_size(); - storage.insert(id, array).await; + storage.insert(id, array).await.unwrap(); idx += 1; } diff --git a/src/core/study/squeeze_integer.rs b/src/core/study/squeeze_integer.rs index 58677e17..cfdb89cc 100644 --- a/src/core/study/squeeze_integer.rs +++ b/src/core/study/squeeze_integer.rs @@ -434,8 +434,8 @@ fn eval_on_arrow( use datafusion::physical_expr_common::datum::apply_cmp; use datafusion::physical_plan::expressions::{BinaryExpr, Literal}; - if let Some(be) = expr.as_any().downcast_ref::() - && let Some(lit) = be.right().as_any().downcast_ref::() + if let Some(be) = expr.downcast_ref::() + && let Some(lit) = be.right().downcast_ref::() { let target_dt = scalar_data_type(lit.value()).unwrap_or_else(|| array.data_type().clone()); let lhs_arr = if &target_dt == array.data_type() { diff --git a/src/datafusion-client/Cargo.toml b/src/datafusion-client/Cargo.toml index a152ab87..ddd207cd 100644 --- a/src/datafusion-client/Cargo.toml +++ b/src/datafusion-client/Cargo.toml @@ -18,6 +18,7 @@ futures = { workspace = true } tonic = { workspace = true } uuid = { workspace = true } liquid-cache-common = { workspace = true } +liquid-cache-datafusion = { workspace = true } fastrace = { workspace = true } fastrace-tonic = { workspace = true } tower = "0.5.3" diff --git a/src/datafusion-client/src/client_exec.rs b/src/datafusion-client/src/client_exec.rs index 67ba8851..658c86dc 100644 --- a/src/datafusion-client/src/client_exec.rs +++ b/src/datafusion-client/src/client_exec.rs @@ -2,14 +2,21 @@ use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; use std::task::{Context, Poll}; use std::time::Duration; -use std::{any::Any, fmt::Formatter, sync::Arc}; +use std::{fmt::Formatter, sync::Arc}; use arrow::array::RecordBatch; use arrow_flight::decode::FlightRecordBatchStream; use arrow_flight::flight_service_client::FlightServiceClient; -use arrow_schema::SchemaRef; +use arrow_schema::{Schema, SchemaRef}; +use datafusion::catalog::memory::DataSourceExec; +use datafusion::common::internal_err; +use datafusion::common::tree_node::{Transformed, TreeNode}; use datafusion::config::ConfigOptions; +use datafusion::datasource::physical_plan::{FileSource, ParquetSource}; use datafusion::execution::object_store::ObjectStoreUrl; +use datafusion::logical_expr::ColumnarValue; +use datafusion::physical_expr::expressions::Literal; +use datafusion::physical_expr::scalar_subquery::ScalarSubqueryExpr; use datafusion::physical_expr_adapter::{BatchAdapter, BatchAdapterFactory}; use datafusion::physical_plan::execution_plan::CardinalityEffect; use datafusion::physical_plan::filter_pushdown::{ @@ -30,8 +37,10 @@ use fastrace::future::FutureExt; use fastrace::prelude::*; use futures::{Stream, TryStreamExt, future::BoxFuture, ready}; use liquid_cache_common::rpc::{ - FetchResults, LiquidCacheActions, RegisterObjectStoreRequest, RegisterPlanRequest, + ColumnSqueezeHint, FetchResults, LiquidCacheActions, RegisterObjectStoreRequest, + RegisterPlanRequest, }; +use liquid_cache_datafusion::cache::ColumnSqueezeHints; use tonic::Request; use uuid::Uuid; @@ -54,6 +63,9 @@ pub struct LiquidCacheClientExec { uuid: Uuid, plan_registered: Arc, properties: Arc, + /// Typed squeeze hints for the scan in `remote_plan`, derived by the client + /// from the full physical plan and shipped to the cache server. + squeeze_hints: ColumnSqueezeHints, } impl std::fmt::Debug for LiquidCacheClientExec { @@ -67,6 +79,7 @@ impl LiquidCacheClientExec { remote_plan: Arc, cache_server: String, object_stores: Vec<(ObjectStoreUrl, HashMap)>, + squeeze_hints: ColumnSqueezeHints, ) -> Self { let properties = Arc::new(PlanProperties::new( remote_plan.equivalence_properties().clone(), // Equivalence Properties @@ -83,6 +96,7 @@ impl LiquidCacheClientExec { uuid, metrics: ExecutionPlanMetricsSet::new(), properties, + squeeze_hints, } } @@ -114,10 +128,6 @@ impl DisplayAs for LiquidCacheClientExec { } impl ExecutionPlan for LiquidCacheClientExec { - fn as_any(&self) -> &dyn Any { - self - } - fn name(&self) -> &str { "LiquidCacheClientExec" } @@ -142,6 +152,7 @@ impl ExecutionPlan for LiquidCacheClientExec { metrics: self.metrics.clone(), uuid: self.uuid, properties: self.properties.clone(), + squeeze_hints: self.squeeze_hints.clone(), })) } @@ -168,6 +179,7 @@ impl ExecutionPlan for LiquidCacheClientExec { self.uuid, partition, self.object_stores.clone(), + squeeze_hints_to_wire(&self.squeeze_hints), ); Ok(Box::pin(FlightStream::new( Some(Box::pin(stream)), @@ -224,6 +236,17 @@ impl ExecutionPlan for LiquidCacheClientExec { } } +/// Convert typed squeeze hints into their wire form (canonical string encoding). +fn squeeze_hints_to_wire(hints: &ColumnSqueezeHints) -> Vec { + hints + .iter() + .map(|(column, expr)| ColumnSqueezeHint { + column: column.clone(), + hint: expr.to_metadata_value(), + }) + .collect() +} + async fn flight_stream( server: String, plan: Arc, @@ -231,7 +254,14 @@ async fn flight_stream( handle: Uuid, partition: usize, object_stores: Vec<(ObjectStoreUrl, HashMap)>, + squeeze_hints: Vec, ) -> Result { + // Materialized scalar-subquery results are embedded in scan predicates as + // `ScalarSubqueryExpr`, which cannot be serialized on its own and is + // meaningless on the remote server. Replace them with literal values + // before the plan is shipped. + let plan = snapshot_scalar_subqueries(plan)?; + let channel = flight_channel(server) .in_span(Span::enter_with_local_parent("connect_channel")) .await?; @@ -264,6 +294,7 @@ async fn flight_stream( let action = LiquidCacheActions::RegisterPlan(RegisterPlanRequest { plan: plan_bytes.to_vec(), handle: handle.into_bytes().to_vec().into(), + squeeze_hints: squeeze_hints.clone(), }) .into(); client @@ -300,6 +331,68 @@ async fn flight_stream( Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream))) } +/// Rewrite scan predicates in `plan` so they no longer contain +/// [`ScalarSubqueryExpr`], making the plan safe to serialize and execute on the +/// remote cache server. +/// +/// DataFusion 54 represents an uncorrelated scalar subquery as a +/// `ScalarSubqueryExpr` whose value is produced at runtime by a sibling +/// `ScalarSubqueryExec`. When such a predicate is pushed into a Parquet scan +/// that we ship to the server, two things break: `datafusion-proto` refuses to +/// serialize a bare `ScalarSubqueryExpr` (it can only be decoded inside its +/// exec), and the server has no way to run the subquery. By the time a scan is +/// shipped the subquery result is already computed, so we substitute its value. +fn snapshot_scalar_subqueries(plan: Arc) -> Result> { + Ok(plan + .transform_up(|node| match rebuild_scan_without_subquery(&node)? { + Some(new_plan) => Ok(Transformed::yes(new_plan)), + None => Ok(Transformed::no(node)), + })? + .data) +} + +/// If `node` is a Parquet scan whose predicate contains a `ScalarSubqueryExpr`, +/// return a rebuilt scan with those exprs replaced by their literal values; +/// otherwise return `None`. +fn rebuild_scan_without_subquery( + node: &Arc, +) -> Result>> { + let Some(data_source_exec) = node.downcast_ref::() else { + return Ok(None); + }; + let Some((file_scan_config, parquet_source)) = + data_source_exec.downcast_to_file_source::() + else { + return Ok(None); + }; + let Some(predicate) = parquet_source.filter() else { + return Ok(None); + }; + + let rewritten = predicate.transform_up(|expr| { + if let Some(subquery) = expr.downcast_ref::() { + let empty = RecordBatch::new_empty(Arc::new(Schema::empty())); + let ColumnarValue::Scalar(value) = subquery.evaluate(&empty)? else { + return internal_err!("scalar subquery produced an array, cannot push down"); + }; + Ok(Transformed::yes( + Arc::new(Literal::new(value)) as Arc + )) + } else { + Ok(Transformed::no(expr)) + } + })?; + + if !rewritten.transformed { + return Ok(None); + } + + let new_source = parquet_source.with_predicate(rewritten.data); + let mut new_config = file_scan_config.clone(); + new_config.file_source = Arc::new(new_source); + Ok(Some(Arc::new(DataSourceExec::new(Arc::new(new_config))))) +} + enum FlightStreamState { Init, GetStream(BoxFuture<'static, Result>), diff --git a/src/datafusion-client/src/lib.rs b/src/datafusion-client/src/lib.rs index 35fbec67..079c7611 100644 --- a/src/datafusion-client/src/lib.rs +++ b/src/datafusion-client/src/lib.rs @@ -92,6 +92,19 @@ impl LiquidCacheClientBuilder { .parquet .binary_as_string = true; session_config.options_mut().execution.batch_size = 8192 * 2; + // Dynamic filters (e.g. a hash join's runtime build-side filter) are pushed + // into scan predicates by DataFusion. In distributed mode those scans are + // serialized and executed on a remote server that can never receive the + // join's runtime updates, and the serialized `DynamicFilterPhysicalExpr` + // carries column indices relative to the join schema rather than the scan, + // so the server fails to decode it. Disable the optimization on the client. + // The master `enable_dynamic_filter_pushdown` only cascades to the + // sub-options when set via the string API, so set each one explicitly. + let optimizer_opts = &mut session_config.options_mut().optimizer; + optimizer_opts.enable_dynamic_filter_pushdown = false; + optimizer_opts.enable_join_dynamic_filter_pushdown = false; + optimizer_opts.enable_topk_dynamic_filter_pushdown = false; + optimizer_opts.enable_aggregate_dynamic_filter_pushdown = false; let runtime_env = Arc::new(RuntimeEnv::default()); diff --git a/src/datafusion-client/src/optimizer.rs b/src/datafusion-client/src/optimizer.rs index c7b8be0e..228e7527 100644 --- a/src/datafusion-client/src/optimizer.rs +++ b/src/datafusion-client/src/optimizer.rs @@ -7,6 +7,8 @@ use datafusion::{ physical_plan::aggregates::AggregateMode, physical_plan::repartition::RepartitionExec, }; +use liquid_cache_datafusion::optimizers::SqueezeHintMap; + use crate::client_exec::LiquidCacheClientExec; /// PushdownOptimizer is a physical optimizer rule that pushes down filters to the liquid cache server. @@ -37,13 +39,13 @@ impl PushdownOptimizer { } /// Apply the optimization by finding nodes to push down and wrapping them - fn optimize_plan(&self, plan: Arc) -> Result> { + fn optimize_plan( + &self, + plan: Arc, + hints: &SqueezeHintMap, + ) -> Result> { // If this node is already a LiquidCacheClientExec, return it as is - if plan - .as_any() - .downcast_ref::() - .is_some() - { + if plan.is::() { return Ok(plan); } @@ -51,10 +53,16 @@ impl PushdownOptimizer { if let Some(candidate) = find_pushdown_candidate(&plan) { // If the current node is the one to be pushed down, wrap it if Arc::ptr_eq(&plan, &candidate) { + // The fragment is single-scan; collect that scan's squeeze + // hints (derived from the full plan, which includes the + // client-side projections that won't be shipped) so the server + // can apply them when it rebuilds the LiquidParquetSource. + let squeeze_hints = hints.for_fragment(&plan); return Ok(Arc::new(LiquidCacheClientExec::new( plan, self.cache_server.clone(), self.object_stores.clone(), + squeeze_hints, ))); } } @@ -64,7 +72,7 @@ impl PushdownOptimizer { let mut children_changed = false; for child in plan.children() { - let new_child = self.optimize_plan(child.clone())?; + let new_child = self.optimize_plan(child.clone(), hints)?; if !Arc::ptr_eq(child, &new_child) { children_changed = true; } @@ -83,48 +91,39 @@ impl PushdownOptimizer { /// Find the highest pushable node fn find_pushdown_candidate(plan: &Arc) -> Option> { // Check if this node is already a LiquidCacheClientExec to avoid redundant wrapping - if plan - .as_any() - .downcast_ref::() - .is_some() - { + if plan.is::() { return None; } - let plan_any = plan.as_any(); - // If we have an AggregateExec (partial, no group by) with a pushable child (direct or through RepartitionExec), push it down - if let Some(agg_exec) = plan_any.downcast_ref::() + if let Some(agg_exec) = plan.downcast_ref::() && matches!(agg_exec.mode(), AggregateMode::Partial) && agg_exec.group_expr().is_empty() { let child = agg_exec.input(); // Check if child is DataSourceExec or RepartitionExec->DataSourceExec - if child.as_any().downcast_ref::().is_some() { + if child.is::() { return Some(plan.clone()); } - if let Some(repart) = child.as_any().downcast_ref::() + if let Some(repart) = child.downcast_ref::() && let Some(repart_child) = repart.children().first() - && repart_child - .as_any() - .downcast_ref::() - .is_some() + && repart_child.is::() { return Some(plan.clone()); } } // If we have a RepartitionExec with a DataSourceExec child, push it down - if let Some(repart_exec) = plan_any.downcast_ref::() + if let Some(repart_exec) = plan.downcast_ref::() && let Some(child) = repart_exec.children().first() - && child.as_any().downcast_ref::().is_some() + && child.is::() { return Some(plan.clone()); } // If this is a DataSourceExec, push it down - if plan_any.downcast_ref::().is_some() { + if plan.is::() { return Some(plan.clone()); } @@ -144,7 +143,12 @@ impl PhysicalOptimizerRule for PushdownOptimizer { plan: Arc, _config: &ConfigOptions, ) -> Result> { - self.optimize_plan(plan) + // Derive squeeze hints from the full physical plan up front: the + // lineage that justifies a hint (e.g. a `date_part` projection) often + // lives above the node we push down, so it must be captured before the + // plan is split into fragments. + let hints = SqueezeHintMap::analyze(&plan); + self.optimize_plan(plan, &hints) } fn name(&self) -> &str { diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q1.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q1.snap index ea888577..7590f16a 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q1.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q1.snap @@ -57,11 +57,15 @@ expression: displayable.tree_render().to_string() │ sum(lineitem.l_quantity), │ │ sum(lineitem │ │ .l_extendedpric │ -│ e), sum(lineitem │ -│ .l_extendedprice │ -│ * Int64(1) - lineitem │ +│ e), sum(__common_expr_1) │ +│ as sum(lineitem │ +│ .l_extendedprice * │ +│ Int64(1) - lineitem │ │ .l_discount), sum │ -│ (lineitem │ +│ (__common_expr_1 * │ +│ Some(1),20,0 + │ +│ lineitem.l_tax) as │ +│ sum(lineitem │ │ .l_extendedpri │ │ ce * Int64(1) - lineitem │ │ .l_discount * Int64(1) │ @@ -96,11 +100,15 @@ expression: displayable.tree_render().to_string() │ sum(lineitem.l_quantity), │ │ sum(lineitem │ │ .l_extendedpric │ -│ e), sum(lineitem │ -│ .l_extendedprice │ -│ * Int64(1) - lineitem │ +│ e), sum(__common_expr_1) │ +│ as sum(lineitem │ +│ .l_extendedprice * │ +│ Int64(1) - lineitem │ │ .l_discount), sum │ -│ (lineitem │ +│ (__common_expr_1 * │ +│ Some(1),20,0 + │ +│ lineitem.l_tax) as │ +│ sum(lineitem │ │ .l_extendedpri │ │ ce * Int64(1) - lineitem │ │ .l_discount * Int64(1) │ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q10.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q10.snap index c7073aff..8643f563 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q10.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q10.snap @@ -39,7 +39,11 @@ expression: displayable.tree_render().to_string() │ aggr: │ │ sum(lineitem │ │ .l_extendedp │ -│ rice * Int64(1) - lineitem│ +│ rice * Some(1),20,0 - │ +│ lineitem.l_discount │ +│ ) as sum(lineitem │ +│ .l_extendedprice │ +│ * Int64(1) - lineitem │ │ .l_discount) │ │ │ │ group_by: │ @@ -69,7 +73,11 @@ expression: displayable.tree_render().to_string() │ aggr: │ │ sum(lineitem │ │ .l_extendedp │ -│ rice * Int64(1) - lineitem│ +│ rice * Some(1),20,0 - │ +│ lineitem.l_discount │ +│ ) as sum(lineitem │ +│ .l_extendedprice │ +│ * Int64(1) - lineitem │ │ .l_discount) │ │ │ │ group_by: │ @@ -81,24 +89,6 @@ expression: displayable.tree_render().to_string() │ mode: Partial │ └─────────────┬─────────────┘ ┌─────────────┴─────────────┐ -│ ProjectionExec │ -│ -------------------- │ -│ c_acctbal: c_acctbal │ -│ c_address: c_address │ -│ c_comment: c_comment │ -│ c_custkey: c_custkey │ -│ c_name: c_name │ -│ c_phone: c_phone │ -│ │ -│ l_discount: │ -│ l_discount │ -│ │ -│ l_extendedprice: │ -│ l_extendedprice │ -│ │ -│ n_name: n_name │ -└─────────────┬─────────────┘ -┌─────────────┴─────────────┐ │ HashJoinExec │ │ -------------------- │ │ on: ├──────────────┐ @@ -112,50 +102,29 @@ expression: displayable.tree_render().to_string() │ object_stores=[] ││ │ │ └─────────────┬─────────────┘└─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -│ DataSourceExec ││ CoalescePartitionsExec │ │ LiquidCacheClientExec │ -│ -------------------- ││ │ │ -------------------- │ -│ files: 1 ││ │ │ server: │ -│ format: parquet ││ │ │ http://localhost:50051, │ -│ ││ │ │ object_stores=[] │ -└───────────────────────────┘└─────────────┬─────────────┘ └─────────────┬─────────────┘ - ┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ - │ HashJoinExec │ │ RepartitionExec │ - │ -------------------- │ │ -------------------- │ - │ on: │ │ partition_count(in->out): │ - │ (c_custkey = o_custkey) ├──────────────┐ │ 1 -> 8 │ - │ │ │ │ │ - │ │ │ │ partitioning_scheme: │ - │ │ │ │ RoundRobinBatch(8) │ - └─────────────┬─────────────┘ │ └─────────────┬─────────────┘ +│ DataSourceExec ││ HashJoinExec │ │ LiquidCacheClientExec │ +│ -------------------- ││ -------------------- │ │ -------------------- │ +│ files: 1 ││ on: ├──────────────┐ │ server: │ +│ format: parquet ││ (o_custkey = c_custkey) │ │ │ http://localhost:50051, │ +│ ││ │ │ │ object_stores=[] │ +└───────────────────────────┘└─────────────┬─────────────┘ │ └─────────────┬─────────────┘ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ LiquidCacheClientExec ││ DataSourceExec │ + │ LiquidCacheClientExec ││ LiquidCacheClientExec ││ RepartitionExec │ │ -------------------- ││ -------------------- ││ -------------------- │ - │ server: ││ server: ││ files: 1 │ - │ http://localhost:50051, ││ http://localhost:50051, ││ format: parquet │ + │ server: ││ server: ││ partition_count(in->out): │ + │ http://localhost:50051, ││ http://localhost:50051, ││ 1 -> 8 │ │ object_stores=[] ││ object_stores=[] ││ │ - │ ││ ││ predicate: │ - │ ││ ││ l_returnflag = R AND │ - │ ││ ││ DynamicFilter [ │ - │ ││ ││ empty ] │ - └─────────────┬─────────────┘└─────────────┬─────────────┘└───────────────────────────┘ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ DataSourceExec ││ RepartitionExec │ - │ -------------------- ││ -------------------- │ - │ files: 1 ││ partition_count(in->out): │ - │ format: parquet ││ 1 -> 8 │ - │ ││ │ - │ predicate: ││ partitioning_scheme: │ - │ DynamicFilter [ empty ] ││ RoundRobinBatch(8) │ - └───────────────────────────┘└─────────────┬─────────────┘ - ┌─────────────┴─────────────┐ - │ DataSourceExec │ - │ -------------------- │ - │ files: 1 │ - │ format: parquet │ - │ │ - │ predicate: │ - │ o_orderdate >= 1993-10-01 │ - │ AND o_orderdate < 1994 │ - │ -01-01 AND DynamicFilter │ - │ [ empty ] │ - └───────────────────────────┘ + │ ││ ││ partitioning_scheme: │ + │ ││ ││ RoundRobinBatch(8) │ + └─────────────┬─────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ + ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ + │ DataSourceExec ││ DataSourceExec ││ DataSourceExec │ + │ -------------------- ││ -------------------- ││ -------------------- │ + │ files: 1 ││ files: 1 ││ files: 1 │ + │ format: parquet ││ format: parquet ││ format: parquet │ + │ ││ ││ │ + │ predicate: ││ ││ predicate: │ + │ o_orderdate >= 1993-10-01 ││ ││ l_returnflag = R │ + │ AND o_orderdate < 1994 ││ ││ │ + │ -01-01 ││ ││ │ + └───────────────────────────┘└───────────────────────────┘└───────────────────────────┘ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q11.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q11.snap index 0833139c..cbc05809 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q11.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q11.snap @@ -3,125 +3,116 @@ source: src/datafusion-client/src/tests/mod.rs expression: displayable.tree_render().to_string() --- ┌───────────────────────────┐ -│ SortPreservingMergeExec │ -│ -------------------- │ -│ value DESC │ -└─────────────┬─────────────┘ -┌─────────────┴─────────────┐ -│ SortExec │ -│ -------------------- │ -│ value@1 DESC │ -└─────────────┬─────────────┘ -┌─────────────┴─────────────┐ -│ ProjectionExec │ -│ -------------------- │ -│ ps_partkey: │ -│ ps_partkey │ -│ │ -│ value: │ -│ sum(partsupp.ps_supplycost│ -│ * partsupp.ps_availqty) │ -└─────────────┬─────────────┘ -┌─────────────┴─────────────┐ -│ NestedLoopJoinExec ├────────────────────────────────────────────────────────────────────────┐ +│ ScalarSubqueryExec ├────────────────────────────────────────────────────────────────────────┐ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -│ ProjectionExec │ │ ProjectionExec │ +│ SortPreservingMergeExec │ │ ProjectionExec │ │ -------------------- │ │ -------------------- │ -│ sum(partsupp.ps_supplycost│ │ join_proj_push_down_1: │ -│ * partsupp.ps_availqty) │ │ CAST(sum(partsupp │ -│ * Float64(0.0001): │ │ .ps_supplycost * │ -│ CAST(CAST(sum(partsupp │ │ partsupp.ps_availqty) AS │ -│ .ps_supplycost * │ │ Decimal128(38, 15)) │ -│ partsupp │ │ │ -│ .ps_availqty) │ │ ps_partkey: │ -│ AS Float64) * 0.0001 AS │ │ ps_partkey │ -│ Decimal128(38, 15)) │ │ │ -│ │ │ sum(partsupp.ps_supplycost│ -│ │ │ * partsupp.ps_availqty): │ -│ │ │ sum(partsupp.ps_supplycost│ -│ │ │ * partsupp.ps_availqty) │ +│ value DESC │ │ sum(partsupp.ps_supplycost│ +│ │ │ * partsupp.ps_availqty) │ +│ │ │ * Float64(0.0001): │ +│ │ │ CAST(CAST(sum(partsupp │ +│ │ │ .ps_supplycost * │ +│ │ │ partsupp │ +│ │ │ .ps_availqty) │ +│ │ │ AS Float64) * 0.0001 AS │ +│ │ │ Decimal128(38, 15)) │ └─────────────┬─────────────┘ └─────────────┬─────────────┘ ┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -│ AggregateExec │ │ AggregateExec │ +│ SortExec │ │ AggregateExec │ │ -------------------- │ │ -------------------- │ -│ aggr: │ │ aggr: │ -│ sum(partsupp.ps_supplycost│ │ sum(partsupp.ps_supplycost│ -│ * partsupp.ps_availqty) │ │ * partsupp.ps_availqty) │ -│ │ │ │ -│ mode: Final │ │ group_by: ps_partkey │ +│ value@1 DESC │ │ aggr: │ +│ │ │ sum(partsupp.ps_supplycost│ +│ │ │ * partsupp.ps_availqty) │ │ │ │ │ -│ │ │ mode: │ -│ │ │ FinalPartitioned │ +│ │ │ mode: Final │ └─────────────┬─────────────┘ └─────────────┬─────────────┘ ┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -│ CoalescePartitionsExec │ │ RepartitionExec │ -│ │ │ -------------------- │ -│ │ │ partition_count(in->out): │ -│ │ │ 8 -> 8 │ +│ ProjectionExec │ │ CoalescePartitionsExec │ +│ -------------------- │ │ │ +│ ps_partkey: │ │ │ +│ ps_partkey │ │ │ │ │ │ │ -│ │ │ partitioning_scheme: │ -│ │ │ Hash([ps_partkey@0], 8) │ +│ value: │ │ │ +│ sum(partsupp.ps_supplycost│ │ │ +│ * partsupp.ps_availqty) │ │ │ └─────────────┬─────────────┘ └─────────────┬─────────────┘ ┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -│ AggregateExec │ │ AggregateExec │ +│ FilterExec │ │ AggregateExec │ │ -------------------- │ │ -------------------- │ -│ aggr: │ │ aggr: │ -│ sum(partsupp.ps_supplycost│ │ sum(partsupp.ps_supplycost│ -│ * partsupp.ps_availqty) │ │ * partsupp.ps_availqty) │ -│ │ │ │ -│ mode: Partial │ │ group_by: ps_partkey │ -│ │ │ mode: Partial │ +│ predicate: │ │ aggr: │ +│ CAST(sum(partsupp │ │ sum(partsupp.ps_supplycost│ +│ .ps_supplycost * │ │ * partsupp.ps_availqty) │ +│ partsupp.ps_availqty) AS │ │ │ +│ Decimal128(38, 15)) > │ │ mode: Partial │ +│ (scalar subquery) │ │ │ └─────────────┬─────────────┘ └─────────────┬─────────────┘ ┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -│ HashJoinExec │ │ HashJoinExec │ +│ AggregateExec │ │ HashJoinExec │ │ -------------------- │ │ -------------------- │ -│ on: ├──────────────┐ │ on: ├──────────────┐ -│(n_nationkey = s_nationkey)│ │ │(n_nationkey = s_nationkey)│ │ -└─────────────┬─────────────┘ │ └─────────────┬─────────────┘ │ -┌─────────────┴─────────────┐┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ LiquidCacheClientExec ││ RepartitionExec │ │ LiquidCacheClientExec ││ RepartitionExec │ -│ -------------------- ││ -------------------- │ │ -------------------- ││ -------------------- │ -│ server: ││ partition_count(in->out): │ │ server: ││ partition_count(in->out): │ -│ http://localhost:50051, ││ 1 -> 8 │ │ http://localhost:50051, ││ 1 -> 8 │ -│ object_stores=[] ││ │ │ object_stores=[] ││ │ -│ ││ partitioning_scheme: │ │ ││ partitioning_scheme: │ -│ ││ RoundRobinBatch(8) │ │ ││ RoundRobinBatch(8) │ -└─────────────┬─────────────┘└─────────────┬─────────────┘ └─────────────┬─────────────┘└─────────────┬─────────────┘ -┌─────────────┴─────────────┐┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ DataSourceExec ││ ProjectionExec │ │ DataSourceExec ││ ProjectionExec │ -│ -------------------- ││ -------------------- │ │ -------------------- ││ -------------------- │ -│ files: 1 ││ ps_availqty: │ │ files: 1 ││ ps_availqty: │ -│ format: parquet ││ ps_availqty │ │ format: parquet ││ ps_availqty │ -│ ││ │ │ ││ │ -│ predicate: ││ ps_supplycost: │ │ predicate: ││ ps_partkey: │ -│ n_name = GERMANY ││ ps_supplycost │ │ n_name = GERMANY ││ ps_partkey │ -│ ││ │ │ ││ │ -│ ││ s_nationkey: │ │ ││ ps_supplycost: │ -│ ││ s_nationkey │ │ ││ ps_supplycost │ -│ ││ │ │ ││ │ -│ ││ │ │ ││ s_nationkey: │ -│ ││ │ │ ││ s_nationkey │ -└───────────────────────────┘└─────────────┬─────────────┘ └───────────────────────────┘└─────────────┬─────────────┘ - ┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ - │ HashJoinExec │ │ HashJoinExec │ - │ -------------------- │ │ -------------------- │ - │ on: ├──────────────┐ │ on: ├──────────────┐ - │ (s_suppkey = ps_suppkey) │ │ │ (s_suppkey = ps_suppkey) │ │ - └─────────────┬─────────────┘ │ └─────────────┬─────────────┘ │ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ LiquidCacheClientExec │ │ LiquidCacheClientExec ││ LiquidCacheClientExec │ - │ -------------------- ││ -------------------- │ │ -------------------- ││ -------------------- │ - │ server: ││ server: │ │ server: ││ server: │ - │ http://localhost:50051, ││ http://localhost:50051, │ │ http://localhost:50051, ││ http://localhost:50051, │ - │ object_stores=[] ││ object_stores=[] │ │ object_stores=[] ││ object_stores=[] │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ └─────────────┬─────────────┘└─────────────┬─────────────┘ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ DataSourceExec ││ DataSourceExec │ │ DataSourceExec ││ DataSourceExec │ - │ -------------------- ││ -------------------- │ │ -------------------- ││ -------------------- │ - │ files: 1 ││ files: 1 │ │ files: 1 ││ files: 1 │ - │ format: parquet ││ format: parquet │ │ format: parquet ││ format: parquet │ - │ ││ │ │ ││ │ - │ predicate: ││ predicate: │ │ predicate: ││ predicate: │ - │ DynamicFilter [ empty ] ││ DynamicFilter [ empty ] │ │ DynamicFilter [ empty ] ││ DynamicFilter [ empty ] │ - └───────────────────────────┘└───────────────────────────┘ └───────────────────────────┘└───────────────────────────┘ +│ aggr: │ │ on: │ +│ sum(partsupp.ps_supplycost│ │(n_nationkey = s_nationkey)│ +│ * partsupp.ps_availqty) │ │ │ +│ │ │ ├──────────────┐ +│ group_by: ps_partkey │ │ │ │ +│ │ │ │ │ +│ mode: │ │ │ │ +│ FinalPartitioned │ │ │ │ +└─────────────┬─────────────┘ └─────────────┬─────────────┘ │ +┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +│ RepartitionExec │ │ LiquidCacheClientExec ││ RepartitionExec │ +│ -------------------- │ │ -------------------- ││ -------------------- │ +│ partition_count(in->out): │ │ server: ││ partition_count(in->out): │ +│ 8 -> 8 │ │ http://localhost:50051, ││ 1 -> 8 │ +│ │ │ object_stores=[] ││ │ +│ partitioning_scheme: │ │ ││ partitioning_scheme: │ +│ Hash([ps_partkey@0], 8) │ │ ││ RoundRobinBatch(8) │ +└─────────────┬─────────────┘ └─────────────┬─────────────┘└─────────────┬─────────────┘ +┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +│ AggregateExec │ │ DataSourceExec ││ HashJoinExec │ +│ -------------------- │ │ -------------------- ││ -------------------- │ +│ aggr: │ │ files: 1 ││ on: │ +│ sum(partsupp.ps_supplycost│ │ format: parquet ││ (s_suppkey = ps_suppkey) │ +│ * partsupp.ps_availqty) │ │ ││ ├──────────────┐ +│ │ │ predicate: ││ │ │ +│ group_by: ps_partkey │ │ n_name = GERMANY ││ │ │ +│ mode: Partial │ │ ││ │ │ +└─────────────┬─────────────┘ └───────────────────────────┘└─────────────┬─────────────┘ │ +┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +│ HashJoinExec │ │ LiquidCacheClientExec ││ LiquidCacheClientExec │ +│ -------------------- │ │ -------------------- ││ -------------------- │ +│ on: ├──────────────┐ │ server: ││ server: │ +│(n_nationkey = s_nationkey)│ │ │ http://localhost:50051, ││ http://localhost:50051, │ +│ │ │ │ object_stores=[] ││ object_stores=[] │ +└─────────────┬─────────────┘ │ └─────────────┬─────────────┘└─────────────┬─────────────┘ +┌─────────────┴─────────────┐┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +│ LiquidCacheClientExec ││ RepartitionExec │ │ DataSourceExec ││ DataSourceExec │ +│ -------------------- ││ -------------------- │ │ -------------------- ││ -------------------- │ +│ server: ││ partition_count(in->out): │ │ files: 1 ││ files: 1 │ +│ http://localhost:50051, ││ 1 -> 8 │ │ format: parquet ││ format: parquet │ +│ object_stores=[] ││ │ │ ││ │ +│ ││ partitioning_scheme: │ │ ││ │ +│ ││ RoundRobinBatch(8) │ │ ││ │ +└─────────────┬─────────────┘└─────────────┬─────────────┘ └───────────────────────────┘└───────────────────────────┘ +┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +│ DataSourceExec ││ HashJoinExec │ +│ -------------------- ││ -------------------- │ +│ files: 1 ││ on: │ +│ format: parquet ││ (s_suppkey = ps_suppkey) ├──────────────┐ +│ ││ │ │ +│ predicate: ││ │ │ +│ n_name = GERMANY ││ │ │ +└───────────────────────────┘└─────────────┬─────────────┘ │ + ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ + │ LiquidCacheClientExec ││ LiquidCacheClientExec │ + │ -------------------- ││ -------------------- │ + │ server: ││ server: │ + │ http://localhost:50051, ││ http://localhost:50051, │ + │ object_stores=[] ││ object_stores=[] │ + └─────────────┬─────────────┘└─────────────┬─────────────┘ + ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ + │ DataSourceExec ││ DataSourceExec │ + │ -------------------- ││ -------------------- │ + │ files: 1 ││ files: 1 │ + │ format: parquet ││ format: parquet │ + └───────────────────────────┘└───────────────────────────┘ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q12.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q12.snap index a0e7a8e5..97026caa 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q12.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q12.snap @@ -113,8 +113,8 @@ expression: displayable.tree_render().to_string() │ files: 1 ││ files: 1 │ │ format: parquet ││ format: parquet │ │ ││ │ -│ predicate: ││ predicate: │ -│ (l_shipmode = MAIL OR ││ DynamicFilter [ empty ] │ +│ predicate: ││ │ +│ (l_shipmode = MAIL OR ││ │ │ l_shipmode = SHIP) ││ │ │ AND l_receiptdate > ││ │ │ l_commitdate AND ││ │ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q14.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q14.snap index 6214d583..9f8cd921 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q14.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q14.snap @@ -24,15 +24,21 @@ expression: displayable.tree_render().to_string() │ -------------------- │ │ aggr: │ │ sum(CASE WHEN part.p_type │ -│ LIKE Utf8("PROMO%") │ -│ THEN lineitem │ +│ LIKE PROMO% THEN │ +│ __common_expr_1 │ +│ ELSE Some(0),38,4 END) │ +│ as sum(CASE WHEN part │ +│ .p_type LIKE Utf8( │ +│ "PROMO%") THEN │ +│ lineitem │ +│ .l_extendedprice │ +│ * Int64(1) - lineitem │ +│ .l_discount ELSE Int64 │ +│ (0) END), sum │ +│ (__common_expr │ +│ _1) as sum(lineitem │ │ .l_extendedprice * │ │ Int64(1) - lineitem │ -│ .l_discount ELSE │ -│ Int64(0) END), sum │ -│ (lineitem │ -│ .l_extendedp │ -│ rice * Int64(1) - lineitem│ │ .l_discount) │ │ │ │ mode: Final │ @@ -45,15 +51,21 @@ expression: displayable.tree_render().to_string() │ -------------------- │ │ aggr: │ │ sum(CASE WHEN part.p_type │ -│ LIKE Utf8("PROMO%") │ -│ THEN lineitem │ +│ LIKE PROMO% THEN │ +│ __common_expr_1 │ +│ ELSE Some(0),38,4 END) │ +│ as sum(CASE WHEN part │ +│ .p_type LIKE Utf8( │ +│ "PROMO%") THEN │ +│ lineitem │ +│ .l_extendedprice │ +│ * Int64(1) - lineitem │ +│ .l_discount ELSE Int64 │ +│ (0) END), sum │ +│ (__common_expr │ +│ _1) as sum(lineitem │ │ .l_extendedprice * │ │ Int64(1) - lineitem │ -│ .l_discount ELSE │ -│ Int64(0) END), sum │ -│ (lineitem │ -│ .l_extendedp │ -│ rice * Int64(1) - lineitem│ │ .l_discount) │ │ │ │ mode: Partial │ @@ -68,10 +80,19 @@ expression: displayable.tree_render().to_string() │ p_type: p_type │ └─────────────┬─────────────┘ ┌─────────────┴─────────────┐ +│ RepartitionExec │ +│ -------------------- │ +│ partition_count(in->out): │ +│ 1 -> 8 │ +│ │ +│ partitioning_scheme: │ +│ RoundRobinBatch(8) │ +└─────────────┬─────────────┘ +┌─────────────┴─────────────┐ │ HashJoinExec │ │ -------------------- │ │ on: ├──────────────┐ -│ (p_partkey = l_partkey) │ │ +│ (l_partkey = p_partkey) │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ │ LiquidCacheClientExec ││ LiquidCacheClientExec │ @@ -81,23 +102,13 @@ expression: displayable.tree_render().to_string() │ object_stores=[] ││ object_stores=[] │ └─────────────┬─────────────┘└─────────────┬─────────────┘ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ DataSourceExec ││ RepartitionExec │ +│ DataSourceExec ││ DataSourceExec │ │ -------------------- ││ -------------------- │ -│ files: 1 ││ partition_count(in->out): │ -│ format: parquet ││ 1 -> 8 │ +│ files: 1 ││ files: 1 │ +│ format: parquet ││ format: parquet │ │ ││ │ -│ ││ partitioning_scheme: │ -│ ││ RoundRobinBatch(8) │ -└───────────────────────────┘└─────────────┬─────────────┘ - ┌─────────────┴─────────────┐ - │ DataSourceExec │ - │ -------------------- │ - │ files: 1 │ - │ format: parquet │ - │ │ - │ predicate: │ - │ l_shipdate >= 1995-09-01 │ - │ AND l_shipdate < 1995 │ - │ -10-01 AND DynamicFilter │ - │ [ empty ] │ - └───────────────────────────┘ +│ predicate: ││ │ +│ l_shipdate >= 1995-09-01 ││ │ +│ AND l_shipdate < 1995 ││ │ +│ -10-01 ││ │ +└───────────────────────────┘└───────────────────────────┘ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q16.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q16.snap index 3c0f6807..6c2e06f7 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q16.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q16.snap @@ -99,44 +99,31 @@ expression: displayable.tree_render().to_string() │ (ps_suppkey = s_suppkey) │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -│ ProjectionExec │ │ LiquidCacheClientExec │ +│ HashJoinExec │ │ LiquidCacheClientExec │ │ -------------------- │ │ -------------------- │ -│ p_brand: p_brand │ │ server: │ -│ p_size: p_size │ │ http://localhost:50051, │ -│ p_type: p_type │ │ object_stores=[] │ -│ │ │ │ -│ ps_suppkey: │ │ │ -│ ps_suppkey │ │ │ -└─────────────┬─────────────┘ └─────────────┬─────────────┘ -┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -│ HashJoinExec │ │ RepartitionExec │ -│ -------------------- │ │ -------------------- │ -│ on: │ │ partition_count(in->out): │ -│ (p_partkey = ps_partkey) ├──────────────┐ │ 1 -> 8 │ -│ │ │ │ │ -│ │ │ │ partitioning_scheme: │ -│ │ │ │ RoundRobinBatch(8) │ +│ on: ├──────────────┐ │ server: │ +│ (p_partkey = ps_partkey) │ │ │ http://localhost:50051, │ +│ │ │ │ object_stores=[] │ └─────────────┬─────────────┘ │ └─────────────┬─────────────┘ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ LiquidCacheClientExec ││ LiquidCacheClientExec ││ DataSourceExec │ +│ LiquidCacheClientExec ││ LiquidCacheClientExec ││ RepartitionExec │ │ -------------------- ││ -------------------- ││ -------------------- │ -│ server: ││ server: ││ files: 1 │ -│ http://localhost:50051, ││ http://localhost:50051, ││ format: parquet │ +│ server: ││ server: ││ partition_count(in->out): │ +│ http://localhost:50051, ││ http://localhost:50051, ││ 1 -> 8 │ │ object_stores=[] ││ object_stores=[] ││ │ -│ ││ ││ predicate: │ -│ ││ ││ s_comment LIKE %Customer │ -│ ││ ││ %Complaints% │ -└─────────────┬─────────────┘└─────────────┬─────────────┘└───────────────────────────┘ -┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ DataSourceExec ││ DataSourceExec │ -│ -------------------- ││ -------------------- │ -│ files: 1 ││ files: 1 │ -│ format: parquet ││ format: parquet │ -│ ││ │ -│ predicate: ││ predicate: │ -│ p_brand != Brand#45 AND ││ DynamicFilter [ empty ] │ -│ p_type NOT LIKE ││ │ -│ MEDIUM POLISHED% ││ │ -│ AND p_size IN (49, 14, ││ │ -│ 23, 45, 19, 3, 36, 9) ││ │ -└───────────────────────────┘└───────────────────────────┘ +│ ││ ││ partitioning_scheme: │ +│ ││ ││ RoundRobinBatch(8) │ +└─────────────┬─────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ +┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +│ DataSourceExec ││ DataSourceExec ││ DataSourceExec │ +│ -------------------- ││ -------------------- ││ -------------------- │ +│ files: 1 ││ files: 1 ││ files: 1 │ +│ format: parquet ││ format: parquet ││ format: parquet │ +│ ││ ││ │ +│ predicate: ││ ││ predicate: │ +│ p_brand != Brand#45 AND ││ ││ s_comment LIKE %Customer │ +│ p_type NOT LIKE ││ ││ %Complaints% │ +│ MEDIUM POLISHED% ││ ││ │ +│ AND p_size IN (49, 14, ││ ││ │ +│ 23, 45, 19, 3, 36, 9) ││ ││ │ +└───────────────────────────┘└───────────────────────────┘└───────────────────────────┘ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q17.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q17.snap index 8c1e1c91..78d6cade 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q17.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q17.snap @@ -39,69 +39,75 @@ expression: displayable.tree_render().to_string() │ filter: │ │ CAST(l_quantity AS │ │ Decimal128(30, │ -│ 15)) < Float64(0.2) * ├───────────────────────────────────────────┐ -│ avg(lineitem.l_quantity) │ │ -│ │ │ -│ on: │ │ -│ (p_partkey = l_partkey) │ │ -└─────────────┬─────────────┘ │ -┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -│ ProjectionExec │ │ ProjectionExec │ -│ -------------------- │ │ -------------------- │ -│ l_extendedprice: │ │ Float64(0.2) * avg │ -│ l_extendedprice │ │ (lineitem │ -│ │ │ .l_quantity): │ -│ l_quantity: │ │ CAST(0.2 * CAST(avg │ -│ l_quantity │ │ (lineitem │ -│ │ │ .l_quantity) AS │ -│ p_partkey: p_partkey │ │ Float64) AS Decimal128 │ -│ │ │ (30, 15)) │ -│ │ │ │ -│ │ │ l_partkey: l_partkey │ -└─────────────┬─────────────┘ └─────────────┬─────────────┘ -┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -│ HashJoinExec │ │ AggregateExec │ -│ -------------------- │ │ -------------------- │ -│ on: │ │ aggr: │ -│ (p_partkey = l_partkey) │ │ avg(lineitem.l_quantity) │ -│ ├──────────────┐ │ │ -│ │ │ │ group_by: l_partkey │ -│ │ │ │ │ -│ │ │ │ mode: │ -│ │ │ │ FinalPartitioned │ -└─────────────┬─────────────┘ │ └─────────────┬─────────────┘ +│ 15)) < Float64(0.2) * ├──────────────┐ +│ avg(lineitem.l_quantity) │ │ +│ │ │ +│ on: │ │ +│ (l_partkey = p_partkey) │ │ +└─────────────┬─────────────┘ │ +┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +│ CoalescePartitionsExec ││ RepartitionExec │ +│ ││ -------------------- │ +│ ││ partition_count(in->out): │ +│ ││ 1 -> 8 │ +│ ││ │ +│ ││ partitioning_scheme: │ +│ ││ RoundRobinBatch(8) │ +└─────────────┬─────────────┘└─────────────┬─────────────┘ +┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +│ ProjectionExec ││ HashJoinExec │ +│ -------------------- ││ -------------------- │ +│ Float64(0.2) * avg ││ on: │ +│ (lineitem ││ (p_partkey = l_partkey) │ +│ .l_quantity): ││ │ +│ CAST(0.2 * CAST(avg ││ │ +│ (lineitem ││ ├──────────────┐ +│ .l_quantity) AS ││ │ │ +│ Float64) AS Decimal128 ││ │ │ +│ (30, 15)) ││ │ │ +│ ││ │ │ +│ l_partkey: l_partkey ││ │ │ +└─────────────┬─────────────┘└─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ LiquidCacheClientExec ││ LiquidCacheClientExec ││ RepartitionExec │ +│ AggregateExec ││ LiquidCacheClientExec ││ LiquidCacheClientExec │ │ -------------------- ││ -------------------- ││ -------------------- │ -│ server: ││ server: ││ partition_count(in->out): │ -│ http://localhost:50051, ││ http://localhost:50051, ││ 1 -> 8 │ -│ object_stores=[] ││ object_stores=[] ││ │ -│ ││ ││ partitioning_scheme: │ -│ ││ ││ Hash([l_partkey@0], 8) │ +│ aggr: ││ server: ││ server: │ +│ avg(lineitem.l_quantity) ││ http://localhost:50051, ││ http://localhost:50051, │ +│ ││ object_stores=[] ││ object_stores=[] │ +│ group_by: l_partkey ││ ││ │ +│ ││ ││ │ +│ mode: ││ ││ │ +│ FinalPartitioned ││ ││ │ └─────────────┬─────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ DataSourceExec ││ DataSourceExec ││ AggregateExec │ +│ RepartitionExec ││ DataSourceExec ││ DataSourceExec │ │ -------------------- ││ -------------------- ││ -------------------- │ -│ files: 1 ││ files: 1 ││ aggr: │ -│ format: parquet ││ format: parquet ││ avg(lineitem.l_quantity) │ +│ partition_count(in->out): ││ files: 1 ││ files: 1 │ +│ 1 -> 8 ││ format: parquet ││ format: parquet │ │ ││ ││ │ -│ predicate: ││ predicate: ││ group_by: l_partkey │ -│ p_brand = Brand#23 AND ││ DynamicFilter [ empty ] ││ mode: Partial │ -│ p_container = MED BOX ││ ││ │ -└───────────────────────────┘└───────────────────────────┘└─────────────┬─────────────┘ - ┌─────────────┴─────────────┐ - │ LiquidCacheClientExec │ - │ -------------------- │ - │ server: │ - │ http://localhost:50051, │ - │ object_stores=[] │ - └─────────────┬─────────────┘ - ┌─────────────┴─────────────┐ - │ DataSourceExec │ - │ -------------------- │ - │ files: 1 │ - │ format: parquet │ - │ │ - │ predicate: │ - │ DynamicFilter [ empty ] │ - └───────────────────────────┘ +│ partitioning_scheme: ││ predicate: ││ │ +│ Hash([l_partkey@0], 8) ││ p_brand = Brand#23 AND ││ │ +│ ││ p_container = MED BOX ││ │ +└─────────────┬─────────────┘└───────────────────────────┘└───────────────────────────┘ +┌─────────────┴─────────────┐ +│ AggregateExec │ +│ -------------------- │ +│ aggr: │ +│ avg(lineitem.l_quantity) │ +│ │ +│ group_by: l_partkey │ +│ mode: Partial │ +└─────────────┬─────────────┘ +┌─────────────┴─────────────┐ +│ LiquidCacheClientExec │ +│ -------------------- │ +│ server: │ +│ http://localhost:50051, │ +│ object_stores=[] │ +└─────────────┬─────────────┘ +┌─────────────┴─────────────┐ +│ DataSourceExec │ +│ -------------------- │ +│ files: 1 │ +│ format: parquet │ +└───────────────────────────┘ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q18.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q18.snap index cfc79efe..f6b22222 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q18.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q18.snap @@ -98,8 +98,8 @@ expression: displayable.tree_render().to_string() │ partition_count(in->out): ││ server: ││ server: ││ files: 1 │ │ 1 -> 8 ││ http://localhost:50051, ││ http://localhost:50051, ││ format: parquet │ │ ││ object_stores=[] ││ object_stores=[] ││ │ -│ partitioning_scheme: ││ ││ ││ predicate: │ -│ Hash([l_orderkey@0], 8) ││ ││ ││ DynamicFilter [ empty ] │ +│ partitioning_scheme: ││ ││ ││ │ +│ Hash([l_orderkey@0], 8) ││ ││ ││ │ └─────────────┬─────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘└───────────────────────────┘ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ │ AggregateExec ││ DataSourceExec ││ DataSourceExec │ @@ -107,8 +107,8 @@ expression: displayable.tree_render().to_string() │ aggr: ││ files: 1 ││ files: 1 │ │ sum(lineitem.l_quantity) ││ format: parquet ││ format: parquet │ │ ││ ││ │ -│ group_by: l_orderkey ││ ││ predicate: │ -│ mode: Partial ││ ││ DynamicFilter [ empty ] │ +│ group_by: l_orderkey ││ ││ │ +│ mode: Partial ││ ││ │ └─────────────┬─────────────┘└───────────────────────────┘└───────────────────────────┘ ┌─────────────┴─────────────┐ │ LiquidCacheClientExec │ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q19.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q19.snap index c71ad72b..080a4932 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q19.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q19.snap @@ -17,7 +17,11 @@ expression: displayable.tree_render().to_string() │ aggr: │ │ sum(lineitem │ │ .l_extendedp │ -│ rice * Int64(1) - lineitem│ +│ rice * Some(1),20,0 - │ +│ lineitem.l_discount │ +│ ) as sum(lineitem │ +│ .l_extendedprice │ +│ * Int64(1) - lineitem │ │ .l_discount) │ │ │ │ mode: Final │ @@ -31,7 +35,11 @@ expression: displayable.tree_render().to_string() │ aggr: │ │ sum(lineitem │ │ .l_extendedp │ -│ rice * Int64(1) - lineitem│ +│ rice * Some(1),20,0 - │ +│ lineitem.l_discount │ +│ ) as sum(lineitem │ +│ .l_extendedprice │ +│ * Int64(1) - lineitem │ │ .l_discount) │ │ │ │ mode: Partial │ @@ -82,20 +90,21 @@ expression: displayable.tree_render().to_string() │ format: parquet ││ 1 -> 8 │ │ ││ │ │ predicate: ││ partitioning_scheme: │ -│ (p_brand = Brand#12 AND ││ RoundRobinBatch(8) │ -│ p_container IN (SM ││ │ -│ CASE, SM BOX, SM PACK ││ │ -│ , SM PKG) AND p_size <= 5 ││ │ -│ OR p_brand = Brand#23 ││ │ +│ p_size >= 1 AND (p_brand =││ RoundRobinBatch(8) │ +│ Brand#12 AND p_container ││ │ +│ IN (SM CASE, SM BOX, SM ││ │ +│ PACK, SM PKG) AND ││ │ +│ p_size <= 5 OR ││ │ +│ p_brand = Brand#23 ││ │ │ AND p_container IN ││ │ │ (MED BAG, MED BOX, MED ││ │ │ PKG, MED PACK) AND ││ │ │ p_size <= 10 OR ││ │ │ p_brand = Brand#34 ││ │ -│ AND p_container IN (LG ││ │ -│ CASE, LG BOX, LG PACK, ││ │ -│ LG PKG) AND p_size <= ││ │ -│ 15) AND p_size >= 1 ││ │ +│ AND p_container IN ││ │ +│ (LG CASE, LG BOX, LG ││ │ +│ PACK, LG PKG) AND ││ │ +│ p_size <= 15) ││ │ └───────────────────────────┘└─────────────┬─────────────┘ ┌─────────────┴─────────────┐ │ DataSourceExec │ @@ -104,20 +113,21 @@ expression: displayable.tree_render().to_string() │ format: parquet │ │ │ │ predicate: │ - │ (l_quantity >= Some(100) │ - │ ,15,2 AND l_quantity <= │ - │ Some(1100),15,2 OR │ - │ l_quantity >= Some │ - │ (1000),15,2 AND │ - │ l_quantity <= │ - │ Some(2000),15,2 OR │ - │ l_quantity >= Some │ - │ (2000),15,2 AND │ - │ l_quantity <= │ - │ Some(3000),15,2) AND │ │ (l_shipmode = AIR OR │ - │ l_shipmode = AIR REG │ - │ ) AND l_shipinstruct = │ - │ DELIVER IN PERSON AND │ - │ DynamicFilter [ empty ] │ + │ l_shipmode = AIR │ + │ REG) AND │ + │ l_shipinstr │ + │ uct = DELIVER IN PERSON │ + │ AND (l_quantity >= │ + │ Some(100),15,2 AND │ + │ l_quantity <= Some │ + │ (1100),15,2 OR │ + │ l_quantity >= │ + │ Some(1000),15,2 AND │ + │ l_quantity <= Some │ + │ (2000),15,2 OR │ + │ l_quantity >= │ + │ Some(2000),15,2 AND │ + │ l_quantity <= Some(3000 │ + │ ),15,2) │ └───────────────────────────┘ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q2.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q2.snap index 4b41530f..633cea2c 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q2.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q2.snap @@ -20,18 +20,6 @@ expression: displayable.tree_render().to_string() │ NULLS LAST │ └─────────────┬─────────────┘ ┌─────────────┴─────────────┐ -│ ProjectionExec │ -│ -------------------- │ -│ n_name: n_name │ -│ p_mfgr: p_mfgr │ -│ p_partkey: p_partkey │ -│ s_acctbal: s_acctbal │ -│ s_address: s_address │ -│ s_comment: s_comment │ -│ s_name: s_name │ -│ s_phone: s_phone │ -└─────────────┬─────────────┘ -┌─────────────┴─────────────┐ │ HashJoinExec │ │ -------------------- │ │ on: │ @@ -63,106 +51,58 @@ expression: displayable.tree_render().to_string() │ │ │ │ FinalPartitioned │ └─────────────┬─────────────┘ │ └─────────────┬─────────────┘ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -│ LiquidCacheClientExec ││ ProjectionExec │ │ RepartitionExec │ -│ -------------------- ││ -------------------- │ │ -------------------- │ -│ server: ││ n_name: n_name │ │ partition_count(in->out): │ -│ http://localhost:50051, ││ │ │ 8 -> 8 │ -│ object_stores=[] ││ n_regionkey: │ │ │ -│ ││ n_regionkey │ │ partitioning_scheme: │ -│ ││ │ │ Hash([ps_partkey@0], 8) │ -│ ││ p_mfgr: p_mfgr │ │ │ -│ ││ p_partkey: p_partkey │ │ │ -│ ││ │ │ │ -│ ││ ps_supplycost: │ │ │ -│ ││ ps_supplycost │ │ │ -│ ││ │ │ │ -│ ││ s_acctbal: s_acctbal │ │ │ -│ ││ s_address: s_address │ │ │ -│ ││ s_comment: s_comment │ │ │ -│ ││ s_name: s_name │ │ │ -│ ││ s_phone: s_phone │ │ │ -└─────────────┬─────────────┘└─────────────┬─────────────┘ └─────────────┬─────────────┘ -┌─────────────┴─────────────┐┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ -│ DataSourceExec ││ HashJoinExec │ │ AggregateExec │ +│ LiquidCacheClientExec ││ HashJoinExec │ │ RepartitionExec │ │ -------------------- ││ -------------------- │ │ -------------------- │ -│ files: 1 ││ on: │ │ aggr: │ -│ format: parquet ││(n_nationkey = s_nationkey)├──────────────┐ │min(partsupp.ps_supplycost)│ -│ ││ │ │ │ │ -│ predicate: ││ │ │ │ group_by: ps_partkey │ -│ r_name = EUROPE ││ │ │ │ mode: Partial │ -└───────────────────────────┘└─────────────┬─────────────┘ │ └─────────────┬─────────────┘ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ ProjectionExec │ │ HashJoinExec │ - │ -------------------- ││ -------------------- │ │ -------------------- │ - │ server: ││ p_mfgr: p_mfgr │ │ on: │ - │ http://localhost:50051, ││ p_partkey: p_partkey │ │(r_regionkey = n_regionkey)│ - │ object_stores=[] ││ │ │ │ - │ ││ ps_supplycost: │ │ │ - │ ││ ps_supplycost │ │ │ - │ ││ │ │ │ - │ ││ s_acctbal: s_acctbal │ │ ├──────────────┐ - │ ││ s_address: s_address │ │ │ │ - │ ││ s_comment: s_comment │ │ │ │ - │ ││ s_name: s_name │ │ │ │ - │ ││ │ │ │ │ - │ ││ s_nationkey: │ │ │ │ - │ ││ s_nationkey │ │ │ │ - │ ││ │ │ │ │ - │ ││ s_phone: s_phone │ │ │ │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ └─────────────┬─────────────┘ │ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ DataSourceExec ││ HashJoinExec │ │ LiquidCacheClientExec ││ ProjectionExec │ - │ -------------------- ││ -------------------- │ │ -------------------- ││ -------------------- │ - │ files: 1 ││ on: │ │ server: ││ n_regionkey: │ - │ format: parquet ││ (s_suppkey = ps_suppkey) │ │ http://localhost:50051, ││ n_regionkey │ - │ ││ │ │ object_stores=[] ││ │ - │ predicate: ││ ├──────────────┐ │ ││ ps_partkey: │ - │ DynamicFilter [ empty ] ││ │ │ │ ││ ps_partkey │ - │ ││ │ │ │ ││ │ - │ ││ │ │ │ ││ ps_supplycost: │ - │ ││ │ │ │ ││ ps_supplycost │ - └───────────────────────────┘└─────────────┬─────────────┘ │ └─────────────┬─────────────┘└─────────────┬─────────────┘ +│ server: ││ on: │ │ partition_count(in->out): │ +│ http://localhost:50051, ││(n_nationkey = s_nationkey)├──────────────┐ │ 8 -> 8 │ +│ object_stores=[] ││ │ │ │ │ +│ ││ │ │ │ partitioning_scheme: │ +│ ││ │ │ │ Hash([ps_partkey@0], 8) │ +└─────────────┬─────────────┘└─────────────┬─────────────┘ │ └─────────────┬─────────────┘ +┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +│ DataSourceExec ││ LiquidCacheClientExec ││ HashJoinExec │ │ AggregateExec │ +│ -------------------- ││ -------------------- ││ -------------------- │ │ -------------------- │ +│ files: 1 ││ server: ││ on: │ │ aggr: │ +│ format: parquet ││ http://localhost:50051, ││ (s_suppkey = ps_suppkey) ├──────────────┐ │min(partsupp.ps_supplycost)│ +│ ││ object_stores=[] ││ │ │ │ │ +│ predicate: ││ ││ │ │ │ group_by: ps_partkey │ +│ r_name = EUROPE ││ ││ │ │ │ mode: Partial │ +└───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ │ └─────────────┬─────────────┘ + ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ + │ DataSourceExec ││ LiquidCacheClientExec ││ RepartitionExec │ │ HashJoinExec │ + │ -------------------- ││ -------------------- ││ -------------------- │ │ -------------------- │ + │ files: 1 ││ server: ││ partition_count(in->out): │ │ on: │ + │ format: parquet ││ http://localhost:50051, ││ 1 -> 8 │ │(r_regionkey = n_regionkey)├──────────────┐ + │ ││ object_stores=[] ││ │ │ │ │ + │ ││ ││ partitioning_scheme: │ │ │ │ + │ ││ ││ RoundRobinBatch(8) │ │ │ │ + └───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ RepartitionExec │ │ DataSourceExec ││ HashJoinExec │ + │ DataSourceExec ││ HashJoinExec │ │ LiquidCacheClientExec ││ HashJoinExec │ │ -------------------- ││ -------------------- │ │ -------------------- ││ -------------------- │ - │ server: ││ partition_count(in->out): │ │ files: 1 ││ on: │ - │ http://localhost:50051, ││ 1 -> 8 │ │ format: parquet ││(n_nationkey = s_nationkey)├──────────────┐ - │ object_stores=[] ││ │ │ ││ │ │ - │ ││ partitioning_scheme: │ │ predicate: ││ │ │ - │ ││ RoundRobinBatch(8) │ │ r_name = EUROPE ││ │ │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ └───────────────────────────┘└─────────────┬─────────────┘ │ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ DataSourceExec ││ HashJoinExec │ │ LiquidCacheClientExec ││ RepartitionExec │ - │ -------------------- ││ -------------------- │ │ -------------------- ││ -------------------- │ - │ files: 1 ││ on: │ │ server: ││ partition_count(in->out): │ - │ format: parquet ││ (p_partkey = ps_partkey) ├──────────────┐ │ http://localhost:50051, ││ 1 -> 8 │ - │ ││ │ │ │ object_stores=[] ││ │ - │ predicate: ││ │ │ │ ││ partitioning_scheme: │ - │ DynamicFilter [ empty ] ││ │ │ │ ││ RoundRobinBatch(8) │ - └───────────────────────────┘└─────────────┬─────────────┘ │ └─────────────┬─────────────┘└─────────────┬─────────────┘ + │ files: 1 ││ on: ├──────────────┐ │ server: ││ on: ├──────────────┐ + │ format: parquet ││ (p_partkey = ps_partkey) │ │ │ http://localhost:50051, ││(n_nationkey = s_nationkey)│ │ + │ ││ │ │ │ object_stores=[] ││ │ │ + └───────────────────────────┘└─────────────┬─────────────┘ │ └─────────────┬─────────────┘└─────────────┬─────────────┘ │ + ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ + │ LiquidCacheClientExec ││ LiquidCacheClientExec ││ DataSourceExec ││ LiquidCacheClientExec ││ RepartitionExec │ + │ -------------------- ││ -------------------- ││ -------------------- ││ -------------------- ││ -------------------- │ + │ server: ││ server: ││ files: 1 ││ server: ││ partition_count(in->out): │ + │ http://localhost:50051, ││ http://localhost:50051, ││ format: parquet ││ http://localhost:50051, ││ 1 -> 8 │ + │ object_stores=[] ││ object_stores=[] ││ ││ object_stores=[] ││ │ + │ ││ ││ predicate: ││ ││ partitioning_scheme: │ + │ ││ ││ r_name = EUROPE ││ ││ RoundRobinBatch(8) │ + └─────────────┬─────────────┘└─────────────┬─────────────┘└───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ LiquidCacheClientExec │ │ DataSourceExec ││ ProjectionExec │ + │ DataSourceExec ││ DataSourceExec │ │ DataSourceExec ││ HashJoinExec │ │ -------------------- ││ -------------------- │ │ -------------------- ││ -------------------- │ - │ server: ││ server: │ │ files: 1 ││ ps_partkey: │ - │ http://localhost:50051, ││ http://localhost:50051, │ │ format: parquet ││ ps_partkey │ - │ object_stores=[] ││ object_stores=[] │ │ ││ │ - │ ││ │ │ predicate: ││ ps_supplycost: │ - │ ││ │ │ DynamicFilter [ empty ] ││ ps_supplycost │ - │ ││ │ │ ││ │ - │ ││ │ │ ││ s_nationkey: │ - │ ││ │ │ ││ s_nationkey │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ └───────────────────────────┘└─────────────┬─────────────┘ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ - │ DataSourceExec ││ DataSourceExec │ │ HashJoinExec │ - │ -------------------- ││ -------------------- │ │ -------------------- │ - │ files: 1 ││ files: 1 │ │ on: │ - │ format: parquet ││ format: parquet │ │ (s_suppkey = ps_suppkey) │ - │ ││ │ │ ├──────────────┐ - │ predicate: ││ predicate: │ │ │ │ - │ p_size = 15 AND p_type ││ DynamicFilter [ empty ] │ │ │ │ - │ LIKE %BRASS ││ AND DynamicFilter [ │ │ │ │ - │ ││ empty ] │ │ │ │ - └───────────────────────────┘└───────────────────────────┘ └─────────────┬─────────────┘ │ + │ files: 1 ││ files: 1 │ │ files: 1 ││ on: │ + │ format: parquet ││ format: parquet │ │ format: parquet ││ (s_suppkey = ps_suppkey) │ + │ ││ │ │ ││ ├──────────────┐ + │ predicate: ││ │ │ ││ │ │ + │ p_size = 15 AND p_type ││ │ │ ││ │ │ + │ LIKE %BRASS ││ │ │ ││ │ │ + └───────────────────────────┘└───────────────────────────┘ └───────────────────────────┘└─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ │ LiquidCacheClientExec ││ LiquidCacheClientExec │ │ -------------------- ││ -------------------- │ @@ -175,7 +115,4 @@ expression: displayable.tree_render().to_string() │ -------------------- ││ -------------------- │ │ files: 1 ││ files: 1 │ │ format: parquet ││ format: parquet │ - │ ││ │ - │ predicate: ││ predicate: │ - │ DynamicFilter [ empty ] ││ DynamicFilter [ empty ] │ └───────────────────────────┘└───────────────────────────┘ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q20.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q20.snap index 7625b1a1..7a145d7b 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q20.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q20.snap @@ -53,8 +53,8 @@ expression: displayable.tree_render().to_string() │ files: 1 ││ files: 1 ││ server: ││ server: ││ aggr: │ │ format: parquet ││ format: parquet ││ http://localhost:50051, ││ http://localhost:50051, ││ sum(lineitem.l_quantity) │ │ ││ ││ object_stores=[] ││ object_stores=[] ││ │ -│ predicate: ││ predicate: ││ ││ ││ group_by: │ -│ n_name = CANADA ││ DynamicFilter [ empty ] ││ ││ ││ l_partkey, l_suppkey │ +│ predicate: ││ ││ ││ ││ group_by: │ +│ n_name = CANADA ││ ││ ││ ││ l_partkey, l_suppkey │ │ ││ ││ ││ ││ │ │ ││ ││ ││ ││ mode: │ │ ││ ││ ││ ││ FinalPartitioned │ @@ -105,6 +105,5 @@ expression: displayable.tree_render().to_string() │ predicate: │ │ l_shipdate >= 1994-01-01 │ │ AND l_shipdate < 1995 │ - │ -01-01 AND DynamicFilter │ - │ [ empty ] │ + │ -01-01 │ └───────────────────────────┘ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q21.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q21.snap index 3f4d740f..d2a6f6f5 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q21.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q21.snap @@ -119,8 +119,8 @@ expression: displayable.tree_render().to_string() │ files: 1 ││ partition_count(in->out): │ │ format: parquet ││ 1 -> 8 │ │ ││ │ - │ predicate: ││ partitioning_scheme: │ - │ DynamicFilter [ empty ] ││ RoundRobinBatch(8) │ + │ ││ partitioning_scheme: │ + │ ││ RoundRobinBatch(8) │ └───────────────────────────┘└─────────────┬─────────────┘ ┌─────────────┴─────────────┐ │ DataSourceExec │ @@ -131,8 +131,4 @@ expression: displayable.tree_render().to_string() │ predicate: │ │ l_receiptdate > │ │ l_commitdate │ - │ AND DynamicFilter [ │ - │ empty ] AND │ - │ DynamicFilter [ │ - │ empty ] │ └───────────────────────────┘ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q22.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q22.snap index 96f074ba..74098724 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q22.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q22.snap @@ -3,124 +3,107 @@ source: src/datafusion-client/src/tests/mod.rs expression: displayable.tree_render().to_string() --- ┌───────────────────────────┐ -│ SortPreservingMergeExec │ -│ -------------------- │ -│ cntrycode ASC NULLS LAST │ -└─────────────┬─────────────┘ -┌─────────────┴─────────────┐ -│ SortExec │ -│ -------------------- │ -│ cntrycode@0 ASC NULLS LAST│ -└─────────────┬─────────────┘ +│ ScalarSubqueryExec ├───────────────────────────────────────────┐ +└─────────────┬─────────────┘ │ +┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +│ SortPreservingMergeExec │ │ AggregateExec │ +│ -------------------- │ │ -------------------- │ +│ cntrycode ASC NULLS LAST │ │ aggr: │ +│ │ │ avg(customer.c_acctbal) │ +│ │ │ │ +│ │ │ mode: Final │ +└─────────────┬─────────────┘ └─────────────┬─────────────┘ +┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +│ SortExec │ │ CoalescePartitionsExec │ +│ -------------------- │ │ │ +│ cntrycode@0 ASC NULLS LAST│ │ │ +└─────────────┬─────────────┘ └─────────────┬─────────────┘ +┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +│ ProjectionExec │ │ LiquidCacheClientExec │ +│ -------------------- │ │ -------------------- │ +│ cntrycode: cntrycode │ │ server: │ +│ │ │ http://localhost:50051, │ +│ numcust: │ │ object_stores=[] │ +│ count(Int64(1)) │ │ │ +│ │ │ │ +│ totacctbal: │ │ │ +│ sum(custsale.c_acctbal) │ │ │ +└─────────────┬─────────────┘ └─────────────┬─────────────┘ +┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +│ AggregateExec │ │ AggregateExec │ +│ -------------------- │ │ -------------------- │ +│ aggr: │ │ aggr: │ +│ count(1), sum(custsale │ │ avg(customer.c_acctbal) │ +│ .c_acctbal) │ │ │ +│ │ │ mode: Partial │ +│ group_by: cntrycode │ │ │ +│ │ │ │ +│ mode: │ │ │ +│ FinalPartitioned │ │ │ +└─────────────┬─────────────┘ └─────────────┬─────────────┘ +┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +│ RepartitionExec │ │ RepartitionExec │ +│ -------------------- │ │ -------------------- │ +│ partition_count(in->out): │ │ partition_count(in->out): │ +│ 8 -> 8 │ │ 1 -> 8 │ +│ │ │ │ +│ partitioning_scheme: │ │ partitioning_scheme: │ +│ Hash([cntrycode@0], 8) │ │ RoundRobinBatch(8) │ +└─────────────┬─────────────┘ └─────────────┬─────────────┘ +┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ +│ AggregateExec │ │ DataSourceExec │ +│ -------------------- │ │ -------------------- │ +│ aggr: │ │ files: 1 │ +│ count(1), sum(custsale │ │ format: parquet │ +│ .c_acctbal) │ │ │ +│ │ │ predicate: │ +│ group_by: cntrycode │ │ c_acctbal > Some(0),15,2 │ +│ mode: Partial │ │ AND substr(c_phone, 1, │ +│ │ │ 2) IN (13, 31, 23, 29, │ +│ │ │ 30, 18, 17) │ +└─────────────┬─────────────┘ └───────────────────────────┘ ┌─────────────┴─────────────┐ │ ProjectionExec │ │ -------------------- │ -│ cntrycode: cntrycode │ -│ │ -│ numcust: │ -│ count(Int64(1)) │ -│ │ -│ totacctbal: │ -│ sum(custsale.c_acctbal) │ -└─────────────┬─────────────┘ -┌─────────────┴─────────────┐ -│ AggregateExec │ -│ -------------------- │ -│ aggr: │ -│ count(1), sum(custsale │ -│ .c_acctbal) │ -│ │ -│ group_by: cntrycode │ +│ c_acctbal: c_acctbal │ │ │ -│ mode: │ -│ FinalPartitioned │ +│ cntrycode: │ +│ substr(c_phone, 1, 2) │ └─────────────┬─────────────┘ ┌─────────────┴─────────────┐ │ RepartitionExec │ │ -------------------- │ │ partition_count(in->out): │ -│ 8 -> 8 │ +│ 1 -> 8 │ │ │ │ partitioning_scheme: │ -│ Hash([cntrycode@0], 8) │ +│ RoundRobinBatch(8) │ └─────────────┬─────────────┘ ┌─────────────┴─────────────┐ -│ AggregateExec │ +│ HashJoinExec │ │ -------------------- │ -│ aggr: │ -│ count(1), sum(custsale │ -│ .c_acctbal) │ -│ │ -│ group_by: cntrycode │ -│ mode: Partial │ -└─────────────┬─────────────┘ -┌─────────────┴─────────────┐ -│ ProjectionExec │ -│ -------------------- │ -│ c_acctbal: c_acctbal │ -│ │ -│ cntrycode: │ -│ substr(c_phone, 1, 2) │ -└─────────────┬─────────────┘ -┌─────────────┴─────────────┐ -│ NestedLoopJoinExec ├──────────────┐ +│ join_type: LeftAnti │ +│ ├──────────────┐ +│ on: │ │ +│ (c_custkey = o_custkey) │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ AggregateExec ││ ProjectionExec │ +│ LiquidCacheClientExec ││ LiquidCacheClientExec │ │ -------------------- ││ -------------------- │ -│ aggr: ││ c_acctbal: c_acctbal │ -│ avg(customer.c_acctbal) ││ c_phone: c_phone │ -│ ││ │ -│ mode: Final ││ join_proj_push_down_1: │ -│ ││ CAST(c_acctbal AS │ -│ ││ Decimal128(19, │ -│ ││ 6)) │ -└─────────────┬─────────────┘└─────────────┬─────────────┘ -┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ CoalescePartitionsExec ││ RepartitionExec │ -│ ││ -------------------- │ -│ ││ partition_count(in->out): │ -│ ││ 1 -> 8 │ -│ ││ │ -│ ││ partitioning_scheme: │ -│ ││ RoundRobinBatch(8) │ +│ server: ││ server: │ +│ http://localhost:50051, ││ http://localhost:50051, │ +│ object_stores=[] ││ object_stores=[] │ └─────────────┬─────────────┘└─────────────┬─────────────┘ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ LiquidCacheClientExec ││ HashJoinExec │ +│ DataSourceExec ││ DataSourceExec │ │ -------------------- ││ -------------------- │ -│ server: ││ join_type: LeftAnti │ -│ http://localhost:50051, ││ ├──────────────┐ -│ object_stores=[] ││ on: │ │ -│ ││ (c_custkey = o_custkey) │ │ -└─────────────┬─────────────┘└─────────────┬─────────────┘ │ -┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ AggregateExec ││ LiquidCacheClientExec ││ LiquidCacheClientExec │ -│ -------------------- ││ -------------------- ││ -------------------- │ -│ aggr: ││ server: ││ server: │ -│ avg(customer.c_acctbal) ││ http://localhost:50051, ││ http://localhost:50051, │ -│ ││ object_stores=[] ││ object_stores=[] │ -│ mode: Partial ││ ││ │ -└─────────────┬─────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ -┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ RepartitionExec ││ DataSourceExec ││ DataSourceExec │ -│ -------------------- ││ -------------------- ││ -------------------- │ -│ partition_count(in->out): ││ files: 1 ││ files: 1 │ -│ 1 -> 8 ││ format: parquet ││ format: parquet │ -│ ││ ││ │ -│ partitioning_scheme: ││ predicate: ││ │ -│ RoundRobinBatch(8) ││ substr(c_phone, 1, 2) IN ││ │ -│ ││ (13, 31, 23, 29, 30, 18, ││ │ -│ ││ 17) ││ │ -└─────────────┬─────────────┘└───────────────────────────┘└───────────────────────────┘ -┌─────────────┴─────────────┐ -│ DataSourceExec │ -│ -------------------- │ -│ files: 1 │ -│ format: parquet │ -│ │ -│ predicate: │ -│ c_acctbal > Some(0),15,2 │ -│ AND substr(c_phone, 1, │ -│ 2) IN (13, 31, 23, 29, │ -│ 30, 18, 17) │ -└───────────────────────────┘ +│ files: 1 ││ files: 1 │ +│ format: parquet ││ format: parquet │ +│ ││ │ +│ predicate: ││ │ +│ substr(c_phone, 1, 2) IN ││ │ +│ (13, 31, 23, 29, 30, 18, ││ │ +│ 17) AND CAST(c_acctbal ││ │ +│ AS Decimal128(19, 6)) ││ │ +│ > (scalar subquery) ││ │ +└───────────────────────────┘└───────────────────────────┘ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q3.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q3.snap index 28bc3ef3..9387f57b 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q3.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q3.snap @@ -43,7 +43,11 @@ expression: displayable.tree_render().to_string() │ aggr: │ │ sum(lineitem │ │ .l_extendedp │ -│ rice * Int64(1) - lineitem│ +│ rice * Some(1),20,0 - │ +│ lineitem.l_discount │ +│ ) as sum(lineitem │ +│ .l_extendedprice │ +│ * Int64(1) - lineitem │ │ .l_discount) │ │ │ │ group_by: │ @@ -70,7 +74,11 @@ expression: displayable.tree_render().to_string() │ aggr: │ │ sum(lineitem │ │ .l_extendedp │ -│ rice * Int64(1) - lineitem│ +│ rice * Some(1),20,0 - │ +│ lineitem.l_discount │ +│ ) as sum(lineitem │ +│ .l_extendedprice │ +│ * Int64(1) - lineitem │ │ .l_discount) │ │ │ │ group_by: │ @@ -109,8 +117,6 @@ expression: displayable.tree_render().to_string() │ object_stores=[] ││ object_stores=[] ││ │ │ ││ ││ predicate: │ │ ││ ││ l_shipdate > 1995-03-15 │ -│ ││ ││ AND DynamicFilter [ │ -│ ││ ││ empty ] │ └─────────────┬─────────────┘└─────────────┬─────────────┘└───────────────────────────┘ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ │ DataSourceExec ││ RepartitionExec │ @@ -129,6 +135,4 @@ expression: displayable.tree_render().to_string() │ │ │ predicate: │ │ o_orderdate < 1995-03-15 │ - │ AND DynamicFilter [ │ - │ empty ] │ └───────────────────────────┘ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q5.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q5.snap index f4ebf794..0c5df443 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q5.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q5.snap @@ -29,7 +29,11 @@ expression: displayable.tree_render().to_string() │ aggr: │ │ sum(lineitem │ │ .l_extendedp │ -│ rice * Int64(1) - lineitem│ +│ rice * Some(1),20,0 - │ +│ lineitem.l_discount │ +│ ) as sum(lineitem │ +│ .l_extendedprice │ +│ * Int64(1) - lineitem │ │ .l_discount) │ │ │ │ group_by: n_name │ @@ -52,7 +56,11 @@ expression: displayable.tree_render().to_string() │ aggr: │ │ sum(lineitem │ │ .l_extendedp │ -│ rice * Int64(1) - lineitem│ +│ rice * Some(1),20,0 - │ +│ lineitem.l_discount │ +│ ) as sum(lineitem │ +│ .l_extendedprice │ +│ * Int64(1) - lineitem │ │ .l_discount) │ │ │ │ group_by: n_name │ @@ -65,66 +73,35 @@ expression: displayable.tree_render().to_string() │(r_regionkey = n_regionkey)│ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ LiquidCacheClientExec ││ ProjectionExec │ -│ -------------------- ││ -------------------- │ -│ server: ││ l_discount: │ -│ http://localhost:50051, ││ l_discount │ -│ object_stores=[] ││ │ -│ ││ l_extendedprice: │ -│ ││ l_extendedprice │ -│ ││ │ -│ ││ n_name: n_name │ -│ ││ │ -│ ││ n_regionkey: │ -│ ││ n_regionkey │ -└─────────────┬─────────────┘└─────────────┬─────────────┘ -┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ DataSourceExec ││ HashJoinExec │ +│ LiquidCacheClientExec ││ HashJoinExec │ │ -------------------- ││ -------------------- │ -│ files: 1 ││ on: │ -│ format: parquet ││(n_nationkey = s_nationkey)├──────────────┐ -│ ││ │ │ -│ predicate: ││ │ │ -│ r_name = ASIA ││ │ │ -└───────────────────────────┘└─────────────┬─────────────┘ │ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ ProjectionExec │ - │ -------------------- ││ -------------------- │ - │ server: ││ l_discount: │ - │ http://localhost:50051, ││ l_discount │ - │ object_stores=[] ││ │ - │ ││ l_extendedprice: │ - │ ││ l_extendedprice │ - │ ││ │ - │ ││ s_nationkey: │ - │ ││ s_nationkey │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ DataSourceExec ││ HashJoinExec │ - │ -------------------- ││ -------------------- │ - │ files: 1 ││ on: │ - │ format: parquet ││ (s_suppkey = l_suppkey), ├──────────────┐ - │ ││ (s_nationkey = │ │ - │ predicate: ││ c_nationkey) │ │ - │ DynamicFilter [ empty ] ││ │ │ - └───────────────────────────┘└─────────────┬─────────────┘ │ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ RepartitionExec │ - │ -------------------- ││ -------------------- │ - │ server: ││ partition_count(in->out): │ - │ http://localhost:50051, ││ 1 -> 8 │ - │ object_stores=[] ││ │ - │ ││ partitioning_scheme: │ - │ ││ RoundRobinBatch(8) │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ +│ server: ││ on: ├──────────────┐ +│ http://localhost:50051, ││(n_nationkey = s_nationkey)│ │ +│ object_stores=[] ││ │ │ +└─────────────┬─────────────┘└─────────────┬─────────────┘ │ +┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +│ DataSourceExec ││ LiquidCacheClientExec ││ HashJoinExec │ +│ -------------------- ││ -------------------- ││ -------------------- │ +│ files: 1 ││ server: ││ on: │ +│ format: parquet ││ http://localhost:50051, ││ (s_suppkey = l_suppkey), ├──────────────┐ +│ ││ object_stores=[] ││ (s_nationkey = │ │ +│ predicate: ││ ││ c_nationkey) │ │ +│ r_name = ASIA ││ ││ │ │ +└───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ │ + ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ + │ DataSourceExec ││ LiquidCacheClientExec ││ RepartitionExec │ + │ -------------------- ││ -------------------- ││ -------------------- │ + │ files: 1 ││ server: ││ partition_count(in->out): │ + │ format: parquet ││ http://localhost:50051, ││ 1 -> 8 │ + │ ││ object_stores=[] ││ │ + │ ││ ││ partitioning_scheme: │ + │ ││ ││ RoundRobinBatch(8) │ + └───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ │ DataSourceExec ││ HashJoinExec │ │ -------------------- ││ -------------------- │ - │ files: 1 ││ on: │ - │ format: parquet ││ (o_orderkey = l_orderkey) ├───────────────────────────────────────────┐ - │ ││ │ │ - │ predicate: ││ │ │ - │ DynamicFilter [ empty ] ││ │ │ + │ files: 1 ││ on: ├───────────────────────────────────────────┐ + │ format: parquet ││ (o_orderkey = l_orderkey) │ │ └───────────────────────────┘└─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ │ CoalescePartitionsExec │ │ LiquidCacheClientExec │ @@ -136,11 +113,8 @@ expression: displayable.tree_render().to_string() ┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐ │ HashJoinExec │ │ DataSourceExec │ │ -------------------- │ │ -------------------- │ - │ on: │ │ files: 1 │ - │ (c_custkey = o_custkey) ├──────────────┐ │ format: parquet │ - │ │ │ │ │ - │ │ │ │ predicate: │ - │ │ │ │ DynamicFilter [ empty ] │ + │ on: ├──────────────┐ │ files: 1 │ + │ (c_custkey = o_custkey) │ │ │ format: parquet │ └─────────────┬─────────────┘ │ └───────────────────────────┘ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ │ LiquidCacheClientExec ││ LiquidCacheClientExec │ @@ -167,6 +141,5 @@ expression: displayable.tree_render().to_string() │ predicate: │ │ o_orderdate >= 1994-01-01 │ │ AND o_orderdate < 1995 │ - │ -01-01 AND DynamicFilter │ - │ [ empty ] │ + │ -01-01 │ └───────────────────────────┘ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q7.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q7.snap index a9af257c..66132d2e 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q7.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q7.snap @@ -95,101 +95,45 @@ expression: displayable.tree_render().to_string() │(n_nationkey = c_nationkey)│ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ LiquidCacheClientExec ││ ProjectionExec │ +│ LiquidCacheClientExec ││ HashJoinExec │ │ -------------------- ││ -------------------- │ -│ server: ││ c_nationkey: │ -│ http://localhost:50051, ││ c_nationkey │ -│ object_stores=[] ││ │ -│ ││ l_discount: │ -│ ││ l_discount │ -│ ││ │ -│ ││ l_extendedprice: │ -│ ││ l_extendedprice │ -│ ││ │ -│ ││ l_shipdate: │ -│ ││ l_shipdate │ -│ ││ │ -│ ││ n_name: n_name │ -└─────────────┬─────────────┘└─────────────┬─────────────┘ -┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ DataSourceExec ││ HashJoinExec │ -│ -------------------- ││ -------------------- │ -│ files: 1 ││ on: │ -│ format: parquet ││(n_nationkey = s_nationkey)│ -│ ││ ├──────────────┐ -│ predicate: ││ │ │ -│ n_name = GERMANY OR n_name││ │ │ -│ = FRANCE ││ │ │ -└───────────────────────────┘└─────────────┬─────────────┘ │ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ ProjectionExec │ - │ -------------------- ││ -------------------- │ - │ server: ││ c_nationkey: │ - │ http://localhost:50051, ││ c_nationkey │ - │ object_stores=[] ││ │ - │ ││ l_discount: │ - │ ││ l_discount │ - │ ││ │ - │ ││ l_extendedprice: │ - │ ││ l_extendedprice │ - │ ││ │ - │ ││ l_shipdate: │ - │ ││ l_shipdate │ - │ ││ │ - │ ││ s_nationkey: │ - │ ││ s_nationkey │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ DataSourceExec ││ HashJoinExec │ - │ -------------------- ││ -------------------- │ - │ files: 1 ││ on: │ - │ format: parquet ││ (c_custkey = o_custkey) │ - │ ││ ├──────────────┐ - │ predicate: ││ │ │ - │ n_name = FRANCE OR n_name ││ │ │ - │ = GERMANY ││ │ │ - └───────────────────────────┘└─────────────┬─────────────┘ │ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ ProjectionExec │ - │ -------------------- ││ -------------------- │ - │ server: ││ l_discount: │ - │ http://localhost:50051, ││ l_discount │ - │ object_stores=[] ││ │ - │ ││ l_extendedprice: │ - │ ││ l_extendedprice │ - │ ││ │ - │ ││ l_shipdate: │ - │ ││ l_shipdate │ - │ ││ │ - │ ││ o_custkey: o_custkey │ - │ ││ │ - │ ││ s_nationkey: │ - │ ││ s_nationkey │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ DataSourceExec ││ HashJoinExec │ - │ -------------------- ││ -------------------- │ - │ files: 1 ││ on: │ - │ format: parquet ││ (o_orderkey = l_orderkey) ├──────────────┐ - │ ││ │ │ - │ predicate: ││ │ │ - │ DynamicFilter [ empty ] ││ │ │ - └───────────────────────────┘└─────────────┬─────────────┘ │ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ HashJoinExec │ - │ -------------------- ││ -------------------- │ - │ server: ││ on: ├──────────────┐ - │ http://localhost:50051, ││ (s_suppkey = l_suppkey) │ │ - │ object_stores=[] ││ │ │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ │ +│ server: ││ on: ├──────────────┐ +│ http://localhost:50051, ││(n_nationkey = s_nationkey)│ │ +│ object_stores=[] ││ │ │ +└─────────────┬─────────────┘└─────────────┬─────────────┘ │ +┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +│ DataSourceExec ││ LiquidCacheClientExec ││ HashJoinExec │ +│ -------------------- ││ -------------------- ││ -------------------- │ +│ files: 1 ││ server: ││ on: │ +│ format: parquet ││ http://localhost:50051, ││ (c_custkey = o_custkey) │ +│ ││ object_stores=[] ││ ├──────────────┐ +│ predicate: ││ ││ │ │ +│ n_name = GERMANY OR n_name││ ││ │ │ +│ = FRANCE ││ ││ │ │ +└───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ │ + ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ + │ DataSourceExec ││ LiquidCacheClientExec ││ HashJoinExec │ + │ -------------------- ││ -------------------- ││ -------------------- │ + │ files: 1 ││ server: ││ on: │ + │ format: parquet ││ http://localhost:50051, ││ (o_orderkey = l_orderkey) │ + │ ││ object_stores=[] ││ ├──────────────┐ + │ predicate: ││ ││ │ │ + │ n_name = FRANCE OR n_name ││ ││ │ │ + │ = GERMANY ││ ││ │ │ + └───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ │ + ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ + │ DataSourceExec ││ LiquidCacheClientExec ││ HashJoinExec │ + │ -------------------- ││ -------------------- ││ -------------------- │ + │ files: 1 ││ server: ││ on: ├──────────────┐ + │ format: parquet ││ http://localhost:50051, ││ (s_suppkey = l_suppkey) │ │ + │ ││ object_stores=[] ││ │ │ + └───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ │ DataSourceExec ││ LiquidCacheClientExec ││ LiquidCacheClientExec │ │ -------------------- ││ -------------------- ││ -------------------- │ │ files: 1 ││ server: ││ server: │ │ format: parquet ││ http://localhost:50051, ││ http://localhost:50051, │ │ ││ object_stores=[] ││ object_stores=[] │ - │ predicate: ││ ││ │ - │ DynamicFilter [ empty ] ││ ││ │ └───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ │ DataSourceExec ││ RepartitionExec │ @@ -197,8 +141,8 @@ expression: displayable.tree_render().to_string() │ files: 1 ││ partition_count(in->out): │ │ format: parquet ││ 1 -> 8 │ │ ││ │ - │ predicate: ││ partitioning_scheme: │ - │ DynamicFilter [ empty ] ││ RoundRobinBatch(8) │ + │ ││ partitioning_scheme: │ + │ ││ RoundRobinBatch(8) │ └───────────────────────────┘└─────────────┬─────────────┘ ┌─────────────┴─────────────┐ │ DataSourceExec │ @@ -209,8 +153,5 @@ expression: displayable.tree_render().to_string() │ predicate: │ │ l_shipdate >= 1995-01-01 │ │ AND l_shipdate <= 1996 │ - │ -12-31 AND DynamicFilter │ - │ [ empty ] AND │ - │ DynamicFilter │ - │ [ empty ] │ + │ -12-31 │ └───────────────────────────┘ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q8.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q8.snap index 723991cc..7f2733a2 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q8.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q8.snap @@ -30,10 +30,18 @@ expression: displayable.tree_render().to_string() │ -------------------- │ │ aggr: │ │ sum(CASE WHEN all_nations │ -│ .nation = Utf8("BRAZIL") │ -│ THEN all_nations.volume │ -│ ELSE Int64(0) END), sum │ -│ (all_nations.volume) │ +│ .nation = BRAZIL THEN │ +│ all_nations.volume │ +│ ELSE Some(0),38,4 END │ +│ ) as sum(CASE WHEN │ +│ all_nations │ +│ .nation = Utf8( │ +│ "BRAZIL") THEN │ +│ all_nations │ +│ .volume ELSE Int64 │ +│ (0) END), sum │ +│ (all_nations │ +│ .volume) │ │ │ │ group_by: o_year │ │ │ @@ -54,10 +62,18 @@ expression: displayable.tree_render().to_string() │ -------------------- │ │ aggr: │ │ sum(CASE WHEN all_nations │ -│ .nation = Utf8("BRAZIL") │ -│ THEN all_nations.volume │ -│ ELSE Int64(0) END), sum │ -│ (all_nations.volume) │ +│ .nation = BRAZIL THEN │ +│ all_nations.volume │ +│ ELSE Some(0),38,4 END │ +│ ) as sum(CASE WHEN │ +│ all_nations │ +│ .nation = Utf8( │ +│ "BRAZIL") THEN │ +│ all_nations │ +│ .volume ELSE Int64 │ +│ (0) END), sum │ +│ (all_nations │ +│ .volume) │ │ │ │ group_by: o_year │ │ mode: Partial │ @@ -82,152 +98,58 @@ expression: displayable.tree_render().to_string() │(r_regionkey = n_regionkey)│ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ LiquidCacheClientExec ││ ProjectionExec │ -│ -------------------- ││ -------------------- │ -│ server: ││ l_discount: │ -│ http://localhost:50051, ││ l_discount │ -│ object_stores=[] ││ │ -│ ││ l_extendedprice: │ -│ ││ l_extendedprice │ -│ ││ │ -│ ││ n_name: n_name │ -│ ││ │ -│ ││ n_regionkey: │ -│ ││ n_regionkey │ -│ ││ │ -│ ││ o_orderdate: │ -│ ││ o_orderdate │ -└─────────────┬─────────────┘└─────────────┬─────────────┘ -┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ DataSourceExec ││ HashJoinExec │ +│ LiquidCacheClientExec ││ HashJoinExec │ │ -------------------- ││ -------------------- │ -│ files: 1 ││ on: │ -│ format: parquet ││(n_nationkey = s_nationkey)├──────────────┐ -│ ││ │ │ -│ predicate: ││ │ │ -│ r_name = AMERICA ││ │ │ -└───────────────────────────┘└─────────────┬─────────────┘ │ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ ProjectionExec │ - │ -------------------- ││ -------------------- │ - │ server: ││ l_discount: │ - │ http://localhost:50051, ││ l_discount │ - │ object_stores=[] ││ │ - │ ││ l_extendedprice: │ - │ ││ l_extendedprice │ - │ ││ │ - │ ││ n_regionkey: │ - │ ││ n_regionkey │ - │ ││ │ - │ ││ o_orderdate: │ - │ ││ o_orderdate │ - │ ││ │ - │ ││ s_nationkey: │ - │ ││ s_nationkey │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ DataSourceExec ││ HashJoinExec │ - │ -------------------- ││ -------------------- │ - │ files: 1 ││ on: ├──────────────┐ - │ format: parquet ││(n_nationkey = c_nationkey)│ │ - └───────────────────────────┘└─────────────┬─────────────┘ │ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ ProjectionExec │ - │ -------------------- ││ -------------------- │ - │ server: ││ c_nationkey: │ - │ http://localhost:50051, ││ c_nationkey │ - │ object_stores=[] ││ │ - │ ││ l_discount: │ - │ ││ l_discount │ - │ ││ │ - │ ││ l_extendedprice: │ - │ ││ l_extendedprice │ - │ ││ │ - │ ││ o_orderdate: │ - │ ││ o_orderdate │ - │ ││ │ - │ ││ s_nationkey: │ - │ ││ s_nationkey │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ DataSourceExec ││ HashJoinExec │ - │ -------------------- ││ -------------------- │ - │ files: 1 ││ on: │ - │ format: parquet ││ (c_custkey = o_custkey) ├──────────────┐ - │ ││ │ │ - │ predicate: ││ │ │ - │ DynamicFilter [ empty ] ││ │ │ - └───────────────────────────┘└─────────────┬─────────────┘ │ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ ProjectionExec │ - │ -------------------- ││ -------------------- │ - │ server: ││ l_discount: │ - │ http://localhost:50051, ││ l_discount │ - │ object_stores=[] ││ │ - │ ││ l_extendedprice: │ - │ ││ l_extendedprice │ - │ ││ │ - │ ││ o_custkey: o_custkey │ - │ ││ │ - │ ││ o_orderdate: │ - │ ││ o_orderdate │ - │ ││ │ - │ ││ s_nationkey: │ - │ ││ s_nationkey │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ DataSourceExec ││ HashJoinExec │ - │ -------------------- ││ -------------------- │ - │ files: 1 ││ on: │ - │ format: parquet ││ (o_orderkey = l_orderkey) ├──────────────┐ - │ ││ │ │ - │ predicate: ││ │ │ - │ DynamicFilter [ empty ] ││ │ │ - └───────────────────────────┘└─────────────┬─────────────┘ │ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ ProjectionExec │ - │ -------------------- ││ -------------------- │ - │ server: ││ l_discount: │ - │ http://localhost:50051, ││ l_discount │ - │ object_stores=[] ││ │ - │ ││ l_extendedprice: │ - │ ││ l_extendedprice │ - │ ││ │ - │ ││ l_orderkey: │ - │ ││ l_orderkey │ - │ ││ │ - │ ││ s_nationkey: │ - │ ││ s_nationkey │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ DataSourceExec ││ HashJoinExec │ - │ -------------------- ││ -------------------- │ - │ files: 1 ││ on: │ - │ format: parquet ││ (s_suppkey = l_suppkey) │ - │ ││ │ - │ predicate: ││ ├──────────────┐ - │ o_orderdate >= 1995-01-01 ││ │ │ - │ AND o_orderdate <= 1996 ││ │ │ - │ -12-31 AND DynamicFilter ││ │ │ - │ [ empty ] ││ │ │ - └───────────────────────────┘└─────────────┬─────────────┘ │ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ RepartitionExec │ - │ -------------------- ││ -------------------- │ - │ server: ││ partition_count(in->out): │ - │ http://localhost:50051, ││ 1 -> 8 │ - │ object_stores=[] ││ │ - │ ││ partitioning_scheme: │ - │ ││ RoundRobinBatch(8) │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ +│ server: ││ on: ├──────────────┐ +│ http://localhost:50051, ││(n_nationkey = s_nationkey)│ │ +│ object_stores=[] ││ │ │ +└─────────────┬─────────────┘└─────────────┬─────────────┘ │ +┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +│ DataSourceExec ││ LiquidCacheClientExec ││ HashJoinExec │ +│ -------------------- ││ -------------------- ││ -------------------- │ +│ files: 1 ││ server: ││ on: │ +│ format: parquet ││ http://localhost:50051, ││(n_nationkey = c_nationkey)├──────────────┐ +│ ││ object_stores=[] ││ │ │ +│ predicate: ││ ││ │ │ +│ r_name = AMERICA ││ ││ │ │ +└───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ │ + ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ + │ DataSourceExec ││ LiquidCacheClientExec ││ HashJoinExec │ + │ -------------------- ││ -------------------- ││ -------------------- │ + │ files: 1 ││ server: ││ on: ├──────────────┐ + │ format: parquet ││ http://localhost:50051, ││ (c_custkey = o_custkey) │ │ + │ ││ object_stores=[] ││ │ │ + └───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ │ + ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ + │ DataSourceExec ││ LiquidCacheClientExec ││ HashJoinExec │ + │ -------------------- ││ -------------------- ││ -------------------- │ + │ files: 1 ││ server: ││ on: ├──────────────┐ + │ format: parquet ││ http://localhost:50051, ││ (o_orderkey = l_orderkey) │ │ + │ ││ object_stores=[] ││ │ │ + └───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ │ + ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ + │ DataSourceExec ││ LiquidCacheClientExec ││ HashJoinExec │ + │ -------------------- ││ -------------------- ││ -------------------- │ + │ files: 1 ││ server: ││ on: ├──────────────┐ + │ format: parquet ││ http://localhost:50051, ││ (s_suppkey = l_suppkey) │ │ + │ ││ object_stores=[] ││ │ │ + └───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ │ + ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ + │ DataSourceExec ││ LiquidCacheClientExec ││ RepartitionExec │ + │ -------------------- ││ -------------------- ││ -------------------- │ + │ files: 1 ││ server: ││ partition_count(in->out): │ + │ format: parquet ││ http://localhost:50051, ││ 1 -> 8 │ + │ ││ object_stores=[] ││ │ + │ predicate: ││ ││ partitioning_scheme: │ + │ o_orderdate >= 1995-01-01 ││ ││ RoundRobinBatch(8) │ + │ AND o_orderdate <= 1996 ││ ││ │ + │ -12-31 ││ ││ │ + └───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ │ DataSourceExec ││ HashJoinExec │ │ -------------------- ││ -------------------- │ - │ files: 1 ││ on: │ - │ format: parquet ││ (p_partkey = l_partkey) ├──────────────┐ - │ ││ │ │ - │ predicate: ││ │ │ - │ DynamicFilter [ empty ] ││ │ │ + │ files: 1 ││ on: ├──────────────┐ + │ format: parquet ││ (p_partkey = l_partkey) │ │ └───────────────────────────┘└─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ │ LiquidCacheClientExec ││ LiquidCacheClientExec │ @@ -242,10 +164,7 @@ expression: displayable.tree_render().to_string() │ files: 1 ││ files: 1 │ │ format: parquet ││ format: parquet │ │ ││ │ - │ predicate: ││ predicate: │ - │ p_type = ECONOMY ANODIZED ││ DynamicFilter [ empty ] │ - │ STEEL ││ AND DynamicFilter [ │ - │ ││ empty ] AND │ - │ ││ DynamicFilter │ - │ ││ [ empty ] │ + │ predicate: ││ │ + │ p_type = ECONOMY ANODIZED ││ │ + │ STEEL ││ │ └───────────────────────────┘└───────────────────────────┘ diff --git a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q9.snap b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q9.snap index e9b53fbb..54d9618e 100644 --- a/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q9.snap +++ b/src/datafusion-client/src/tests/snapshots/liquid_cache_datafusion_client__tests__tpch_q9.snap @@ -78,105 +78,40 @@ expression: displayable.tree_render().to_string() │(n_nationkey = s_nationkey)│ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ LiquidCacheClientExec ││ ProjectionExec │ +│ LiquidCacheClientExec ││ HashJoinExec │ │ -------------------- ││ -------------------- │ -│ server: ││ l_discount: │ -│ http://localhost:50051, ││ l_discount │ -│ object_stores=[] ││ │ -│ ││ l_extendedprice: │ -│ ││ l_extendedprice │ -│ ││ │ -│ ││ l_quantity: │ -│ ││ l_quantity │ -│ ││ │ -│ ││ o_orderdate: │ -│ ││ o_orderdate │ -│ ││ │ -│ ││ ps_supplycost: │ -│ ││ ps_supplycost │ -│ ││ │ -│ ││ s_nationkey: │ -│ ││ s_nationkey │ -└─────────────┬─────────────┘└─────────────┬─────────────┘ -┌─────────────┴─────────────┐┌─────────────┴─────────────┐ -│ DataSourceExec ││ HashJoinExec │ -│ -------------------- ││ -------------------- │ -│ files: 1 ││ on: ├──────────────┐ -│ format: parquet ││ (o_orderkey = l_orderkey) │ │ -└───────────────────────────┘└─────────────┬─────────────┘ │ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ ProjectionExec │ - │ -------------------- ││ -------------------- │ - │ server: ││ l_discount: │ - │ http://localhost:50051, ││ l_discount │ - │ object_stores=[] ││ │ - │ ││ l_extendedprice: │ - │ ││ l_extendedprice │ - │ ││ │ - │ ││ l_orderkey: │ - │ ││ l_orderkey │ - │ ││ │ - │ ││ l_quantity: │ - │ ││ l_quantity │ - │ ││ │ - │ ││ ps_supplycost: │ - │ ││ ps_supplycost │ - │ ││ │ - │ ││ s_nationkey: │ - │ ││ s_nationkey │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ DataSourceExec ││ HashJoinExec │ - │ -------------------- ││ -------------------- │ - │ files: 1 ││ on: ├──────────────┐ - │ format: parquet ││ (ps_suppkey = l_suppkey), │ │ - │ ││ (ps_partkey = l_partkey) │ │ - └───────────────────────────┘└─────────────┬─────────────┘ │ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ ProjectionExec │ - │ -------------------- ││ -------------------- │ - │ server: ││ l_discount: │ - │ http://localhost:50051, ││ l_discount │ - │ object_stores=[] ││ │ - │ ││ l_extendedprice: │ - │ ││ l_extendedprice │ - │ ││ │ - │ ││ l_orderkey: │ - │ ││ l_orderkey │ - │ ││ │ - │ ││ l_partkey: l_partkey │ - │ ││ │ - │ ││ l_quantity: │ - │ ││ l_quantity │ - │ ││ │ - │ ││ l_suppkey: l_suppkey │ - │ ││ │ - │ ││ s_nationkey: │ - │ ││ s_nationkey │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ DataSourceExec ││ HashJoinExec │ - │ -------------------- ││ -------------------- │ - │ files: 1 ││ on: ├──────────────┐ - │ format: parquet ││ (s_suppkey = l_suppkey) │ │ - └───────────────────────────┘└─────────────┬─────────────┘ │ - ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ - │ LiquidCacheClientExec ││ RepartitionExec │ - │ -------------------- ││ -------------------- │ - │ server: ││ partition_count(in->out): │ - │ http://localhost:50051, ││ 1 -> 8 │ - │ object_stores=[] ││ │ - │ ││ partitioning_scheme: │ - │ ││ RoundRobinBatch(8) │ - └─────────────┬─────────────┘└─────────────┬─────────────┘ +│ server: ││ on: ├──────────────┐ +│ http://localhost:50051, ││ (o_orderkey = l_orderkey) │ │ +│ object_stores=[] ││ │ │ +└─────────────┬─────────────┘└─────────────┬─────────────┘ │ +┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ +│ DataSourceExec ││ LiquidCacheClientExec ││ HashJoinExec │ +│ -------------------- ││ -------------------- ││ -------------------- │ +│ files: 1 ││ server: ││ on: ├──────────────┐ +│ format: parquet ││ http://localhost:50051, ││ (ps_suppkey = l_suppkey), │ │ +│ ││ object_stores=[] ││ (ps_partkey = l_partkey) │ │ +└───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ │ + ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ + │ DataSourceExec ││ LiquidCacheClientExec ││ HashJoinExec │ + │ -------------------- ││ -------------------- ││ -------------------- │ + │ files: 1 ││ server: ││ on: ├──────────────┐ + │ format: parquet ││ http://localhost:50051, ││ (s_suppkey = l_suppkey) │ │ + │ ││ object_stores=[] ││ │ │ + └───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ │ + ┌─────────────┴─────────────┐┌─────────────┴─────────────┐┌─────────────┴─────────────┐ + │ DataSourceExec ││ LiquidCacheClientExec ││ RepartitionExec │ + │ -------------------- ││ -------------------- ││ -------------------- │ + │ files: 1 ││ server: ││ partition_count(in->out): │ + │ format: parquet ││ http://localhost:50051, ││ 1 -> 8 │ + │ ││ object_stores=[] ││ │ + │ ││ ││ partitioning_scheme: │ + │ ││ ││ RoundRobinBatch(8) │ + └───────────────────────────┘└─────────────┬─────────────┘└─────────────┬─────────────┘ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ │ DataSourceExec ││ HashJoinExec │ │ -------------------- ││ -------------------- │ - │ files: 1 ││ on: │ - │ format: parquet ││ (p_partkey = l_partkey) ├──────────────┐ - │ ││ │ │ - │ predicate: ││ │ │ - │ DynamicFilter [ empty ] ││ │ │ + │ files: 1 ││ on: ├──────────────┐ + │ format: parquet ││ (p_partkey = l_partkey) │ │ └───────────────────────────┘└─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐┌─────────────┴─────────────┐ │ LiquidCacheClientExec ││ LiquidCacheClientExec │ @@ -191,12 +126,6 @@ expression: displayable.tree_render().to_string() │ files: 1 ││ files: 1 │ │ format: parquet ││ format: parquet │ │ ││ │ - │ predicate: ││ predicate: │ - │ p_name LIKE %green% ││ DynamicFilter [ empty ] │ - │ ││ AND DynamicFilter [ │ - │ ││ empty ] AND │ - │ ││ DynamicFilter │ - │ ││ [ empty ] AND │ - │ ││ DynamicFilter │ - │ ││ [ empty ] │ + │ predicate: ││ │ + │ p_name LIKE %green% ││ │ └───────────────────────────┘└───────────────────────────┘ diff --git a/src/datafusion-local/README.md b/src/datafusion-local/README.md index 142049b0..db2df15f 100644 --- a/src/datafusion-local/README.md +++ b/src/datafusion-local/README.md @@ -8,7 +8,7 @@ This crate provides an in-process version of LiquidCache that doesn't require a ```rust use liquid_cache_datafusion_local::{ - storage::cache_policies::FiloPolicy, + storage::cache_policies::LiquidPolicy, LiquidCacheLocalBuilder, }; use datafusion::prelude::SessionConfig; @@ -19,9 +19,9 @@ async fn main() -> Result<(), Box> { let temp_dir = TempDir::new().unwrap(); let (ctx, _cache) = LiquidCacheLocalBuilder::new() - .with_max_cache_bytes(1024 * 1024 * 1024) // 1GB + .with_max_memory_bytes(1024 * 1024 * 1024) // 1GB .with_cache_dir(temp_dir.path().to_path_buf()) - .with_cache_policy(Box::new(FiloPolicy::new())) + .with_cache_policy(Box::new(LiquidPolicy::new())) .build(SessionConfig::new()) .await?; diff --git a/src/datafusion-local/src/lib.rs b/src/datafusion-local/src/lib.rs index 6416c6b7..2bb2485f 100644 --- a/src/datafusion-local/src/lib.rs +++ b/src/datafusion-local/src/lib.rs @@ -11,10 +11,9 @@ use datafusion::error::Result; use datafusion::logical_expr::ScalarUDF; use datafusion::prelude::{SessionConfig, SessionContext}; use liquid_cache::cache::squeeze_policies::{SqueezePolicy, TranscodeSqueezeEvict}; -use liquid_cache::cache::{AlwaysHydrate, HydrationPolicy}; -use liquid_cache::cache_policies::CachePolicy; -use liquid_cache::cache_policies::LiquidPolicy; -use liquid_cache_datafusion::optimizers::{LineageOptimizer, LocalModeOptimizer}; +use liquid_cache::cache::{AlwaysHydrate, HydrationPolicy, default_max_memory_bytes}; +use liquid_cache::cache_policies::{CachePolicy, LiquidPolicy}; +use liquid_cache_datafusion::optimizers::LocalModeOptimizer; use liquid_cache_datafusion::{ LiquidCacheParquet, LiquidCacheParquetRef, VariantGetUdf, VariantPretty, VariantToJsonUdf, }; @@ -41,7 +40,7 @@ pub use liquid_cache_common as common; /// let temp_dir = TempDir::new().unwrap(); /// /// let (ctx, _) = LiquidCacheLocalBuilder::new() -/// .with_max_cache_bytes(1024 * 1024 * 1024) // 1GB +/// .with_max_memory_bytes(1024 * 1024 * 1024) // 1GB /// .with_cache_dir(temp_dir.path().to_path_buf()) /// .with_cache_policy(Box::new(LiquidPolicy::new())) /// .build(SessionConfig::new()) @@ -58,8 +57,10 @@ pub use liquid_cache_common as common; pub struct LiquidCacheLocalBuilder { /// Size of batches for caching batch_size: usize, - /// Maximum cache size in bytes - max_cache_bytes: usize, + /// Maximum memory size in bytes + max_memory_bytes: usize, + /// Maximum disk size in bytes + max_disk_bytes: usize, /// Directory for disk cache cache_dir: PathBuf, /// Cache policy @@ -68,26 +69,25 @@ pub struct LiquidCacheLocalBuilder { squeeze_policy: Box, /// Hydration policy hydration_policy: Box, - span: fastrace::Span, - - eager_shredding: bool, - /// Maximum total file size for a scan to be routed through LiquidCache. max_scan_bytes: Option, + span: fastrace::Span, } impl Default for LiquidCacheLocalBuilder { fn default() -> Self { + let max_memory_bytes = default_max_memory_bytes(); + let max_disk_bytes = max_memory_bytes.saturating_mul(10); Self { batch_size: 8192, - max_cache_bytes: 1024 * 1024 * 1024, // 1GB + max_memory_bytes, + max_disk_bytes, cache_dir: std::env::temp_dir(), cache_policy: Box::new(LiquidPolicy::new()), squeeze_policy: Box::new(TranscodeSqueezeEvict), hydration_policy: Box::new(AlwaysHydrate::new()), - span: fastrace::Span::enter_with_local_parent("liquid_cache_datafusion_local_builder"), - eager_shredding: true, max_scan_bytes: None, + span: fastrace::Span::enter_with_local_parent("liquid_cache_datafusion_local_builder"), } } } @@ -104,9 +104,17 @@ impl LiquidCacheLocalBuilder { self } - /// Set maximum cache size in bytes - pub fn with_max_cache_bytes(mut self, max_cache_bytes: usize) -> Self { - self.max_cache_bytes = max_cache_bytes; + /// Set maximum memory size in bytes. + /// Default is half of available system memory. + pub fn with_max_memory_bytes(mut self, max_memory_bytes: usize) -> Self { + self.max_memory_bytes = max_memory_bytes; + self + } + + /// Set maximum disk size in bytes. + /// Default is 10x the default memory size. + pub fn with_max_disk_bytes(mut self, max_disk_bytes: usize) -> Self { + self.max_disk_bytes = max_disk_bytes; self } @@ -140,15 +148,9 @@ impl LiquidCacheLocalBuilder { self } - /// Set enable shredding - pub fn with_eager_shredding(mut self, eager_shredding: bool) -> Self { - self.eager_shredding = eager_shredding; - self - } - - /// Set maximum total file size (in bytes) for a scan to be routed - /// through LiquidCache. Scans exceeding this threshold are read - /// directly from the parquet source, bypassing the cache entirely. + /// Set the maximum total file size (in bytes) for a scan to be routed + /// through LiquidCache. Scans exceeding this threshold are read directly + /// from the parquet source, bypassing the cache entirely. pub fn with_max_scan_bytes(mut self, max_bytes: u64) -> Self { self.max_scan_bytes = Some(max_bytes); self @@ -173,20 +175,33 @@ impl LiquidCacheLocalBuilder { let store = t4::mount(self.cache_dir.join("liquid_cache.t4")) .await .map_err(|e| datafusion::error::DataFusionError::External(Box::new(e)))?; + #[cfg(not(test))] let cache = LiquidCacheParquet::new( self.batch_size, - self.max_cache_bytes, + self.max_memory_bytes, + self.max_disk_bytes, store, self.cache_policy, self.squeeze_policy, self.hydration_policy, ) .await; - let cache_ref = Arc::new(cache); - let date_extract_optimizer = Arc::new(LineageOptimizer::new()); + #[cfg(test)] + let cache = LiquidCacheParquet::new_with_squeeze_victim_concurrency( + self.batch_size, + self.max_memory_bytes, + self.max_disk_bytes, + store, + self.cache_policy, + self.squeeze_policy, + self.hydration_policy, + false, + ) + .await; + let cache_ref = Arc::new(cache); - let mut optimizer = LocalModeOptimizer::new(cache_ref.clone(), self.eager_shredding); + let mut optimizer = LocalModeOptimizer::new(cache_ref.clone()); if let Some(max_bytes) = self.max_scan_bytes { optimizer = optimizer.with_max_scan_bytes(max_bytes); } @@ -194,7 +209,6 @@ impl LiquidCacheLocalBuilder { let state = datafusion::execution::SessionStateBuilder::new() .with_config(config) .with_default_features() - .with_optimizer_rule(date_extract_optimizer) .with_physical_optimizer_rule(Arc::new(optimizer)) .build(); diff --git a/src/datafusion-local/src/tests/date_optimizer.rs b/src/datafusion-local/src/tests/date_optimizer.rs index ab421b2b..2d5fb998 100644 --- a/src/datafusion-local/src/tests/date_optimizer.rs +++ b/src/datafusion-local/src/tests/date_optimizer.rs @@ -37,7 +37,7 @@ async fn general_test(sql: &str) -> CacheStatsSummary { // Set up the session context with liquid cache let lc_builder = LiquidCacheLocalBuilder::new() - .with_max_cache_bytes(1024 * 1024) + .with_max_memory_bytes(1024 * 1024) .with_cache_dir(cache_dir.path().to_path_buf()) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) .with_cache_policy(Box::new(liquid_cache::cache_policies::LiquidPolicy::new())); @@ -97,26 +97,26 @@ async fn general_test(sql: &str) -> CacheStatsSummary { async fn test_date_extraction() { let sql = r#"select AVG(EXTRACT(YEAR from date_a)) as year from test_table"#; let stats = general_test(sql).await; - assert_eq!(stats.stats.runtime.hit_date32_expression_calls, 81); + assert_eq!(stats.stats.runtime.hit_date32_expression_calls, 86); } #[tokio::test] async fn date_extraction_month() { let sql = r#"select AVG(EXTRACT(MONTH from date_a)) as month from test_table"#; let stats = general_test(sql).await; - assert_eq!(stats.stats.runtime.hit_date32_expression_calls, 73); + assert_eq!(stats.stats.runtime.hit_date32_expression_calls, 78); } #[tokio::test] async fn date_extraction_day() { let sql = r#"select AVG(EXTRACT(DAY from date_a)) as day from test_table"#; let stats = general_test(sql).await; - assert_eq!(stats.stats.runtime.hit_date32_expression_calls, 81); + assert_eq!(stats.stats.runtime.hit_date32_expression_calls, 86); } #[tokio::test] async fn test_date_extraction_case2() { let sql = r#"select AVG(EXTRACT(YEAR from date_a) + 1) as year, (SELECT MAX(EXTRACT(YEAR from date_a)) FROM test_table) as max_year from test_table"#; let stats = general_test(sql).await; - assert_eq!(stats.stats.runtime.hit_date32_expression_calls, 162); // we know this. + assert_eq!(stats.stats.runtime.hit_date32_expression_calls, 172); // we know this. } diff --git a/src/datafusion-local/src/tests/mod.rs b/src/datafusion-local/src/tests/mod.rs index 67d2d31a..f92b9852 100644 --- a/src/datafusion-local/src/tests/mod.rs +++ b/src/datafusion-local/src/tests/mod.rs @@ -103,7 +103,7 @@ async fn create_session_context_with_liquid_cache( let mut config = SessionConfig::new(); config.options_mut().execution.target_partitions = 4; let (ctx, cache) = LiquidCacheLocalBuilder::new() - .with_max_cache_bytes(cache_size_bytes) + .with_max_memory_bytes(cache_size_bytes) .with_cache_dir(cache_dir.to_path_buf()) .with_squeeze_policy(squeeze_policy) .with_cache_policy(Box::new(LiquidPolicy::new())) @@ -342,7 +342,7 @@ async fn test_provide_schema2() { config.options_mut().execution.target_partitions = 4; let (liquid_ctx, cache) = LiquidCacheLocalBuilder::new() .with_cache_dir(cache_dir.path().to_path_buf()) - .with_max_cache_bytes(1024 * 1024) + .with_max_memory_bytes(1024 * 1024) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) .build(config) .await diff --git a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__os_selection.snap b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__os_selection.snap index 33cb1a4f..fe7b869c 100644 --- a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__os_selection.snap +++ b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__os_selection.snap @@ -41,8 +41,8 @@ entries.memory.liquid: 4 entries.memory.squeezed_liquid: 1 entries.disk.liquid: 0 entries.disk.arrow: 0 -usage.memory_bytes: 954674 -usage.disk_bytes: 156304 +usage.memory_bytes: 954618 +usage.disk_bytes: 164536 RuntimeStatsSnapshot: get: 3 get_with_selection: 3 @@ -51,8 +51,10 @@ RuntimeStatsSnapshot: get_squeezed_needs_io: 1 try_read_liquid_calls: 0 hit_date32_expression_calls: 0 - read_io_count: 1 + read_io_count: 3 write_io_count: 0 + disk_evictions: 0 + disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_count: 2141 squeezed_total_count: 2164 diff --git a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__provide_schema2.snap b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__provide_schema2.snap index 526b2b0e..3b21a3f3 100644 --- a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__provide_schema2.snap +++ b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__provide_schema2.snap @@ -26,7 +26,7 @@ entries.memory.liquid: 1 entries.memory.squeezed_liquid: 0 entries.disk.liquid: 2 entries.disk.arrow: 0 -usage.memory_bytes: 949843 +usage.memory_bytes: 1000915 usage.disk_bytes: 35000 RuntimeStatsSnapshot: get: 4 @@ -38,6 +38,8 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 0 write_io_count: 0 + disk_evictions: 0 + disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_count: 0 squeezed_total_count: 0 @@ -169,7 +171,7 @@ entries.memory.liquid: 1 entries.memory.squeezed_liquid: 0 entries.disk.liquid: 2 entries.disk.arrow: 0 -usage.memory_bytes: 949843 +usage.memory_bytes: 1000915 usage.disk_bytes: 35000 RuntimeStatsSnapshot: get: 5 @@ -181,6 +183,8 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 0 write_io_count: 0 + disk_evictions: 0 + disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_count: 0 squeezed_total_count: 0 @@ -213,7 +217,7 @@ entries.memory.liquid: 3 entries.memory.squeezed_liquid: 0 entries.disk.liquid: 0 entries.disk.arrow: 0 -usage.memory_bytes: 985232 +usage.memory_bytes: 1036304 usage.disk_bytes: 35000 RuntimeStatsSnapshot: get: 4 @@ -225,6 +229,8 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 0 write_io_count: 0 + disk_evictions: 0 + disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_count: 0 squeezed_total_count: 0 diff --git a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__provide_schema_with_filter.snap b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__provide_schema_with_filter.snap index f1ffecfd..e518b4d6 100644 --- a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__provide_schema_with_filter.snap +++ b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__provide_schema_with_filter.snap @@ -42,7 +42,7 @@ entries.memory.liquid: 0 entries.memory.squeezed_liquid: 0 entries.disk.liquid: 0 entries.disk.arrow: 0 -usage.memory_bytes: 443700 +usage.memory_bytes: 574612 usage.disk_bytes: 0 RuntimeStatsSnapshot: get: 13 @@ -54,6 +54,8 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 0 write_io_count: 0 + disk_evictions: 0 + disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_count: 0 squeezed_total_count: 0 diff --git a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__referer_filtering.snap b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__referer_filtering.snap index 767b66d3..df9e1613 100644 --- a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__referer_filtering.snap +++ b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__referer_filtering.snap @@ -35,13 +35,13 @@ values: stats: entries.total: 8 entries.after_first_run: 8 -entries.memory.arrow: 2 -entries.memory.liquid: 6 +entries.memory.arrow: 1 +entries.memory.liquid: 7 entries.memory.squeezed_liquid: 0 entries.disk.liquid: 0 entries.disk.arrow: 0 -usage.memory_bytes: 885884 -usage.disk_bytes: 729144 +usage.memory_bytes: 917947 +usage.disk_bytes: 877216 RuntimeStatsSnapshot: get: 2 get_with_selection: 2 @@ -50,8 +50,10 @@ RuntimeStatsSnapshot: get_squeezed_needs_io: 0 try_read_liquid_calls: 0 hit_date32_expression_calls: 0 - read_io_count: 0 + read_io_count: 5 write_io_count: 0 + disk_evictions: 0 + disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_count: 0 squeezed_total_count: 0 diff --git a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__single_column_filter_projection.snap b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__single_column_filter_projection.snap index b5db557e..5b5807c2 100644 --- a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__single_column_filter_projection.snap +++ b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__single_column_filter_projection.snap @@ -26,7 +26,7 @@ entries.memory.liquid: 0 entries.memory.squeezed_liquid: 0 entries.disk.liquid: 0 entries.disk.arrow: 0 -usage.memory_bytes: 197072 +usage.memory_bytes: 262528 usage.disk_bytes: 0 RuntimeStatsSnapshot: get: 1 @@ -38,6 +38,8 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 0 write_io_count: 0 + disk_evictions: 0 + disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_count: 0 squeezed_total_count: 0 diff --git a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__basic_squeeze.snap b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__basic_squeeze.snap index f9e15ff4..45d651a9 100644 --- a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__basic_squeeze.snap +++ b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__basic_squeeze.snap @@ -9,6 +9,8 @@ event=squeeze_begin victims=[0] event=squeeze_victim entry=0 event=insert_success entry=0 kind=MemoryLiquid event=insert_success entry=262144 kind=MemoryArrow +event=eval_predicate entry=0 selection=true cached=MemoryLiquid +event=read entry=262144 selection=true expr=None cached=MemoryArrow event=insert_failed entry=1 kind=MemoryArrow event=squeeze_begin victims=[262144,0] event=squeeze_victim entry=262144 @@ -23,10 +25,12 @@ event=squeeze_victim entry=1 event=insert_success entry=1 kind=MemoryLiquid event=squeeze_victim entry=262144 event=io_write entry=262144 kind=DiskLiquid bytes=17448 +event=insert_success entry=262144 kind=DiskLiquid event=squeeze_victim entry=0 event=insert_success entry=0 kind=DiskLiquid -event=insert_success entry=262144 kind=DiskLiquid event=insert_success entry=262145 kind=MemoryArrow +event=eval_predicate entry=1 selection=true cached=MemoryLiquid +event=read entry=262145 selection=true expr=None cached=MemoryArrow event=insert_failed entry=2 kind=MemoryArrow event=squeeze_begin victims=[262145,1] event=squeeze_victim entry=262145 @@ -41,66 +45,30 @@ event=squeeze_victim entry=2 event=insert_success entry=2 kind=MemoryLiquid event=squeeze_victim entry=262145 event=io_write entry=262145 kind=DiskLiquid bytes=17448 +event=insert_success entry=262145 kind=DiskLiquid event=squeeze_victim entry=1 event=insert_success entry=1 kind=DiskLiquid -event=insert_success entry=262145 kind=DiskLiquid event=insert_success entry=262146 kind=MemoryArrow -event=eval_predicate entry=0 selection=true cached=DiskLiquid -event=io_read_liquid entry=0 bytes=63528 -event=hydrate entry=0 cached=DiskLiquid new=MemoryLiquid -event=insert_failed entry=0 kind=MemoryLiquid +event=eval_predicate entry=2 selection=true cached=MemoryLiquid +event=read entry=262146 selection=true expr=None cached=MemoryArrow +event=insert_failed entry=4294967296 kind=MemoryArrow event=squeeze_begin victims=[262146,2] event=squeeze_victim entry=262146 event=insert_success entry=262146 kind=MemoryLiquid event=squeeze_victim entry=2 event=io_write entry=2 kind=MemorySqueezedLiquid bytes=63528 event=insert_success entry=2 kind=MemorySqueezedLiquid -event=insert_success entry=0 kind=MemoryLiquid -event=read entry=262144 selection=true expr=None cached=DiskLiquid -event=io_read_liquid entry=262144 bytes=17448 -event=hydrate entry=262144 cached=DiskLiquid new=MemoryLiquid -event=insert_success entry=262144 kind=MemoryLiquid -event=eval_predicate entry=1 selection=true cached=DiskLiquid -event=io_read_liquid entry=1 bytes=63528 -event=hydrate entry=1 cached=DiskLiquid new=MemoryLiquid -event=insert_failed entry=1 kind=MemoryLiquid -event=squeeze_begin victims=[262146,0,262144,2] +event=insert_success entry=4294967296 kind=MemoryArrow +event=insert_failed entry=4295229440 kind=MemoryArrow +event=squeeze_begin victims=[4294967296,262146,2] +event=squeeze_victim entry=4294967296 +event=insert_success entry=4294967296 kind=MemoryLiquid event=squeeze_victim entry=262146 event=io_write entry=262146 kind=DiskLiquid bytes=17448 -event=squeeze_victim entry=0 -event=io_write entry=0 kind=MemorySqueezedLiquid bytes=63528 -event=squeeze_victim entry=262144 -event=io_write entry=262144 kind=DiskLiquid bytes=17448 +event=insert_success entry=262146 kind=DiskLiquid event=squeeze_victim entry=2 event=insert_success entry=2 kind=DiskLiquid -event=insert_success entry=262146 kind=DiskLiquid -event=insert_success entry=0 kind=MemorySqueezedLiquid -event=insert_success entry=262144 kind=DiskLiquid -event=insert_success entry=1 kind=MemoryLiquid -event=read entry=262145 selection=true expr=None cached=DiskLiquid -event=io_read_liquid entry=262145 bytes=17448 -event=hydrate entry=262145 cached=DiskLiquid new=MemoryLiquid -event=insert_success entry=262145 kind=MemoryLiquid -event=eval_predicate entry=2 selection=true cached=DiskLiquid -event=io_read_liquid entry=2 bytes=63528 -event=hydrate entry=2 cached=DiskLiquid new=MemoryLiquid -event=insert_failed entry=2 kind=MemoryLiquid -event=squeeze_begin victims=[1,262145,0] -event=squeeze_victim entry=1 -event=io_write entry=1 kind=MemorySqueezedLiquid bytes=63528 -event=squeeze_victim entry=262145 -event=io_write entry=262145 kind=DiskLiquid bytes=17448 -event=squeeze_victim entry=0 -event=insert_success entry=0 kind=DiskLiquid -event=insert_success entry=1 kind=MemorySqueezedLiquid -event=insert_success entry=262145 kind=DiskLiquid -event=insert_success entry=2 kind=MemoryLiquid -event=read entry=262146 selection=true expr=None cached=DiskLiquid -event=io_read_liquid entry=262146 bytes=17448 -event=hydrate entry=262146 cached=DiskLiquid new=MemoryLiquid -event=insert_success entry=262146 kind=MemoryLiquid -event=insert_success entry=4294967296 kind=MemoryArrow event=insert_success entry=4295229440 kind=MemoryArrow -event=eval_predicate entry=4294967296 selection=true cached=MemoryArrow +event=eval_predicate entry=4294967296 selection=true cached=MemoryLiquid event=read entry=4295229440 selection=true expr=None cached=MemoryArrow ] diff --git a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_distinct_search_phase.snap b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_distinct_search_phase.snap index 80af5e42..fa146807 100644 --- a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_distinct_search_phase.snap +++ b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_distinct_search_phase.snap @@ -4,32 +4,25 @@ expression: trace --- EventTrace: [ event=insert_success entry=2555904 kind=MemoryArrow -event=insert_failed entry=2555905 kind=MemoryArrow -event=squeeze_begin victims=[2555904] -event=squeeze_victim entry=2555904 -event=insert_success entry=2555904 kind=MemoryLiquid +event=eval_predicate entry=2555904 selection=true cached=MemoryArrow +event=read entry=2555904 selection=true expr=None cached=MemoryArrow event=insert_success entry=2555905 kind=MemoryArrow +event=eval_predicate entry=2555905 selection=true cached=MemoryArrow +event=read entry=2555905 selection=true expr=None cached=MemoryArrow event=insert_failed entry=2555906 kind=MemoryArrow -event=squeeze_begin victims=[2555905,2555904] +event=squeeze_begin victims=[2555904,2555905] +event=squeeze_victim entry=2555904 +event=insert_success entry=2555904 kind=MemoryLiquid event=squeeze_victim entry=2555905 event=insert_success entry=2555905 kind=MemoryLiquid -event=squeeze_victim entry=2555904 -event=io_write entry=2555904 kind=MemorySqueezedLiquid bytes=27320 -event=insert_success entry=2555904 kind=MemorySqueezedLiquid event=insert_success entry=2555906 kind=MemoryArrow -event=eval_predicate entry=2555904 selection=true cached=MemorySqueezedLiquid -event=read entry=2555904 selection=true expr=None cached=MemorySqueezedLiquid -event=io_read_squeezed_backing entry=2555904 bytes=27320 -event=eval_predicate entry=2555905 selection=true cached=MemoryLiquid -event=read entry=2555905 selection=true expr=None cached=MemoryLiquid event=eval_predicate entry=2555906 selection=true cached=MemoryArrow event=read entry=2555906 selection=true expr=None cached=MemoryArrow event=insert_success entry=4297523200 kind=MemoryArrow event=eval_predicate entry=4297523200 selection=true cached=MemoryArrow event=read entry=4297523200 selection=true expr=None cached=MemoryArrow -event=eval_predicate entry=2555904 selection=true cached=MemorySqueezedLiquid -event=read entry=2555904 selection=true expr=None cached=MemorySqueezedLiquid -event=io_read_squeezed_backing entry=2555904 bytes=27320 +event=eval_predicate entry=2555904 selection=true cached=MemoryLiquid +event=read entry=2555904 selection=true expr=None cached=MemoryLiquid event=eval_predicate entry=2555905 selection=true cached=MemoryLiquid event=read entry=2555905 selection=true expr=None cached=MemoryLiquid event=eval_predicate entry=2555906 selection=true cached=MemoryArrow diff --git a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_strings.snap b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_strings.snap index 4451f2bd..2943782b 100644 --- a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_strings.snap +++ b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_strings.snap @@ -15,24 +15,34 @@ event=io_write entry=851968 kind=DiskLiquid bytes=139416 event=insert_success entry=851968 kind=DiskLiquid event=insert_failed entry=917504 kind=MemoryArrow event=insert_success entry=917504 kind=MemoryLiquid +event=eval_predicate entry=917504 selection=true cached=MemoryLiquid +event=read entry=851968 selection=true expr=None cached=DiskLiquid +event=io_read_liquid entry=851968 bytes=139416 +event=hydrate entry=851968 cached=DiskLiquid new=MemoryLiquid +event=insert_success entry=851968 kind=MemoryLiquid event=insert_success entry=851969 kind=MemoryArrow event=insert_failed entry=917505 kind=MemoryArrow -event=squeeze_begin victims=[851969,917504] +event=squeeze_begin victims=[851969,917504,851968] event=squeeze_victim entry=851969 event=insert_success entry=851969 kind=MemoryLiquid event=squeeze_victim entry=917504 event=io_write entry=917504 kind=MemorySqueezedLiquid bytes=136440 event=insert_success entry=917504 kind=MemorySqueezedLiquid +event=squeeze_victim entry=851968 +event=io_write entry=851968 kind=DiskLiquid bytes=139416 +event=insert_success entry=851968 kind=DiskLiquid event=insert_success entry=917505 kind=MemoryArrow +event=eval_predicate entry=917505 selection=true cached=MemoryArrow +event=read entry=851969 selection=true expr=None cached=MemoryLiquid event=insert_failed entry=851970 kind=MemoryArrow event=squeeze_begin victims=[917505,851969,917504] event=squeeze_victim entry=917505 event=insert_success entry=917505 kind=MemoryLiquid event=squeeze_victim entry=851969 event=io_write entry=851969 kind=DiskLiquid bytes=139376 +event=insert_success entry=851969 kind=DiskLiquid event=squeeze_victim entry=917504 event=insert_success entry=917504 kind=DiskLiquid -event=insert_success entry=851969 kind=DiskLiquid event=insert_success entry=851970 kind=MemoryArrow event=insert_failed entry=917506 kind=MemoryArrow event=squeeze_begin victims=[851970,917505] @@ -45,34 +55,18 @@ event=insert_failed entry=917506 kind=MemoryArrow event=squeeze_begin victims=[851970,917505] event=squeeze_victim entry=851970 event=io_write entry=851970 kind=DiskLiquid bytes=146184 +event=insert_success entry=851970 kind=DiskLiquid event=squeeze_victim entry=917505 event=insert_success entry=917505 kind=DiskLiquid -event=insert_success entry=851970 kind=DiskLiquid event=insert_success entry=917506 kind=MemoryArrow -event=eval_predicate entry=917504 selection=true cached=DiskLiquid -event=io_read_liquid entry=917504 bytes=136440 -event=hydrate entry=917504 cached=DiskLiquid new=MemoryLiquid -event=insert_failed entry=917504 kind=MemoryLiquid -event=squeeze_begin victims=[917506] -event=squeeze_victim entry=917506 -event=insert_success entry=917506 kind=MemoryLiquid -event=insert_success entry=917504 kind=MemoryLiquid -event=read entry=851968 selection=true expr=None cached=DiskLiquid -event=io_read_liquid entry=851968 bytes=139416 -event=hydrate entry=851968 cached=DiskLiquid new=MemoryLiquid -event=insert_success entry=851968 kind=MemoryLiquid -event=eval_predicate entry=917505 selection=true cached=DiskLiquid -event=io_read_liquid entry=917505 bytes=141576 -event=hydrate entry=917505 cached=DiskLiquid new=MemoryLiquid -event=insert_success entry=917505 kind=MemoryLiquid -event=read entry=851969 selection=true expr=None cached=DiskLiquid -event=io_read_liquid entry=851969 bytes=139376 -event=hydrate entry=851969 cached=DiskLiquid new=MemoryLiquid -event=insert_success entry=851969 kind=MemoryLiquid -event=eval_predicate entry=917506 selection=true cached=MemoryLiquid +event=eval_predicate entry=917506 selection=true cached=MemoryArrow event=read entry=851970 selection=true expr=None cached=DiskLiquid event=io_read_liquid entry=851970 bytes=146184 event=hydrate entry=851970 cached=DiskLiquid new=MemoryLiquid +event=insert_failed entry=851970 kind=MemoryLiquid +event=squeeze_begin victims=[917506] +event=squeeze_victim entry=917506 +event=insert_success entry=917506 kind=MemoryLiquid event=insert_success entry=851970 kind=MemoryLiquid event=insert_success entry=4295819264 kind=MemoryArrow event=insert_success entry=4295884800 kind=MemoryArrow diff --git a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_substrings_search.snap b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_substrings_search.snap index dd377dfa..9a8975b9 100644 --- a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_substrings_search.snap +++ b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_substrings_search.snap @@ -4,21 +4,16 @@ expression: trace --- EventTrace: [ event=insert_success entry=2555904 kind=MemoryArrow -event=insert_failed entry=2555905 kind=MemoryArrow -event=squeeze_begin victims=[2555904] -event=squeeze_victim entry=2555904 -event=insert_success entry=2555904 kind=MemoryLiquid +event=eval_predicate entry=2555904 selection=true cached=MemoryArrow event=insert_success entry=2555905 kind=MemoryArrow +event=eval_predicate entry=2555905 selection=true cached=MemoryArrow event=insert_failed entry=2555906 kind=MemoryArrow -event=squeeze_begin victims=[2555905,2555904] +event=squeeze_begin victims=[2555904,2555905] +event=squeeze_victim entry=2555904 +event=insert_success entry=2555904 kind=MemoryLiquid event=squeeze_victim entry=2555905 event=insert_success entry=2555905 kind=MemoryLiquid -event=squeeze_victim entry=2555904 -event=io_write entry=2555904 kind=MemorySqueezedLiquid bytes=28612 -event=insert_success entry=2555904 kind=MemorySqueezedLiquid event=insert_success entry=2555906 kind=MemoryArrow -event=eval_predicate entry=2555904 selection=true cached=MemorySqueezedLiquid -event=eval_predicate entry=2555905 selection=true cached=MemoryLiquid event=eval_predicate entry=2555906 selection=true cached=MemoryArrow event=insert_success entry=4297523200 kind=MemoryArrow event=eval_predicate entry=4297523200 selection=true cached=MemoryArrow diff --git a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_substrings_search_title.snap b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_substrings_search_title.snap index c8bba373..12ee8a2c 100644 --- a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_substrings_search_title.snap +++ b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_substrings_search_title.snap @@ -4,23 +4,16 @@ expression: trace --- EventTrace: [ event=insert_success entry=131072 kind=MemoryArrow -event=insert_failed entry=131073 kind=MemoryArrow -event=squeeze_begin victims=[131072] -event=squeeze_victim entry=131072 -event=insert_success entry=131072 kind=MemoryLiquid +event=eval_predicate entry=131072 selection=true cached=MemoryArrow event=insert_success entry=131073 kind=MemoryArrow +event=eval_predicate entry=131073 selection=true cached=MemoryArrow event=insert_failed entry=131074 kind=MemoryArrow -event=squeeze_begin victims=[131073,131072] +event=squeeze_begin victims=[131072,131073] +event=squeeze_victim entry=131072 +event=insert_success entry=131072 kind=MemoryLiquid event=squeeze_victim entry=131073 event=insert_success entry=131073 kind=MemoryLiquid -event=squeeze_victim entry=131072 -event=io_write entry=131072 kind=MemorySqueezedLiquid bytes=257888 -event=insert_success entry=131072 kind=MemorySqueezedLiquid event=insert_success entry=131074 kind=MemoryArrow -event=eval_predicate entry=131072 selection=true cached=MemorySqueezedLiquid -event=io_read_squeezed_backing entry=131072 bytes=257888 -event=decompress_squeezed entry=131072 decompressed=1054 total=1724 -event=eval_predicate entry=131073 selection=true cached=MemoryLiquid event=eval_predicate entry=131074 selection=true cached=MemoryArrow event=insert_success entry=4295098368 kind=MemoryArrow event=eval_predicate entry=4295098368 selection=true cached=MemoryArrow diff --git a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__url_prefix_filtering.snap b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__url_prefix_filtering.snap index dc545bd8..e83ef490 100644 --- a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__url_prefix_filtering.snap +++ b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__url_prefix_filtering.snap @@ -56,7 +56,7 @@ entries.memory.liquid: 1 entries.memory.squeezed_liquid: 1 entries.disk.liquid: 0 entries.disk.arrow: 0 -usage.memory_bytes: 912529 +usage.memory_bytes: 912473 usage.disk_bytes: 139416 RuntimeStatsSnapshot: get: 4 @@ -68,6 +68,8 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 1 write_io_count: 0 + disk_evictions: 0 + disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_count: 0 squeezed_total_count: 0 diff --git a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__url_selection_and_ordering.snap b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__url_selection_and_ordering.snap index f0a631fa..7b6d7f6d 100644 --- a/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__url_selection_and_ordering.snap +++ b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__url_selection_and_ordering.snap @@ -36,24 +36,26 @@ values: stats: entries.total: 4 entries.after_first_run: 4 -entries.memory.arrow: 1 -entries.memory.liquid: 1 -entries.memory.squeezed_liquid: 2 -entries.disk.liquid: 0 +entries.memory.arrow: 0 +entries.memory.liquid: 2 +entries.memory.squeezed_liquid: 1 +entries.disk.liquid: 1 entries.disk.arrow: 0 -usage.memory_bytes: 227389 -usage.disk_bytes: 564392 +usage.memory_bytes: 189553 +usage.disk_bytes: 706252 RuntimeStatsSnapshot: get: 3 get_with_selection: 3 eval_predicate: 4 get_squeezed_success: 0 - get_squeezed_needs_io: 4 + get_squeezed_needs_io: 0 try_read_liquid_calls: 0 hit_date32_expression_calls: 0 read_io_count: 4 - write_io_count: 0 + write_io_count: 4 + disk_evictions: 0 + disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 - squeezed_decompressed_count: 4362 - squeezed_total_count: 8814 + squeezed_decompressed_count: 0 + squeezed_total_count: 0 squeeze_io_saved: 0 diff --git a/src/datafusion-local/src/tests/squeeze.rs b/src/datafusion-local/src/tests/squeeze.rs index 6a12b4a7..89b2eba8 100644 --- a/src/datafusion-local/src/tests/squeeze.rs +++ b/src/datafusion-local/src/tests/squeeze.rs @@ -10,7 +10,7 @@ const TEST_FILE: &str = "../../examples/nano_hits.parquet"; async fn basic_squeeze() { let cache_dir = TempDir::new().unwrap(); let (ctx, cache) = LiquidCacheLocalBuilder::new() - .with_max_cache_bytes(1024 * 128) + .with_max_memory_bytes(1024 * 128) .with_cache_dir(cache_dir.path().to_path_buf()) .build(SessionConfig::new()) .await @@ -37,7 +37,7 @@ async fn basic_squeeze() { async fn squeeze_strings() { let cache_dir = TempDir::new().unwrap(); let (ctx, cache) = LiquidCacheLocalBuilder::new() - .with_max_cache_bytes(1024 * 1024) + .with_max_memory_bytes(1024 * 1024) .with_cache_dir(cache_dir.path().to_path_buf()) .build(SessionConfig::new()) .await @@ -47,7 +47,7 @@ async fn squeeze_strings() { .unwrap(); let plan = ctx - .sql("SELECT COUNT(DISTINCT(\"URL\")) FROM hits WHERE \"Referer\" <> 0") + .sql("SELECT COUNT(DISTINCT(\"URL\")) FROM hits WHERE \"Referer\" <> '0'") .await .unwrap(); let result = plan.collect().await.unwrap(); @@ -64,7 +64,7 @@ async fn squeeze_strings() { async fn squeeze_substrings_search() { let cache_dir = TempDir::new().unwrap(); let (ctx, cache) = LiquidCacheLocalBuilder::new() - .with_max_cache_bytes(1024 * 256) + .with_max_memory_bytes(1024 * 256) .with_cache_dir(cache_dir.path().to_path_buf()) .build(SessionConfig::new()) .await @@ -88,7 +88,7 @@ async fn squeeze_substrings_search() { async fn squeeze_substrings_search_title() { let cache_dir = TempDir::new().unwrap(); let (ctx, cache) = LiquidCacheLocalBuilder::new() - .with_max_cache_bytes(1024 * 1024 * 4) + .with_max_memory_bytes(1024 * 1024 * 4) .with_cache_dir(cache_dir.path().to_path_buf()) .build(SessionConfig::new()) .await @@ -113,7 +113,7 @@ async fn squeeze_substrings_search_title() { async fn squeeze_distinct_search_phase() { let cache_dir = TempDir::new().unwrap(); let (ctx, cache) = LiquidCacheLocalBuilder::new() - .with_max_cache_bytes(1024 * 256) + .with_max_memory_bytes(1024 * 256) .with_cache_dir(cache_dir.path().to_path_buf()) .build(SessionConfig::new()) .await diff --git a/src/datafusion-local/src/tests/variants.rs b/src/datafusion-local/src/tests/variants.rs index e74b0c03..aa550ab5 100644 --- a/src/datafusion-local/src/tests/variants.rs +++ b/src/datafusion-local/src/tests/variants.rs @@ -170,7 +170,7 @@ async fn test_variant_transcoding_falls_back_to_disk_arrow() { let (ctx, cache) = LiquidCacheLocalBuilder::new() .with_batch_size(1) - .with_max_cache_bytes(64) + .with_max_memory_bytes(64) .with_cache_dir(cache_dir.path().to_path_buf()) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) .build(SessionConfig::new()) @@ -376,7 +376,7 @@ async fn test_large_variant_squeeze() { let (ctx, _cache) = LiquidCacheLocalBuilder::new() .with_cache_dir(cache_dir.path().to_path_buf()) - .with_max_cache_bytes(1024) + .with_max_memory_bytes(1024) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) .build(SessionConfig::new()) .await @@ -416,7 +416,7 @@ async fn variant_multi_queries() { let (ctx, _cache) = LiquidCacheLocalBuilder::new() .with_cache_dir(cache_dir.path().to_path_buf()) - .with_max_cache_bytes(1024) + .with_max_memory_bytes(1024) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) .build(SessionConfig::new()) .await @@ -465,7 +465,7 @@ async fn variant_multi_queries_complex() { let (ctx, cache) = LiquidCacheLocalBuilder::new() .with_cache_dir(cache_dir.path().to_path_buf()) - .with_max_cache_bytes(1024 * 600) + .with_max_memory_bytes(1024 * 600) .with_batch_size(8) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) .build(SessionConfig::new()) diff --git a/src/datafusion-server/Cargo.toml b/src/datafusion-server/Cargo.toml index a0101d22..192d1563 100644 --- a/src/datafusion-server/Cargo.toml +++ b/src/datafusion-server/Cargo.toml @@ -23,9 +23,9 @@ liquid-cache-datafusion = { workspace = true } object_store = { workspace = true, features = ["aws", "http"] } liquid-cache-common = { workspace = true } tempfile = { workspace = true } -axum = "0.8.8" +axum = "0.8.9" serde = { workspace = true } -tower-http = { version = "0.6.8", features = ["cors"] } +tower-http = { version = "0.6.11", features = ["cors"] } sysinfo = { version = "0.38.4", default-features = false, features = [ "component", "disk", diff --git a/src/datafusion-server/src/admin_server/handlers.rs b/src/datafusion-server/src/admin_server/handlers.rs index 87e4e12e..8fcdf9c5 100644 --- a/src/datafusion-server/src/admin_server/handlers.rs +++ b/src/datafusion-server/src/admin_server/handlers.rs @@ -121,7 +121,7 @@ pub(crate) async fn get_parquet_cache_usage_handler( #[derive(Serialize)] pub(crate) struct CacheInfo { batch_size: usize, - max_cache_bytes: u64, + max_memory_bytes: u64, memory_usage_bytes: u64, disk_usage_bytes: u64, } @@ -130,12 +130,12 @@ pub(crate) async fn get_cache_info_handler(State(state): State>) - info!("Getting cache info..."); let cache = state.liquid_cache.cache(); let batch_size = cache.batch_size(); - let max_cache_bytes = cache.max_cache_bytes() as u64; + let max_memory_bytes = cache.max_memory_bytes() as u64; let memory_usage_bytes = cache.memory_usage_bytes() as u64; let disk_usage_bytes = cache.disk_usage_bytes() as u64; Json(CacheInfo { batch_size, - max_cache_bytes, + max_memory_bytes, memory_usage_bytes, disk_usage_bytes, }) @@ -403,17 +403,15 @@ impl From<&Arc> for ExecutionPlanWithStats { fn get_liquid_exec_info(plan: &Arc) -> Option { let mut rv = None; plan.apply(|node| { - let Some(data_source) = node.as_any().downcast_ref::() else { + let Some(data_source) = node.downcast_ref::() else { return Ok(TreeNodeRecursion::Continue); }; let file_scan_config = data_source .data_source() - .as_any() .downcast_ref::() .expect("FileScanConfig not found"); let Some(liquid_source) = file_scan_config .file_source() - .as_any() .downcast_ref::() else { return Ok(TreeNodeRecursion::Continue); diff --git a/src/datafusion-server/src/errors.rs b/src/datafusion-server/src/errors.rs index 37da134b..a8e00c79 100644 --- a/src/datafusion-server/src/errors.rs +++ b/src/datafusion-server/src/errors.rs @@ -26,8 +26,27 @@ where /// Convert anyhow::Error to tonic Status with detailed error information including stack trace pub fn anyhow_to_status(err: anyhow::Error) -> Status { - // Format the error with full error chain and backtrace for debugging - let error_with_context = format!("{err:?}"); + // Format the error with full error chain and backtrace for debugging. + let full = format!("{err:?}"); + // The gRPC status message travels in an HTTP/2 trailer whose size is bounded + // by the peer's max header list size. A long message (e.g. a DataFusion + // error carrying a backtrace) overflows that limit and surfaces on the + // client as an opaque `h2 protocol error` instead of a usable error, so cap + // the message that goes on the wire. + const MAX_STATUS_MESSAGE_LEN: usize = 8 * 1024; + let error_with_context = if full.len() > MAX_STATUS_MESSAGE_LEN { + let end = (0..=MAX_STATUS_MESSAGE_LEN) + .rev() + .find(|&i| full.is_char_boundary(i)) + .unwrap_or(0); + format!( + "{}... (truncated, {} bytes total)", + &full[..end], + full.len() + ) + } else { + full + }; // Determine the appropriate gRPC status code based on error type if let Some(datafusion_err) = err.downcast_ref::() { diff --git a/src/datafusion-server/src/lib.rs b/src/datafusion-server/src/lib.rs index abba734b..4da84881 100644 --- a/src/datafusion-server/src/lib.rs +++ b/src/datafusion-server/src/lib.rs @@ -35,8 +35,9 @@ use datafusion::{ use datafusion_proto::bytes::physical_plan_from_bytes; use fastrace::prelude::SpanContext; use futures::{Stream, TryStreamExt}; +use liquid_cache::cache::CacheExpression; use liquid_cache_common::rpc::{FetchResults, LiquidCacheActions}; -use liquid_cache_datafusion::cache::LiquidCacheParquetRef; +use liquid_cache_datafusion::cache::{ColumnSqueezeHints, LiquidCacheParquetRef}; use log::info; use prost::bytes::Bytes; use service::LiquidCacheServiceInner; @@ -124,11 +125,11 @@ impl LiquidCacheService { /// # Arguments /// /// * `ctx` - The [SessionContext] to use - /// * `max_cache_bytes` - The maximum number of bytes to cache in memory + /// * `max_memory_bytes` - The maximum number of bytes to cache in memory /// * `disk_cache_dir` - The directory to store the disk cache pub async fn new( ctx: SessionContext, - max_cache_bytes: Option, + max_memory_bytes: Option, disk_cache_dir: Option, cache_policy: Box, squeeze_policy: Box, @@ -145,7 +146,7 @@ impl LiquidCacheService { Ok(Self { inner: LiquidCacheServiceInner::new( Arc::new(ctx), - max_cache_bytes, + max_memory_bytes, disk_cache_dir, cache_policy, squeeze_policy, @@ -249,10 +250,15 @@ impl LiquidCacheService { Ok(Response::new(Box::pin(output))) } LiquidCacheActions::RegisterPlan(cmd) => { - let plan = cmd.plan; - let plan = physical_plan_from_bytes(&plan, &self.inner.get_ctx().task_ctx())?; + let plan = physical_plan_from_bytes(&cmd.plan, &self.inner.get_ctx().task_ctx())?; let handle = Uuid::from_bytes_ref(cmd.handle.as_ref().try_into()?); - self.inner.register_plan(*handle, plan); + let mut squeeze_hints = ColumnSqueezeHints::default(); + for hint in &cmd.squeeze_hints { + if let Some(expr) = CacheExpression::from_metadata_value(&hint.hint) { + squeeze_hints.insert(hint.column.clone(), Arc::new(expr)); + } + } + self.inner.register_plan(*handle, plan, squeeze_hints); let output = futures::stream::iter(vec![Ok(arrow_flight::Result { body: Bytes::default(), })]); diff --git a/src/datafusion-server/src/service.rs b/src/datafusion-server/src/service.rs index 1d9cbe46..06015fa3 100644 --- a/src/datafusion-server/src/service.rs +++ b/src/datafusion-server/src/service.rs @@ -9,9 +9,9 @@ use liquid_cache::{ByteCache, cache::squeeze_policies::SqueezePolicy}; use liquid_cache::{cache::HydrationPolicy, cache_policies::CachePolicy}; use liquid_cache_common::rpc::ExecutionMetricsResponse; use liquid_cache_datafusion::{ - cache::{LiquidCacheParquet, LiquidCacheParquetRef}, + cache::{ColumnSqueezeHints, LiquidCacheParquet, LiquidCacheParquetRef}, extract_execution_metrics, - optimizers::rewrite_data_source_plan, + optimizers::{SqueezeHintMap, rewrite_data_source_plan_with_hints}, }; use log::{debug, info}; use object_store::ObjectStore; @@ -46,7 +46,7 @@ pub(crate) struct LiquidCacheServiceInner { impl LiquidCacheServiceInner { pub async fn new( default_ctx: Arc, - max_cache_bytes: Option, + max_memory_bytes: Option, disk_cache_dir: PathBuf, cache_policy: Box, squeeze_policy: Box, @@ -64,7 +64,8 @@ impl LiquidCacheServiceInner { let liquid_cache = Arc::new( LiquidCacheParquet::new( batch_size, - max_cache_bytes.unwrap_or(usize::MAX), + max_memory_bytes.unwrap_or(usize::MAX), + usize::MAX, store, cache_policy, squeeze_policy, @@ -146,11 +147,22 @@ impl LiquidCacheServiceInner { } } - pub(crate) fn register_plan(&self, handle: Uuid, plan: Arc) { + pub(crate) fn register_plan( + &self, + handle: Uuid, + plan: Arc, + squeeze_hints: ColumnSqueezeHints, + ) { let cache = self.cache(); + // Hints the server can derive from the fragment itself (e.g. a + // `date_part` inside a pushed-down partial aggregate). The client ships + // hints for lineage that only exists in the client-side part of the + // plan; those take precedence on conflict. + let mut hints = SqueezeHintMap::analyze(&plan).for_fragment(&plan); + hints.extend(squeeze_hints); self.execution_plans.write().unwrap().insert( handle, - ExecutionPlanEntry::new(rewrite_data_source_plan(plan, cache, true)), + ExecutionPlanEntry::new(rewrite_data_source_plan_with_hints(plan, cache, &hints)), ); } diff --git a/src/datafusion-server/src/tests/mod.rs b/src/datafusion-server/src/tests/mod.rs index 55bc62ad..56210b51 100644 --- a/src/datafusion-server/src/tests/mod.rs +++ b/src/datafusion-server/src/tests/mod.rs @@ -50,7 +50,7 @@ async fn run_sql( let handle = Uuid::new_v4(); let ctx = service.get_ctx(); let plan = get_physical_plan(sql, ctx).await; - service.register_plan(handle, plan); + service.register_plan(handle, plan, Default::default()); let plan = service.get_plan(&handle).unwrap(); let batches = collect(plan.plan, ctx.task_ctx()).await.unwrap(); pretty_format_batches(&batches).unwrap().to_string() diff --git a/src/datafusion/Cargo.toml b/src/datafusion/Cargo.toml index 9a927edb..0dfd8d59 100644 --- a/src/datafusion/Cargo.toml +++ b/src/datafusion/Cargo.toml @@ -21,10 +21,7 @@ object_store = { workspace = true, features = ["http"] } liquid-cache-common = { workspace = true } liquid-cache = { workspace = true } fastrace = { workspace = true } -async-trait = { workspace = true } parquet-variant-json = { workspace = true } -parquet-variant-compute = { workspace = true } -serde = { workspace = true } serde_json = { workspace = true } t4 = { workspace = true } @@ -32,8 +29,8 @@ t4 = { workspace = true } [dev-dependencies] tempfile = "3.27.0" divan = "0.1" -rand = "0.10.0" -shuttle = "0.8.1" +rand = "0.10.1" +shuttle = "0.9.1" tokio-test = "0.4" serde_json = { workspace = true } diff --git a/src/datafusion/README.md b/src/datafusion/README.md index c8c56184..028c1610 100644 --- a/src/datafusion/README.md +++ b/src/datafusion/README.md @@ -1,3 +1,38 @@ # liquid-cache-datafusion Parquet reader with liquid array caching and optimized data formats. + +## Squeeze-hint (lineage) pushdown + +A *squeeze hint* tells the cache how a column is used, so under memory pressure it +keeps only what the query needs instead of the whole column — e.g. just the `YEAR` +of a date read via `EXTRACT(YEAR FROM d)`, the paths of a `variant_get`, or a +substring fingerprint for `LIKE '%foo%'`. + +A physical optimizer rule drives the whole flow: + +```text + physical plan + | + v + LocalModeOptimizer (physical optimizer rule) + | 1. analyze the plan -> one CacheExpression per scan column + | 2. find the parquet scan (ParquetSource) + | 3. replace it with a LiquidParquetSource carrying the hints + v + LiquidParquetSource + | on open, passes the hints down as squeeze hints + v + liquid cache + | squeezes under memory pressure + v + keeps only the hinted form (e.g. YEAR); full data stays on disk +``` + +The analysis is conservative: a column used in a way the analyzer doesn't model +gets no hint, so the cache never drops data a query still needs. + +**Flight mode** is the same flow split across the wire: the server only sees the +pushed-down fragment (which may lack the lineage), so the client derives the hints +from the full plan and ships them with the plan, and the server attaches them to +the `LiquidParquetSource` it builds. diff --git a/src/datafusion/bench/filter_pushdown.rs b/src/datafusion/bench/filter_pushdown.rs index 7488865a..897d8e2e 100644 --- a/src/datafusion/bench/filter_pushdown.rs +++ b/src/datafusion/bench/filter_pushdown.rs @@ -43,7 +43,8 @@ fn setup_cache() -> (Arc, tempfile::TempDir) { let store = tokio_test::block_on(t4::mount(&store_path)).expect("failed to mount t4 store"); let cache = tokio_test::block_on(LiquidCacheParquet::new( BATCH_SIZE, - 1024 * 1024 * 1024, // max_cache_bytes (1GB) + 1024 * 1024 * 1024, // max_memory_bytes (1GB) + usize::MAX, store, Box::new(LiquidPolicy::new()), Box::new(TranscodeSqueezeEvict), diff --git a/src/datafusion/src/cache/column.rs b/src/datafusion/src/cache/column.rs index a4a1f71d..1f582fd8 100644 --- a/src/datafusion/src/cache/column.rs +++ b/src/datafusion/src/cache/column.rs @@ -1,22 +1,16 @@ use arrow::{ - array::{Array, ArrayRef, AsArray, BooleanArray}, + array::{Array, ArrayRef, BooleanArray}, buffer::BooleanBuffer, compute::prep_null_mask_filter, record_batch::RecordBatch, }; -use arrow_schema::{ArrowError, DataType, Field, Schema}; -use liquid_cache::cache::{CacheExpression, LiquidCache, LiquidExpr}; -use liquid_cache::utils::VariantSchema; -use liquid_cache::utils::typed_struct_contains_path; +use arrow_schema::{ArrowError, Field, Schema}; +use liquid_cache::cache::{CacheExpression, CacheFull, LiquidCache, LiquidExpr}; use parquet::arrow::arrow_reader::ArrowPredicate; -use parquet_variant_compute::{VariantArray, VariantType, shred_variant, unshred_variant}; use crate::{ LiquidPredicate, cache::{BatchID, ColumnAccessPath, ParquetArrayID}, - optimizers::{ - DATE_MAPPING_METADATA_KEY, STRING_FINGERPRINT_METADATA_KEY, variant_mappings_from_field, - }, }; use std::sync::Arc; @@ -32,42 +26,19 @@ pub struct CachedColumn { /// A reference to a cached column. pub type CachedColumnRef = Arc; -fn infer_expression(field: &Field) -> Option { - if let Some(mapping) = field.metadata().get(DATE_MAPPING_METADATA_KEY) - && matches!( - field.data_type(), - DataType::Date32 | DataType::Timestamp(_, _) - ) - && let Some(expr) = CacheExpression::try_from_date_part_str(mapping) - { - return Some(expr); - } - if field - .metadata() - .contains_key(STRING_FINGERPRINT_METADATA_KEY) - && is_string_type(field.data_type()) - { - return Some(CacheExpression::substring_search()); - } - if field.try_extension_type::().is_ok() - && let Some(mappings) = variant_mappings_from_field(field) - { - let typed_specs: Vec<_> = mappings - .into_iter() - .filter_map(|mapping| mapping.data_type.map(|data_type| (mapping.path, data_type))) - .collect(); - if !typed_specs.is_empty() { - return Some(CacheExpression::variant_get_many(typed_specs)); - } - } - None -} - /// Error type for inserting an arrow array into the cache. #[derive(Debug)] pub enum InsertArrowArrayError { /// The array is already cached. AlreadyCached, + /// The cache does not have enough disk budget to accept the array. + CacheFull, +} + +impl From for InsertArrowArrayError { + fn from(_: CacheFull) -> Self { + Self::CacheFull + } } impl CachedColumn { @@ -75,16 +46,24 @@ impl CachedColumn { field: Arc, cache_store: Arc, column_access_path: ColumnAccessPath, + expression: Option>, is_predicate_column: bool, ) -> Self { - let expression = infer_expression(field.as_ref()).map(Arc::new); - if let Some(expr) = expression.as_ref() { + // Register the column's squeeze hint. Squeeze hints are column-scoped; + // `ParquetCacheMetadata` keys them by column (the batch id is masked + // off), so registering once on any batch covers every batch. + // + // The read-path `expression` is the typed lineage hint derived from the + // plan (date/variant/substring). A pure predicate column carries no such + // hint, but still registers `PredicateColumn` to guide squeezing — it is + // deliberately *not* stored as `expression`, since it does not change how + // the column is materialized on read. + let squeeze_hint = expression + .clone() + .or_else(|| is_predicate_column.then(|| Arc::new(CacheExpression::PredicateColumn))); + if let Some(hint) = squeeze_hint { let hint_entry_id = column_access_path.entry_id(BatchID::from_raw(0)).into(); - cache_store.add_squeeze_hint(&hint_entry_id, expr.clone()); - } else if is_predicate_column { - let hint_entry_id = column_access_path.entry_id(BatchID::from_raw(0)).into(); - cache_store - .add_squeeze_hint(&hint_entry_id, Arc::new(CacheExpression::PredicateColumn)); + cache_store.add_squeeze_hint(&hint_entry_id, hint); } Self { field, @@ -180,17 +159,12 @@ impl CachedColumn { filter: &BooleanBuffer, ) -> Option { let entry_id = self.entry_id(batch_id).into(); - let mut array = self - .cache_store + self.cache_store .get(&entry_id) .with_selection(filter) .with_optional_expression_hint(self.expression()) .read() - .await?; - if let Some(transformed) = maybe_shred_variant_array(&array, self.field.as_ref()) { - array = transformed; - } - Some(array) + .await } #[cfg(test)] @@ -209,164 +183,9 @@ impl CachedColumn { return Err(InsertArrowArrayError::AlreadyCached); } - let mut array = array; - if let Some(transformed) = maybe_shred_variant_array(&array, self.field.as_ref()) { - array = transformed; - } self.cache_store .insert(self.entry_id(batch_id).into(), array) - .await; + .await?; Ok(()) } } - -fn maybe_shred_variant_array(array: &ArrayRef, field: &Field) -> Option { - let mappings = variant_mappings_from_field(field)?; - let typed_specs: Vec<(String, DataType)> = mappings - .into_iter() - .filter_map(|mapping| mapping.data_type.map(|data_type| (mapping.path, data_type))) - .collect(); - if typed_specs.is_empty() { - return None; - } - shred_variant_array(array, field, &typed_specs) -} - -fn shred_variant_array( - array: &ArrayRef, - field: &Field, - specs: &[(String, DataType)], -) -> Option { - if specs.is_empty() { - return None; - } - - let variant_array = VariantArray::try_new(array.as_ref()).ok()?; - let missing_specs: Vec<_> = specs - .iter() - .filter(|(path, _)| !variant_contains_typed_field(&variant_array, path)) - .collect(); - if missing_specs.is_empty() { - return None; - } - - let target_fields = match field.data_type() { - DataType::Struct(fields) => fields.clone(), - _ => return None, - }; - let typed_schema = target_fields - .iter() - .find(|child| child.name() == "typed_value") - .cloned()?; - let mut schema = VariantSchema::new(Some(typed_schema.as_ref())); - for (path, data_type) in missing_specs { - schema.insert_path(path, data_type); - } - let shredding_schema = schema.shredding_type()?; - let unshredded = unshred_variant(&variant_array).ok()?; - let shredded = shred_variant(&unshredded, &shredding_schema).ok()?; - Some(Arc::new(shredded.into_inner())) -} - -fn variant_contains_typed_field(array: &VariantArray, path: &str) -> bool { - let Some(typed_field) = array.typed_value_field() else { - return false; - }; - let Some(typed_root) = typed_field.as_struct_opt() else { - return false; - }; - typed_struct_contains_path(typed_root, path) -} - -fn is_string_type(data_type: &DataType) -> bool { - match data_type { - DataType::Utf8 | DataType::Utf8View | DataType::LargeUtf8 => true, - DataType::Dictionary(_, value_type) => is_string_type(value_type.as_ref()), - _ => false, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::optimizers::{ - VARIANT_MAPPING_METADATA_KEY, VariantField, enrich_variant_field_type, - }; - use arrow::array::{ArrayRef, StringArray, StructArray}; - use parquet::variant::{VariantType, json_to_variant}; - use serde_json::json; - use std::collections::HashMap; - - #[test] - fn shredding_adds_all_variant_paths() { - let values = StringArray::from(vec![ - Some(r#"{"name":"Alice","age":30}"#), - Some(r#"{"name":"Bob","age":27}"#), - ]); - let variant = json_to_variant(&(Arc::new(values) as ArrayRef)).expect("variant"); - - let mut metadata = HashMap::new(); - metadata.insert( - VARIANT_MAPPING_METADATA_KEY.to_string(), - serde_json::to_string(&vec![ - json!({"path": "name", "type": "Utf8"}), - json!({"path": "age", "type": "Int64"}), - ]) - .unwrap(), - ); - - let variant_fields = vec![ - VariantField { - path: "name".to_string(), - data_type: Some(DataType::Utf8), - }, - VariantField { - path: "age".to_string(), - data_type: Some(DataType::Int64), - }, - ]; - - let base_field = Field::new("variant", variant.inner().data_type().clone(), true) - .with_extension_type(VariantType) - .with_metadata(metadata); - let enriched = enrich_variant_field_type(base_field.as_ref(), &variant_fields) - .with_metadata(base_field.metadata().clone()); - let array: ArrayRef = ArrayRef::from(variant); - - let shredded = maybe_shred_variant_array(&array, enriched.as_ref()) - .expect("variant should be shredded"); - let shredded_struct = shredded - .as_any() - .downcast_ref::() - .expect("struct array"); - let typed_value = shredded_struct - .column_by_name("typed_value") - .expect("typed_value column"); - let typed_struct = typed_value - .as_any() - .downcast_ref::() - .expect("typed struct"); - - let name_struct = typed_struct - .column_by_name("name") - .expect("name path") - .as_any() - .downcast_ref::() - .expect("name struct"); - let name_values = name_struct - .column_by_name("typed_value") - .expect("name typed value"); - assert_eq!(name_values.data_type(), &DataType::Utf8); - - let age_struct = typed_struct - .column_by_name("age") - .expect("age path") - .as_any() - .downcast_ref::() - .expect("age struct"); - let age_values = age_struct - .column_by_name("typed_value") - .expect("age typed value"); - assert_eq!(age_values.data_type(), &DataType::Int64); - } -} diff --git a/src/datafusion/src/cache/mod.rs b/src/datafusion/src/cache/mod.rs index b9ad5658..7c2fda5b 100644 --- a/src/datafusion/src/cache/mod.rs +++ b/src/datafusion/src/cache/mod.rs @@ -1,7 +1,7 @@ //! This module contains the cache implementation for the Parquet reader. //! -use crate::io::ParquetIoContext; +use crate::io::ParquetCacheMetadata; use crate::reader::{LiquidPredicate, extract_multi_column_or}; use crate::sync::Mutex; use ahash::AHashMap; @@ -10,9 +10,10 @@ use arrow::buffer::BooleanBuffer; use arrow_schema::{ArrowError, Field, Schema, SchemaRef}; use liquid_cache::cache::squeeze_policies::SqueezePolicy; use liquid_cache::cache::{ - CachePolicy, EventTrace, HydrationPolicy, LiquidCache, LiquidCacheBuilder, + CacheExpression, CachePolicy, EventTrace, HydrationPolicy, LiquidCache, LiquidCacheBuilder, }; use parquet::arrow::arrow_reader::ArrowPredicate; +use std::collections::HashMap; use std::path::Path; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; @@ -26,6 +27,16 @@ pub use column::{CachedColumn, CachedColumnRef}; pub(crate) use id::ColumnAccessPath; pub use id::{BatchID, ParquetArrayID}; +/// Typed squeeze hints for a single file, keyed by file-schema column name. +/// +/// Produced by the physical squeeze-hint analyzer (local mode) or shipped from +/// the client (Flight mode), and attached to the +/// [`LiquidParquetSource`](crate::LiquidParquetSource) that opens the file. +pub type ColumnSqueezeHints = HashMap>; + +/// One column of a row group: (file column index, field, squeeze hint, is-predicate). +type CachedColumnSpec = (u64, Arc, Option>, bool); + #[derive(Default, Debug)] struct ColumnMaps { // invariant: Arc::ptr_eq(map[field.name()], map[field.id()]) @@ -48,15 +59,16 @@ impl CachedRowGroup { cache_store: Arc, row_group_idx: u64, file_idx: u64, - columns: &[(u64, Arc, bool)], + columns: &[CachedColumnSpec], ) -> Self { let mut column_maps = ColumnMaps::default(); - for (column_id, field, is_predicate_column) in columns { + for (column_id, field, expression, is_predicate_column) in columns { let column_access_path = ColumnAccessPath::new(file_idx, row_group_idx, *column_id); let column = Arc::new(CachedColumn::new( Arc::clone(field), Arc::clone(&cache_store), column_access_path, + expression.clone(), *is_predicate_column, )); column_maps.by_id.insert(*column_id, column.clone()); @@ -175,14 +187,21 @@ pub struct CachedFile { cache_store: Arc, file_id: u64, file_schema: SchemaRef, + squeeze_hints: Arc, } impl CachedFile { - fn new(cache_store: Arc, file_id: u64, file_schema: SchemaRef) -> Self { + fn new( + cache_store: Arc, + file_id: u64, + file_schema: SchemaRef, + squeeze_hints: Arc, + ) -> Self { Self { cache_store, file_id, file_schema, + squeeze_hints, } } @@ -192,14 +211,20 @@ impl CachedFile { row_group_id: u64, predicate_column_ids: Vec, ) -> CachedRowGroupRef { - let columns: Vec<(u64, Arc, bool)> = self + let columns: Vec = self .file_schema .fields() .iter() .enumerate() .map(|(idx, field)| { let is_predicate_column = predicate_column_ids.contains(&idx); - (idx as u64, Arc::clone(field), is_predicate_column) + let expression = self.squeeze_hints.get(field.name()).cloned(); + ( + idx as u64, + Arc::clone(field), + expression, + is_predicate_column, + ) }) .collect(); @@ -243,21 +268,51 @@ impl LiquidCacheParquet { /// Create a new cache for parquet files. pub async fn new( batch_size: usize, - max_cache_bytes: usize, + max_memory_bytes: usize, + max_disk_bytes: usize, store: t4::Store, cache_policy: Box, squeeze_policy: Box, hydration_policy: Box, + ) -> Self { + Self::new_with_squeeze_victim_concurrency( + batch_size, + max_memory_bytes, + max_disk_bytes, + store, + cache_policy, + squeeze_policy, + hydration_policy, + !cfg!(test), + ) + .await + } + + /// Create a new cache for parquet files with explicit victim squeeze concurrency. + #[doc(hidden)] + #[allow(clippy::too_many_arguments)] + pub async fn new_with_squeeze_victim_concurrency( + batch_size: usize, + max_memory_bytes: usize, + max_disk_bytes: usize, + store: t4::Store, + cache_policy: Box, + squeeze_policy: Box, + hydration_policy: Box, + squeeze_victims_concurrently: bool, ) -> Self { assert!(batch_size.is_power_of_two()); - let io_context = Arc::new(ParquetIoContext::new(store)); + let metadata = Arc::new(ParquetCacheMetadata::new()); let cache_storage = LiquidCacheBuilder::new() .with_batch_size(batch_size) - .with_max_cache_bytes(max_cache_bytes) + .with_max_memory_bytes(max_memory_bytes) + .with_max_disk_bytes(max_disk_bytes) .with_squeeze_policy(squeeze_policy) .with_cache_policy(cache_policy) .with_hydration_policy(hydration_policy) - .with_io_context(io_context) + .with_metadata(metadata) + .with_store(store) + .with_squeeze_victims_concurrently(squeeze_victims_concurrently) .build() .await; @@ -273,6 +328,17 @@ impl LiquidCacheParquet { &self, file_path: String, full_file_schema: SchemaRef, + ) -> CachedFileRef { + self.register_or_get_file_with_hints(file_path, full_file_schema, Arc::default()) + } + + /// Register a file in the cache, attaching typed squeeze hints derived from + /// the query plan (keyed by file-schema column name). + pub fn register_or_get_file_with_hints( + &self, + file_path: String, + full_file_schema: SchemaRef, + squeeze_hints: Arc, ) -> CachedFileRef { let mut files = self.files.lock().unwrap(); let file_id = *files @@ -284,6 +350,7 @@ impl LiquidCacheParquet { self.cache_store.clone(), file_id, full_file_schema, + squeeze_hints, )) } @@ -292,9 +359,14 @@ impl LiquidCacheParquet { self.cache_store.config().batch_size() } - /// Get the max cache bytes of the cache. - pub fn max_cache_bytes(&self) -> usize { - self.cache_store.config().max_cache_bytes() + /// Get the max memory bytes of the cache. + pub fn max_memory_bytes(&self) -> usize { + self.cache_store.config().max_memory_bytes() + } + + /// Get the max disk bytes of the cache. + pub fn max_disk_bytes(&self) -> usize { + self.cache_store.config().max_disk_bytes() } /// Get the memory usage of the cache in bytes. @@ -340,8 +412,8 @@ impl LiquidCacheParquet { /// This is for admin use only. /// This has no guarantees that some new entry will not be inserted in the meantime, or some entries are promoted to memory again. /// You mostly want to use this when no one else is using the cache. - pub async fn flush_data(&self) { - self.cache_store.flush_all_to_disk().await; + pub async fn flush_data(&self) -> Result<(), liquid_cache::cache::CacheFull> { + self.cache_store.flush_all_to_disk().await } /// Get the storage of the cache. @@ -384,6 +456,7 @@ mod tests { let cache = LiquidCacheParquet::new( batch_size, usize::MAX, + usize::MAX, store, Box::new(LiquidPolicy::new()), Box::new(TranscodeSqueezeEvict), diff --git a/src/datafusion/src/cache/stats.rs b/src/datafusion/src/cache/stats.rs index ef5603fe..9527f08d 100644 --- a/src/datafusion/src/cache/stats.rs +++ b/src/datafusion/src/cache/stats.rs @@ -129,15 +129,15 @@ impl LiquidCacheParquet { CacheEntry::MemoryArrow(array) => Some(array.len() as u64), CacheEntry::MemoryLiquid(array) => Some(array.len() as u64), CacheEntry::MemorySqueezedLiquid(array) => Some(array.len() as u64), - CacheEntry::DiskLiquid(_) => None, - CacheEntry::DiskArrow(_) => None, // We'd need to read it to get the count + CacheEntry::DiskLiquid { .. } => None, + CacheEntry::DiskArrow { .. } => None, // We'd need to read it to get the count }; let cache_type = match cached_batch { CacheEntry::MemoryArrow(_) => "InMemory", CacheEntry::MemoryLiquid(_) => "LiquidMemory", CacheEntry::MemorySqueezedLiquid(_) => "LiquidSqueezed", - CacheEntry::DiskLiquid(_) => "OnDiskLiquid", - CacheEntry::DiskArrow(_) => "OnDiskArrow", + CacheEntry::DiskLiquid { .. } => "OnDiskLiquid", + CacheEntry::DiskArrow { .. } => "OnDiskArrow", }; let reference_count = cached_batch.reference_count(); let entry_id = ParquetArrayID::from(*entry_id); @@ -188,6 +188,7 @@ mod tests { let cache = LiquidCacheParquet::new( 1024, usize::MAX, + usize::MAX, store, Box::new(LiquidPolicy::new()), Box::new(Evict), diff --git a/src/datafusion/src/io/mod.rs b/src/datafusion/src/io/mod.rs index 667a84c0..0982379f 100644 --- a/src/datafusion/src/io/mod.rs +++ b/src/datafusion/src/io/mod.rs @@ -1,34 +1,22 @@ use std::{ collections::VecDeque, - ops::Range, sync::{Arc, RwLock}, }; use ahash::AHashMap; -use bytes::Bytes; -use liquid_cache::cache::{CacheExpression, EntryID, IoContext, LiquidCompressorStates}; +use liquid_cache::cache::{CacheExpression, EntryID, EntryMetadata, LiquidCompressorStates}; use crate::cache::{ColumnAccessPath, ParquetArrayID}; -/// Convert an [`EntryID`] to a t4 key (8-byte little-endian representation). -fn entry_id_to_key(entry_id: &EntryID) -> Vec { - usize::from(*entry_id).to_le_bytes().to_vec() -} - -#[derive(Debug)] -pub(crate) struct ParquetIoContext { +#[derive(Debug, Default)] +pub(crate) struct ParquetCacheMetadata { compressor_states: RwLock>>, expression_hints: RwLock>, - store: t4::Store, } -impl ParquetIoContext { - pub fn new(store: t4::Store) -> Self { - Self { - compressor_states: RwLock::new(AHashMap::new()), - expression_hints: RwLock::new(AHashMap::new()), - store, - } +impl ParquetCacheMetadata { + pub fn new() -> Self { + Self::default() } } @@ -67,8 +55,7 @@ impl ColumnExpressionTracker { } } -#[async_trait::async_trait] -impl IoContext for ParquetIoContext { +impl EntryMetadata for ParquetCacheMetadata { fn add_squeeze_hint(&self, entry_id: &EntryID, expression: Arc) { let column_path = ColumnAccessPath::from(ParquetArrayID::from(*entry_id)); let mut guard = self.expression_hints.write().unwrap(); @@ -92,46 +79,6 @@ impl IoContext for ParquetIoContext { .or_insert_with(|| Arc::new(LiquidCompressorStates::new())) .clone() } - - #[inline(never)] - #[fastrace::trace] - async fn read( - &self, - entry_id: &EntryID, - range: Option>, - ) -> Result { - let key = entry_id_to_key(entry_id); - match range { - Some(range) => { - let len = range.end - range.start; - let bytes = self - .store - .get_range(&key, range.start, len) - .await - .map_err(|e| std::io::Error::other(e.to_string()))?; - Ok(Bytes::from(bytes)) - } - None => { - let bytes = self - .store - .get(&key) - .await - .map_err(|e| std::io::Error::other(e.to_string()))?; - Ok(Bytes::from(bytes)) - } - } - } - - #[inline(never)] - #[fastrace::trace] - async fn write(&self, entry_id: &EntryID, data: Bytes) -> Result<(), std::io::Error> { - let key = entry_id_to_key(entry_id); - self.store - .put(key, data.to_vec()) - .await - .map_err(|e| std::io::Error::other(e.to_string()))?; - Ok(()) - } } #[cfg(test)] @@ -144,38 +91,36 @@ mod tests { EntryID::from(usize::from(id)) } - fn make_ctx() -> ParquetIoContext { - let tmp = tempfile::tempdir().unwrap(); - let store = tokio_test::block_on(t4::mount(tmp.path().join("liquid_cache.t4"))).unwrap(); - ParquetIoContext::new(store) + fn make_meta() -> ParquetCacheMetadata { + ParquetCacheMetadata::new() } #[test] fn squeeze_hint_tracks_majority() { - let ctx = make_ctx(); + let meta = make_meta(); let e = entry(1, 2, 3); let month = Arc::new(CacheExpression::extract_date32(Date32Field::Month)); let year = Arc::new(CacheExpression::extract_date32(Date32Field::Year)); - ctx.add_squeeze_hint(&e, month.clone()); - ctx.add_squeeze_hint(&e, month.clone()); - ctx.add_squeeze_hint(&e, year.clone()); + meta.add_squeeze_hint(&e, month.clone()); + meta.add_squeeze_hint(&e, month.clone()); + meta.add_squeeze_hint(&e, year.clone()); - let majority = ctx.squeeze_hint(&e).expect("hint"); + let majority = meta.squeeze_hint(&e).expect("hint"); assert_eq!(majority, month); } #[test] fn squeeze_hint_prefers_recent_on_tie() { - let ctx = make_ctx(); + let meta = make_meta(); let e = entry(9, 9, 9); let year = Arc::new(CacheExpression::extract_date32(Date32Field::Year)); let day = Arc::new(CacheExpression::extract_date32(Date32Field::Day)); - ctx.add_squeeze_hint(&e, year.clone()); - ctx.add_squeeze_hint(&e, day.clone()); + meta.add_squeeze_hint(&e, year.clone()); + meta.add_squeeze_hint(&e, day.clone()); - let majority = ctx.squeeze_hint(&e).expect("hint"); + let majority = meta.squeeze_hint(&e).expect("hint"); assert_eq!(majority, day); } } diff --git a/src/datafusion/src/lib.rs b/src/datafusion/src/lib.rs index 8db55316..2c631f69 100644 --- a/src/datafusion/src/lib.rs +++ b/src/datafusion/src/lib.rs @@ -6,7 +6,6 @@ pub mod optimizers; mod reader; mod sync; pub(crate) mod utils; -pub use liquid_cache::utils::VariantSchema; pub mod cache; pub use cache::{LiquidCacheParquet, LiquidCacheParquetRef}; diff --git a/src/datafusion/src/optimizers/lineage_opt.rs b/src/datafusion/src/optimizers/lineage_opt.rs deleted file mode 100644 index 58ff79c0..00000000 --- a/src/datafusion/src/optimizers/lineage_opt.rs +++ /dev/null @@ -1,1871 +0,0 @@ -//! This module has a logical optimizer that detects columns that are only used via compatible `EXTRACT` projections. -//! It then attaches the metadata to expression adapter factory, which is then passed to the physical plan. -//! The physical optimizer will move the metadata to the fields of the schema. - -use std::cmp::Ordering; -use std::collections::hash_map::Entry; -use std::collections::{HashMap, HashSet}; -use std::str::FromStr; -use std::sync::{Arc, Mutex, OnceLock}; - -use arrow::compute::kernels::cast_utils::IntervalUnit; -use arrow_schema::{DataType, Schema, SchemaRef}; -use datafusion::common::tree_node::{Transformed, TreeNode, TreeNodeRecursion}; -use datafusion::common::{ - Column, Constraints, DFSchema, DataFusionError, ExprSchema, Result, ScalarValue, TableReference, -}; -use datafusion::datasource::listing::{ListingTable, ListingTableConfig}; -use datafusion::datasource::{TableProvider, provider_as_source, source_as_provider}; -use datafusion::logical_expr::logical_plan::{ - Aggregate, Distinct, DistinctOn, Filter, Join, Limit, LogicalPlan, Partitioning, Projection, - Repartition, Sort, SubqueryAlias, TableScan, Union, Window, -}; -use datafusion::logical_expr::{Expr, TableSource}; -use datafusion::optimizer::{ApplyOrder, OptimizerConfig, OptimizerRule}; -use datafusion::physical_expr_adapter::{ - DefaultPhysicalExprAdapterFactory, PhysicalExprAdapter, PhysicalExprAdapterFactory, -}; - -/// Supported components for `EXTRACT` clauses. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub(crate) enum SupportedIntervalUnit { - Year, - Month, - Day, - DayOfWeek, -} - -impl SupportedIntervalUnit { - pub(crate) fn metadata_value(self) -> &'static str { - match self { - SupportedIntervalUnit::Year => "YEAR", - SupportedIntervalUnit::Month => "MONTH", - SupportedIntervalUnit::Day => "DAY", - SupportedIntervalUnit::DayOfWeek => "DOW", - } - } -} - -/// Metadata describing a Date32/Timestamp column that participates in an `EXTRACT`. -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct DateExtraction { - pub(crate) column: Column, - pub(crate) components: HashSet, -} - -/// Metadata describing a Variant column that participates in a `variant_get`. -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct VariantExtraction { - pub(crate) column: Column, - pub(crate) fields: Vec, -} - -impl PartialOrd for VariantExtraction { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for VariantExtraction { - fn cmp(&self, other: &Self) -> Ordering { - self.column - .flat_name() - .cmp(&other.column.flat_name()) - .then_with(|| self.fields.cmp(&other.fields)) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct VariantField { - pub(crate) path: String, - pub(crate) data_type: Option, -} - -impl PartialOrd for VariantField { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for VariantField { - fn cmp(&self, other: &Self) -> Ordering { - self.path.cmp(&other.path).then_with(|| { - let self_ty = self - .data_type - .as_ref() - .map(|dt| dt.to_string()) - .unwrap_or_default(); - let other_ty = other - .data_type - .as_ref() - .map(|dt| dt.to_string()) - .unwrap_or_default(); - self_ty.cmp(&other_ty) - }) - } -} - -/// Annotation that should be attached to a column in the file schema. -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum ColumnAnnotation { - DatePart(HashSet), - VariantPaths(Vec), - SubstringSearch, -} - -pub(crate) fn serialize_date_part(units: &HashSet) -> String { - let mut sorted_units: Vec<&SupportedIntervalUnit> = units.iter().collect(); - // Sort by a consistent order: Year, Month, Day, DayOfWeek - sorted_units.sort_by_key(|unit| match unit { - SupportedIntervalUnit::Year => 0, - SupportedIntervalUnit::Month => 1, - SupportedIntervalUnit::Day => 2, - SupportedIntervalUnit::DayOfWeek => 3, - }); - sorted_units - .iter() - .map(|unit| unit.metadata_value()) - .collect::>() - .join(",") -} - -/// Logical optimizer that analyses the logical plan to detect columns that -/// are only used via compatible `EXTRACT` or `variant_get` projections. -#[derive(Debug, Default)] -pub struct LineageOptimizer; - -impl LineageOptimizer { - /// Create a new optimizer. - pub fn new() -> Self { - Self - } -} - -impl OptimizerRule for LineageOptimizer { - fn name(&self) -> &str { - "LineageOptimizer" - } - - fn apply_order(&self) -> Option { - // so that it won't recursively apply the rule to every node. - None - } - - fn rewrite( - &self, - plan: LogicalPlan, - _config: &dyn OptimizerConfig, - ) -> Result, DataFusionError> { - let mut analyzer = LineageAnalyzer::default(); - let _ = analyzer.analyze_plan(&plan)?; - let table_usage = analyzer.finish(); - let mut date_findings = table_usage.find_date32_extractions(); - date_findings.sort_by_key(|a| a.column.flat_name()); - - let mut variant_findings = table_usage.find_variant_gets(); - variant_findings.sort(); - - let mut substring_findings = table_usage.find_substring_searches(); - substring_findings.sort_by_key(|a| a.flat_name()); - - let annotations = - build_annotation_map(&date_findings, &variant_findings, &substring_findings); - annotate_plan_with_extractions(plan, &annotations) - } -} - -type LineageMap = HashMap>; - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -struct ColumnKey { - relation: Option, - name: String, -} - -impl ColumnKey { - fn new(relation: Option, name: impl Into) -> Self { - Self { - relation, - name: name.into(), - } - } - - fn from_column(column: &Column) -> Self { - Self { - relation: column.relation.clone(), - name: column.name.clone(), - } - } - - fn to_column(&self) -> Column { - Column::new(self.relation.clone(), self.name.clone()) - } -} - -#[derive(Debug, Clone)] -struct ColumnUsage { - base: ColumnKey, - data_type: DataType, - operations: Vec, -} - -impl ColumnUsage { - fn new_base(column: &Column, data_type: DataType) -> Self { - Self { - base: ColumnKey::from_column(column), - data_type, - operations: Vec::new(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum Operation { - Extract(SupportedIntervalUnit), - VariantGet { - path: String, - data_type: Option, - }, - SubstringSearch, - Other, -} - -#[derive(Debug)] -struct UsageStats { - data_type: DataType, - usages: Vec>, -} - -impl UsageStats { - fn new(data_type: DataType) -> Self { - Self { - data_type, - usages: Vec::new(), - } - } - - fn apply(&mut self, usage: &ColumnUsage) { - self.usages.push(usage.operations.clone()); - } -} - -#[derive(Default)] -struct LineageAnalyzer { - stats: HashMap, -} - -impl LineageAnalyzer { - fn analyze_plan(&mut self, plan: &LogicalPlan) -> Result { - match plan { - LogicalPlan::TableScan(scan) => self.analyze_table_scan(scan), - LogicalPlan::Projection(projection) => self.analyze_projection(projection), - LogicalPlan::Filter(filter) => self.analyze_filter(filter), - LogicalPlan::Aggregate(aggregate) => self.analyze_aggregate(aggregate), - LogicalPlan::Sort(sort) => self.analyze_sort(sort), - LogicalPlan::Join(join) => self.analyze_join(join), - LogicalPlan::SubqueryAlias(alias) => self.analyze_subquery_alias(alias), - LogicalPlan::Window(window) => self.analyze_window(window), - LogicalPlan::Limit(limit) => self.analyze_limit(limit), - LogicalPlan::Repartition(repartition) => self.analyze_repartition(repartition), - LogicalPlan::Union(union) => self.analyze_union(union), - LogicalPlan::Distinct(distinct) => self.analyze_distinct(distinct), - other => { - let mut merged = LineageMap::new(); - for input in other.inputs() { - let child = self.analyze_plan(input)?; - merged = merge_lineage_maps(merged, child); - } - Ok(merged) - } - } - } - - fn analyze_table_scan(&mut self, scan: &TableScan) -> Result { - let schema = scan.projected_schema.as_ref(); - let mut map = LineageMap::new(); - for (index, column) in schema.columns().iter().enumerate() { - let field = schema.field(index); - let usage = ColumnUsage::new_base(column, field.data_type().clone()); - map.insert(usage.base.clone(), vec![usage]); - } - - for filter in &scan.filters { - let usages = lineage_for_expr(filter, &map, schema)?; - self.record(&usages); - } - - Ok(map) - } - - fn analyze_projection(&mut self, projection: &Projection) -> Result { - let input_map = self.analyze_plan(projection.input.as_ref())?; - let input_schema = projection.input.schema(); - let mut output = LineageMap::new(); - for (expr, column) in projection.expr.iter().zip(projection.schema.columns()) { - let usages = lineage_for_expr(expr, &input_map, input_schema.as_ref())?; - self.record(&usages); - output.insert(ColumnKey::from_column(&column), usages); - } - Ok(output) - } - - fn analyze_filter(&mut self, filter: &Filter) -> Result { - let input_map = self.analyze_plan(filter.input.as_ref())?; - let schema = filter.input.schema(); - let usages = lineage_for_expr(&filter.predicate, &input_map, schema.as_ref())?; - self.record(&usages); - Ok(input_map) - } - - fn analyze_sort(&mut self, sort: &Sort) -> Result { - let input_map = self.analyze_plan(sort.input.as_ref())?; - let schema = sort.input.schema(); - for sort_expr in &sort.expr { - let usages = lineage_for_expr(&sort_expr.expr, &input_map, schema.as_ref())?; - self.record(&usages); - } - Ok(input_map) - } - - fn analyze_aggregate(&mut self, aggregate: &Aggregate) -> Result { - let input_map = self.analyze_plan(aggregate.input.as_ref())?; - let schema = aggregate.input.schema(); - let mut output = LineageMap::new(); - let mut expr_iter = aggregate - .group_expr - .iter() - .chain(aggregate.aggr_expr.iter()); - - for column in aggregate.schema.columns() { - if let Some(expr) = expr_iter.next() { - let usages = lineage_for_expr(expr, &input_map, schema.as_ref())?; - self.record(&usages); - output.insert(ColumnKey::from_column(&column), usages); - } else { - output.insert(ColumnKey::from_column(&column), Vec::new()); - } - } - - Ok(output) - } - - fn analyze_join(&mut self, join: &Join) -> Result { - let left_map = self.analyze_plan(join.left.as_ref())?; - let right_map = self.analyze_plan(join.right.as_ref())?; - let left_schema = join.left.schema(); - let right_schema = join.right.schema(); - - for (left_expr, right_expr) in &join.on { - let left_usages = lineage_for_expr(left_expr, &left_map, left_schema.as_ref())?; - self.record(&left_usages); - let right_usages = lineage_for_expr(right_expr, &right_map, right_schema.as_ref())?; - self.record(&right_usages); - } - - if let Some(filter) = &join.filter { - let mut combined = left_map.clone(); - merge_lineage_map_inplace(&mut combined, &right_map); - let usages = lineage_for_expr(filter, &combined, join.schema.as_ref())?; - self.record(&usages); - } - - let left_columns = left_schema.columns(); - let right_columns = right_schema.columns(); - let mut output_columns = join.schema.columns().into_iter(); - let mut output = LineageMap::new(); - - for column in left_columns { - if let Some(output_column) = output_columns.next() { - let key = ColumnKey::from_column(&output_column); - let usages = left_map - .get(&ColumnKey::from_column(&column)) - .cloned() - .unwrap_or_default(); - output.insert(key, usages); - } - } - - for column in right_columns { - if let Some(output_column) = output_columns.next() { - let key = ColumnKey::from_column(&output_column); - let usages = right_map - .get(&ColumnKey::from_column(&column)) - .cloned() - .unwrap_or_default(); - output.insert(key, usages); - } - } - - Ok(output) - } - - fn analyze_subquery_alias(&mut self, alias: &SubqueryAlias) -> Result { - let input_map = self.analyze_plan(alias.input.as_ref())?; - let input_columns = alias.input.schema().columns(); - let mut output = LineageMap::new(); - for (input_column, output_column) in - input_columns.iter().zip(alias.schema.columns().into_iter()) - { - let key = ColumnKey::from_column(&output_column); - let usages = input_map - .get(&ColumnKey::from_column(input_column)) - .cloned() - .unwrap_or_default(); - output.insert(key, usages); - } - Ok(output) - } - - fn analyze_window(&mut self, window: &Window) -> Result { - let input_map = self.analyze_plan(window.input.as_ref())?; - let input_schema = window.input.schema(); - - let input_cols = input_schema.columns(); - let output_cols = window.schema.columns(); - let mut output = LineageMap::new(); - - for (input_column, output_column) in input_cols.iter().zip(output_cols.iter()) { - let key = ColumnKey::from_column(output_column); - let usages = input_map - .get(&ColumnKey::from_column(input_column)) - .cloned() - .unwrap_or_default(); - output.insert(key, usages); - } - - for (expr, output_column) in window - .window_expr - .iter() - .zip(output_cols.into_iter().skip(input_cols.len())) - { - let usages = lineage_for_expr(expr, &input_map, input_schema.as_ref())?; - self.record(&usages); - output.insert(ColumnKey::from_column(&output_column), usages); - } - - Ok(output) - } - - fn analyze_limit(&mut self, limit: &Limit) -> Result { - let map = self.analyze_plan(limit.input.as_ref())?; - let schema = limit.input.schema(); - if let Some(skip) = &limit.skip { - let usages = lineage_for_expr(skip, &map, schema.as_ref())?; - self.record(&usages); - } - if let Some(fetch) = &limit.fetch { - let usages = lineage_for_expr(fetch, &map, schema.as_ref())?; - self.record(&usages); - } - Ok(map) - } - - fn analyze_repartition(&mut self, repartition: &Repartition) -> Result { - let map = self.analyze_plan(repartition.input.as_ref())?; - let schema = repartition.input.schema(); - if let Partitioning::Hash(exprs, _) | Partitioning::DistributeBy(exprs) = - &repartition.partitioning_scheme - { - for expr in exprs { - let usages = lineage_for_expr(expr, &map, schema.as_ref())?; - self.record(&usages); - } - } - Ok(map) - } - - fn analyze_union(&mut self, union: &Union) -> Result { - let mut input_maps: Vec = Vec::with_capacity(union.inputs.len()); - for input in &union.inputs { - input_maps.push(self.analyze_plan(input.as_ref())?); - } - - let mut output = LineageMap::new(); - for output_column in union.schema.columns() { - let key = ColumnKey::from_column(&output_column); - let mut combined: Vec = Vec::new(); - for map in &input_maps { - for (candidate_key, usages) in map { - if candidate_key.name == key.name { - combined.extend(usages.clone()); - } - } - } - output.insert(key, combined); - } - Ok(output) - } - - fn analyze_distinct(&mut self, distinct: &Distinct) -> Result { - match distinct { - Distinct::All(plan) => self.analyze_plan(plan.as_ref()), - Distinct::On(distinct_on) => self.analyze_distinct_on(distinct_on), - } - } - - fn analyze_distinct_on(&mut self, distinct_on: &DistinctOn) -> Result { - let input_map = self.analyze_plan(distinct_on.input.as_ref())?; - let schema = distinct_on.input.schema(); - - for expr in &distinct_on.on_expr { - let usages = lineage_for_expr(expr, &input_map, schema.as_ref())?; - self.record(&usages); - } - for expr in &distinct_on.select_expr { - let usages = lineage_for_expr(expr, &input_map, schema.as_ref())?; - self.record(&usages); - } - if let Some(sort_exprs) = &distinct_on.sort_expr { - for sort_expr in sort_exprs { - let usages = lineage_for_expr(&sort_expr.expr, &input_map, schema.as_ref())?; - self.record(&usages); - } - } - - let mut output = LineageMap::new(); - for (expr, column) in distinct_on - .select_expr - .iter() - .zip(distinct_on.schema.columns().into_iter()) - { - let usages = lineage_for_expr(expr, &input_map, schema.as_ref())?; - output.insert(ColumnKey::from_column(&column), usages); - } - Ok(output) - } - - fn record(&mut self, usages: &[ColumnUsage]) { - for usage in usages { - let entry = self - .stats - .entry(usage.base.clone()) - .or_insert_with(|| UsageStats::new(usage.data_type.clone())); - entry.apply(usage); - } - } - - fn finish(self) -> TableColumnUsage { - TableColumnUsage { usage: self.stats } - } -} - -struct TableColumnUsage { - usage: HashMap, -} - -impl TableColumnUsage { - fn find_date32_extractions(&self) -> Vec { - let mut extractions = Vec::new(); - for (key, stats) in self.usage.iter() { - if is_date_part_type(&stats.data_type) { - // Collect all extract units from paths where the first n operations are all extracts - let mut all_units = HashSet::new(); - let mut all_paths_valid = true; - - for usage in &stats.usages { - // Collect all Extract units from the leading sequence of extracts - let mut path_units = HashSet::new(); - for op in usage { - match op { - Operation::Extract(unit) => { - path_units.insert(unit); - } - _ => { - // Stop at first non-extract operation - break; - } - } - } - - if path_units.is_empty() { - // This path doesn't start with Extract, so skip this column - all_paths_valid = false; - break; - } - - // Union the units from this path into the overall set - all_units.extend(path_units); - } - - if all_paths_valid && !all_units.is_empty() { - extractions.push(DateExtraction { - column: key.to_column(), - components: all_units, - }); - } - } - } - extractions - } - - fn find_variant_gets(&self) -> Vec { - let mut gets = Vec::new(); - for (key, stats) in self.usage.iter() { - if stats.usages.is_empty() { - continue; - } - - let mut field_map: HashMap = HashMap::new(); - let mut valid = true; - let mut saw_variant_get = false; - for usage in &stats.usages { - match usage.first() { - Some(Operation::VariantGet { path, data_type }) => { - saw_variant_get = true; - match field_map.entry(path.clone()) { - Entry::Vacant(entry) => { - entry.insert(VariantField { - path: path.clone(), - data_type: data_type.clone(), - }); - } - Entry::Occupied(entry) => { - let current = entry.into_mut(); - let conflict = match (¤t.data_type, data_type) { - (Some(existing), Some(new_ty)) => existing != new_ty, - (Some(_), None) | (None, Some(_)) => true, - (None, None) => false, - }; - if conflict { - valid = false; - break; - } - } - } - } - // A passthrough of the base column (no operations) should not invalidate - // the variant metadata, but also does not contribute a path. - None => continue, - _ => { - valid = false; - break; - } - } - } - - if valid && saw_variant_get && !field_map.is_empty() { - let mut fields: Vec = field_map.into_values().collect(); - fields.sort(); - gets.push(VariantExtraction { - column: key.to_column(), - fields, - }); - } - } - gets - } - - fn find_substring_searches(&self) -> Vec { - let mut columns = Vec::new(); - for (key, stats) in self.usage.iter() { - if !is_string_type(&stats.data_type) { - continue; - } - - if stats.usages.is_empty() { - continue; - } - - let mut saw_substring = false; - let mut valid = true; - for usage in &stats.usages { - let has_substring = usage - .iter() - .any(|op| matches!(op, Operation::SubstringSearch)); - if has_substring { - saw_substring = true; - continue; - } - if !usage.is_empty() { - valid = false; - break; - } - } - - if valid && saw_substring { - columns.push(key.to_column()); - } - } - columns - } -} - -fn build_annotation_map( - date_findings: &[DateExtraction], - variant_findings: &[VariantExtraction], - substring_findings: &[Column], -) -> HashMap { - let mut annotations: HashMap = HashMap::new(); - for extraction in date_findings { - annotations.insert( - ColumnKey::from_column(&extraction.column), - ColumnAnnotation::DatePart(extraction.components.clone()), - ); - } - for extraction in variant_findings { - annotations.insert( - ColumnKey::from_column(&extraction.column), - ColumnAnnotation::VariantPaths(extraction.fields.clone()), - ); - } - for column in substring_findings { - annotations.insert( - ColumnKey::from_column(column), - ColumnAnnotation::SubstringSearch, - ); - } - annotations -} - -fn annotate_plan_with_extractions( - plan: LogicalPlan, - annotations: &HashMap, -) -> Result, DataFusionError> { - if annotations.is_empty() { - return Ok(Transformed::no(plan)); - } - - plan.transform_up(|logical_plan| match logical_plan { - LogicalPlan::TableScan(mut scan) => { - let table_annotations = annotations_for_table_scan(&scan, annotations); - let mut changed = false; - - if let Some(source) = annotate_listing_table_source(&scan.source, &table_annotations)? { - scan.source = source; - changed = true; - } - - if changed { - Ok(Transformed::yes(LogicalPlan::TableScan(scan))) - } else { - Ok(Transformed::no(LogicalPlan::TableScan(scan))) - } - } - other => Ok(Transformed::no(other)), - }) -} - -fn annotations_for_table_scan( - scan: &TableScan, - annotations: &HashMap, -) -> HashMap { - let mut table_annotations = HashMap::new(); - - for (qualifier_opt, field_ref) in scan.projected_schema.iter() { - let qualifier_owned = qualifier_opt.cloned(); - let name = field_ref.name().clone(); - if let Some(unit) = annotations - .get(&ColumnKey::new(qualifier_owned.clone(), name.clone())) - .cloned() - .or_else(|| { - annotations - .get(&ColumnKey::new(None, name.clone())) - .cloned() - }) - { - table_annotations.insert(name, unit); - } - } - - table_annotations -} - -fn annotate_listing_table_source( - source: &Arc, - annotations: &HashMap, -) -> Result>, DataFusionError> { - if annotations.is_empty() { - return Ok(None); - } - - let provider = match source_as_provider(source) { - Ok(provider) => provider, - Err(_) => return Ok(None), - }; - - let Some(listing) = provider.as_any().downcast_ref::() else { - return Ok(None); - }; - - let metadata_copy = annotations.clone(); - let new_factory: Arc = Arc::new( - LineageExtractPhysicalExprAdapterFactory::new(annotations.clone()), - ); - register_factory_metadata(&new_factory, metadata_copy); - let mut new_listing = ListingTable::try_new( - ListingTableConfig::new_with_multi_paths(listing.table_paths().clone()) - .with_listing_options(listing.options().clone()) - .with_schema(listing_file_schema(listing)) - .with_expr_adapter_factory(new_factory), - )?; - new_listing = new_listing.with_constraints(listing_constraints(listing)); - new_listing = new_listing.with_definition( - listing - .get_table_definition() - .map(std::string::ToString::to_string), - ); - - let new_provider: Arc = Arc::new(new_listing); - Ok(Some(provider_as_source(new_provider))) -} - -#[derive(Debug)] -struct LineageExtractPhysicalExprAdapterFactory { - base: Arc, - _annotations: HashMap, -} - -impl LineageExtractPhysicalExprAdapterFactory { - fn new(annotations: HashMap) -> Self { - Self { - base: Arc::new(DefaultPhysicalExprAdapterFactory), - _annotations: annotations, - } - } -} - -impl PhysicalExprAdapterFactory for LineageExtractPhysicalExprAdapterFactory { - fn create( - &self, - logical_file_schema: SchemaRef, - physical_file_schema: SchemaRef, - ) -> Result, DataFusionError> { - self.base.create(logical_file_schema, physical_file_schema) - } -} - -fn listing_file_schema(listing: &ListingTable) -> SchemaRef { - let table_schema = listing.schema(); - let file_field_count = table_schema - .fields() - .len() - .saturating_sub(listing.options().table_partition_cols.len()); - let fields = table_schema - .fields() - .iter() - .take(file_field_count) - .cloned() - .collect::>(); - Arc::new(Schema::new(fields).with_metadata(table_schema.metadata().clone())) -} - -fn listing_constraints(listing: &ListingTable) -> Constraints { - listing.constraints().cloned().unwrap_or_default() -} - -fn factory_registry() -> &'static Mutex>> { - static REGISTRY: OnceLock>>> = - OnceLock::new(); - REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) -} - -fn register_factory_metadata( - factory: &Arc, - metadata: HashMap, -) { - let key = Arc::as_ptr(factory) as *const () as usize; - factory_registry().lock().unwrap().insert(key, metadata); -} - -pub(crate) fn metadata_from_factory( - factory: &Arc, - column: &str, -) -> Option { - let key = Arc::as_ptr(factory) as *const () as usize; - factory_registry() - .lock() - .unwrap() - .get(&key) - .and_then(|map| map.get(column).cloned()) -} - -fn merge_lineage_maps(mut base: LineageMap, other: LineageMap) -> LineageMap { - for (key, usages) in other { - base.entry(key).or_default().extend(usages); - } - base -} - -fn merge_lineage_map_inplace(base: &mut LineageMap, other: &LineageMap) { - for (key, usages) in other { - base.entry(key.clone()).or_default().extend(usages.clone()); - } -} - -fn lineage_for_expr( - expr: &Expr, - input_lineage: &LineageMap, - schema: &DFSchema, -) -> Result> { - match expr { - Expr::Column(column) => { - let key = ColumnKey::from_column(column); - if let Some(usages) = input_lineage.get(&key) { - Ok(usages.clone()) - } else { - let field = schema.field_from_column(column)?; - Ok(vec![ColumnUsage::new_base( - column, - field.data_type().clone(), - )]) - } - } - Expr::Alias(alias) => lineage_for_expr(&alias.expr, input_lineage, schema), - Expr::ScalarFunction(func) => { - let func_name = func.func.name(); - if func_name.eq_ignore_ascii_case("date_part") - && func.args.len() == 2 - && let Some(component) = part_to_unit(&func.args[0]) - { - let mut usages = lineage_for_expr(&func.args[1], input_lineage, schema)?; - for usage in &mut usages { - usage.operations.push(Operation::Extract(component)); - } - return Ok(usages); - } else if func_name.eq_ignore_ascii_case("variant_get") - && (func.args.len() == 2 || func.args.len() == 3) - && let Some(path) = literal_utf8(&func.args[1]) - { - let type_hint = func.args.get(2).and_then(literal_data_type); - let mut usages = lineage_for_expr(&func.args[0], input_lineage, schema)?; - for usage in &mut usages { - usage.operations.push(Operation::VariantGet { - path: path.clone(), - data_type: type_hint.clone(), - }); - } - return Ok(usages); - } - propagate_other(expr, input_lineage, schema) - } - Expr::Like(like) => { - if !like.case_insensitive - && like.escape_char.is_none() - && let Some(pattern) = literal_utf8(&like.pattern) - && is_substring_pattern(pattern.as_bytes()) - { - let mut usages = lineage_for_expr(&like.expr, input_lineage, schema)?; - for usage in &mut usages { - usage.operations.push(Operation::SubstringSearch); - } - return Ok(usages); - } - propagate_other(expr, input_lineage, schema) - } - Expr::Cast(cast) => { - let mut usages = lineage_for_expr(&cast.expr, input_lineage, schema)?; - for usage in &mut usages { - usage.operations.push(Operation::Other); - } - Ok(usages) - } - Expr::TryCast(cast) => { - let mut usages = lineage_for_expr(&cast.expr, input_lineage, schema)?; - for usage in &mut usages { - usage.operations.push(Operation::Other); - } - Ok(usages) - } - Expr::Literal(_, _) => Ok(Vec::new()), - Expr::ScalarSubquery(_) | Expr::Exists { .. } => Ok(Vec::new()), - Expr::Placeholder(_) => Ok(Vec::new()), - #[allow(deprecated)] - Expr::Wildcard { .. } => { - let mut usages = Vec::new(); - for column_usages in input_lineage.values() { - usages.extend(column_usages.clone()); - } - Ok(usages) - } - _ => propagate_other(expr, input_lineage, schema), - } -} - -fn propagate_other( - expr: &Expr, - input_lineage: &LineageMap, - schema: &DFSchema, -) -> Result> { - let mut combined: Vec = Vec::new(); - expr.apply_children(|child| { - let mut usages = lineage_for_expr(child, input_lineage, schema)?; - for usage in &mut usages { - usage.operations.push(Operation::Other); - } - combined.extend(usages); - Ok(TreeNodeRecursion::Continue) - })?; - Ok(combined) -} - -fn literal_utf8(expr: &Expr) -> Option { - match expr { - Expr::Literal(value, _) => match value { - ScalarValue::Utf8(Some(v)) | ScalarValue::LargeUtf8(Some(v)) => Some(v.clone()), - ScalarValue::Utf8View(Some(v)) => Some(v.clone()), - _ => None, - }, - _ => None, - } -} - -fn is_substring_pattern(pattern: &[u8]) -> bool { - if pattern.len() < 2 { - return false; - } - if pattern[0] != b'%' || pattern[pattern.len() - 1] != b'%' { - return false; - } - let inner = &pattern[1..pattern.len() - 1]; - if inner.is_empty() { - return false; - } - !inner.iter().any(|b| *b == b'%' || *b == b'_') -} - -fn is_string_type(data_type: &DataType) -> bool { - match data_type { - DataType::Utf8 | DataType::Utf8View | DataType::LargeUtf8 => true, - DataType::Dictionary(_, value_type) => is_string_type(value_type.as_ref()), - _ => false, - } -} - -fn is_date_part_type(data_type: &DataType) -> bool { - matches!(data_type, DataType::Date32 | DataType::Timestamp(_, _)) -} - -fn literal_data_type(expr: &Expr) -> Option { - literal_utf8(expr).and_then(|spec| DataType::from_str(&spec).ok()) -} - -fn part_to_unit(expr: &Expr) -> Option { - let value = match expr { - Expr::Literal(literal, _) => literal, - _ => return None, - }; - let text = match value { - ScalarValue::Utf8(Some(v)) - | ScalarValue::LargeUtf8(Some(v)) - | ScalarValue::Utf8View(Some(v)) => v.as_str(), - _ => return None, - }; - let lowered = text.to_ascii_lowercase(); - match lowered.as_str() { - "dow" | "dayofweek" | "day_of_week" => { - return Some(SupportedIntervalUnit::DayOfWeek); - } - _ => {} - } - let unit = IntervalUnit::from_str(text).ok()?; - match unit { - IntervalUnit::Year => Some(SupportedIntervalUnit::Year), - IntervalUnit::Month => Some(SupportedIntervalUnit::Month), - IntervalUnit::Day => Some(SupportedIntervalUnit::Day), - _ => None, - } -} - -#[cfg(test)] -mod tests { - use crate::optimizers::{ - DATE_MAPPING_METADATA_KEY, LocalModeOptimizer, VARIANT_MAPPING_METADATA_KEY, - }; - use crate::{LiquidCacheParquet, VariantGetUdf, VariantToJsonUdf}; - use liquid_cache::cache::AlwaysHydrate; - - use super::*; - use arrow::array::{ArrayRef, Date32Array, StringArray, TimestampMicrosecondArray}; - use arrow_schema::{Field, Schema, TimeUnit}; - use datafusion::catalog::memory::DataSourceExec; - use datafusion::datasource::physical_plan::FileScanConfig; - use datafusion::execution::SessionStateBuilder; - use datafusion::logical_expr::ScalarUDF; - use datafusion::physical_plan::ExecutionPlan; - use datafusion::prelude::{ParquetReadOptions, SessionConfig, SessionContext}; - use liquid_cache::cache::squeeze_policies::TranscodeSqueezeEvict; - use liquid_cache::cache_policies::LiquidPolicy; - use parquet::arrow::ArrowWriter; - use parquet::variant::{VariantArray, json_to_variant}; - use serde::Deserialize; - use tempfile::TempDir; - - // ───────────────────────────────────────────────────────────────────────────── - // Setup helpers - lean versions for different test scenarios - // ───────────────────────────────────────────────────────────────────────────── - - async fn create_physical_optimizer() -> LocalModeOptimizer { - let tmp_dir = tempfile::tempdir().unwrap(); - let store = t4::mount(tmp_dir.path().join("liquid_cache.t4")) - .await - .unwrap(); - LocalModeOptimizer::with_cache(Arc::new( - LiquidCacheParquet::new( - 1024, - 1024 * 1024 * 1024, - store, - Box::new(LiquidPolicy::new()), - Box::new(TranscodeSqueezeEvict), - Box::new(AlwaysHydrate::new()), - ) - .await, - )) - } - - async fn create_session_context(optimizer: Arc) -> SessionContext { - let physical_optimizer = create_physical_optimizer().await; - let state = SessionStateBuilder::new() - .with_config(SessionConfig::new()) - .with_default_features() - .with_optimizer_rule(optimizer as Arc) - .with_physical_optimizer_rule(Arc::new(physical_optimizer)) - .build(); - SessionContext::new_with_state(state) - } - - fn write_date_parquet(path: &std::path::Path) { - let schema = Arc::new(Schema::new(vec![ - Field::new( - "event_ts", - DataType::Timestamp(TimeUnit::Microsecond, None), - false, - ), - Field::new("date", DataType::Date32, false), - Field::new("date_copy", DataType::Date32, false), - ])); - - let timestamps: ArrayRef = Arc::new(TimestampMicrosecondArray::from(vec![ - Some(1_609_459_200_000_000), - Some(1_640_995_200_000_000), - Some(1_672_358_400_000_000), - ])); - let dates: ArrayRef = Arc::new(Date32Array::from(vec![ - Some(20210101), - Some(20220202), - Some(20230303), - ])); - let batch = arrow::record_batch::RecordBatch::try_new( - Arc::clone(&schema), - vec![timestamps, dates.clone(), dates], - ) - .unwrap(); - - let file = std::fs::File::create(path).unwrap(); - let mut writer = ArrowWriter::try_new(file, Arc::clone(&schema), None).unwrap(); - writer.write(&batch).unwrap(); - writer.close().unwrap(); - } - - fn write_variant_parquet(path: &std::path::Path) { - let values = StringArray::from(vec![ - Some(r#"{"name": "Alice", "age": 30}"#), - Some(r#"{"name": "Bob", "age": 25}"#), - Some(r#"{"name": "Charlie"}"#), - ]); - let input_array: ArrayRef = Arc::new(values); - let variant: VariantArray = - json_to_variant(&input_array).expect("variant conversion for test data"); - - let schema = Arc::new(Schema::new(vec![variant.field("data")])); - let batch = arrow::record_batch::RecordBatch::try_new( - Arc::clone(&schema), - vec![ArrayRef::from(variant)], - ) - .expect("variant batch"); - - let file = std::fs::File::create(path).expect("create variant parquet file"); - let mut writer = - ArrowWriter::try_new(file, batch.schema(), None).expect("create variant writer"); - writer.write(&batch).expect("write variant batch"); - writer.close().expect("close variant writer"); - } - - /// Setup for tests that only need a single date table (table_a) - async fn setup_single_date_table() -> (TempDir, SessionContext, Arc) { - let temp_dir = TempDir::new().unwrap(); - let table_path = temp_dir.path().join("table_a.parquet"); - write_date_parquet(&table_path); - - let optimizer = Arc::new(LineageOptimizer::new()); - let ctx = create_session_context(optimizer.clone()).await; - ctx.register_parquet( - "table_a", - table_path.to_str().unwrap(), - ParquetReadOptions::default(), - ) - .await - .unwrap(); - - (temp_dir, ctx, optimizer) - } - - /// Setup for tests that need two date tables (table_a and table_b) for joins - async fn setup_dual_date_tables() -> (TempDir, SessionContext, Arc) { - let temp_dir = TempDir::new().unwrap(); - let table_a_path = temp_dir.path().join("table_a.parquet"); - let table_b_path = temp_dir.path().join("table_b.parquet"); - write_date_parquet(&table_a_path); - write_date_parquet(&table_b_path); - - let optimizer = Arc::new(LineageOptimizer::new()); - let ctx = create_session_context(optimizer.clone()).await; - ctx.register_parquet( - "table_a", - table_a_path.to_str().unwrap(), - ParquetReadOptions::default(), - ) - .await - .unwrap(); - ctx.register_parquet( - "table_b", - table_b_path.to_str().unwrap(), - ParquetReadOptions::default(), - ) - .await - .unwrap(); - - (temp_dir, ctx, optimizer) - } - - /// Setup for tests that only need a variant table - async fn setup_variant_table() -> (TempDir, SessionContext, Arc) { - let temp_dir = TempDir::new().unwrap(); - let variant_path = temp_dir.path().join("variants_test.parquet"); - write_variant_parquet(&variant_path); - - let optimizer = Arc::new(LineageOptimizer::new()); - let ctx = create_session_context(optimizer.clone()).await; - ctx.register_udf(ScalarUDF::new_from_impl(VariantGetUdf::default())); - ctx.register_udf(ScalarUDF::new_from_impl(VariantToJsonUdf::default())); - ctx.register_parquet( - "variants_test", - variant_path.to_str().unwrap(), - ParquetReadOptions::default().skip_metadata(false), - ) - .await - .unwrap(); - - (temp_dir, ctx, optimizer) - } - - /// Setup for tests that need both date table and variant table - async fn setup_date_and_variant_tables() -> (TempDir, SessionContext, Arc) { - let temp_dir = TempDir::new().unwrap(); - let table_a_path = temp_dir.path().join("table_a.parquet"); - let variant_path = temp_dir.path().join("variants_test.parquet"); - write_date_parquet(&table_a_path); - write_variant_parquet(&variant_path); - - let optimizer = Arc::new(LineageOptimizer::new()); - let ctx = create_session_context(optimizer.clone()).await; - ctx.register_udf(ScalarUDF::new_from_impl(VariantGetUdf::default())); - ctx.register_udf(ScalarUDF::new_from_impl(VariantToJsonUdf::default())); - ctx.register_parquet( - "table_a", - table_a_path.to_str().unwrap(), - ParquetReadOptions::default(), - ) - .await - .unwrap(); - ctx.register_parquet( - "variants_test", - variant_path.to_str().unwrap(), - ParquetReadOptions::default().skip_metadata(false), - ) - .await - .unwrap(); - - (temp_dir, ctx, optimizer) - } - - // ───────────────────────────────────────────────────────────────────────────── - // Test utilities - // ───────────────────────────────────────────────────────────────────────────── - - fn extract_field_metadata( - plan: &Arc, - metadata_key: &str, - ) -> HashMap { - let mut field_metadata_map = HashMap::new(); - - plan.apply(|node| { - let Some(data_source) = node.as_any().downcast_ref::() else { - return Ok(TreeNodeRecursion::Continue); - }; - let Some(file_scan_config) = data_source - .data_source() - .as_any() - .downcast_ref::() - else { - return Ok(TreeNodeRecursion::Continue); - }; - - let file_schema = &file_scan_config.file_schema(); - for field in file_schema.fields() { - if let Some(metadata_value) = field.metadata().get(metadata_key) { - field_metadata_map.insert(field.name().to_string(), metadata_value.clone()); - } - } - Ok(TreeNodeRecursion::Continue) - }) - .unwrap(); - field_metadata_map - } - - #[derive(Debug, Deserialize)] - struct VariantMetadataEntry { - path: String, - #[serde(rename = "type")] - data_type: Option, - } - - fn parse_variant_metadata(value: &str) -> Vec { - serde_json::from_str(value).unwrap_or_else(|_| { - vec![VariantMetadataEntry { - path: value.to_string(), - data_type: None, - }] - }) - } - - fn variant_paths_from_metadata(value: &str) -> Vec { - parse_variant_metadata(value) - .into_iter() - .map(|entry| entry.path) - .collect() - } - - /// Assert metadata on physical plan matches expected date and variant extractions - async fn assert_metadata( - ctx: &SessionContext, - sql: &str, - expected_date: Vec<(&str, &str)>, - expected_variant: Vec<&str>, - ) { - let df = ctx.sql(sql).await.unwrap(); - let (state, plan) = df.into_parts(); - let optimized = state.optimize(&plan).unwrap(); - let physical_plan = state.create_physical_plan(&optimized).await.unwrap(); - - let date_metadata = extract_field_metadata(&physical_plan, DATE_MAPPING_METADATA_KEY); - let variant_metadata = extract_field_metadata(&physical_plan, VARIANT_MAPPING_METADATA_KEY); - - // Check date metadata - let expected_date_map: HashMap = expected_date - .into_iter() - .map(|(col, val)| (col.to_string(), val.to_string())) - .collect(); - assert_eq!( - date_metadata, expected_date_map, - "date metadata mismatch for SQL: {}", - sql - ); - - // Check variant metadata - if expected_variant.is_empty() { - assert!( - !variant_metadata.contains_key("data"), - "variant metadata should not be present for SQL: {}", - sql - ); - } else { - let mut actual = variant_metadata - .get("data") - .map(|v| variant_paths_from_metadata(v)) - .unwrap_or_default(); - actual.sort(); - let mut expected: Vec = expected_variant - .into_iter() - .map(|s| s.to_string()) - .collect(); - expected.sort(); - assert_eq!( - actual, expected, - "variant metadata mismatch for SQL: {}", - sql - ); - } - } - - // ───────────────────────────────────────────────────────────────────────────── - // Date extraction tests - single table - // ───────────────────────────────────────────────────────────────────────────── - - #[tokio::test] - async fn extract_day_basic() { - let (_dir, ctx, _) = setup_single_date_table().await; - assert_metadata( - &ctx, - "SELECT EXTRACT(DAY FROM date) AS day FROM table_a", - vec![("date", "DAY")], - vec![], - ) - .await; - } - - #[tokio::test] - async fn extract_dow_basic() { - let (_dir, ctx, _) = setup_single_date_table().await; - assert_metadata( - &ctx, - "SELECT date_part('dow', date) AS dow FROM table_a", - vec![("date", "DOW")], - vec![], - ) - .await; - } - - #[tokio::test] - async fn extract_day_lowercase() { - let (_dir, ctx, _) = setup_single_date_table().await; - assert_metadata( - &ctx, - "SELECT EXTRACT(day FROM date) AS day FROM table_a", - vec![("date", "DAY")], - vec![], - ) - .await; - } - - #[tokio::test] - async fn extract_day_qualified_column() { - let (_dir, ctx, _) = setup_single_date_table().await; - assert_metadata( - &ctx, - "SELECT EXTRACT(DAY FROM table_a.date) FROM table_a", - vec![("date", "DAY")], - vec![], - ) - .await; - } - - #[tokio::test] - async fn extract_day_in_avg() { - let (_dir, ctx, _) = setup_single_date_table().await; - assert_metadata( - &ctx, - "SELECT AVG(EXTRACT(DAY FROM date)) AS avg_day FROM table_a", - vec![("date", "DAY")], - vec![], - ) - .await; - } - - #[tokio::test] - async fn extract_day_in_expression() { - let (_dir, ctx, _) = setup_single_date_table().await; - assert_metadata( - &ctx, - "SELECT AVG(EXTRACT(DAY FROM date) + 1) AS avg_day FROM table_a", - vec![("date", "DAY")], - vec![], - ) - .await; - } - - #[tokio::test] - async fn extract_day_in_subqueries() { - let (_dir, ctx, _) = setup_single_date_table().await; - assert_metadata( - &ctx, - "SELECT (SELECT MAX(EXTRACT(DAY FROM date)) FROM table_a) AS max_day, (SELECT MIN(EXTRACT(DAY FROM date)) FROM table_a) AS min_day", - vec![("date", "DAY")], - vec![], - ) - .await; - } - - // ───────────────────────────────────────────────────────────────────────────── - // Date extraction tests - multiple components - // ───────────────────────────────────────────────────────────────────────────── - - #[tokio::test] - async fn extract_day_and_month() { - let (_dir, ctx, _) = setup_single_date_table().await; - assert_metadata( - &ctx, - "SELECT EXTRACT(DAY FROM date) AS day, EXTRACT(MONTH FROM date) AS month FROM table_a", - vec![("date", "MONTH,DAY")], - vec![], - ) - .await; - } - - #[tokio::test] - async fn extract_day_and_month_subqueries() { - let (_dir, ctx, _) = setup_single_date_table().await; - assert_metadata( - &ctx, - "SELECT (SELECT MAX(EXTRACT(DAY FROM date)) FROM table_a) AS max_day, (SELECT MIN(EXTRACT(Month FROM date)) FROM table_a) AS min_day", - vec![("date", "MONTH,DAY")], - vec![], - ) - .await; - } - - // ───────────────────────────────────────────────────────────────────────────── - // Date extraction tests - multi table (joins) - // ───────────────────────────────────────────────────────────────────────────── - - #[tokio::test] - async fn extract_from_joined_tables() { - let (_dir, ctx, _) = setup_dual_date_tables().await; - // Both tables have "date" column - metadata HashMap stores by column name only, - // so we verify at least one extraction is present (iteration order determines which) - let df = ctx - .sql("SELECT EXTRACT(YEAR FROM table_a.date) AS year, EXTRACT(DAY FROM table_b.date) AS day FROM table_a INNER JOIN table_b ON table_a.event_ts = table_b.event_ts") - .await - .unwrap(); - let (state, plan) = df.into_parts(); - let optimized = state.optimize(&plan).unwrap(); - let physical_plan = state.create_physical_plan(&optimized).await.unwrap(); - let metadata = extract_field_metadata(&physical_plan, DATE_MAPPING_METADATA_KEY); - - // Both tables' date columns should have extraction metadata - assert!( - metadata.contains_key("date"), - "date column should have extraction metadata" - ); - let value = metadata.get("date").unwrap(); - assert!( - value == "YEAR" || value == "DAY", - "expected YEAR or DAY, got {}", - value - ); - } - - // ───────────────────────────────────────────────────────────────────────────── - // Date extraction tests - no metadata (inconsistent usage) - // ───────────────────────────────────────────────────────────────────────────── - - #[tokio::test] - async fn no_extraction_with_interval_arithmetic() { - let (_dir, ctx, _) = setup_single_date_table().await; - assert_metadata( - &ctx, - "SELECT EXTRACT(DAY FROM date + INTERVAL '1 day') AS day FROM table_a", - vec![], - vec![], - ) - .await; - } - - #[tokio::test] - async fn no_extraction_when_column_used_directly() { - let (_dir, ctx, _) = setup_single_date_table().await; - assert_metadata(&ctx, "SELECT date FROM table_a", vec![], vec![]).await; - } - - #[tokio::test] - async fn no_extraction_when_used_in_join_condition() { - let (_dir, ctx, _) = setup_dual_date_tables().await; - assert_metadata( - &ctx, - "SELECT EXTRACT(DAY FROM table_a.date) AS day FROM table_a INNER JOIN table_b ON table_a.date = table_b.date", - vec![], - vec![], - ) - .await; - } - - #[tokio::test] - async fn timestamp_extraction_supported() { - let (_dir, ctx, _) = setup_single_date_table().await; - assert_metadata( - &ctx, - "SELECT EXTRACT(YEAR FROM event_ts) AS year FROM table_a", - vec![("event_ts", "YEAR")], - vec![], - ) - .await; - } - - #[tokio::test] - async fn timestamp_dow_extraction() { - let (_dir, ctx, _) = setup_single_date_table().await; - assert_metadata( - &ctx, - "SELECT date_part('dow', event_ts) AS dow FROM table_a", - vec![("event_ts", "DOW")], - vec![], - ) - .await; - } - - // ───────────────────────────────────────────────────────────────────────────── - // Date extraction - metadata isolation test - // ───────────────────────────────────────────────────────────────────────────── - - #[tokio::test] - async fn metadata_only_on_extracted_fields() { - let (_dir, ctx, _) = setup_single_date_table().await; - // Only 'date' should have metadata, not 'event_ts' - assert_metadata( - &ctx, - "SELECT EXTRACT(YEAR FROM date) AS year, event_ts FROM table_a", - vec![("date", "YEAR")], - vec![], - ) - .await; - } - - // ───────────────────────────────────────────────────────────────────────────── - // Variant extraction tests - basic - // ───────────────────────────────────────────────────────────────────────────── - - #[tokio::test] - async fn variant_get_single_path() { - let (_dir, ctx, _) = setup_variant_table().await; - assert_metadata( - &ctx, - "SELECT variant_to_json(variant_get(data, 'name')) FROM variants_test", - vec![], - vec!["name"], - ) - .await; - } - - #[tokio::test] - async fn variant_get_duplicate_paths() { - let (_dir, ctx, _) = setup_variant_table().await; - assert_metadata( - &ctx, - "SELECT variant_get(data, 'name'), variant_get(data, 'name') AS name2 FROM variants_test", - vec![], - vec!["name"], - ) - .await; - } - - #[tokio::test] - async fn variant_get_with_to_json() { - let (_dir, ctx, _) = setup_variant_table().await; - assert_metadata( - &ctx, - "SELECT variant_to_json(variant_get(data, 'age')), variant_to_json(variant_get(data, 'age')) AS age2 FROM variants_test", - vec![], - vec!["age"], - ) - .await; - } - - #[tokio::test] - async fn variant_get_qualified_column() { - let (_dir, ctx, _) = setup_variant_table().await; - assert_metadata( - &ctx, - "SELECT variant_get(variants_test.data, 'name') as name1, variant_get(variants_test.data, 'name') as name2 FROM variants_test", - vec![], - vec!["name"], - ) - .await; - } - - #[tokio::test] - async fn variant_get_in_aggregates() { - let (_dir, ctx, _) = setup_variant_table().await; - assert_metadata( - &ctx, - "SELECT COUNT(variant_get(data, 'age')), MAX(variant_get(data, 'age')) FROM variants_test", - vec![], - vec!["age"], - ) - .await; - } - - #[tokio::test] - async fn variant_get_with_where_clause() { - let (_dir, ctx, _) = setup_variant_table().await; - assert_metadata( - &ctx, - "SELECT variant_get(data, 'name') FROM variants_test WHERE variant_get(data, 'name') IS NOT NULL", - vec![], - vec!["name"], - ) - .await; - } - - #[tokio::test] - async fn variant_get_in_subqueries() { - let (_dir, ctx, _) = setup_variant_table().await; - assert_metadata( - &ctx, - "SELECT (SELECT MAX(variant_get(data, 'name')) FROM variants_test) AS max_name, (SELECT MIN(variant_get(data, 'name')) FROM variants_test) AS min_name", - vec![], - vec!["name"], - ) - .await; - } - - #[tokio::test] - async fn variant_get_multiple_paths() { - let (_dir, ctx, _) = setup_variant_table().await; - assert_metadata( - &ctx, - "SELECT variant_get(data, 'name'), variant_get(data, 'date') FROM variants_test", - vec![], - vec!["date", "name"], - ) - .await; - } - - #[tokio::test] - async fn variant_get_nested() { - let (_dir, ctx, _) = setup_variant_table().await; - assert_metadata( - &ctx, - "SELECT variant_get(variant_get(data, 'name'), 'age') FROM variants_test", - vec![], - vec!["name"], - ) - .await; - } - - #[tokio::test] - async fn variant_get_conflicting_types_no_metadata() { - let (_dir, ctx, _) = setup_variant_table().await; - // Same path with different type hints - should not produce metadata - assert_metadata( - &ctx, - "SELECT variant_get(data, 'name', 'Utf8'), variant_get(data, 'name') FROM variants_test", - vec![], - vec![], - ) - .await; - } - - // ───────────────────────────────────────────────────────────────────────────── - // Variant extraction tests - type metadata - // ───────────────────────────────────────────────────────────────────────────── - - #[tokio::test] - async fn variant_get_type_hint_propagated() { - let (_dir, ctx, _) = setup_variant_table().await; - - let df = ctx - .sql("SELECT variant_get(data, 'name', 'Utf8') FROM variants_test") - .await - .unwrap(); - let (state, plan) = df.into_parts(); - let optimized = state.optimize(&plan).unwrap(); - let physical_plan = state.create_physical_plan(&optimized).await.unwrap(); - - let metadata = extract_field_metadata(&physical_plan, VARIANT_MAPPING_METADATA_KEY); - - let entries = metadata - .get("data") - .map(|value| parse_variant_metadata(value)) - .unwrap_or_default(); - let entry = entries - .iter() - .find(|entry| entry.path == "name") - .expect("variant metadata entry for name"); - assert_eq!(entry.data_type.as_deref(), Some("Utf8")); - } - - #[tokio::test] - async fn variant_get_conflicting_types_in_filter() { - let (_dir, ctx, _) = setup_variant_table().await; - assert_metadata( - &ctx, - "SELECT variant_to_json(variant_get(data, 'name')) FROM variants_test WHERE variant_get(data, 'name', 'Utf8') = 'Bob'", - vec![], - vec![], - ) - .await; - } - - // ───────────────────────────────────────────────────────────────────────────── - // Variant extraction tests - edge cases - // ───────────────────────────────────────────────────────────────────────────── - - #[tokio::test] - async fn variant_get_multiple_paths_with_types() { - let (_dir, ctx, _) = setup_variant_table().await; - assert_metadata( - &ctx, - "SELECT variant_get(data, 'did', 'Utf8') as user_id, - MAX(TO_TIMESTAMP_MICROS(variant_get(data, 'time_us', 'Int64'))) - MIN(TO_TIMESTAMP_MICROS(variant_get(data, 'time_us', 'Int64'))) - FROM variants_test GROUP BY user_id", - vec![], - vec!["did", "time_us"], - ) - .await; - } - - // ───────────────────────────────────────────────────────────────────────────── - // Mixed date extract and variant tests - // ───────────────────────────────────────────────────────────────────────────── - - #[tokio::test] - async fn mixed_date_and_variant_basic() { - let (_dir, ctx, _) = setup_date_and_variant_tables().await; - assert_metadata( - &ctx, - "SELECT EXTRACT(DAY FROM table_a.date) AS day, variant_get(variants_test.data, 'name') AS name FROM table_a CROSS JOIN variants_test", - vec![("date", "DAY")], - vec!["name"], - ) - .await; - } - - #[tokio::test] - async fn mixed_multiple_date_and_variant() { - let (_dir, ctx, _) = setup_date_and_variant_tables().await; - assert_metadata( - &ctx, - "SELECT EXTRACT(YEAR FROM table_a.date) AS year, EXTRACT(MONTH FROM table_a.date_copy) AS month, variant_get(variants_test.data, 'name') AS name, variant_get(variants_test.data, 'age') AS age FROM table_a CROSS JOIN variants_test", - vec![("date", "YEAR"), ("date_copy", "MONTH")], - vec!["age", "name"], - ) - .await; - } - - #[tokio::test] - async fn mixed_date_in_where_clause() { - let (_dir, ctx, _) = setup_date_and_variant_tables().await; - assert_metadata( - &ctx, - "SELECT variant_get(variants_test.data, 'name') AS name FROM variants_test CROSS JOIN table_a WHERE EXTRACT(DAY FROM table_a.date) > 1", - vec![("date", "DAY")], - vec!["name"], - ) - .await; - } - - #[tokio::test] - async fn mixed_variant_in_where_clause() { - let (_dir, ctx, _) = setup_date_and_variant_tables().await; - assert_metadata( - &ctx, - "SELECT EXTRACT(DAY FROM table_a.date) AS day FROM table_a CROSS JOIN variants_test WHERE variant_get(variants_test.data, 'name') IS NOT NULL", - vec![("date", "DAY")], - vec!["name"], - ) - .await; - } - - #[tokio::test] - async fn mixed_date_with_variant_subquery() { - let (_dir, ctx, _) = setup_date_and_variant_tables().await; - assert_metadata( - &ctx, - "SELECT EXTRACT(YEAR FROM table_a.date) AS year, (SELECT variant_get(variants_test.data, 'name') FROM variants_test LIMIT 1) AS name FROM table_a", - vec![("date", "YEAR")], - vec!["name"], - ) - .await; - } - - #[tokio::test] - async fn mixed_in_aggregates() { - let (_dir, ctx, _) = setup_date_and_variant_tables().await; - assert_metadata( - &ctx, - "SELECT AVG(EXTRACT(DAY FROM table_a.date)) AS avg_day, COUNT(variant_get(variants_test.data, 'name')) AS name_count FROM table_a CROSS JOIN variants_test", - vec![("date", "DAY")], - vec!["name"], - ) - .await; - } -} diff --git a/src/datafusion/src/optimizers/mod.rs b/src/datafusion/src/optimizers/mod.rs index 19b6416c..804a5222 100644 --- a/src/datafusion/src/optimizers/mod.rs +++ b/src/datafusion/src/optimizers/mod.rs @@ -1,117 +1,42 @@ //! Optimizers for the Parquet module -mod lineage_opt; +mod squeeze_hint; -use std::{str::FromStr, sync::Arc}; +use std::sync::Arc; -use arrow_schema::{DataType, Field, Fields, Schema, SchemaRef}; use datafusion::{ catalog::memory::DataSourceExec, common::tree_node::{Transformed, TreeNode, TreeNodeRecursion}, config::ConfigOptions, - datasource::{ - physical_plan::{FileSource, ParquetSource}, - source::DataSource, - table_schema::TableSchema, - }, - physical_expr_adapter::PhysicalExprAdapterFactory, + datasource::{physical_plan::ParquetSource, source::DataSource}, physical_optimizer::PhysicalOptimizerRule, physical_plan::ExecutionPlan, }; -pub use lineage_opt::LineageOptimizer; -pub(crate) use lineage_opt::VariantField; -use crate::{ - LiquidCacheParquetRef, LiquidParquetSource, - optimizers::lineage_opt::{ColumnAnnotation, metadata_from_factory, serialize_date_part}, -}; -use liquid_cache::utils::VariantSchema; -use serde::{Deserialize, Serialize}; - -pub(crate) const DATE_MAPPING_METADATA_KEY: &str = "liquid.cache.date_mapping"; -pub(crate) const VARIANT_MAPPING_METADATA_KEY: &str = "liquid.cache.variant_path"; -pub(crate) const VARIANT_MAPPING_TYPE_METADATA_KEY: &str = "liquid.cache.variant_type"; -pub(crate) const STRING_FINGERPRINT_METADATA_KEY: &str = "liquid.cache.string_fingerprint"; - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct VariantMappingSerdeEntry { - path: String, - #[serde(rename = "type", skip_serializing_if = "Option::is_none")] - data_type: Option, -} - -pub(crate) fn serialize_variant_mappings(fields: &[VariantField]) -> Option { - if fields.is_empty() { - return None; - } - - let entries: Vec = fields - .iter() - .map(|field| VariantMappingSerdeEntry { - path: field.path.clone(), - data_type: field - .data_type - .as_ref() - .map(|data_type| data_type.to_string()), - }) - .collect(); - - serde_json::to_string(&entries).ok() -} - -fn deserialize_variant_mappings(raw: &str) -> Option> { - let entries: Vec = serde_json::from_str(raw).ok()?; - let mut fields = Vec::with_capacity(entries.len()); - for entry in entries { - let data_type = match entry.data_type { - Some(spec) => Some(DataType::from_str(&spec).ok()?), - None => None, - }; - fields.push(VariantField { - path: entry.path, - data_type, - }); - } - Some(fields) -} - -pub(crate) fn variant_mappings_from_field(field: &Field) -> Option> { - let metadata = field.metadata(); - let raw = metadata.get(VARIANT_MAPPING_METADATA_KEY)?; - if let Some(parsed) = deserialize_variant_mappings(raw) { - return Some(parsed); - } +pub(crate) use squeeze_hint::HintAnalyzer; +pub use squeeze_hint::SqueezeHintMap; - let data_type = metadata - .get(VARIANT_MAPPING_TYPE_METADATA_KEY) - .and_then(|spec| DataType::from_str(spec).ok()); +use crate::{LiquidCacheParquetRef, LiquidParquetSource, cache::ColumnSqueezeHints}; - Some(vec![VariantField { - path: raw.clone(), - data_type, - }]) -} - -/// Physical optimizer rule for local mode liquid cache +/// Physical optimizer rule for local mode liquid cache. /// -/// This optimizer rewrites DataSourceExec nodes that read Parquet files -/// to use LiquidParquetSource instead of the default ParquetSource +/// Rewrites `DataSourceExec` parquet scans to use [`LiquidParquetSource`], and +/// in the same pass derives typed squeeze hints from the full physical plan +/// (via the squeeze-hint analyzer) and attaches each scan's hints to its source. #[derive(Debug)] pub struct LocalModeOptimizer { cache: LiquidCacheParquetRef, - eager_shredding: bool, /// When set, parquet scans whose total file size exceeds this threshold /// are left as vanilla DataFusion reads instead of being wrapped by - /// LiquidCache. `None` means cache every scan (current default). + /// LiquidCache. `None` means cache every scan. max_scan_bytes: Option, } impl LocalModeOptimizer { /// Create an optimizer with an existing cache instance - pub fn new(cache: LiquidCacheParquetRef, eager_shredding: bool) -> Self { + pub fn new(cache: LiquidCacheParquetRef) -> Self { Self { cache, - eager_shredding, max_scan_bytes: None, } } @@ -120,14 +45,13 @@ impl LocalModeOptimizer { pub fn with_cache(cache: LiquidCacheParquetRef) -> Self { Self { cache, - eager_shredding: true, max_scan_bytes: None, } } - /// Set maximum total file size (in bytes) for a parquet scan to be + /// Set the maximum total file size (in bytes) for a parquet scan to be /// routed through LiquidCache. Scans exceeding this are read directly - /// from the underlying parquet source. + /// from the underlying parquet source, bypassing the cache. pub fn with_max_scan_bytes(mut self, max_bytes: u64) -> Self { self.max_scan_bytes = Some(max_bytes); self @@ -140,13 +64,23 @@ impl PhysicalOptimizerRule for LocalModeOptimizer { plan: Arc, _config: &ConfigOptions, ) -> Result, datafusion::error::DataFusionError> { + let analysis = HintAnalyzer::analyze(&plan); + let cache = self.cache.clone(); let max_scan_bytes = self.max_scan_bytes; - let cache = &self.cache; - let eager = self.eager_shredding; - let rewritten = plan - .transform_up(|node| try_optimize_parquet_source(node, cache, eager, max_scan_bytes)) - .unwrap(); - Ok(rewritten.data) + let mut convert = |node: &Arc, hints: ColumnSqueezeHints| { + // Leave oversized scans as vanilla parquet reads, bypassing the cache. + if let Some(max_bytes) = max_scan_bytes + && parquet_scan_total_bytes(node).is_some_and(|total| total > max_bytes) + { + return None; + } + convert_parquet_scan(node, &cache, hints) + }; + Ok(squeeze_hint::rewrite_with_hints( + plan, + &mut convert, + &analysis, + )) } fn name(&self) -> &str { @@ -154,199 +88,77 @@ impl PhysicalOptimizerRule for LocalModeOptimizer { } fn schema_check(&self) -> bool { - // We deliberately enrich scan schemas with metadata describing variant/date - // extractions, so allow the optimizer to adjust schema metadata. - false + true } } -/// Rewrite the data source plan to use liquid cache. -pub fn rewrite_data_source_plan( +/// Rewrite the data source plan to use liquid cache, attaching `hints` (keyed by +/// file-schema column name) to every parquet scan it rewrites. +/// +/// This is the entry point used by the cache server, where hints are derived on +/// the client (which has the full plan) and shipped alongside the pushed +/// fragment, which is always single-scan. +pub fn rewrite_data_source_plan_with_hints( plan: Arc, cache: &LiquidCacheParquetRef, - eager_shredding: bool, + hints: &ColumnSqueezeHints, ) -> Arc { - let rewritten = plan - .transform_up(|node| try_optimize_parquet_source(node, cache, eager_shredding, None)) - .unwrap(); - rewritten.data + plan.transform_up( + |node| match convert_parquet_scan(&node, cache, hints.clone()) { + Some(new_node) => Ok(Transformed::new( + new_node, + true, + TreeNodeRecursion::Continue, + )), + None => Ok(Transformed::no(node)), + }, + ) + .unwrap() + .data } -fn try_optimize_parquet_source( +/// Rewrite the data source plan to use liquid cache (no squeeze hints). +pub fn rewrite_data_source_plan( plan: Arc, cache: &LiquidCacheParquetRef, - eager_shredding: bool, - max_scan_bytes: Option, -) -> Result>, datafusion::error::DataFusionError> { - let any_plan = plan.as_any(); - if let Some(data_source_exec) = any_plan.downcast_ref::() - && let Some((file_scan_config, parquet_source)) = - data_source_exec.downcast_to_file_source::() - { - // Skip caching if the scan's total file size exceeds the threshold. - if let Some(max_bytes) = max_scan_bytes { - let total: u64 = file_scan_config - .file_groups - .iter() - .flat_map(|g| g.files()) - .map(|f| f.object_meta.size) - .sum(); - if total > max_bytes { - log::info!( - "Skipping LiquidCache for scan with total size {} bytes (threshold: {} bytes)", - total, - max_bytes - ); - return Ok(Transformed::no(plan)); - } - } - - let mut new_config = file_scan_config.clone(); - - let mut new_source = - LiquidParquetSource::from_parquet_source(parquet_source.clone(), cache.clone()); - if let Some(expr_adapter_factory) = file_scan_config.expr_adapter_factory.as_ref() { - let new_schema = enrich_source_schema( - file_scan_config.file_schema(), - expr_adapter_factory, - eager_shredding, - ); - let table_partition_cols = new_source.table_schema().table_partition_cols(); - let new_table_schema = - TableSchema::new(Arc::new(new_schema), table_partition_cols.clone()); - new_source = new_source.with_table_schema(new_table_schema); - } - - new_config.file_source = Arc::new(new_source); - let new_file_source: Arc = Arc::new(new_config); - let new_plan = Arc::new(DataSourceExec::new(new_file_source)); - - return Ok(Transformed::new( - new_plan, - true, - TreeNodeRecursion::Continue, - )); - } - Ok(Transformed::no(plan)) -} - -fn enrich_source_schema( - file_schema: &SchemaRef, - expr_adapter_factory: &Arc, - eager_shredding: bool, -) -> Schema { - let mut new_fields = vec![]; - for field in file_schema.fields() { - if let Some(annotation) = metadata_from_factory(expr_adapter_factory, field.name()) { - new_fields.push(process_field_annotation(field, annotation, eager_shredding)); - } else { - new_fields.push(field.clone()); - } - } - Schema::new(new_fields) -} - -fn process_field_annotation( - field: &Arc, - annotation: ColumnAnnotation, - eager_shredding: bool, -) -> Arc { - let mut field_metadata = field.metadata().clone(); - let mut updated_field = Field::clone(field.as_ref()); - match annotation { - ColumnAnnotation::DatePart(unit) => { - field_metadata.insert( - DATE_MAPPING_METADATA_KEY.to_string(), - serialize_date_part(&unit), - ); - } - ColumnAnnotation::VariantPaths(paths) => { - if eager_shredding { - if let Some(serialized) = serialize_variant_mappings(&paths) { - field_metadata.insert(VARIANT_MAPPING_METADATA_KEY.to_string(), serialized); - } - updated_field = enrich_variant_field_type(&updated_field, &paths); - } - } - ColumnAnnotation::SubstringSearch => { - field_metadata.insert( - STRING_FINGERPRINT_METADATA_KEY.to_string(), - "substring".into(), - ); - } - } - Arc::new(updated_field.with_metadata(field_metadata)) -} - -pub(crate) fn enrich_variant_field_type(field: &Field, fields: &[VariantField]) -> Field { - let typed_specs: Vec<&VariantField> = fields - .iter() - .filter(|field| field.data_type.is_some()) - .collect(); - if typed_specs.is_empty() { - return Field::clone(field); - } - - let new_type = match field.data_type() { - DataType::Struct(children) => { - let mut rewritten = Vec::with_capacity(children.len() + 1); - let mut replaced = false; - for child in children.iter() { - if child.name() == "typed_value" { - rewritten.push(build_variant_typed_value_field( - Some(child.as_ref()), - &typed_specs, - )); - replaced = true; - } else { - let mut child_field = child.as_ref().clone(); - if child_field.name() == "value" { - child_field = - Field::new(child_field.name(), child_field.data_type().clone(), true) - .with_metadata(child_field.metadata().clone()); - } - rewritten.push(Arc::new(child_field)); - } - } - if !replaced { - rewritten.push(build_variant_typed_value_field(None, &typed_specs)); - } - DataType::Struct(Fields::from(rewritten)) - } - other => other.clone(), - }; - Field::clone(field).with_data_type(new_type) +) -> Arc { + rewrite_data_source_plan_with_hints(plan, cache, &ColumnSqueezeHints::default()) } -pub(crate) fn enrich_schema_for_cache(schema: &SchemaRef) -> SchemaRef { - let mut fields = vec![]; - for field in schema.fields() { - let new_field = if let Some(mappings) = variant_mappings_from_field(field.as_ref()) { - Arc::new(enrich_variant_field_type(field.as_ref(), &mappings)) - } else { - field.clone() - }; - fields.push(new_field); - } - Arc::new(Schema::new(fields)) +/// Total size in bytes of all files scanned by a parquet `DataSourceExec`, or +/// `None` if `node` is not such a scan. +fn parquet_scan_total_bytes(node: &Arc) -> Option { + let data_source_exec = node.downcast_ref::()?; + let (file_scan_config, _) = data_source_exec.downcast_to_file_source::()?; + Some( + file_scan_config + .file_groups + .iter() + .flat_map(|g| g.files()) + .map(|f| f.object_meta.size) + .sum(), + ) } -fn build_variant_typed_value_field( - existing: Option<&Field>, - specs: &[&VariantField], -) -> Arc { - let mut schema = VariantSchema::new(existing); - for spec in specs { - if let Some(data_type) = spec.data_type.as_ref() { - schema.insert_path(&spec.path, data_type); - } - } - - Arc::new(Field::new( - "typed_value", - DataType::Struct(Fields::from(schema.typed_fields())), - true, - )) +/// If `node` is a `DataSourceExec` over a `ParquetSource`, return an equivalent +/// node backed by [`LiquidParquetSource`] carrying `hints`. +fn convert_parquet_scan( + node: &Arc, + cache: &LiquidCacheParquetRef, + hints: ColumnSqueezeHints, +) -> Option> { + let data_source_exec = node.downcast_ref::()?; + let (file_scan_config, parquet_source) = + data_source_exec.downcast_to_file_source::()?; + + let new_source = + LiquidParquetSource::from_parquet_source(parquet_source.clone(), cache.clone()) + .with_squeeze_hints(Arc::new(hints)); + + let mut new_config = file_scan_config.clone(); + new_config.file_source = Arc::new(new_source); + let new_file_source: Arc = Arc::new(new_config); + Some(Arc::new(DataSourceExec::new(new_file_source))) } #[cfg(test)] @@ -371,6 +183,7 @@ mod tests { LiquidCacheParquet::new( 8192, 1000000, + usize::MAX, store, Box::new(LiquidPolicy::new()), Box::new(TranscodeSqueezeEvict), @@ -378,19 +191,16 @@ mod tests { ) .await, ); - let rewritten = rewrite_data_source_plan(plan, &liquid_cache, true); + let rewritten = rewrite_data_source_plan(plan, &liquid_cache); rewritten .apply(|node| { - if let Some(plan) = node.as_any().downcast_ref::() { + if let Some(plan) = node.downcast_ref::() { let data_source = plan.data_source(); - let any_source = data_source.as_any(); - let source = any_source.downcast_ref::().unwrap(); + let source = data_source.downcast_ref::().unwrap(); let file_source = source.file_source(); - let any_file_source = file_source.as_any(); - let _parquet_source = any_file_source - .downcast_ref::() - .unwrap(); + let _parquet_source = + file_source.downcast_ref::().unwrap(); let schema = source.file_schema().as_ref(); assert_eq!(schema, expected_schema.as_ref()); } @@ -416,4 +226,86 @@ mod tests { let plan = df.create_physical_plan().await.unwrap(); rewrite_plan_inner(plan.clone()).await; } + + async fn build_cache() -> LiquidCacheParquetRef { + let tmp_dir = tempfile::tempdir().unwrap(); + let store = t4::mount(tmp_dir.path().join("liquid_cache.t4")) + .await + .unwrap(); + Arc::new( + LiquidCacheParquet::new( + 8192, + 1000000, + usize::MAX, + store, + Box::new(LiquidPolicy::new()), + Box::new(TranscodeSqueezeEvict), + Box::new(AlwaysHydrate::new()), + ) + .await, + ) + } + + /// True if any parquet scan in `plan` was rewritten to `LiquidParquetSource`. + fn has_liquid_source(plan: &Arc) -> bool { + let mut found = false; + plan.apply(|node| { + if let Some(exec) = node.downcast_ref::() + && let Some(cfg) = exec.data_source().downcast_ref::() + && cfg + .file_source() + .downcast_ref::() + .is_some() + { + found = true; + return Ok(TreeNodeRecursion::Stop); + } + Ok(TreeNodeRecursion::Continue) + }) + .unwrap(); + found + } + + /// A parquet scan whose total file size exceeds `max_scan_bytes` is left as + /// a plain `ParquetSource`; one under the threshold is still wrapped in + /// `LiquidParquetSource`. + #[tokio::test] + async fn test_max_scan_bytes_pass_through() { + let ctx = SessionContext::new(); + ctx.register_parquet( + "nano_hits", + "../../examples/nano_hits.parquet", + Default::default(), + ) + .await + .unwrap(); + let plan = ctx + .sql("SELECT * FROM nano_hits WHERE \"URL\" like 'https://%' limit 10") + .await + .unwrap() + .create_physical_plan() + .await + .unwrap(); + let config = ConfigOptions::default(); + + // Threshold below the file size → scan bypasses the cache. + let capped = LocalModeOptimizer::new(build_cache().await) + .with_max_scan_bytes(1) + .optimize(plan.clone(), &config) + .unwrap(); + assert!( + !has_liquid_source(&capped), + "oversized scan should stay a plain ParquetSource" + ); + + // Threshold above the file size → scan is cached as usual. + let uncapped = LocalModeOptimizer::new(build_cache().await) + .with_max_scan_bytes(u64::MAX) + .optimize(plan, &config) + .unwrap(); + assert!( + has_liquid_source(&uncapped), + "under-threshold scan should be wrapped in LiquidParquetSource" + ); + } } diff --git a/src/datafusion/src/optimizers/squeeze_hint.rs b/src/datafusion/src/optimizers/squeeze_hint.rs new file mode 100644 index 00000000..77c75007 --- /dev/null +++ b/src/datafusion/src/optimizers/squeeze_hint.rs @@ -0,0 +1,800 @@ +//! Physical-plan lineage analysis for squeeze hints. +//! +//! This replaces the old logical [`LineageOptimizer`](super) + global +//! `Arc::as_ptr` registry + field-metadata-string machinery. We analyze the +//! *physical* plan directly: for every parquet scan we look at how each of its +//! output columns is consumed by the operators above it (and by the scan's own +//! pushed-down projection/filter), and we derive a typed [`CacheExpression`] +//! per file column describing the cheapest faithful squeeze. +//! +//! Working on the physical plan buys two things over the previous logical +//! approach: +//! +//! * physical columns are positional, so two join inputs that both expose a +//! column named `date` can never collide (the old name-keyed metadata map +//! did); and +//! * the result is a plain value we can hand straight to the scan's +//! [`LiquidParquetSource`](crate::LiquidParquetSource) — no out-of-band +//! registry, no stringly schema metadata, and the same code runs in local +//! mode (full plan) and on the cache server (deserialized fragment). +//! +//! The analysis is deliberately *conservative*: any column that reaches an +//! operator we do not understand is treated as "used raw", which suppresses its +//! hint. Emitting no hint only costs a missed optimization; emitting a wrong +//! hint would let the cache drop data the query still needs. + +use std::collections::HashMap; +use std::str::FromStr; + +use arrow_schema::DataType; +use datafusion::common::ScalarValue; +use datafusion::common::tree_node::{Transformed, TreeNode, TreeNodeRecursion}; +use datafusion::datasource::physical_plan::{FileScanConfig, FileSource, ParquetSource}; +use datafusion::datasource::source::DataSourceExec; +use datafusion::logical_expr::JoinType; +use datafusion::physical_expr::Partitioning; +use datafusion::physical_expr::ScalarFunctionExpr; +use datafusion::physical_plan::ExecutionPlan; +use datafusion::physical_plan::PhysicalExpr; +use datafusion::physical_plan::aggregates::AggregateExec; +use datafusion::physical_plan::coalesce_partitions::CoalescePartitionsExec; +use datafusion::physical_plan::expressions::{CastExpr, Column, LikeExpr, Literal}; +use datafusion::physical_plan::filter::FilterExec; +use datafusion::physical_plan::joins::HashJoinExec; +use datafusion::physical_plan::limit::{GlobalLimitExec, LocalLimitExec}; +use datafusion::physical_plan::projection::ProjectionExec; +use datafusion::physical_plan::repartition::RepartitionExec; +use datafusion::physical_plan::sorts::sort::SortExec; +use datafusion::physical_plan::sorts::sort_preserving_merge::SortPreservingMergeExec; +use liquid_cache::cache::CacheExpression; +use liquid_cache::liquid_array::Date32Field; + +use crate::cache::ColumnSqueezeHints; + +/// Stable identity of a node within a single analysis pass: the data address of +/// its `Arc`. Valid only for the lifetime of one [`HintAnalyzer::analyze`] call +/// (no clones happen between analysis and the rewrite that consumes it), unlike +/// the old cross-process `Arc::as_ptr` registry. +type NodePtr = usize; + +fn node_ptr(plan: &std::sync::Arc) -> NodePtr { + std::sync::Arc::as_ptr(plan) as *const () as usize +} + +/// One operation applied to a base column on its way up the plan. +#[derive(Debug, Clone, PartialEq, Eq)] +enum Op { + Extract(Date32Field), + Variant { + path: String, + data_type: Option, + }, + Substring, + /// Any other consumption (arithmetic, comparison, cast, unknown op, …). + Other, +} + +/// Lineage of one value: which scan column it ultimately came from, and the +/// chain of operations applied to it (innermost first). +#[derive(Debug, Clone)] +struct Usage { + scan: NodePtr, + col: usize, + ops: Vec, +} + +/// Per-node lineage: for each output column, the set of base-column usages that +/// flow into it. Indexed by the node's output schema position. +type LineageMap = Vec>; + +/// Accumulated op chains observed for a single (scan, file column). +#[derive(Default)] +struct ColumnStats { + usages: Vec>, +} + +/// Analyzes a physical plan and produces, per parquet scan, the typed squeeze +/// hint for each of its file columns. +#[derive(Default)] +pub(crate) struct HintAnalyzer { + /// (scan ptr, file column index) -> observed op chains. + stats: HashMap<(NodePtr, usize), ColumnStats>, + /// scan ptr -> file column names and types, for resolving hints at the end. + scan_columns: HashMap>, +} + +/// Squeeze hints derived from a physical plan, keyed by the analyzed scan +/// nodes. Valid only for the plan it was analyzed from (keyed by `Arc` identity). +/// +/// Local mode consumes this directly during the parquet-scan rewrite; the Flight +/// client uses [`Self::for_fragment`] to extract the hints for the single scan +/// inside each pushed-down fragment so they can be shipped to the cache server. +pub struct SqueezeHintMap { + per_scan: HashMap, +} + +impl SqueezeHintMap { + /// Analyze a physical plan and derive per-scan squeeze hints. + pub fn analyze(plan: &std::sync::Arc) -> Self { + Self { + per_scan: HintAnalyzer::analyze(plan), + } + } + + /// Whether any scan in the analyzed plan produced a hint. + pub fn is_empty(&self) -> bool { + self.per_scan.is_empty() + } + + /// Merge the hints for every parquet scan reachable under `fragment`. + /// + /// Pushed-down fragments are single-scan, so this returns that scan's hints; + /// `fragment` must be a node from the same plan this map was analyzed from. + pub fn for_fragment(&self, fragment: &std::sync::Arc) -> ColumnSqueezeHints { + let mut merged = ColumnSqueezeHints::default(); + fragment + .apply(|node| { + if let Some(hints) = self.per_scan.get(&node_ptr(node)) { + for (name, expr) in hints { + merged.insert(name.clone(), expr.clone()); + } + } + Ok(TreeNodeRecursion::Continue) + }) + .unwrap(); + merged + } +} + +impl HintAnalyzer { + /// Analyze `plan` and return, keyed by scan node pointer, the squeeze hints + /// for that scan's file columns. + pub(crate) fn analyze( + plan: &std::sync::Arc, + ) -> HashMap { + let mut analyzer = HintAnalyzer::default(); + let root = analyzer.visit(plan); + // Columns that escape the top of the analyzed plan (returned to the + // caller / user) are, by definition, consumed as-is. + analyzer.record_escape(&root); + analyzer.finish() + } + + fn record(&mut self, usages: &[Usage]) { + for usage in usages { + self.stats + .entry((usage.scan, usage.col)) + .or_default() + .usages + .push(usage.ops.clone()); + } + } + + /// Record `usages` as escaping (used exactly as-is, no extra op). + fn record_escape(&mut self, map: &LineageMap) { + for column in map { + self.record(column); + } + } + + /// Record every base column reachable through `map` as used opaquely, so no + /// hint survives for a column flowing into an operator we don't model. + fn record_opaque(&mut self, map: &LineageMap) { + for column in map { + for usage in column { + let mut ops = usage.ops.clone(); + ops.push(Op::Other); + self.stats + .entry((usage.scan, usage.col)) + .or_default() + .usages + .push(ops); + } + } + } + + fn visit(&mut self, plan: &std::sync::Arc) -> LineageMap { + if let Some(dse) = plan.downcast_ref::() { + if let Some((cfg, parquet)) = dse.downcast_to_file_source::() { + return self.visit_scan(plan, parquet, cfg); + } + return opaque(plan); + } + + if let Some(proj) = plan.downcast_ref::() { + let child = self.visit(proj.input()); + return proj + .expr() + .iter() + .map(|pe| lineage_for_expr(&pe.expr, &child)) + .collect(); + } + + if let Some(filter) = plan.downcast_ref::() { + let child = self.visit(filter.input()); + let usages = lineage_for_expr(filter.predicate(), &child); + self.record(&usages); + return child; + } + + if let Some(agg) = plan.downcast_ref::() { + let child = self.visit(agg.input()); + for expr in agg.group_expr().input_exprs() { + let usages = lineage_for_expr(&expr, &child); + self.record(&usages); + } + for aggr in agg.aggr_expr() { + for expr in aggr.expressions() { + let usages = lineage_for_expr(&expr, &child); + self.record(&usages); + } + } + return opaque(plan); + } + + if let Some(sort) = plan.downcast_ref::() { + let child = self.visit(sort.input()); + for sort_expr in sort.expr().iter() { + let usages = lineage_for_expr(&sort_expr.expr, &child); + self.record(&usages); + } + return child; + } + + if let Some(repart) = plan.downcast_ref::() { + let child = self.visit(repart.input()); + if let Partitioning::Hash(exprs, _) = repart.partitioning() { + for expr in exprs { + let usages = lineage_for_expr(expr, &child); + self.record(&usages); + } + } + return child; + } + + // Schema-preserving passthroughs: the child lineage flows up unchanged. + // (Batch coalescing is handled inside arrow-rs in DF 54, so there is no + // dedicated CoalesceBatchesExec to model here.) + if plan.downcast_ref::().is_some() + || plan.downcast_ref::().is_some() + || plan.downcast_ref::().is_some() + || plan.downcast_ref::().is_some() + { + let children = plan.children(); + if children.len() == 1 { + return self.visit(children[0]); + } + } + + if let Some(join) = plan.downcast_ref::() { + return self.visit_hash_join(plan, join); + } + + // Unknown operator: analyze children but treat every column they expose + // as used opaquely, then expose nothing upward. + for child in plan.children() { + let child_map = self.visit(child); + self.record_opaque(&child_map); + } + opaque(plan) + } + + fn visit_scan( + &mut self, + plan: &std::sync::Arc, + parquet: &ParquetSource, + _cfg: &FileScanConfig, + ) -> LineageMap { + let ptr = node_ptr(plan); + let table_schema = parquet.table_schema(); + let file_schema = table_schema.file_schema(); + let file_field_count = file_schema.fields().len(); + let table_field_count = table_schema.table_schema().fields().len(); + + self.scan_columns.insert( + ptr, + file_schema + .fields() + .iter() + .map(|f| (f.name().to_string(), f.data_type().clone())) + .collect(), + ); + + // Base lineage in terms of the *table* schema (file columns first, then + // partition columns). Partition columns are not cached, so they carry + // no base usage. + let base: LineageMap = (0..table_field_count) + .map(|i| { + if i < file_field_count { + vec![Usage { + scan: ptr, + col: i, + ops: Vec::new(), + }] + } else { + Vec::new() + } + }) + .collect(); + + // A pushed-down filter consumes columns directly at the scan (with + // filter pushdown enabled, `WHERE col LIKE '%x%'` lives here rather than + // in a FilterExec above). Record those usages so substring searches are + // detected and columns used in other predicates are not wrongly squeezed. + if let Some(predicate) = parquet.filter() { + let usages = lineage_for_expr(&predicate, &base); + self.record(&usages); + } + + // Apply the scan's pushed-down projection (which may itself contain + // date_part/variant_get), if any. + let out = match parquet.projection() { + Some(projection) => projection + .as_ref() + .iter() + .map(|pe| lineage_for_expr(&pe.expr, &base)) + .collect::(), + None => base, + }; + + // Guard: the lineage map must align with the node's output schema, or + // parent operators would index the wrong columns. If it doesn't, fall + // back to opaque (no hints) rather than risk a wrong attribution. + if out.len() != plan.schema().fields().len() { + return opaque(plan); + } + out + } + + fn visit_hash_join( + &mut self, + plan: &std::sync::Arc, + join: &HashJoinExec, + ) -> LineageMap { + let left = self.visit(join.left()); + let right = self.visit(join.right()); + + // Join keys consume their columns as-is. + for (l, r) in join.on() { + let lu = lineage_for_expr(l, &left); + self.record(&lu); + let ru = lineage_for_expr(r, &right); + self.record(&ru); + } + + // Only equi-joins whose output is a straight concatenation of the two + // inputs, and that carry no residual filter, pass lineage through. Any + // other shape (semi/anti/mark joins, a residual filter we don't map) + // is treated opaquely. + let passthrough = join.filter().is_none() + && matches!( + join.join_type(), + JoinType::Inner | JoinType::Left | JoinType::Right | JoinType::Full + ); + + if passthrough { + let mut out = left; + out.extend(right); + if out.len() == plan.schema().fields().len() { + return out; + } + // Width mismatch: fall through to opaque handling below. + self.record_opaque(&out); + return opaque(plan); + } + + self.record_opaque(&left); + self.record_opaque(&right); + opaque(plan) + } + + fn finish(self) -> HashMap { + let mut per_scan: HashMap = HashMap::new(); + + for ((scan, col), stats) in &self.stats { + let Some(columns) = self.scan_columns.get(scan) else { + continue; + }; + let Some((name, data_type)) = columns.get(*col) else { + continue; + }; + if let Some(expr) = derive_hint(data_type, &stats.usages) { + per_scan + .entry(*scan) + .or_default() + .insert(name.clone(), std::sync::Arc::new(expr)); + } + } + + per_scan + } +} + +/// An output column with no derivable base lineage (each entry empty). +fn opaque(plan: &std::sync::Arc) -> LineageMap { + vec![Vec::new(); plan.schema().fields().len()] +} + +/// Compute the lineage of a physical expression against the lineage of its +/// input columns. Mirrors the logical analyzer's per-expression rules. +fn lineage_for_expr(expr: &std::sync::Arc, input: &LineageMap) -> Vec { + if let Some(column) = expr.downcast_ref::() { + return input.get(column.index()).cloned().unwrap_or_default(); + } + + if let Some(sf) = expr.downcast_ref::() { + let name = sf.fun().name(); + let args = sf.args(); + if name.eq_ignore_ascii_case("date_part") + && args.len() == 2 + && let Some(field) = literal_date_field(&args[0]) + { + let mut usages = lineage_for_expr(&args[1], input); + for usage in &mut usages { + usage.ops.push(Op::Extract(field)); + } + return usages; + } + if name.eq_ignore_ascii_case("variant_get") + && (args.len() == 2 || args.len() == 3) + && let Some(path) = literal_utf8(&args[1]) + { + let data_type = args.get(2).and_then(literal_data_type); + let mut usages = lineage_for_expr(&args[0], input); + for usage in &mut usages { + usage.ops.push(Op::Variant { + path: path.clone(), + data_type: data_type.clone(), + }); + } + return usages; + } + return propagate_other(expr, input); + } + + if let Some(like) = expr.downcast_ref::() { + if !like.case_insensitive() + && let Some(pattern) = literal_utf8(like.pattern()) + && is_substring_pattern(pattern.as_bytes()) + { + let mut usages = lineage_for_expr(like.expr(), input); + for usage in &mut usages { + usage.ops.push(Op::Substring); + } + return usages; + } + return propagate_other(expr, input); + } + + if let Some(cast) = expr.downcast_ref::() { + let mut usages = lineage_for_expr(cast.expr(), input); + for usage in &mut usages { + usage.ops.push(Op::Other); + } + return usages; + } + + if expr.downcast_ref::().is_some() { + return Vec::new(); + } + + propagate_other(expr, input) +} + +/// Default propagation: any base column reached through `expr`'s children is +/// consumed via some unmodelled operation, recorded as [`Op::Other`]. +fn propagate_other(expr: &std::sync::Arc, input: &LineageMap) -> Vec { + let mut combined = Vec::new(); + for child in expr.children() { + let mut usages = lineage_for_expr(child, input); + for usage in &mut usages { + usage.ops.push(Op::Other); + } + combined.extend(usages); + } + combined +} + +/// Decide the squeeze hint for one file column from its observed op chains. +fn derive_hint(data_type: &DataType, usages: &[Vec]) -> Option { + if usages.is_empty() { + return None; + } + if let Some(expr) = derive_date(data_type, usages) { + return Some(expr); + } + if let Some(expr) = derive_variant(usages) { + return Some(expr); + } + if let Some(expr) = derive_substring(data_type, usages) { + return Some(expr); + } + None +} + +fn derive_date(data_type: &DataType, usages: &[Vec]) -> Option { + if !is_date_part_type(data_type) { + return None; + } + let mut fields = Vec::new(); + for chain in usages { + // Take the leading run of Extract ops; the column must never be used + // any other way (including raw passthrough). + let mut leading = Vec::new(); + for op in chain { + match op { + Op::Extract(field) => leading.push(*field), + _ => break, + } + } + if leading.is_empty() { + return None; + } + fields.extend(leading); + } + CacheExpression::extract_date32_many(fields) +} + +fn derive_variant(usages: &[Vec]) -> Option { + let mut requests: Vec<(String, DataType)> = Vec::new(); + let mut seen: HashMap> = HashMap::new(); + let mut saw_variant = false; + + for chain in usages { + match chain.first() { + Some(Op::Variant { path, data_type }) => { + saw_variant = true; + match seen.get(path) { + Some(existing) => { + // Conflicting requested types for the same path: bail. + if existing != data_type { + return None; + } + } + None => { + seen.insert(path.clone(), data_type.clone()); + // A variant_get without an explicit type hint cannot be + // squeezed to a typed column; only record typed paths. + if let Some(dt) = data_type { + requests.push((path.clone(), dt.clone())); + } + } + } + } + // Raw passthrough of a variant column does not invalidate the hint: + // the squeezed representation keeps a disk backing for full reads. + None => continue, + _ => return None, + } + } + + if saw_variant && !requests.is_empty() { + Some(CacheExpression::variant_get_many(requests)) + } else { + None + } +} + +fn derive_substring(data_type: &DataType, usages: &[Vec]) -> Option { + if !is_string_type(data_type) { + return None; + } + let mut saw_substring = false; + for chain in usages { + if chain.iter().any(|op| matches!(op, Op::Substring)) { + saw_substring = true; + continue; + } + if !chain.is_empty() { + return None; + } + } + saw_substring.then(CacheExpression::substring_search) +} + +fn literal_utf8(expr: &std::sync::Arc) -> Option { + let literal = expr.downcast_ref::()?; + match literal.value() { + ScalarValue::Utf8(Some(v)) + | ScalarValue::LargeUtf8(Some(v)) + | ScalarValue::Utf8View(Some(v)) => Some(v.clone()), + _ => None, + } +} + +fn literal_data_type(expr: &std::sync::Arc) -> Option { + literal_utf8(expr).and_then(|spec| DataType::from_str(&spec).ok()) +} + +fn literal_date_field(expr: &std::sync::Arc) -> Option { + let text = literal_utf8(expr)?; + let lowered = text.to_ascii_lowercase(); + match lowered.as_str() { + "dow" | "dayofweek" | "day_of_week" => return Some(Date32Field::DayOfWeek), + _ => {} + } + match lowered.as_str() { + "year" => Some(Date32Field::Year), + "month" => Some(Date32Field::Month), + "day" => Some(Date32Field::Day), + _ => None, + } +} + +fn is_substring_pattern(pattern: &[u8]) -> bool { + if pattern.len() < 2 { + return false; + } + if pattern[0] != b'%' || pattern[pattern.len() - 1] != b'%' { + return false; + } + let inner = &pattern[1..pattern.len() - 1]; + if inner.is_empty() { + return false; + } + !inner.iter().any(|b| *b == b'%' || *b == b'_') +} + +fn is_string_type(data_type: &DataType) -> bool { + match data_type { + DataType::Utf8 | DataType::Utf8View | DataType::LargeUtf8 => true, + DataType::Dictionary(_, value_type) => is_string_type(value_type.as_ref()), + _ => false, + } +} + +fn is_date_part_type(data_type: &DataType) -> bool { + matches!(data_type, DataType::Date32 | DataType::Timestamp(_, _)) +} + +/// Converts one parquet scan node, given its derived hints, into its +/// liquid-cache-backed equivalent. Returns `None` for non-parquet nodes. +pub(crate) type ScanConverter<'a> = dyn FnMut( + &std::sync::Arc, + ColumnSqueezeHints, + ) -> Option> + + 'a; + +/// Rewrite every parquet scan in `plan`, attaching the squeeze hints derived for +/// it. `hints` resolves a scan's node pointer to its hints; scans absent from +/// the map get [`ColumnSqueezeHints::default`]. +pub(crate) fn rewrite_with_hints( + plan: std::sync::Arc, + convert: &mut ScanConverter<'_>, + hints: &HashMap, +) -> std::sync::Arc { + plan.transform_up(|node| { + let ptr = node_ptr(&node); + let scan_hints = hints.get(&ptr).cloned().unwrap_or_default(); + if let Some(new_node) = convert(&node, scan_hints) { + Ok(Transformed::new( + new_node, + true, + TreeNodeRecursion::Continue, + )) + } else { + Ok(Transformed::no(node)) + } + }) + .unwrap() + .data +} + +#[cfg(test)] +mod tests { + use super::*; + use arrow::array::{ArrayRef, Date32Array, StringArray, TimestampMicrosecondArray}; + use arrow_schema::{Field, Schema, TimeUnit}; + use datafusion::prelude::{SessionConfig, SessionContext}; + use parquet::arrow::ArrowWriter; + use std::sync::Arc; + + fn write_fixture(path: &std::path::Path) { + let schema = Arc::new(Schema::new(vec![ + Field::new( + "event_ts", + DataType::Timestamp(TimeUnit::Microsecond, None), + false, + ), + Field::new("date", DataType::Date32, false), + Field::new("url", DataType::Utf8, false), + ])); + let timestamps: ArrayRef = Arc::new(TimestampMicrosecondArray::from(vec![ + 1_609_459_200_000_000, + 1_640_995_200_000_000, + ])); + let dates: ArrayRef = Arc::new(Date32Array::from(vec![18_900, 19_000])); + let urls: ArrayRef = Arc::new(StringArray::from(vec![ + "https://example.com/a", + "https://example.com/b", + ])); + let batch = arrow::record_batch::RecordBatch::try_new( + Arc::clone(&schema), + vec![timestamps, dates, urls], + ) + .unwrap(); + let file = std::fs::File::create(path).unwrap(); + let mut writer = ArrowWriter::try_new(file, schema, None).unwrap(); + writer.write(&batch).unwrap(); + writer.close().unwrap(); + } + + async fn hints_for(sql: &str) -> ColumnSqueezeHints { + let mut config = SessionConfig::new(); + // Mirror liquid cache: predicates are pushed into the parquet scan. + config.options_mut().execution.parquet.pushdown_filters = true; + let ctx = SessionContext::new_with_config(config); + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("fixture.parquet"); + write_fixture(&path); + ctx.register_parquet("t", path.to_str().unwrap(), Default::default()) + .await + .unwrap(); + + let df = ctx.sql(sql).await.unwrap(); + let plan = df.create_physical_plan().await.unwrap(); + let map = SqueezeHintMap::analyze(&plan); + // Single-table queries: one scan, so the merged fragment hints are it. + map.for_fragment(&plan) + } + + fn date(fields: &[Date32Field]) -> Arc { + Arc::new(CacheExpression::extract_date32_many(fields.iter().copied()).unwrap()) + } + + #[tokio::test] + async fn extract_single_component() { + let hints = hints_for("SELECT EXTRACT(YEAR FROM date) AS y FROM t").await; + assert_eq!(hints.get("date"), Some(&date(&[Date32Field::Year]))); + } + + #[tokio::test] + async fn extract_multiple_components_are_unioned() { + let hints = + hints_for("SELECT EXTRACT(DAY FROM date) AS d, EXTRACT(MONTH FROM date) AS m FROM t") + .await; + assert_eq!( + hints.get("date"), + Some(&date(&[Date32Field::Month, Date32Field::Day])) + ); + } + + #[tokio::test] + async fn extract_from_timestamp_column() { + let hints = hints_for("SELECT EXTRACT(YEAR FROM event_ts) AS y FROM t").await; + assert_eq!(hints.get("event_ts"), Some(&date(&[Date32Field::Year]))); + } + + #[tokio::test] + async fn raw_column_gets_no_hint() { + let hints = hints_for("SELECT date FROM t").await; + assert_eq!(hints.get("date"), None); + } + + #[tokio::test] + async fn mixed_raw_and_extract_gets_no_hint() { + // `date` escapes raw in the projection, so it cannot be squeezed. + let hints = hints_for("SELECT date, EXTRACT(YEAR FROM date) AS y FROM t").await; + assert_eq!(hints.get("date"), None); + } + + #[tokio::test] + async fn substring_search_in_filter() { + let hints = hints_for("SELECT date FROM t WHERE url LIKE '%example%'").await; + assert_eq!( + hints.get("url").map(|e| e.as_ref()), + Some(&CacheExpression::substring_search()) + ); + } + + #[tokio::test] + async fn anchored_like_is_not_substring() { + let hints = hints_for("SELECT date FROM t WHERE url LIKE 'https://%'").await; + // A prefix LIKE is not a substring search; no substring hint. + assert!( + hints + .get("url") + .is_none_or(|e| !matches!(e.as_ref(), CacheExpression::SubstringSearch)) + ); + } +} diff --git a/src/datafusion/src/reader/plantime/mod.rs b/src/datafusion/src/reader/plantime/mod.rs index 17a2498a..b52a9bcf 100644 --- a/src/datafusion/src/reader/plantime/mod.rs +++ b/src/datafusion/src/reader/plantime/mod.rs @@ -1,3 +1,5 @@ +#[cfg(test)] +pub(crate) use source::CachedMetaReaderFactory; pub use source::LiquidParquetSource; pub(crate) use source::ParquetMetadataCacheReader; diff --git a/src/datafusion/src/reader/plantime/opener.rs b/src/datafusion/src/reader/plantime/opener.rs index 0bf5b7c1..b1b7e836 100644 --- a/src/datafusion/src/reader/plantime/opener.rs +++ b/src/datafusion/src/reader/plantime/opener.rs @@ -1,441 +1,413 @@ -use std::sync::Arc; - -use crate::{ - cache::LiquidCacheParquetRef, - optimizers::enrich_schema_for_cache, - reader::{ - plantime::{row_filter::build_row_filter, row_group_filter::RowGroupAccessPlanFilter}, - runtime::LiquidStreamBuilder, - }, -}; -use ahash::AHashMap; -use arrow::array::{RecordBatch, RecordBatchOptions}; -use arrow_schema::{Field, Schema, SchemaRef}; -use datafusion::{ - common::exec_err, - datasource::{ - listing::PartitionedFile, - physical_plan::{ - FileOpenFuture, FileOpener, ParquetFileMetrics, - parquet::{PagePruningAccessPlanFilter, ParquetAccessPlan}, - }, - table_schema::TableSchema, - }, - error::DataFusionError, - physical_expr::PhysicalExprSimplifier, - physical_expr::projection::ProjectionExprs, - physical_expr::utils::reassign_expr_columns, - physical_expr_adapter::{PhysicalExprAdapterFactory, replace_columns_with_literals}, - physical_expr_common::physical_expr::is_dynamic_physical_expr, - physical_optimizer::pruning::{FilePruner, PruningPredicate, build_pruning_predicate}, - physical_plan::{ - PhysicalExpr, - metrics::{Count, ExecutionPlanMetricsSet, MetricBuilder}, - }, -}; -use futures::StreamExt; -use futures::TryStreamExt; -use log::debug; -use parquet::arrow::{ - ParquetRecordBatchStreamBuilder, ProjectionMask, - arrow_reader::{ArrowReaderMetadata, ArrowReaderOptions}, -}; -use parquet::file::metadata::ParquetMetaData; - -use super::source::CachedMetaReaderFactory; - -pub struct LiquidParquetOpener { - partition_index: usize, - projection: ProjectionExprs, - batch_size: usize, - limit: Option, - predicate: Option>, - table_schema: TableSchema, - metrics: ExecutionPlanMetricsSet, - parquet_file_reader_factory: Arc, - reorder_filters: bool, - liquid_cache: LiquidCacheParquetRef, - expr_adapter_factory: Arc, - span: Option>, -} - -impl LiquidParquetOpener { - #[allow(clippy::too_many_arguments)] - pub fn new( - partition_index: usize, - projection: ProjectionExprs, - batch_size: usize, - limit: Option, - predicate: Option>, - table_schema: TableSchema, - metrics: ExecutionPlanMetricsSet, - liquid_cache: LiquidCacheParquetRef, - parquet_file_reader_factory: Arc, - reorder_filters: bool, - expr_adapter_factory: Arc, - span: Option>, - ) -> Self { - Self { - partition_index, - projection, - batch_size, - limit, - predicate, - table_schema, - metrics, - liquid_cache, - parquet_file_reader_factory, - reorder_filters, - expr_adapter_factory, - span, - } - } -} - -// transfer lineage metadata from tagged schema to dst schema -// The two schema must from the same file. -fn transfer_lineage_metadata_to_file_schema( - tagged_schema: SchemaRef, - dst_schema: SchemaRef, -) -> Schema { - let mut new_fields = vec![]; - - let mut tagged_fields = AHashMap::new(); - for field in tagged_schema.fields().iter() { - tagged_fields.insert(field.name().to_string(), field.clone()); - } - for field in dst_schema.fields().iter() { - let tagged_field = match tagged_fields.get(field.name()) { - Some(tagged_field) => { - let new_field = Field::clone(field).with_metadata(tagged_field.metadata().clone()); - Arc::new(new_field) - } - None => field.clone(), - }; - new_fields.push(tagged_field); - } - let dst_metadata = dst_schema.metadata().clone(); - Schema::new(new_fields).with_metadata(dst_metadata) -} - -impl FileOpener for LiquidParquetOpener { - fn open(&self, partitioned_file: PartitionedFile) -> Result { - let file_range = partitioned_file.range.clone(); - let extensions = partitioned_file.extensions.clone(); - let file_name = partitioned_file.object_meta.location.to_string(); - let file_metrics = ParquetFileMetrics::new(self.partition_index, &file_name, &self.metrics); - - let metadata_size_hint = partitioned_file.metadata_size_hint; - - let lc = self.liquid_cache.clone(); - let file_loc = partitioned_file.object_meta.location.to_string(); - - let mut async_file_reader = self.parquet_file_reader_factory.create_liquid_reader( - self.partition_index, - partitioned_file.clone(), - metadata_size_hint, - &self.metrics, - ); - - let batch_size = self.batch_size; - let logical_file_schema = Arc::clone(self.table_schema.file_schema()); - let output_schema = Arc::new( - self.projection - .project_schema(self.table_schema.table_schema())?, - ); - let mut projection = self.projection.clone(); - let mut predicate = self.predicate.clone(); - let mut literal_columns = std::collections::HashMap::new(); - for (field, value) in self - .table_schema - .table_partition_cols() - .iter() - .zip(partitioned_file.partition_values.iter()) - { - literal_columns.insert(field.name().clone(), value.clone()); - } - if !literal_columns.is_empty() { - projection = projection.try_map_exprs(|expr| { - replace_columns_with_literals(Arc::clone(&expr), &literal_columns) - })?; - predicate = predicate - .map(|p| replace_columns_with_literals(p, &literal_columns)) - .transpose()?; - } - let reorder_predicates = self.reorder_filters; - let limit = self.limit; - - let predicate_creation_errors = - MetricBuilder::new(&self.metrics).global_counter("num_predicate_creation_errors"); - - let expr_adapter_factory = Arc::clone(&self.expr_adapter_factory); - let span = self.span.clone(); - Ok(Box::pin(async move { - // Prune this file using the file level statistics and partition values. - // Since dynamic filters may have been updated since planning it is possible that we are able - // to prune files now that we couldn't prune at planning time. - // It is assumed that there is no point in doing pruning here if the predicate is not dynamic, - // as it would have been done at planning time. - // We'll also check this after every record batch we read, - // and if at some point we are able to prove we can prune the file using just the file level statistics - // we can end the stream early. - let mut file_pruner = predicate - .as_ref() - .filter(|p| is_dynamic_physical_expr(p) || partitioned_file.has_statistics()) - .and_then(|p| { - FilePruner::try_new( - Arc::clone(p), - &logical_file_schema, - &partitioned_file, - predicate_creation_errors.clone(), - ) - }); - - if let Some(file_pruner) = &mut file_pruner - && file_pruner.should_prune()? - { - file_metrics.files_ranges_pruned_statistics.add_pruned(1); - return Ok(futures::stream::empty().boxed()); - } - - file_metrics.files_ranges_pruned_statistics.add_matched(1); - - let mut options = ArrowReaderOptions::new() - .with_page_index_policy(parquet::file::metadata::PageIndexPolicy::Required); - let mut metadata_timer = file_metrics.metadata_load_time.timer(); - - // Begin by loading the metadata from the underlying reader (note - // the returned metadata may actually include page indexes as some - // readers may return page indexes even when not requested -- for - // example when they are cached) - let mut reader_metadata = - ArrowReaderMetadata::load_async(&mut async_file_reader, options.clone()).await?; - - // Note about schemas: we are actually dealing with **3 different schemas** here: - // - The table schema as defined by the TableProvider. - // This is what the user sees, what they get when they `SELECT * FROM table`, etc. - // - The logical file schema: this is the table schema minus any hive partition columns and projections. - // This is what the physical file schema is coerced to. - // - The physical file schema: this is the schema as defined by the parquet file. This is what the parquet file actually contains. - let physical_file_schema = Arc::clone(reader_metadata.schema()); - let physical_file_schema = Arc::new(transfer_lineage_metadata_to_file_schema( - Arc::clone(&logical_file_schema), - Arc::clone(&physical_file_schema), - )); - let cache_full_schema = enrich_schema_for_cache(&physical_file_schema); - options = options.with_schema(Arc::clone(&physical_file_schema)); - reader_metadata = - ArrowReaderMetadata::try_new(Arc::clone(reader_metadata.metadata()), options)?; - debug_assert!( - Arc::strong_count(reader_metadata.metadata()) > 1, - "meta data must be cached already" - ); - - let rewriter = expr_adapter_factory.create( - Arc::clone(&logical_file_schema), - Arc::clone(&physical_file_schema), - )?; - let simplifier = PhysicalExprSimplifier::new(&physical_file_schema); - predicate = predicate - .map(|p| simplifier.simplify(rewriter.rewrite(p)?)) - .transpose()?; - projection = projection.try_map_exprs(|p| simplifier.simplify(rewriter.rewrite(p)?))?; - - let (pruning_predicate, page_pruning_predicate) = build_pruning_predicates( - predicate.as_ref(), - &physical_file_schema, - &predicate_creation_errors, - ); - - metadata_timer.stop(); - - let mut builder = ParquetRecordBatchStreamBuilder::new_with_metadata( - async_file_reader.clone(), - reader_metadata.clone(), - ); - let indices = projection.column_indices(); - let mask = ProjectionMask::roots(builder.parquet_schema(), indices); - - // Filter pushdown: evaluate predicates during scan - let row_filter = predicate.as_ref().and_then(|p| { - let row_filter = build_row_filter( - p, - &physical_file_schema, - reader_metadata.metadata(), - reorder_predicates, - &file_metrics, - ); - - match row_filter { - Ok(Some(filter)) => Some(filter), - Ok(None) => None, - Err(e) => { - debug!("Ignoring error building row filter for '{predicate:?}': {e:?}"); - None - } - } - }); - - // Determine which row groups to actually read. The idea is to skip - // as many row groups as possible based on the metadata and query - let file_metadata: Arc = Arc::clone(builder.metadata()); - let predicate = pruning_predicate.as_ref().map(|p| p.as_ref()); - let rg_metadata = file_metadata.row_groups(); - // track which row groups to actually read - let access_plan = create_initial_plan(&file_name, extensions, rg_metadata.len())?; - let mut row_groups = RowGroupAccessPlanFilter::new(access_plan); - // if there is a range restricting what parts of the file to read - if let Some(range) = file_range.as_ref() { - row_groups.prune_by_range(rg_metadata, range); - } - // If there is a predicate that can be evaluated against the metadata - if let Some(predicate) = predicate.as_ref() { - row_groups.prune_by_statistics( - &physical_file_schema, - builder.parquet_schema(), - rg_metadata, - predicate, - &file_metrics, - ); - - if !row_groups.is_empty() { - row_groups - .prune_by_bloom_filters( - &physical_file_schema, - &mut builder, - predicate, - &file_metrics, - ) - .await; - } - } - - let mut access_plan = row_groups.build(); - - // page index pruning: if all data on individual pages can - // be ruled using page metadata, rows from other columns - // with that range can be skipped as well - if !access_plan.is_empty() - && let Some(p) = page_pruning_predicate - { - access_plan = p.prune_plan_with_page_index( - access_plan, - &physical_file_schema, - builder.parquet_schema(), - file_metadata.as_ref(), - &file_metrics, - ); - } - - let row_group_indexes = access_plan.row_group_indexes(); - let row_selection = access_plan.into_overall_row_selection(rg_metadata)?; - - let mut liquid_builder = - LiquidStreamBuilder::new(async_file_reader, Arc::clone(reader_metadata.metadata())) - .with_batch_size(batch_size) - .with_row_groups(row_group_indexes) - .with_projection(mask) - .with_selection(row_selection) - .with_limit(limit); - - if let Some(row_filter) = row_filter { - liquid_builder = liquid_builder.with_row_filter(row_filter); - } - - if let Some(s) = &span { - let span = fastrace::Span::enter_with_parent("liquid_stream", s); - liquid_builder = liquid_builder.with_span(span); - } - - let liquid_cache = lc.register_or_get_file(file_loc, Arc::clone(&cache_full_schema)); - - let stream = liquid_builder.build(liquid_cache)?; - - let stream_schema = Arc::clone(stream.schema()); - let replace_schema = !stream_schema.eq(&output_schema); - let projection = - projection.try_map_exprs(|expr| reassign_expr_columns(expr, &stream_schema))?; - let projector = projection.make_projector(&stream_schema)?; - - let adapted = stream - .map_err(|e| DataFusionError::External(Box::new(e))) - .map(move |batch| { - batch.and_then(|batch| { - let batch = projector.project_batch(&batch)?; - if replace_schema { - let (_schema, arrays, num_rows) = batch.into_parts(); - let options = RecordBatchOptions::new().with_row_count(Some(num_rows)); - RecordBatch::try_new_with_options( - Arc::clone(&output_schema), - arrays, - &options, - ) - .map_err(Into::into) - } else { - Ok(batch) - } - }) - }); - - Ok(adapted.boxed()) - })) - } -} - -fn create_initial_plan( - file_name: &str, - extensions: Option>, - row_group_count: usize, -) -> Result { - if let Some(extensions) = extensions { - if let Some(access_plan) = extensions.downcast_ref::() { - let plan_len = access_plan.len(); - if plan_len != row_group_count { - return exec_err!( - "Invalid ParquetAccessPlan for {file_name}. Specified {plan_len} row groups, but file has {row_group_count}" - ); - } - - // check row group count matches the plan - return Ok(access_plan.clone()); - } else { - debug!("ParquetExec Ignoring unknown extension specified for {file_name}"); - } - } - - // default to scanning all row groups - Ok(ParquetAccessPlan::new_all(row_group_count)) -} - -pub(crate) fn build_pruning_predicates( - predicate: Option<&Arc>, - file_schema: &SchemaRef, - predicate_creation_errors: &Count, -) -> ( - Option>, - Option>, -) { - let Some(predicate) = predicate.as_ref() else { - return (None, None); - }; - let pruning_predicate = build_pruning_predicate( - Arc::clone(predicate), - file_schema, - predicate_creation_errors, - ); - let page_pruning_predicate = build_page_pruning_predicate(predicate, file_schema); - (pruning_predicate, Some(page_pruning_predicate)) -} - -/// Build a page pruning predicate from an optional predicate expression. -/// If the predicate is None or the predicate cannot be converted to a page pruning -/// predicate, return None. -pub(crate) fn build_page_pruning_predicate( - predicate: &Arc, - file_schema: &SchemaRef, -) -> Arc { - Arc::new(PagePruningAccessPlanFilter::new( - predicate, - Arc::clone(file_schema), - )) -} +use std::sync::Arc; + +use crate::{ + cache::{ColumnSqueezeHints, LiquidCacheParquetRef}, + reader::{ + plantime::{row_filter::build_row_filter, row_group_filter::RowGroupAccessPlanFilter}, + runtime::LiquidStreamBuilder, + }, +}; +use arrow::array::{RecordBatch, RecordBatchOptions}; +use arrow_schema::SchemaRef; +use datafusion::{ + common::exec_err, + datasource::{ + listing::PartitionedFile, + physical_plan::{ + FileOpenFuture, FileOpener, ParquetFileMetrics, + parquet::{PagePruningAccessPlanFilter, ParquetAccessPlan}, + }, + table_schema::TableSchema, + }, + error::DataFusionError, + physical_expr::PhysicalExprSimplifier, + physical_expr::projection::ProjectionExprs, + physical_expr::utils::reassign_expr_columns, + physical_expr_adapter::{PhysicalExprAdapterFactory, replace_columns_with_literals}, + physical_expr_common::physical_expr::is_dynamic_physical_expr, + physical_optimizer::pruning::{FilePruner, PruningPredicate, build_pruning_predicate}, + physical_plan::{ + PhysicalExpr, + metrics::{Count, ExecutionPlanMetricsSet, MetricBuilder}, + }, +}; +use futures::StreamExt; +use futures::TryStreamExt; +use log::debug; +use parquet::arrow::{ + ParquetRecordBatchStreamBuilder, ProjectionMask, + arrow_reader::{ArrowReaderMetadata, ArrowReaderOptions}, +}; +use parquet::file::metadata::ParquetMetaData; + +use super::source::CachedMetaReaderFactory; + +pub struct LiquidParquetOpener { + partition_index: usize, + projection: ProjectionExprs, + batch_size: usize, + limit: Option, + predicate: Option>, + table_schema: TableSchema, + metrics: ExecutionPlanMetricsSet, + parquet_file_reader_factory: Arc, + reorder_filters: bool, + liquid_cache: LiquidCacheParquetRef, + expr_adapter_factory: Arc, + span: Option>, + squeeze_hints: Arc, +} + +impl LiquidParquetOpener { + #[allow(clippy::too_many_arguments)] + pub fn new( + partition_index: usize, + projection: ProjectionExprs, + batch_size: usize, + limit: Option, + predicate: Option>, + table_schema: TableSchema, + metrics: ExecutionPlanMetricsSet, + liquid_cache: LiquidCacheParquetRef, + parquet_file_reader_factory: Arc, + reorder_filters: bool, + expr_adapter_factory: Arc, + span: Option>, + squeeze_hints: Arc, + ) -> Self { + Self { + partition_index, + projection, + batch_size, + limit, + predicate, + table_schema, + metrics, + liquid_cache, + parquet_file_reader_factory, + reorder_filters, + expr_adapter_factory, + span, + squeeze_hints, + } + } +} + +impl FileOpener for LiquidParquetOpener { + fn open(&self, partitioned_file: PartitionedFile) -> Result { + let file_range = partitioned_file.range.clone(); + let access_plan_ext = partitioned_file.extensions.get_arc::(); + let file_name = partitioned_file.object_meta.location.to_string(); + let file_metrics = ParquetFileMetrics::new(self.partition_index, &file_name, &self.metrics); + + let metadata_size_hint = partitioned_file.metadata_size_hint; + + let lc = self.liquid_cache.clone(); + let file_loc = partitioned_file.object_meta.location.to_string(); + + let mut async_file_reader = self.parquet_file_reader_factory.create_liquid_reader( + self.partition_index, + partitioned_file.clone(), + metadata_size_hint, + &self.metrics, + ); + + let batch_size = self.batch_size; + let logical_file_schema = Arc::clone(self.table_schema.file_schema()); + let output_schema = Arc::new( + self.projection + .project_schema(self.table_schema.table_schema())?, + ); + let mut projection = self.projection.clone(); + let mut predicate = self.predicate.clone(); + let mut literal_columns = std::collections::HashMap::new(); + for (field, value) in self + .table_schema + .table_partition_cols() + .iter() + .zip(partitioned_file.partition_values.iter()) + { + literal_columns.insert(field.name().clone(), value.clone()); + } + if !literal_columns.is_empty() { + projection = projection.try_map_exprs(|expr| { + replace_columns_with_literals(Arc::clone(&expr), &literal_columns) + })?; + predicate = predicate + .map(|p| replace_columns_with_literals(p, &literal_columns)) + .transpose()?; + } + let reorder_predicates = self.reorder_filters; + let limit = self.limit; + + let predicate_creation_errors = + MetricBuilder::new(&self.metrics).global_counter("num_predicate_creation_errors"); + + let expr_adapter_factory = Arc::clone(&self.expr_adapter_factory); + let span = self.span.clone(); + let squeeze_hints = Arc::clone(&self.squeeze_hints); + Ok(Box::pin(async move { + // Prune this file using the file level statistics and partition values. + // Since dynamic filters may have been updated since planning it is possible that we are able + // to prune files now that we couldn't prune at planning time. + // It is assumed that there is no point in doing pruning here if the predicate is not dynamic, + // as it would have been done at planning time. + // We'll also check this after every record batch we read, + // and if at some point we are able to prove we can prune the file using just the file level statistics + // we can end the stream early. + let mut file_pruner = predicate + .as_ref() + .filter(|p| is_dynamic_physical_expr(p) || partitioned_file.has_statistics()) + .and_then(|p| { + FilePruner::try_new( + Arc::clone(p), + &logical_file_schema, + &partitioned_file, + predicate_creation_errors.clone(), + ) + }); + + if let Some(file_pruner) = &mut file_pruner + && file_pruner.should_prune()? + { + file_metrics.files_ranges_pruned_statistics.add_pruned(1); + return Ok(futures::stream::empty().boxed()); + } + + file_metrics.files_ranges_pruned_statistics.add_matched(1); + + let mut options = ArrowReaderOptions::new() + .with_page_index_policy(parquet::file::metadata::PageIndexPolicy::Required); + let mut metadata_timer = file_metrics.metadata_load_time.timer(); + + // Begin by loading the metadata from the underlying reader (note + // the returned metadata may actually include page indexes as some + // readers may return page indexes even when not requested -- for + // example when they are cached) + let mut reader_metadata = + ArrowReaderMetadata::load_async(&mut async_file_reader, options.clone()).await?; + + // Note about schemas: we are actually dealing with **3 different schemas** here: + // - The table schema as defined by the TableProvider. + // This is what the user sees, what they get when they `SELECT * FROM table`, etc. + // - The logical file schema: this is the table schema minus any hive partition columns and projections. + // This is what the physical file schema is coerced to. + // - The physical file schema: this is the schema as defined by the parquet file. This is what the parquet file actually contains. + let physical_file_schema = Arc::clone(reader_metadata.schema()); + let cache_full_schema = Arc::clone(&physical_file_schema); + options = options.with_schema(Arc::clone(&physical_file_schema)); + reader_metadata = + ArrowReaderMetadata::try_new(Arc::clone(reader_metadata.metadata()), options)?; + debug_assert!( + Arc::strong_count(reader_metadata.metadata()) > 1, + "meta data must be cached already" + ); + + let rewriter = expr_adapter_factory.create( + Arc::clone(&logical_file_schema), + Arc::clone(&physical_file_schema), + )?; + let simplifier = PhysicalExprSimplifier::new(&physical_file_schema); + predicate = predicate + .map(|p| simplifier.simplify(rewriter.rewrite(p)?)) + .transpose()?; + projection = projection.try_map_exprs(|p| simplifier.simplify(rewriter.rewrite(p)?))?; + + let (pruning_predicate, page_pruning_predicate) = build_pruning_predicates( + predicate.as_ref(), + &physical_file_schema, + &predicate_creation_errors, + ); + + metadata_timer.stop(); + + let mut builder = ParquetRecordBatchStreamBuilder::new_with_metadata( + async_file_reader.clone(), + reader_metadata.clone(), + ); + let indices = projection.column_indices(); + let mask = ProjectionMask::roots(builder.parquet_schema(), indices); + + // Filter pushdown: evaluate predicates during scan + let row_filter = predicate.as_ref().and_then(|p| { + let row_filter = build_row_filter( + p, + &physical_file_schema, + reader_metadata.metadata(), + reorder_predicates, + &file_metrics, + ); + + match row_filter { + Ok(Some(filter)) => Some(filter), + Ok(None) => None, + Err(e) => { + debug!("Ignoring error building row filter for '{predicate:?}': {e:?}"); + None + } + } + }); + + // Determine which row groups to actually read. The idea is to skip + // as many row groups as possible based on the metadata and query + let file_metadata: Arc = Arc::clone(builder.metadata()); + let predicate = pruning_predicate.as_ref().map(|p| p.as_ref()); + let rg_metadata = file_metadata.row_groups(); + // track which row groups to actually read + let access_plan = create_initial_plan(&file_name, access_plan_ext, rg_metadata.len())?; + let mut row_groups = RowGroupAccessPlanFilter::new(access_plan); + // if there is a range restricting what parts of the file to read + if let Some(range) = file_range.as_ref() { + row_groups.prune_by_range(rg_metadata, range); + } + // If there is a predicate that can be evaluated against the metadata + if let Some(predicate) = predicate.as_ref() { + row_groups.prune_by_statistics( + &physical_file_schema, + builder.parquet_schema(), + rg_metadata, + predicate, + &file_metrics, + ); + + if !row_groups.is_empty() { + row_groups + .prune_by_bloom_filters( + &physical_file_schema, + &mut builder, + predicate, + &file_metrics, + ) + .await; + } + } + + let mut access_plan = row_groups.build(); + + // page index pruning: if all data on individual pages can + // be ruled using page metadata, rows from other columns + // with that range can be skipped as well + if !access_plan.is_empty() + && let Some(p) = page_pruning_predicate + { + access_plan = p.prune_plan_with_page_index( + access_plan, + &physical_file_schema, + builder.parquet_schema(), + file_metadata.as_ref(), + &file_metrics, + ); + } + + let row_group_indexes = access_plan.row_group_indexes(); + let row_selection = access_plan.into_overall_row_selection(rg_metadata)?; + + let mut liquid_builder = + LiquidStreamBuilder::new(async_file_reader, Arc::clone(reader_metadata.metadata())) + .with_batch_size(batch_size) + .with_row_groups(row_group_indexes) + .with_projection(mask) + .with_selection(row_selection) + .with_limit(limit); + + if let Some(row_filter) = row_filter { + liquid_builder = liquid_builder.with_row_filter(row_filter); + } + + if let Some(s) = &span { + let span = fastrace::Span::enter_with_parent("liquid_stream", s); + liquid_builder = liquid_builder.with_span(span); + } + + let liquid_cache = lc.register_or_get_file_with_hints( + file_loc, + Arc::clone(&cache_full_schema), + squeeze_hints, + ); + + let stream = liquid_builder.build(liquid_cache)?; + + let stream_schema = Arc::clone(stream.schema()); + let replace_schema = !stream_schema.eq(&output_schema); + let projection = + projection.try_map_exprs(|expr| reassign_expr_columns(expr, &stream_schema))?; + let projector = projection.make_projector(&stream_schema)?; + + let adapted = stream + .map_err(|e| DataFusionError::External(Box::new(e))) + .map(move |batch| { + batch.and_then(|batch| { + let batch = projector.project_batch(&batch)?; + if replace_schema { + let (_schema, arrays, num_rows) = batch.into_parts(); + let options = RecordBatchOptions::new().with_row_count(Some(num_rows)); + RecordBatch::try_new_with_options( + Arc::clone(&output_schema), + arrays, + &options, + ) + .map_err(Into::into) + } else { + Ok(batch) + } + }) + }); + + Ok(adapted.boxed()) + })) + } +} + +fn create_initial_plan( + file_name: &str, + access_plan: Option>, + row_group_count: usize, +) -> Result { + if let Some(access_plan) = access_plan { + let plan_len = access_plan.len(); + if plan_len != row_group_count { + return exec_err!( + "Invalid ParquetAccessPlan for {file_name}. Specified {plan_len} row groups, but file has {row_group_count}" + ); + } + + // check row group count matches the plan + return Ok(access_plan.as_ref().clone()); + } + + // default to scanning all row groups + Ok(ParquetAccessPlan::new_all(row_group_count)) +} + +pub(crate) fn build_pruning_predicates( + predicate: Option<&Arc>, + file_schema: &SchemaRef, + predicate_creation_errors: &Count, +) -> ( + Option>, + Option>, +) { + let Some(predicate) = predicate.as_ref() else { + return (None, None); + }; + let pruning_predicate = build_pruning_predicate( + Arc::clone(predicate), + file_schema, + predicate_creation_errors, + ); + let page_pruning_predicate = build_page_pruning_predicate(predicate, file_schema); + (pruning_predicate, Some(page_pruning_predicate)) +} + +/// Build a page pruning predicate from an optional predicate expression. +/// If the predicate is None or the predicate cannot be converted to a page pruning +/// predicate, return None. +pub(crate) fn build_page_pruning_predicate( + predicate: &Arc, + file_schema: &SchemaRef, +) -> Arc { + Arc::new(PagePruningAccessPlanFilter::new( + predicate, + Arc::clone(file_schema), + )) +} diff --git a/src/datafusion/src/reader/plantime/row_filter.rs b/src/datafusion/src/reader/plantime/row_filter.rs index f895acad..fe9c5ea1 100644 --- a/src/datafusion/src/reader/plantime/row_filter.rs +++ b/src/datafusion/src/reader/plantime/row_filter.rs @@ -361,7 +361,7 @@ impl TreeNodeVisitor<'_> for PushdownChecker<'_> { type Node = Arc; fn f_down(&mut self, node: &Self::Node) -> Result { - if let Some(column) = node.as_any().downcast_ref::() + if let Some(column) = node.downcast_ref::() && let Some(recursion) = self.check_single_column(column.name()) { return Ok(recursion); @@ -499,7 +499,7 @@ pub fn build_row_filter( } fn get_priority(expr: &Arc) -> u8 { - if let Some(binary) = expr.as_any().downcast_ref::() { + if let Some(binary) = expr.downcast_ref::() { match binary.op() { Operator::Eq | Operator::NotEq => 0, // Highest priority Operator::LikeMatch | Operator::ILikeMatch => 1, @@ -507,7 +507,7 @@ fn get_priority(expr: &Arc) -> u8 { Operator::Lt | Operator::LtEq | Operator::Gt | Operator::GtEq => 3, _ => 4, } - } else if expr.as_any().downcast_ref::().is_some() { + } else if expr.is::() { 1 // LIKE expressions } else { 5 // All other expression types diff --git a/src/datafusion/src/reader/plantime/row_group_filter.rs b/src/datafusion/src/reader/plantime/row_group_filter.rs index e0cd5fdc..eb26b34b 100644 --- a/src/datafusion/src/reader/plantime/row_group_filter.rs +++ b/src/datafusion/src/reader/plantime/row_group_filter.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -use arrow::{array::ArrayRef, array::BooleanArray, datatypes::Schema}; +use arrow::{array::ArrayRef, array::BooleanArray, array::UInt64Array, datatypes::Schema}; use datafusion::common::{Column, Result, ScalarValue}; use datafusion::datasource::listing::FileRange; use datafusion::datasource::physical_plan::ParquetFileMetrics; @@ -329,7 +329,7 @@ impl PruningStatistics for BloomFilterStatistics { None } - fn row_counts(&self, _column: &Column) -> Option { + fn row_counts(&self) -> Option { None } @@ -412,13 +412,13 @@ impl PruningStatistics for RowGroupPruningStatistics<'_> { .map(|counts| Arc::new(counts) as ArrayRef) } - fn row_counts(&self, column: &Column) -> Option { - // row counts are the same for all columns in a row group - self.statistics_converter(column) - .and_then(|c| Ok(c.row_group_row_counts(self.metadata_iter())?)) - .ok() - .flatten() - .map(|counts| Arc::new(counts) as ArrayRef) + fn row_counts(&self) -> Option { + // Row counts are container-level — read directly from row group metadata. + let counts: UInt64Array = self + .metadata_iter() + .map(|rg| Some(rg.num_rows() as u64)) + .collect(); + Some(Arc::new(counts) as ArrayRef) } fn contained(&self, _column: &Column, _values: &HashSet) -> Option { diff --git a/src/datafusion/src/reader/plantime/source.rs b/src/datafusion/src/reader/plantime/source.rs index 4efb9788..79d715b7 100644 --- a/src/datafusion/src/reader/plantime/source.rs +++ b/src/datafusion/src/reader/plantime/source.rs @@ -1,5 +1,5 @@ use super::opener::LiquidParquetOpener; -use crate::cache::LiquidCacheParquetRef; +use crate::cache::{ColumnSqueezeHints, LiquidCacheParquetRef}; use ahash::{HashMap, HashMapExt}; use arrow_schema::Schema; use bytes::Bytes; @@ -32,7 +32,6 @@ use parquet::{ file::metadata::{PageIndexPolicy, ParquetMetaData, ParquetMetaDataReader}, }; use std::{ - any::Any, ops::Range, sync::{Arc, LazyLock}, }; @@ -59,9 +58,6 @@ impl CachedMetaReaderFactory { ) -> ParquetMetadataCacheReader { let path = partitioned_file.object_meta.location.clone(); let store = Arc::clone(&self.store); - // Pass file_size to take the size-aware metadata path; parquet 58.1's - // suffix fallback mislabels `remainder_start` as 0 and panics when - // loading page indexes. let mut inner = ParquetObjectReader::new(store, path.clone()) .with_file_size(partitioned_file.object_meta.size); @@ -179,6 +175,7 @@ pub struct LiquidParquetSource { projection: ProjectionExprs, table_schema: TableSchema, span: Option>, + squeeze_hints: Arc, } impl LiquidParquetSource { @@ -202,6 +199,20 @@ impl LiquidParquetSource { } } + /// Attach typed squeeze hints (keyed by file-schema column name) derived + /// from the query plan. These flow to the cache when the file is opened. + pub fn with_squeeze_hints(&self, squeeze_hints: Arc) -> Self { + Self { + squeeze_hints, + ..self.clone() + } + } + + /// The typed squeeze hints currently attached to this source. + pub fn squeeze_hints(&self) -> &Arc { + &self.squeeze_hints + } + /// Set predicate information, also sets pruning_predicate and page_pruning_predicate attributes pub fn with_predicate( mut self, @@ -260,6 +271,7 @@ impl LiquidParquetSource { pruning_predicate: None, page_pruning_predicate: None, span: None, + squeeze_hints: Arc::default(), }; if let Some(predicate) = predicate { @@ -276,10 +288,6 @@ impl LiquidParquetSource { } impl FileSource for LiquidParquetSource { - fn as_any(&self) -> &dyn Any { - self - } - fn create_file_opener( &self, object_store: Arc, @@ -311,6 +319,7 @@ impl FileSource for LiquidParquetSource { self.reorder_filters(), expr_adapter_factory, execution_span.map(Arc::new), + Arc::clone(&self.squeeze_hints), ); Ok(Arc::new(opener)) diff --git a/src/datafusion/src/reader/runtime/liquid_cache_reader.rs b/src/datafusion/src/reader/runtime/liquid_cache_reader.rs index 87c123ec..4a5a6b27 100644 --- a/src/datafusion/src/reader/runtime/liquid_cache_reader.rs +++ b/src/datafusion/src/reader/runtime/liquid_cache_reader.rs @@ -3,16 +3,21 @@ use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; -use arrow::array::{Array, RecordBatch}; +use arrow::array::{Array, ArrayRef, BooleanArray, RecordBatch}; use arrow::buffer::BooleanBuffer; use arrow::compute::prep_null_mask_filter; use arrow::record_batch::RecordBatchOptions; -use arrow_schema::{ArrowError, SchemaRef}; -use futures::{Stream, future::BoxFuture}; -use parquet::arrow::arrow_reader::{RowSelection, RowSelector}; - -use crate::cache::{BatchID, CachedRowGroupRef}; -use crate::reader::plantime::LiquidRowFilter; +use arrow_schema::{ArrowError, Schema, SchemaRef}; +use futures::{Stream, StreamExt, future::BoxFuture, stream::BoxStream}; +use parquet::arrow::arrow_reader::{ + ArrowPredicate, ArrowReaderMetadata, ArrowReaderOptions, RowSelection, RowSelector, +}; +use parquet::arrow::{ParquetRecordBatchStreamBuilder, ProjectionMask}; +use parquet::errors::ParquetError; +use parquet::file::metadata::ParquetMetaData; + +use crate::cache::{BatchID, CachedRowGroupRef, InsertArrowArrayError}; +use crate::reader::plantime::{LiquidRowFilter, ParquetMetadataCacheReader}; use crate::reader::runtime::utils::take_next_batch; use crate::utils::{boolean_buffer_and_then, row_selector_to_boolean_buffer}; @@ -22,7 +27,7 @@ pub(crate) struct LiquidCacheReader { } enum ReaderState { - Ready(LiquidCacheReaderInner), + Ready(Box), Processing( BoxFuture< 'static, @@ -48,27 +53,56 @@ struct LiquidCacheReaderInner { schema: SchemaRef, batch_size: usize, projection_columns: Vec, + parquet_fallback: ParquetFallback, + last_pull: Option<(BatchID, RecordBatch)>, +} + +pub(crate) struct LiquidCacheReaderConfig { + pub(crate) batch_size: usize, + pub(crate) selection: RowSelection, + pub(crate) row_filter: Option, + pub(crate) cached_row_group: CachedRowGroupRef, + pub(crate) projection_columns: Vec, + pub(crate) schema: SchemaRef, + pub(crate) parquet_fallback: ParquetFallbackConfig, +} + +#[derive(Clone)] +pub(crate) struct ParquetFallbackConfig { + pub(crate) row_group_idx: usize, + pub(crate) metadata: Arc, + pub(crate) input: ParquetMetadataCacheReader, + pub(crate) cache_projection: ProjectionMask, + pub(crate) cache_column_ids: Vec, + pub(crate) cache_batch_size: usize, + pub(crate) row_count: usize, +} + +struct ParquetFallback { + row_group_idx: usize, + metadata: Arc, + input: ParquetMetadataCacheReader, + cache_projection: ProjectionMask, + cache_column_ids: Vec, + cache_batch_size: usize, + row_count: usize, + stream: Option>>, + next_batch_id: BatchID, } impl LiquidCacheReader { - pub(crate) fn new( - batch_size: usize, - selection: RowSelection, - row_filter: Option, - cached_row_group: CachedRowGroupRef, - projection_columns: Vec, - schema: SchemaRef, - ) -> Self { + pub(crate) fn new(config: LiquidCacheReaderConfig) -> Self { let inner = LiquidCacheReaderInner::new( - batch_size, - selection, - cached_row_group, - projection_columns, - Arc::clone(&schema), + config.batch_size, + config.selection, + config.cached_row_group, + config.projection_columns, + Arc::clone(&config.schema), + ParquetFallback::new(config.parquet_fallback), ); Self { - state: ReaderState::Ready(inner), - row_filter, + state: ReaderState::Ready(Box::new(inner)), + row_filter: config.row_filter, } } @@ -96,7 +130,7 @@ impl Stream for LiquidCacheReader { } Poll::Ready((inner, row_filter, result)) => { self.row_filter = row_filter; - self.state = ReaderState::Ready(inner); + self.state = ReaderState::Ready(Box::new(inner)); match result { ProcessResult::Emit(item) => return Poll::Ready(Some(item)), ProcessResult::Skip => continue, @@ -106,6 +140,7 @@ impl Stream for LiquidCacheReader { ReaderState::Ready(mut inner) => { match take_next_batch(&mut inner.selection, inner.batch_size) { Some(selection) => { + let inner = *inner; let future = inner.next_batch(self.row_filter.take(), selection); self.state = ReaderState::Processing(future); continue; @@ -125,6 +160,86 @@ impl Stream for LiquidCacheReader { } } +impl ParquetFallback { + fn new(config: ParquetFallbackConfig) -> Self { + Self { + row_group_idx: config.row_group_idx, + metadata: config.metadata, + input: config.input, + cache_projection: config.cache_projection, + cache_column_ids: config.cache_column_ids, + cache_batch_size: config.cache_batch_size, + row_count: config.row_count, + stream: None, + next_batch_id: BatchID::from_raw(0), + } + } + + async fn fetch_batch(&mut self, batch_id: BatchID) -> Result { + if self.stream.is_none() || batch_id != self.next_batch_id { + self.rebuild_stream(batch_id)?; + } + + let stream = self.stream.as_mut().expect("fallback stream is present"); + let record_batch = stream.next().await.transpose()?.ok_or_else(|| { + ParquetError::General(format!( + "parquet fallback ended before batch {}", + *batch_id as usize + )) + })?; + + self.next_batch_id = batch_id; + self.next_batch_id.inc(); + Ok(record_batch) + } + + fn rebuild_stream(&mut self, batch_id: BatchID) -> Result<(), ParquetError> { + let reader_metadata = + ArrowReaderMetadata::try_new(Arc::clone(&self.metadata), ArrowReaderOptions::new())?; + let row_selection = + build_row_selection_from(batch_id, self.cache_batch_size, self.row_count); + + let stream = + ParquetRecordBatchStreamBuilder::new_with_metadata(self.input.clone(), reader_metadata) + .with_projection(self.cache_projection.clone()) + .with_row_groups(vec![self.row_group_idx]) + .with_batch_size(self.cache_batch_size) + .with_row_selection(row_selection) + .build()? + .boxed(); + + self.stream = Some(stream); + self.next_batch_id = batch_id; + Ok(()) + } +} + +fn build_row_selection_from( + batch_id: BatchID, + batch_size: usize, + row_count: usize, +) -> RowSelection { + let start = usize::from(*batch_id) * batch_size; + let mut selectors = Vec::new(); + + if start > 0 { + selectors.push(RowSelector::skip(start.min(row_count))); + } + + if start >= row_count { + return RowSelection::from(selectors); + } + + let mut remaining = row_count - start; + while remaining > 0 { + let selected = remaining.min(batch_size); + selectors.push(RowSelector::select(selected)); + remaining -= selected; + } + + RowSelection::from(selectors) +} + impl LiquidCacheReaderInner { fn new( batch_size: usize, @@ -132,6 +247,7 @@ impl LiquidCacheReaderInner { cached_row_group: CachedRowGroupRef, projection_columns: Vec, schema: SchemaRef, + parquet_fallback: ParquetFallback, ) -> Self { Self { cached_row_group, @@ -140,6 +256,8 @@ impl LiquidCacheReaderInner { schema, batch_size, projection_columns, + parquet_fallback, + last_pull: None, } } @@ -151,6 +269,7 @@ impl LiquidCacheReaderInner { Box::pin(async move { let mut inner = self; let mut row_filter = row_filter; + inner.last_pull = None; let result = match inner .build_predicate_filter(&mut row_filter, selection) @@ -191,7 +310,7 @@ impl LiquidCacheReaderInner { break; } - let boolean_array = self + let boolean_array = match self .cached_row_group .evaluate_selection_with_predicate( self.current_batch_id, @@ -199,7 +318,13 @@ impl LiquidCacheReaderInner { predicate, ) .await - .expect("item must be in cache")?; + { + Some(result) => result?, + None => { + self.evaluate_predicate_after_materialize(&input_selection, predicate) + .await? + } + }; let boolean_mask = if boolean_array.null_count() == 0 { boolean_array.into_parts().0 @@ -215,7 +340,7 @@ impl LiquidCacheReaderInner { #[fastrace::trace] async fn read_from_cache( - &self, + &mut self, selection: &BooleanBuffer, ) -> Result, ArrowError> { let selected_rows = selection.count_set_bits(); @@ -232,7 +357,7 @@ impl LiquidCacheReaderInner { } let mut arrays = Vec::with_capacity(self.projection_columns.len()); - for &column_idx in &self.projection_columns { + for column_idx in self.projection_columns.clone() { let column = self .cached_row_group .get_column(column_idx as u64) @@ -244,13 +369,18 @@ impl LiquidCacheReaderInner { let array = column .get_arrow_array_with_filter(self.current_batch_id, selection) - .await - .ok_or_else(|| { - ArrowError::ComputeError(format!( - "column {column_idx} batch {} not cached", - *self.current_batch_id as usize - )) - })?; + .await; + + let array = match array { + Some(array) => array, + None => { + let record_batch = self + .read_parquet_batch_and_fill_cache(self.current_batch_id) + .await?; + let array = self.parquet_array(&record_batch, column_idx)?; + filter_array(array, selection)? + } + }; arrays.push(array); } @@ -259,6 +389,124 @@ impl LiquidCacheReaderInner { RecordBatch::try_new(self.schema.clone(), arrays).unwrap(), )) } + + async fn read_parquet_batch_and_fill_cache( + &mut self, + batch_id: BatchID, + ) -> Result { + if let Some((pulled_batch_id, record_batch)) = &self.last_pull + && *pulled_batch_id == batch_id + { + return Ok(record_batch.clone()); + } + + let record_batch = self + .parquet_fallback + .fetch_batch(batch_id) + .await + .map_err(|e| ArrowError::ComputeError(format!("parquet fallback read failed: {e}")))?; + + for (col_idx, file_column_id) in self + .parquet_fallback + .cache_column_ids + .iter() + .copied() + .enumerate() + { + let column = self + .cached_row_group + .get_column(file_column_id as u64) + .ok_or_else(|| { + ArrowError::ComputeError(format!( + "column {file_column_id} not present in liquid cache" + )) + })?; + let array = Arc::clone(record_batch.column(col_idx)); + + match column.insert(batch_id, array).await { + Ok(()) | Err(InsertArrowArrayError::AlreadyCached) => {} + Err(InsertArrowArrayError::CacheFull) => {} + } + } + + self.last_pull = Some((batch_id, record_batch.clone())); + Ok(record_batch) + } + + async fn evaluate_predicate_after_materialize( + &mut self, + selection: &BooleanBuffer, + predicate: &mut crate::reader::LiquidPredicate, + ) -> Result { + let record_batch = self + .read_parquet_batch_and_fill_cache(self.current_batch_id) + .await?; + + if let Some(result) = self + .cached_row_group + .evaluate_selection_with_predicate(self.current_batch_id, selection, predicate) + .await + { + return result; + } + + let column_ids = predicate.predicate_column_ids(); + let mut arrays = Vec::with_capacity(column_ids.len()); + let mut fields = Vec::with_capacity(column_ids.len()); + + for column_id in column_ids { + let array = self.parquet_array(&record_batch, column_id)?; + arrays.push(filter_array(array, selection)?); + + let field = self + .cached_row_group + .get_column(column_id as u64) + .ok_or_else(|| { + ArrowError::ComputeError(format!( + "column {column_id} not present in liquid cache" + )) + })? + .field() + .as_ref() + .clone(); + fields.push(field); + } + + let schema = Arc::new(Schema::new(fields)); + let predicate_batch = if arrays.is_empty() { + let options = + RecordBatchOptions::new().with_row_count(Some(selection.count_set_bits())); + RecordBatch::try_new_with_options(schema, arrays, &options)? + } else { + RecordBatch::try_new(schema, arrays)? + }; + + predicate.evaluate(predicate_batch) + } + + fn parquet_array( + &self, + record_batch: &RecordBatch, + file_column_id: usize, + ) -> Result { + let position = self + .parquet_fallback + .cache_column_ids + .iter() + .position(|column_id| *column_id == file_column_id) + .ok_or_else(|| { + ArrowError::ComputeError(format!( + "column {file_column_id} not present in parquet fallback projection" + )) + })?; + + Ok(Arc::clone(record_batch.column(position))) + } +} + +fn filter_array(array: ArrayRef, selection: &BooleanBuffer) -> Result { + let selection_array = BooleanArray::new(selection.clone(), None); + arrow::compute::filter(array.as_ref(), &selection_array) } #[cfg(test)] @@ -266,11 +514,14 @@ mod tests { use super::*; use crate::{ cache::LiquidCacheParquet, + reader::plantime::CachedMetaReaderFactory, reader::{FilterCandidateBuilder, LiquidPredicate, LiquidRowFilter}, }; use arrow::array::{ArrayRef, Int32Array}; use arrow::record_batch::RecordBatch; use arrow_schema::{DataType, Field, Schema, SchemaRef}; + use datafusion::datasource::listing::PartitionedFile; + use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet; use datafusion::{ logical_expr::Operator, physical_expr::PhysicalExpr, @@ -280,31 +531,90 @@ mod tests { use futures::{StreamExt, pin_mut}; use liquid_cache::cache::{AlwaysHydrate, squeeze_policies::Evict}; use liquid_cache::cache_policies::LiquidPolicy; + use object_store::local::LocalFileSystem; use parquet::arrow::{ - ArrowWriter, + ArrowWriter, ProjectionMask, arrow_reader::{ArrowReaderMetadata, ArrowReaderOptions, RowSelection, RowSelector}, }; + use std::fs::File; use std::sync::Arc; - async fn make_row_group( + struct TestRowGroup { batch_size: usize, - batches: &[Vec], - ) -> (CachedRowGroupRef, SchemaRef) { + row_group: CachedRowGroupRef, + schema: SchemaRef, + fallback: ParquetFallbackConfig, + _tmp_dir: tempfile::TempDir, + } + + struct ReaderRequest { + selection: RowSelection, + row_filter: Option, + projection_columns: Vec, + schema: SchemaRef, + } + + impl TestRowGroup { + fn reader(&self, request: ReaderRequest) -> LiquidCacheReader { + LiquidCacheReader::new(LiquidCacheReaderConfig { + batch_size: self.batch_size, + selection: request.selection, + row_filter: request.row_filter, + cached_row_group: Arc::clone(&self.row_group), + projection_columns: request.projection_columns, + schema: request.schema, + parquet_fallback: self.fallback.clone(), + }) + } + } + + async fn make_row_group(batch_size: usize, batches: &[Vec]) -> TestRowGroup { let tmp_dir = tempfile::tempdir().unwrap(); + let field = Arc::new(Field::new("col0", DataType::Int32, false)); + let schema = Arc::new(Schema::new(vec![field.clone()])); + let parquet_path = tmp_dir.path().join("data.parquet"); + let file = File::create(&parquet_path).unwrap(); + let mut writer = ArrowWriter::try_new(file, Arc::clone(&schema), None).unwrap(); + for values in batches { + let array: ArrayRef = Arc::new(Int32Array::from(values.clone())); + let batch = RecordBatch::try_new(Arc::clone(&schema), vec![array]).unwrap(); + writer.write(&batch).unwrap(); + } + writer.close().unwrap(); + + let metadata_file = File::open(&parquet_path).unwrap(); + let reader_metadata = + ArrowReaderMetadata::load(&metadata_file, ArrowReaderOptions::new()).unwrap(); + let object_store = Arc::new(LocalFileSystem::new_with_prefix(tmp_dir.path()).unwrap()); + let partitioned_file = PartitionedFile::new( + "data.parquet", + std::fs::metadata(&parquet_path).unwrap().len(), + ); + let metrics = ExecutionPlanMetricsSet::new(); + let input = CachedMetaReaderFactory::new(object_store).create_liquid_reader( + 0, + partitioned_file, + None, + &metrics, + ); + let projection = ProjectionMask::roots( + reader_metadata.metadata().file_metadata().schema_descr(), + [0], + ); + let store = t4::mount(tmp_dir.path().join("liquid_cache.t4")) .await .unwrap(); let cache = LiquidCacheParquet::new( batch_size, usize::MAX, + usize::MAX, store, Box::new(LiquidPolicy::new()), Box::new(Evict), Box::new(AlwaysHydrate::new()), ) .await; - let field = Arc::new(Field::new("col0", DataType::Int32, false)); - let schema = Arc::new(Schema::new(vec![field.clone()])); let file = cache.register_or_get_file("test".to_string(), schema.clone()); let row_group = file.create_row_group(0, vec![]); let column = row_group.get_column(0).unwrap(); @@ -317,7 +627,21 @@ mod tests { .expect("cache insert"); } - (row_group, schema) + TestRowGroup { + batch_size, + row_group, + schema, + fallback: ParquetFallbackConfig { + row_group_idx: 0, + metadata: Arc::clone(reader_metadata.metadata()), + input, + cache_projection: projection, + cache_column_ids: vec![0], + cache_batch_size: batch_size, + row_count: flatten_batches(batches).len(), + }, + _tmp_dir: tmp_dir, + } } fn flatten_batches(batches: &[Vec]) -> Vec { @@ -377,11 +701,15 @@ mod tests { #[tokio::test] async fn reads_batches_in_order() { let batch_size = 2; - let (row_group, schema) = make_row_group(batch_size, &[vec![1, 2], vec![3, 4]]).await; + let test = make_row_group(batch_size, &[vec![1, 2], vec![3, 4]]).await; let selection = RowSelection::from(vec![RowSelector::select(4)]); - let reader = - LiquidCacheReader::new(batch_size, selection, None, row_group, vec![0], schema); + let reader = test.reader(ReaderRequest { + selection, + row_filter: None, + projection_columns: vec![0], + schema: Arc::clone(&test.schema), + }); let batches = collect_batches(reader); assert_eq!(batches.len(), 2); @@ -392,11 +720,15 @@ mod tests { #[tokio::test] async fn skips_unselected_batches() { let batch_size = 2; - let (row_group, schema) = make_row_group(batch_size, &[vec![1, 2], vec![3, 4]]).await; + let test = make_row_group(batch_size, &[vec![1, 2], vec![3, 4]]).await; let selection = RowSelection::from(vec![RowSelector::skip(2), RowSelector::select(2)]); - let reader = - LiquidCacheReader::new(batch_size, selection, None, row_group, vec![0], schema); + let reader = test.reader(ReaderRequest { + selection, + row_filter: None, + projection_columns: vec![0], + schema: Arc::clone(&test.schema), + }); let batches = collect_batches(reader); assert_eq!(batches.len(), 1); @@ -406,17 +738,15 @@ mod tests { #[tokio::test] async fn empty_projection_emits_schema_only_batches() { let batch_size = 2; - let (row_group, _) = make_row_group(batch_size, &[vec![10, 11]]).await; + let test = make_row_group(batch_size, &[vec![10, 11]]).await; let selection = RowSelection::from(vec![RowSelector::select(2)]); - let reader = LiquidCacheReader::new( - batch_size, + let reader = test.reader(ReaderRequest { selection, - None, - row_group, - Vec::new(), - Arc::new(Schema::new(Vec::::new())), - ); + row_filter: None, + projection_columns: Vec::new(), + schema: Arc::new(Schema::new(Vec::::new())), + }); let batches = collect_batches(reader); assert_eq!(batches.len(), 1); @@ -428,18 +758,16 @@ mod tests { #[tokio::test] async fn into_filter_returns_stored_filter_after_completion() { let batch_size = 2; - let (row_group, schema) = make_row_group(batch_size, &[vec![1, 2]]).await; + let test = make_row_group(batch_size, &[vec![1, 2]]).await; let selection = RowSelection::from(Vec::::new()); let filter = LiquidRowFilter::new(Vec::new()); - let mut reader = LiquidCacheReader::new( - batch_size, + let mut reader = test.reader(ReaderRequest { selection, - Some(filter), - row_group, - vec![0], - schema, - ); + row_filter: Some(filter), + projection_columns: vec![0], + schema: Arc::clone(&test.schema), + }); let waker = futures::task::noop_waker(); let mut cx = Context::from_waker(&waker); @@ -456,18 +784,16 @@ mod tests { let batches = vec![vec![1, 2], vec![3, 4]]; let batch_size = 2; let all_values = flatten_batches(&batches); - let (row_group, schema) = make_row_group(batch_size, &batches).await; - let filter = make_gt_filter(Arc::clone(&schema), &all_values, 2); + let test = make_row_group(batch_size, &batches).await; + let filter = make_gt_filter(Arc::clone(&test.schema), &all_values, 2); let selection = RowSelection::from(vec![RowSelector::select(4)]); - let reader = LiquidCacheReader::new( - batch_size, + let reader = test.reader(ReaderRequest { selection, - Some(filter), - row_group, - vec![0], - schema, - ); + row_filter: Some(filter), + projection_columns: vec![0], + schema: Arc::clone(&test.schema), + }); let batches = collect_batches(reader); assert_eq!(batches.len(), 1); @@ -499,18 +825,16 @@ mod tests { let batches = vec![vec![1, 2], vec![3, 4], vec![5, 6]]; let batch_size = 2; let all_values = flatten_batches(&batches); - let (row_group, schema) = make_row_group(batch_size, &batches).await; - let filter = make_or_filter(Arc::clone(&schema), &all_values, 4, 2); + let test = make_row_group(batch_size, &batches).await; + let filter = make_or_filter(Arc::clone(&test.schema), &all_values, 4, 2); let selection = RowSelection::from(vec![RowSelector::select(6)]); - let reader = LiquidCacheReader::new( - batch_size, + let reader = test.reader(ReaderRequest { selection, - Some(filter), - row_group, - vec![0], - schema, - ); + row_filter: Some(filter), + projection_columns: vec![0], + schema: Arc::clone(&test.schema), + }); let batches = collect_batches(reader); assert_eq!(batches.len(), 2); @@ -523,22 +847,20 @@ mod tests { let batches = vec![vec![1, 2, 3, 4]]; let batch_size = 4; let all_values = flatten_batches(&batches); - let (row_group, schema) = make_row_group(batch_size, &batches).await; - let filter = make_gt_filter(Arc::clone(&schema), &all_values, 2); + let test = make_row_group(batch_size, &batches).await; + let filter = make_gt_filter(Arc::clone(&test.schema), &all_values, 2); let selection = RowSelection::from(vec![ RowSelector::skip(1), RowSelector::select(2), RowSelector::skip(1), ]); - let reader = LiquidCacheReader::new( - batch_size, + let reader = test.reader(ReaderRequest { selection, - Some(filter), - row_group, - vec![0], - schema, - ); + row_filter: Some(filter), + projection_columns: vec![0], + schema: Arc::clone(&test.schema), + }); let mut batches = collect_batches(reader); assert_eq!(batches.len(), 1); @@ -550,18 +872,16 @@ mod tests { let batches = vec![vec![1, 2]]; let batch_size = 2; let all_values = flatten_batches(&batches); - let (row_group, schema) = make_row_group(batch_size, &batches).await; - let filter = make_gt_filter(Arc::clone(&schema), &all_values, 10); + let test = make_row_group(batch_size, &batches).await; + let filter = make_gt_filter(Arc::clone(&test.schema), &all_values, 10); let selection = RowSelection::from(vec![RowSelector::select(2)]); - let reader = LiquidCacheReader::new( - batch_size, + let reader = test.reader(ReaderRequest { selection, - Some(filter), - row_group, - vec![0], - schema, - ); + row_filter: Some(filter), + projection_columns: vec![0], + schema: Arc::clone(&test.schema), + }); let next_batch = futures::executor::block_on(async { pin_mut!(reader); diff --git a/src/datafusion/src/reader/runtime/liquid_predicate.rs b/src/datafusion/src/reader/runtime/liquid_predicate.rs index 54e5fd25..2cae21e2 100644 --- a/src/datafusion/src/reader/runtime/liquid_predicate.rs +++ b/src/datafusion/src/reader/runtime/liquid_predicate.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use datafusion::logical_expr::Operator; use datafusion::physical_expr::PhysicalExpr; use datafusion::physical_expr::expressions::{ - BinaryExpr, CastColumnExpr, CastExpr, Column, LikeExpr, Literal, TryCastExpr, + BinaryExpr, CastExpr, Column, LikeExpr, Literal, TryCastExpr, }; /// Extract multiple column-literal expressions from a nested OR structure. @@ -18,7 +18,7 @@ pub(crate) fn extract_multi_column_or( expr: &'a Arc, result: &mut Vec<(&'a str, Arc)>, ) -> bool { - if let Some(binary) = expr.as_any().downcast_ref::() + if let Some(binary) = expr.downcast_ref::() && binary.op() == &Operator::Or { // Recursively collect from left and right @@ -43,25 +43,19 @@ pub(crate) fn extract_multi_column_or( } fn extract_column_literal(expr: &Arc) -> Option<(&str, Arc)> { - if let Some(binary) = expr.as_any().downcast_ref::() - && binary.right().as_any().downcast_ref::().is_some() + if let Some(binary) = expr.downcast_ref::() + && binary.right().is::() { return extract_column_literal(binary.left()); - } else if let Some(like_expr) = expr.as_any().downcast_ref::() - && like_expr - .pattern() - .as_any() - .downcast_ref::() - .is_some() + } else if let Some(like_expr) = expr.downcast_ref::() + && like_expr.pattern().is::() { return extract_column_literal(like_expr.expr()); - } else if let Some(cast_expr) = expr.as_any().downcast_ref::() { + } else if let Some(cast_expr) = expr.downcast_ref::() { return extract_column_literal(cast_expr.expr()); - } else if let Some(cast_column_expr) = expr.as_any().downcast_ref::() { - return extract_column_literal(cast_column_expr.expr()); - } else if let Some(try_cast_expr) = expr.as_any().downcast_ref::() { + } else if let Some(try_cast_expr) = expr.downcast_ref::() { return extract_column_literal(try_cast_expr.expr()); - } else if let Some(column) = expr.as_any().downcast_ref::() { + } else if let Some(column) = expr.downcast_ref::() { return Some((column.name(), Arc::clone(expr))); } None diff --git a/src/datafusion/src/reader/runtime/liquid_stream.rs b/src/datafusion/src/reader/runtime/liquid_stream.rs index b93dfb2f..2933d5d2 100644 --- a/src/datafusion/src/reader/runtime/liquid_stream.rs +++ b/src/datafusion/src/reader/runtime/liquid_stream.rs @@ -1,15 +1,14 @@ -use crate::cache::{BatchID, CachedFileRef, CachedRowGroupRef, InsertArrowArrayError}; +use crate::cache::{CachedFileRef, CachedRowGroupRef}; use crate::reader::plantime::{LiquidRowFilter, ParquetMetadataCacheReader}; use arrow::array::RecordBatch; use arrow_schema::{Schema, SchemaRef}; use fastrace::Event; use fastrace::local::LocalSpan; -use futures::{FutureExt, Stream, StreamExt, future::BoxFuture}; -use parquet::arrow::arrow_reader::{ArrowPredicate, ArrowReaderMetadata, ArrowReaderOptions}; +use futures::Stream; use parquet::{ arrow::{ - ParquetRecordBatchStreamBuilder, ProjectionMask, - arrow_reader::{RowSelection, RowSelector}, + ProjectionMask, + arrow_reader::{ArrowPredicate, RowSelection, RowSelector}, }, errors::ParquetError, file::metadata::ParquetMetaData, @@ -22,11 +21,12 @@ use std::{ task::{Context, Poll}, }; -use super::liquid_cache_reader::LiquidCacheReader; +use super::liquid_cache_reader::{ + LiquidCacheReader, LiquidCacheReaderConfig, ParquetFallbackConfig, +}; use super::utils::{get_root_column_ids, limit_row_selection, offset_row_selection}; type PlanResult = Option; -type FillCacheResult = Result<(ReaderFactory, PlanningContext), ParquetError>; struct ReaderFactory { metadata: Arc, @@ -99,18 +99,11 @@ impl ReaderFactory { *limit -= rows_after; } - let row_count = meta.num_rows() as usize; - let cache_batch_size = self.cached_file.batch_size(); - let mut cache_projection = projection.clone(); if let Some(ref predicate_projection) = predicate_projection { cache_projection.union(predicate_projection); } - let selection_for_cache = selection.clone(); - let selection_batches = - collect_selection_batches(&selection_for_cache, cache_batch_size, row_count); - let schema_descr = self.metadata.file_metadata().schema_descr(); let cache_column_ids = get_root_column_ids(schema_descr, &cache_projection); let predicate_column_ids = if let Some(ref predicate_projection) = predicate_projection { @@ -123,8 +116,6 @@ impl ReaderFactory { .create_row_group(row_group_idx as u64, predicate_column_ids); let projection_column_ids = get_root_column_ids(schema_descr, &projection); - let missing_batches = - compute_missing_batches(&cached_row_group, &cache_column_ids, &selection_batches); let context = PlanningContext { row_group_idx, @@ -134,104 +125,10 @@ impl ReaderFactory { cache_projection, projection_column_ids, cache_column_ids, - missing_batches, }; Some(context) } - - /// Fills the cache by reading missing batches from parquet using official parquet reader - async fn fill_cache_from_parquet(self, context: PlanningContext) -> FillCacheResult { - let row_count = self.metadata.row_group(context.row_group_idx).num_rows() as usize; - let cache_batch_size = context.cached_row_group.batch_size(); - - if context.cache_column_ids.is_empty() || context.missing_batches.is_empty() { - return Ok((self, context)); - } - - // Build row selection for the missing batches - let backfill_selection = - build_selection_for_batches(&context.missing_batches, cache_batch_size, row_count); - - if !backfill_selection.selects_any() { - return Ok((self, context)); - } - - // Clone the reader for this operation (cheap since it's Arc-based) - let reader_clone: ParquetMetadataCacheReader = self.input.clone(); - - // Use official parquet async reader - let options = ArrowReaderOptions::new(); - let reader_metadata = ArrowReaderMetadata::try_new(Arc::clone(&self.metadata), options)?; - - let mut stream = - ParquetRecordBatchStreamBuilder::new_with_metadata(reader_clone, reader_metadata) - .with_projection(context.cache_projection.clone()) - .with_row_groups(vec![context.row_group_idx]) - .with_row_selection(backfill_selection) - .with_batch_size(cache_batch_size) - .build()?; - - let mut processed_batches = 0usize; - - // Get the original column indices in projection order - let column_ids = get_root_column_ids( - self.metadata.file_metadata().schema_descr(), - &context.cache_projection, - ); - - while let Some(batch_result) = stream.next().await { - let record_batch = batch_result?; - if record_batch.num_rows() == 0 { - continue; - } - - let Some(batch_id) = context.missing_batches.get(processed_batches) else { - return Err(ParquetError::General( - "parquet stream produced more batches than expected".to_string(), - )); - }; - - let batch_index = usize::from(**batch_id); - let batch_start = batch_index * cache_batch_size; - let expected_len = ((batch_index + 1) * cache_batch_size) - .min(row_count) - .saturating_sub(batch_start.min(row_count)); - - debug_assert!( - record_batch.num_rows() <= cache_batch_size, - "parquet batch larger than cache batch size" - ); - debug_assert_eq!( - record_batch.num_rows(), - expected_len, - "parquet batch length does not match expected cache slice" - ); - - let batch_id = *batch_id; - insert_batch_into_cache( - &record_batch, - &column_ids, - batch_id, - cache_batch_size, - row_count, - &context.cached_row_group, - ) - .await?; - - processed_batches += 1; - } - - if processed_batches != context.missing_batches.len() { - return Err(ParquetError::General(format!( - "expected {} batches from parquet stream, received {}", - context.missing_batches.len(), - processed_batches - ))); - } - - Ok((self, context)) - } } fn build_projection_schema(file_schema: &SchemaRef, projection_column_ids: &[usize]) -> SchemaRef { @@ -243,166 +140,6 @@ fn build_projection_schema(file_schema: &SchemaRef, projection_column_ids: &[usi Arc::new(Schema::new(fields)) } -fn collect_selection_batches( - selection: &RowSelection, - batch_size: usize, - row_count: usize, -) -> Vec { - let mut batches = Vec::new(); - let mut current_row = 0usize; - let selectors: Vec = selection.clone().into(); - - for selector in selectors { - if selector.skip { - current_row += selector.row_count; - continue; - } - - let start = current_row; - let end = (current_row + selector.row_count).min(row_count); - if start >= end { - current_row = current_row.saturating_add(selector.row_count); - continue; - } - - let start_batch = start / batch_size; - let end_batch = (end - 1) / batch_size; - for batch_idx in start_batch..=end_batch { - let batch_id = BatchID::from_raw(batch_idx as u16); - let is_duplicate = batches.last().is_some_and(|last| last == &batch_id); - if !is_duplicate { - batches.push(batch_id); - } - } - current_row += selector.row_count; - } - - batches -} - -fn compute_missing_batches( - cached_row_group: &CachedRowGroupRef, - column_ids: &[usize], - selection_batches: &[BatchID], -) -> Vec { - if column_ids.is_empty() || selection_batches.is_empty() { - return Vec::new(); - } - - let mut columns = Vec::with_capacity(column_ids.len()); - for &column_idx in column_ids { - columns.push(cached_row_group.get_column(column_idx as u64)); - } - - let mut missing = Vec::new(); - - 'batch: for &batch_id in selection_batches { - for column in &columns { - match column { - Some(column) => { - if !column.is_cached(batch_id) { - if missing.last().is_some_and(|last| last == &batch_id) { - continue 'batch; - } - missing.push(batch_id); - continue 'batch; - } - } - None => { - if missing.last().is_some_and(|last| last == &batch_id) { - continue 'batch; - } - missing.push(batch_id); - continue 'batch; - } - } - } - } - - missing -} - -fn build_selection_for_batches( - batches: &[BatchID], - batch_size: usize, - row_count: usize, -) -> RowSelection { - if batches.is_empty() { - return RowSelection::from(Vec::::new()); - } - - let mut selectors = Vec::new(); - let mut current_row = 0usize; - - for batch_id in batches { - let batch_idx = usize::from(**batch_id); - let start = batch_idx * batch_size; - if start >= row_count { - continue; - } - let end = ((batch_idx + 1) * batch_size).min(row_count); - - if start > current_row { - selectors.push(RowSelector::skip(start - current_row)); - } - - selectors.push(RowSelector::select(end - start)); - current_row = end; - } - - RowSelection::from(selectors) -} - -async fn insert_batch_into_cache( - record_batch: &RecordBatch, - column_ids: &[usize], - batch_id: BatchID, - batch_size: usize, - row_count: usize, - cached_row_group: &CachedRowGroupRef, -) -> Result<(), ParquetError> { - if column_ids.is_empty() || record_batch.num_rows() == 0 { - return Ok(()); - } - - debug_assert_eq!(record_batch.num_columns(), column_ids.len()); - - let batch_idx = usize::from(*batch_id); - let start = batch_idx * batch_size; - if start >= row_count { - return Ok(()); - } - let end = ((batch_idx + 1) * batch_size).min(row_count); - let len = end - start; - - debug_assert!( - len <= batch_size, - "cache batch length exceeded configured batch size" - ); - debug_assert_eq!( - record_batch.num_rows(), - len, - "record batch length does not match cache batch window" - ); - - for (col_idx, column_id) in column_ids.iter().enumerate() { - let column = cached_row_group.get_column(*column_id as u64).unwrap(); - let array = Arc::clone(record_batch.column(col_idx)); - - if let Err(err) = column.insert(batch_id, array).await - && !matches!(err, InsertArrowArrayError::AlreadyCached) - { - return Err(ParquetError::General(format!( - "Failed to insert batch {} for column {} into cache: {err:?}", - batch_idx, column_id - ))); - } - debug_assert!(column.is_cached(batch_id)); - } - - Ok(()) -} - /// Context for planning what to read from cache vs parquet struct PlanningContext { row_group_idx: usize, @@ -412,23 +149,48 @@ struct PlanningContext { cache_projection: ProjectionMask, projection_column_ids: Vec, cache_column_ids: Vec, - missing_batches: Vec, +} + +fn build_liquid_cache_reader( + reader_factory: &mut ReaderFactory, + context: PlanningContext, + schema: SchemaRef, +) -> LiquidCacheReader { + let row_count = reader_factory + .metadata + .row_group(context.row_group_idx) + .num_rows() as usize; + let cache_batch_size = context.cached_row_group.batch_size(); + LiquidCacheReader::new(LiquidCacheReaderConfig { + batch_size: context.batch_size, + selection: context.selection, + row_filter: reader_factory.filter.take(), + cached_row_group: context.cached_row_group, + projection_columns: context.projection_column_ids, + schema, + parquet_fallback: ParquetFallbackConfig { + row_group_idx: context.row_group_idx, + metadata: Arc::clone(&reader_factory.metadata), + input: reader_factory.input.clone(), + cache_projection: context.cache_projection, + cache_column_ids: context.cache_column_ids, + cache_batch_size, + row_count, + }, + }) } enum StreamState { /// At the start of a new row group, or the end of the parquet stream Init, - /// Reading from parquet and filling cache - FillCache(BoxFuture<'static, FillCacheResult>), /// Decoding a batch from cache - ReadFromCache(LiquidCacheReader), + ReadFromCache(Box), } impl std::fmt::Debug for StreamState { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { StreamState::Init => write!(f, "StreamState::Init"), - StreamState::FillCache(_) => write!(f, "StreamState::FillingCache"), StreamState::ReadFromCache(_) => write!(f, "StreamState::Decoding"), } } @@ -603,7 +365,7 @@ impl Stream for LiquidStream { match state { StreamState::ReadFromCache(mut batch_reader) => { - match Pin::new(&mut batch_reader).poll_next(cx) { + match Pin::new(&mut *batch_reader).poll_next(cx) { Poll::Ready(Some(Ok(batch))) => { self.state = StreamState::ReadFromCache(batch_reader); return Poll::Ready(Some(Ok(batch))); @@ -612,6 +374,7 @@ impl Stream for LiquidStream { panic!("Decoding next batch error: {e:?}"); } Poll::Ready(None) => { + let batch_reader = *batch_reader; let filter = batch_reader.into_filter(); self.reader.as_mut().unwrap().filter = filter; // state left as Init, continue loop to plan next row group @@ -643,57 +406,18 @@ impl Stream for LiquidStream { ); match maybe_context { Some(context) => { - if !context.missing_batches.is_empty() - && !context.cache_column_ids.is_empty() - { - LocalSpan::add_event(Event::new("LiquidStream::fill_cache")); - let reader = self.reader.take().expect("lost reader"); - let fut = reader.fill_cache_from_parquet(context).boxed(); - self.state = StreamState::FillCache(fut); - } else { - LocalSpan::add_event(Event::new("LiquidStream::read_from_cache")); - let reader_factory = self.reader.as_mut().unwrap(); - let batch_reader = LiquidCacheReader::new( - context.batch_size, - context.selection, - reader_factory.filter.take(), - context.cached_row_group, - context.projection_column_ids, - Arc::clone(&self.schema), - ); - self.state = StreamState::ReadFromCache(batch_reader); - } + LocalSpan::add_event(Event::new("LiquidStream::read_from_cache")); + let schema = Arc::clone(&self.schema); + let reader_factory = self.reader.as_mut().unwrap(); + let batch_reader = + build_liquid_cache_reader(reader_factory, context, schema); + self.state = StreamState::ReadFromCache(Box::new(batch_reader)); } None => { self.state = StreamState::Init; } } } - StreamState::FillCache(mut f) => match f.as_mut().poll(cx) { - Poll::Pending => { - self.state = StreamState::FillCache(f); - return Poll::Pending; - } - Poll::Ready(result) => match result { - Ok((reader_factory, context)) => { - self.reader = Some(reader_factory); - LocalSpan::add_event(Event::new("LiquidStream::read_from_cache")); - let reader_factory = self.reader.as_mut().unwrap(); - let batch_reader = LiquidCacheReader::new( - context.batch_size, - context.selection, - reader_factory.filter.take(), - context.cached_row_group, - context.projection_column_ids, - Arc::clone(&self.schema), - ); - self.state = StreamState::ReadFromCache(batch_reader); - } - Err(e) => { - panic!("Filling cache error: {e:?}"); - } - }, - }, } } } @@ -702,31 +426,316 @@ impl Stream for LiquidStream { #[cfg(test)] mod tests { use super::*; - use crate::cache::LiquidCacheParquet; - use arrow::array::{ArrayRef, Int32Array}; + use crate::cache::{BatchID, CachedFileRef, LiquidCacheParquet}; + use crate::reader::plantime::{ + CachedMetaReaderFactory, FilterCandidateBuilder, LiquidPredicate, + }; + use arrow::array::{Array, ArrayRef, Int32Array}; use arrow_schema::{DataType, Field, Schema}; + use datafusion::common::ScalarValue; + use datafusion::datasource::listing::PartitionedFile; + use datafusion::logical_expr::Operator; + use datafusion::physical_expr::PhysicalExpr; + use datafusion::physical_expr::expressions::{BinaryExpr, Column, Literal}; + use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet; + use futures::StreamExt; use liquid_cache::cache::AlwaysHydrate; use liquid_cache::cache::squeeze_policies::Evict; use liquid_cache::cache_policies::LiquidPolicy; - use parquet::arrow::arrow_reader::RowSelection; + use object_store::local::LocalFileSystem; + use parquet::arrow::ArrowWriter; + use parquet::arrow::arrow_reader::{ArrowReaderMetadata, ArrowReaderOptions}; + use std::fs::File; use std::sync::Arc; - async fn make_cache(batch_size: usize, schema: SchemaRef) -> CachedRowGroupRef { + fn write_two_row_group_file(path: &std::path::Path, schema: SchemaRef) { + let file = File::create(path).unwrap(); + let mut writer = ArrowWriter::try_new(file, schema.clone(), None).unwrap(); + let batch0 = RecordBatch::try_new( + schema.clone(), + vec![ + Arc::new(Int32Array::from(vec![0, 1, 2, 3])), + Arc::new(Int32Array::from(vec![10, 11, 12, 13])), + ], + ) + .unwrap(); + let batch1 = RecordBatch::try_new( + schema, + vec![ + Arc::new(Int32Array::from(vec![4, 5, 6, 7])), + Arc::new(Int32Array::from(vec![14, 15, 16, 17])), + ], + ) + .unwrap(); + writer.write(&batch0).unwrap(); + writer.flush().unwrap(); + writer.write(&batch1).unwrap(); + writer.close().unwrap(); + } + + fn write_single_row_group_file(path: &std::path::Path, schema: SchemaRef, a: Vec) { + let file = File::create(path).unwrap(); + let mut writer = ArrowWriter::try_new(file, schema.clone(), None).unwrap(); + let b: Vec<_> = a.iter().map(|value| value + 1000).collect(); + let batch = RecordBatch::try_new( + schema, + vec![Arc::new(Int32Array::from(a)), Arc::new(Int32Array::from(b))], + ) + .unwrap(); + writer.write(&batch).unwrap(); + writer.close().unwrap(); + } + + async fn make_liquid_stream( + max_memory_bytes: usize, + max_disk_bytes: usize, + row_filter: Option, + ) -> ( + LiquidStream, + Arc, + CachedFileRef, + tempfile::TempDir, + ) { + let schema = Arc::new(Schema::new(vec![ + Field::new("a", DataType::Int32, false), + Field::new("b", DataType::Int32, false), + ])); let tmp_dir = tempfile::tempdir().unwrap(); + let parquet_path = tmp_dir.path().join("data.parquet"); + write_two_row_group_file(&parquet_path, schema.clone()); + let metadata_file = File::open(&parquet_path).unwrap(); + let reader_metadata = + ArrowReaderMetadata::load(&metadata_file, ArrowReaderOptions::new()).unwrap(); + let object_store = Arc::new(LocalFileSystem::new_with_prefix(tmp_dir.path()).unwrap()); + let partitioned_file = PartitionedFile::new( + "data.parquet", + std::fs::metadata(&parquet_path).unwrap().len(), + ); + let metrics = ExecutionPlanMetricsSet::new(); + let input = CachedMetaReaderFactory::new(object_store).create_liquid_reader( + 0, + partitioned_file, + None, + &metrics, + ); + let store = t4::mount(tmp_dir.path().join("liquid_cache.t4")) .await .unwrap(); - let cache = LiquidCacheParquet::new( - batch_size, - usize::MAX, - store, - Box::new(LiquidPolicy::new()), - Box::new(Evict), - Box::new(AlwaysHydrate::new()), - ) - .await; - let file = cache.register_or_get_file("test.parquet".to_string(), schema); - file.create_row_group(0, vec![]) + let cache = Arc::new( + LiquidCacheParquet::new( + 4, + max_memory_bytes, + max_disk_bytes, + store, + Box::new(LiquidPolicy::new()), + Box::new(Evict), + Box::new(AlwaysHydrate::new()), + ) + .await, + ); + let cached_file = cache.register_or_get_file("data.parquet".to_string(), schema); + let projection = ProjectionMask::roots( + reader_metadata.metadata().file_metadata().schema_descr(), + [0, 1], + ); + let mut builder = LiquidStreamBuilder::new(input, Arc::clone(reader_metadata.metadata())) + .with_batch_size(4) + .with_row_groups(vec![0, 1]) + .with_projection(projection); + if let Some(row_filter) = row_filter { + builder = builder.with_row_filter(row_filter); + } + let stream = builder.build(cached_file.clone()).unwrap(); + (stream, cache, cached_file, tmp_dir) + } + + async fn collect_liquid_values(stream: LiquidStream) -> (Vec, Vec) { + let batches = stream + .map(|batch| batch.expect("valid liquid stream batch")) + .collect::>() + .await; + let mut a = Vec::new(); + let mut b = Vec::new(); + for batch in batches { + let a_array = batch + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + let b_array = batch + .column(1) + .as_any() + .downcast_ref::() + .unwrap(); + a.extend(a_array.iter().map(|value| value.unwrap())); + b.extend(b_array.iter().map(|value| value.unwrap())); + } + (a, b) + } + + async fn collect_projected_a(stream: LiquidStream) -> Vec { + let batches = stream + .map(|batch| batch.expect("valid liquid stream batch")) + .collect::>() + .await; + let mut a = Vec::new(); + for batch in batches { + let a_array = batch + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + a.extend(a_array.iter().map(|value| value.unwrap())); + } + a + } + + fn gt_filter(schema: SchemaRef, literal: i32) -> LiquidRowFilter { + gt_filter_on(schema, "a", 0, literal) + } + + fn gt_filter_on( + schema: SchemaRef, + col_name: &str, + col_idx: usize, + literal: i32, + ) -> LiquidRowFilter { + let expr: Arc = Arc::new(BinaryExpr::new( + Arc::new(Column::new(col_name, col_idx)), + Operator::Gt, + Arc::new(Literal::new(ScalarValue::Int32(Some(literal)))), + )); + let tmp_meta = tempfile::NamedTempFile::new().unwrap(); + write_two_row_group_file(tmp_meta.path(), schema.clone()); + let file = File::open(tmp_meta.path()).unwrap(); + let metadata = ArrowReaderMetadata::load(&file, ArrowReaderOptions::new()).unwrap(); + let builder = FilterCandidateBuilder::new(expr, schema); + let candidate = builder.build(metadata.metadata()).unwrap().unwrap(); + let projection = candidate.projection(metadata.metadata()); + let predicate = LiquidPredicate::try_new(candidate, projection).unwrap(); + LiquidRowFilter::new(vec![predicate]) + } + + async fn make_liquid_stream_with_projection( + max_memory_bytes: usize, + max_disk_bytes: usize, + row_filter: Option, + projection_columns: Vec, + ) -> ( + LiquidStream, + Arc, + CachedFileRef, + tempfile::TempDir, + ) { + let schema = Arc::new(Schema::new(vec![ + Field::new("a", DataType::Int32, false), + Field::new("b", DataType::Int32, false), + ])); + let tmp_dir = tempfile::tempdir().unwrap(); + let parquet_path = tmp_dir.path().join("data.parquet"); + write_two_row_group_file(&parquet_path, schema.clone()); + let metadata_file = File::open(&parquet_path).unwrap(); + let reader_metadata = + ArrowReaderMetadata::load(&metadata_file, ArrowReaderOptions::new()).unwrap(); + let object_store = Arc::new(LocalFileSystem::new_with_prefix(tmp_dir.path()).unwrap()); + let partitioned_file = PartitionedFile::new( + "data.parquet", + std::fs::metadata(&parquet_path).unwrap().len(), + ); + let metrics = ExecutionPlanMetricsSet::new(); + let input = CachedMetaReaderFactory::new(object_store).create_liquid_reader( + 0, + partitioned_file, + None, + &metrics, + ); + + let store = t4::mount(tmp_dir.path().join("liquid_cache.t4")) + .await + .unwrap(); + let cache = Arc::new( + LiquidCacheParquet::new( + 4, + max_memory_bytes, + max_disk_bytes, + store, + Box::new(LiquidPolicy::new()), + Box::new(Evict), + Box::new(AlwaysHydrate::new()), + ) + .await, + ); + let cached_file = cache.register_or_get_file("data.parquet".to_string(), schema); + let projection = ProjectionMask::roots( + reader_metadata.metadata().file_metadata().schema_descr(), + projection_columns, + ); + let mut builder = LiquidStreamBuilder::new(input, Arc::clone(reader_metadata.metadata())) + .with_batch_size(4) + .with_row_groups(vec![0, 1]) + .with_projection(projection); + if let Some(row_filter) = row_filter { + builder = builder.with_row_filter(row_filter); + } + let stream = builder.build(cached_file.clone()).unwrap(); + (stream, cache, cached_file, tmp_dir) + } + + async fn make_single_row_group_stream( + parquet_a: Vec, + projection_columns: Vec, + ) -> (LiquidStream, CachedFileRef, tempfile::TempDir) { + let schema = Arc::new(Schema::new(vec![ + Field::new("a", DataType::Int32, false), + Field::new("b", DataType::Int32, false), + ])); + let tmp_dir = tempfile::tempdir().unwrap(); + let parquet_path = tmp_dir.path().join("data.parquet"); + write_single_row_group_file(&parquet_path, schema.clone(), parquet_a); + let metadata_file = File::open(&parquet_path).unwrap(); + let reader_metadata = + ArrowReaderMetadata::load(&metadata_file, ArrowReaderOptions::new()).unwrap(); + let object_store = Arc::new(LocalFileSystem::new_with_prefix(tmp_dir.path()).unwrap()); + let partitioned_file = PartitionedFile::new( + "data.parquet", + std::fs::metadata(&parquet_path).unwrap().len(), + ); + let metrics = ExecutionPlanMetricsSet::new(); + let input = CachedMetaReaderFactory::new(object_store).create_liquid_reader( + 0, + partitioned_file, + None, + &metrics, + ); + + let store = t4::mount(tmp_dir.path().join("liquid_cache.t4")) + .await + .unwrap(); + let cache = Arc::new( + LiquidCacheParquet::new( + 4, + usize::MAX, + usize::MAX, + store, + Box::new(LiquidPolicy::new()), + Box::new(Evict), + Box::new(AlwaysHydrate::new()), + ) + .await, + ); + let cached_file = cache.register_or_get_file("data.parquet".to_string(), schema); + let projection = ProjectionMask::roots( + reader_metadata.metadata().file_metadata().schema_descr(), + projection_columns, + ); + let stream = LiquidStreamBuilder::new(input, Arc::clone(reader_metadata.metadata())) + .with_batch_size(4) + .with_row_groups(vec![0]) + .with_projection(projection) + .build(cached_file.clone()) + .unwrap(); + (stream, cached_file, tmp_dir) } async fn insert_batches( @@ -744,139 +753,132 @@ mod tests { } } - #[test] - fn collect_selection_batches_marks_all_selected_batches() { - let selection = RowSelection::from(vec![ - RowSelector::select(3), - RowSelector::skip(2), - RowSelector::select(5), - ]); - let batches = collect_selection_batches(&selection, 4, 10); - let expected = vec![ - BatchID::from_raw(0), - BatchID::from_raw(1), - BatchID::from_raw(2), - ]; - assert_eq!(batches, expected); + async fn is_cached(row_group: &CachedRowGroupRef, column_id: usize, batch_idx: u16) -> bool { + row_group + .get_column(column_id as u64) + .unwrap() + .get_arrow_array_test_only(BatchID::from_raw(batch_idx)) + .await + .is_some() } - #[test] - fn collect_selection_batches_handles_empty_selection() { - let selection = RowSelection::from(vec![]); - let batches = collect_selection_batches(&selection, 4, 10); - let expected: Vec = vec![]; - assert_eq!(batches, expected); - } + #[tokio::test] + async fn cache_full_keeps_inserted_batches_and_skips_failed_inserts() { + let one_array_memory = Arc::new(Int32Array::from(vec![0, 1, 2, 3])).get_array_memory_size(); + let (stream, _cache, cached_file, _tmp_dir) = + make_liquid_stream(one_array_memory * 3, 0, None).await; - #[test] - fn collect_selection_batches_handles_selection_beyond_row_count() { - let selection = RowSelection::from(vec![ - RowSelector::select(5), // Select 5 rows - RowSelector::skip(2), // Skip 2 rows - RowSelector::select(10), // Select 10 rows (but only 3 rows left) - ]); - let batches = collect_selection_batches(&selection, 4, 8); - // Total rows: 8 - // First selector: select 5 rows (rows 0-4) -> batches 0, 1 - // Skip 2 rows (rows 5-6) - // Third selector: select 10 rows from row 7, but only 1 row left -> batch 1 - let expected = vec![BatchID::from_raw(0), BatchID::from_raw(1)]; - assert_eq!(batches, expected); + let (a, b) = collect_liquid_values(stream).await; + + assert_eq!(a, vec![0, 1, 2, 3, 4, 5, 6, 7]); + assert_eq!(b, vec![10, 11, 12, 13, 14, 15, 16, 17]); + + let row_group0 = cached_file.create_row_group(0, vec![]); + let row_group1 = cached_file.create_row_group(1, vec![]); + assert!(is_cached(&row_group0, 0, 0).await); + assert!(is_cached(&row_group0, 1, 0).await); + assert!(is_cached(&row_group1, 0, 0).await); + assert!(!is_cached(&row_group1, 1, 0).await); } #[tokio::test] - async fn compute_missing_batches_identifies_partial_columns() { + async fn cache_full_with_row_filter_keeps_lookaside_results_correct() { let schema = Arc::new(Schema::new(vec![ - Field::new("col_0", DataType::Int32, false), - Field::new("col_1", DataType::Int32, false), - Field::new("col_2", DataType::Int32, false), + Field::new("a", DataType::Int32, false), + Field::new("b", DataType::Int32, false), ])); - let row_group = make_cache(4, schema.clone()).await; - insert_batches(&row_group, 0, &[(0, &[1, 2, 3, 4]), (2, &[9, 9, 9, 9])]).await; - insert_batches(&row_group, 2, &[(0, &[5, 6, 7, 8])]).await; - - let selection_batches = vec![ - BatchID::from_raw(0), - BatchID::from_raw(1), - BatchID::from_raw(2), - ]; + let one_array_memory = Arc::new(Int32Array::from(vec![0, 1, 2, 3])).get_array_memory_size(); + let filter = gt_filter(schema, 2); + let (stream, _cache, cached_file, _tmp_dir) = + make_liquid_stream(one_array_memory * 3, 0, Some(filter)).await; - let missing_for_col0 = compute_missing_batches(&row_group, &[0], &selection_batches); - assert_eq!(missing_for_col0, vec![BatchID::from_raw(1)]); + let (a, b) = collect_liquid_values(stream).await; - let missing_for_col2 = compute_missing_batches(&row_group, &[2], &selection_batches); - assert_eq!( - missing_for_col2, - vec![BatchID::from_raw(1), BatchID::from_raw(2),] - ); + assert_eq!(a, vec![3, 4, 5, 6, 7]); + assert_eq!(b, vec![13, 14, 15, 16, 17]); - let missing_for_col1 = compute_missing_batches(&row_group, &[1], &selection_batches); - assert_eq!( - missing_for_col1, - vec![ - BatchID::from_raw(0), - BatchID::from_raw(1), - BatchID::from_raw(2), - ] - ); + let row_group0 = cached_file.create_row_group(0, vec![]); + let row_group1 = cached_file.create_row_group(1, vec![]); + assert!(is_cached(&row_group0, 0, 0).await); + assert!(is_cached(&row_group0, 1, 0).await); + assert!(is_cached(&row_group1, 0, 0).await); + assert!(!is_cached(&row_group1, 1, 0).await); } - #[test] - fn build_selection_for_batches_generates_sparse_selectors() { - let selection = - build_selection_for_batches(&[BatchID::from_raw(1), BatchID::from_raw(3)], 4, 20); - let selectors: Vec = selection.into(); - assert_eq!( - selectors, - vec![ - RowSelector::skip(4), - RowSelector::select(4), - RowSelector::skip(4), - RowSelector::select(4), - ] - ); - } + #[tokio::test] + async fn mid_scan_eviction_recovers() { + let (stream, _cache, cached_file, _tmp_dir) = make_liquid_stream(0, 0, None).await; - #[test] - fn build_selection_for_batches_handles_empty_batches() { - let selection = build_selection_for_batches(&[], 4, 20); - let selectors: Vec = selection.into(); - assert_eq!(selectors, vec![]); + let (a, b) = collect_liquid_values(stream).await; + + assert_eq!(a, vec![0, 1, 2, 3, 4, 5, 6, 7]); + assert_eq!(b, vec![10, 11, 12, 13, 14, 15, 16, 17]); + + let row_group0 = cached_file.create_row_group(0, vec![]); + let row_group1 = cached_file.create_row_group(1, vec![]); + assert!(!is_cached(&row_group0, 0, 0).await); + assert!(!is_cached(&row_group0, 1, 0).await); + assert!(!is_cached(&row_group1, 0, 0).await); + assert!(!is_cached(&row_group1, 1, 0).await); } - #[test] - fn build_selection_for_batches_handles_batch_beyond_row_count() { - let selection = - build_selection_for_batches(&[BatchID::from_raw(5), BatchID::from_raw(6)], 4, 16); - let selectors: Vec = selection.into(); - // Total rows: 16, so valid batches are 0-3 (rows 0-15) - // Batch 5: start=20, end=min(24,16)=16, but 20 >= 16, so skipped - // Batch 6: start=24, end=min(28,16)=16, but 24 >= 16, so skipped - // Result should be empty selection - assert_eq!(selectors, vec![]); + #[tokio::test] + async fn predicate_fallback_uses_predicate_projection() { + let schema = Arc::new(Schema::new(vec![ + Field::new("a", DataType::Int32, false), + Field::new("b", DataType::Int32, false), + ])); + let one_array_memory = Arc::new(Int32Array::from(vec![0, 1, 2, 3])).get_array_memory_size(); + let filter = gt_filter_on(schema, "b", 1, 13); + let (stream, _cache, cached_file, _tmp_dir) = + make_liquid_stream_with_projection(one_array_memory * 3, 0, Some(filter), vec![0]) + .await; + + let a_values = collect_projected_a(stream).await; + + assert_eq!(a_values, vec![4, 5, 6, 7]); + + let row_group0 = cached_file.create_row_group(0, vec![]); + let row_group1 = cached_file.create_row_group(1, vec![]); + assert!(is_cached(&row_group0, 0, 0).await); + assert!(is_cached(&row_group0, 1, 0).await); + assert!(is_cached(&row_group1, 0, 0).await); + assert!(!is_cached(&row_group1, 1, 0).await); } - #[test] - fn build_selection_for_batches_handles_single_batch() { - let selection = build_selection_for_batches(&[BatchID::from_raw(2)], 4, 20); - let selectors: Vec = selection.into(); - // Batch 2: rows 8-11 - // Should skip 8 rows then select 4 rows - assert_eq!( - selectors, - vec![RowSelector::skip(8), RowSelector::select(4),] - ); + #[tokio::test] + async fn missing_column_falls_back_to_parquet() { + let (stream, _cache, cached_file, _tmp_dir) = + make_liquid_stream(usize::MAX, usize::MAX, None).await; + let row_group0 = cached_file.create_row_group(0, vec![]); + let row_group1 = cached_file.create_row_group(1, vec![]); + insert_batches(&row_group0, 0, &[(0, &[0, 1, 2, 3])]).await; + insert_batches(&row_group1, 0, &[(0, &[4, 5, 6, 7])]).await; + + let (a, b) = collect_liquid_values(stream).await; + + assert_eq!(a, vec![0, 1, 2, 3, 4, 5, 6, 7]); + assert_eq!(b, vec![10, 11, 12, 13, 14, 15, 16, 17]); + assert!(is_cached(&row_group0, 1, 0).await); + assert!(is_cached(&row_group1, 1, 0).await); } - #[test] - fn build_selection_for_batches_handles_partial_last_batch() { - let selection = build_selection_for_batches(&[BatchID::from_raw(4)], 4, 18); - let selectors: Vec = selection.into(); - // Batch 4: start=16, end=min(20,18)=18 - // Should skip 16 rows then select 2 rows (18-16=2) - assert_eq!( - selectors, - vec![RowSelector::skip(16), RowSelector::select(2),] - ); + #[tokio::test] + async fn fallback_stream_advances_across_misses() { + let parquet_a = vec![ + 100, 101, 102, 103, 4, 5, 6, 7, 200, 201, 202, 203, 12, 13, 14, 15, + ]; + let (stream, cached_file, _tmp_dir) = + make_single_row_group_stream(parquet_a, vec![0]).await; + let row_group = cached_file.create_row_group(0, vec![]); + insert_batches(&row_group, 0, &[(0, &[0, 1, 2, 3]), (2, &[8, 9, 10, 11])]).await; + + let a_values = collect_projected_a(stream).await; + + assert_eq!(a_values, (0..16).collect::>()); + assert!(is_cached(&row_group, 0, 0).await); + assert!(is_cached(&row_group, 0, 1).await); + assert!(is_cached(&row_group, 0, 2).await); + assert!(is_cached(&row_group, 0, 3).await); } } diff --git a/src/datafusion/src/reader/variant_udf.rs b/src/datafusion/src/reader/variant_udf.rs index 43c76313..f4507b65 100644 --- a/src/datafusion/src/reader/variant_udf.rs +++ b/src/datafusion/src/reader/variant_udf.rs @@ -133,10 +133,6 @@ impl Default for VariantGetUdf { } impl ScalarUDFImpl for VariantGetUdf { - fn as_any(&self) -> &dyn std::any::Any { - self - } - fn name(&self) -> &str { "variant_get" } @@ -299,10 +295,6 @@ impl Default for VariantPretty { } impl ScalarUDFImpl for VariantPretty { - fn as_any(&self) -> &dyn std::any::Any { - self - } - fn name(&self) -> &str { "variant_pretty" } @@ -382,10 +374,6 @@ impl Default for VariantToJsonUdf { } impl ScalarUDFImpl for VariantToJsonUdf { - fn as_any(&self) -> &dyn std::any::Any { - self - } - fn name(&self) -> &str { "variant_to_json" } diff --git a/src/datafusion/src/utils.rs b/src/datafusion/src/utils.rs index 64b70909..7cc14427 100644 --- a/src/datafusion/src/utils.rs +++ b/src/datafusion/src/utils.rs @@ -273,8 +273,7 @@ pub fn extract_execution_metrics( let mut bytes_scanned = 0; let _ = plan.apply(|node| { - let any_plan = node.as_any(); - if let Some(data_source_exec) = any_plan.downcast_ref::() + if let Some(data_source_exec) = node.downcast_ref::() && let Some(metrics) = data_source_exec.metrics() { let aggregated_metrics = metrics