From ec06bf86b1891ae738cd829df1c76c3f58f23c39 Mon Sep 17 00:00:00 2001 From: Xiangpeng Hao Date: Wed, 29 Apr 2026 17:53:44 -0500 Subject: [PATCH 01/13] Remove eager shredding (#491) some nice refactoring to move things around --- .github/workflows/ci.yml | 91 +- AGENTS.md | 9 - Cargo.lock | 1117 +++++++++-------- Cargo.toml | 18 +- benchmark/Cargo.toml | 8 +- benchmark/README.md | 2 +- benchmark/bench_server.rs | 10 +- benchmark/in_process.rs | 8 +- benchmark/src/inprocess_runner.rs | 17 +- dev/README.md | 4 +- dev/dev-tools/Cargo.toml | 2 +- examples/Cargo.toml | 2 +- examples/example_inprocess_cache_eviction.rs | 2 +- examples/example_inprocess_insertion.rs | 2 +- examples/example_local.rs | 2 +- examples/example_server.rs | 2 +- flake.lock | 18 +- src/core/Cargo.toml | 10 +- src/core/src/cache/budget.rs | 8 +- src/core/src/cache/builders.rs | 23 +- src/core/src/cache/core.rs | 44 +- src/core/src/cache/observer/stats.rs | 4 +- src/core/src/cache/tests/policies.rs | 4 +- ...he__tests__policies__default_policies.snap | 2 +- ...ts__squeezed__read_squeezed_date_time.snap | 2 +- ...__squeezed__read_squeezed_int64_array.snap | 2 +- ..._squeezed__read_squeezed_variant_path.snap | 2 +- src/core/src/cache/tests/squeezed.rs | 6 +- src/core/src/cache/utils.rs | 14 +- src/core/study/cache_storage.rs | 2 +- src/datafusion-local/README.md | 2 +- src/datafusion-local/src/lib.rs | 43 +- .../src/tests/date_optimizer.rs | 2 +- src/datafusion-local/src/tests/mod.rs | 4 +- ..._local__tests__squeeze__basic_squeeze.snap | 14 +- ...ocal__tests__squeeze__squeeze_strings.snap | 4 +- src/datafusion-local/src/tests/squeeze.rs | 10 +- src/datafusion-local/src/tests/variants.rs | 8 +- src/datafusion-server/Cargo.toml | 2 +- .../src/admin_server/handlers.rs | 6 +- src/datafusion-server/src/lib.rs | 6 +- src/datafusion-server/src/service.rs | 6 +- src/datafusion/Cargo.toml | 6 +- src/datafusion/bench/filter_pushdown.rs | 2 +- src/datafusion/src/cache/column.rs | 176 +-- src/datafusion/src/cache/mod.rs | 34 +- src/datafusion/src/lib.rs | 1 - src/datafusion/src/optimizers/lineage_opt.rs | 83 +- src/datafusion/src/optimizers/mod.rs | 192 +-- src/datafusion/src/reader/plantime/opener.rs | 871 +++++++------ 50 files changed, 1316 insertions(+), 1593 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4b97efa..429bd6f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,13 +150,13 @@ jobs: 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-cache-mb 256 &> server.log & + 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-cache-mb 256 + 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 @@ -190,13 +190,13 @@ jobs: 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-cache-mb 256 &> server.log & + 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-cache-mb 256 + 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 @@ -205,45 +205,46 @@ jobs: 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-cache-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-cache-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 + # Diable for now, because the upstream datafusion has a bug. + # 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 @@ -291,7 +292,7 @@ jobs: 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-cache-mb 10 + 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 @@ -339,7 +340,7 @@ jobs: --iteration 5 \ --reset-cache \ --bench-mode liquid \ - --max-cache-mb 64 + --max-memory-mb 64 - name: Run DataFusion benchmark (plain parquet) run: | 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..2f874975 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" @@ -338,7 +338,7 @@ dependencies = [ "arrow-schema", "chrono", "half", - "indexmap 2.13.0", + "indexmap 2.14.0", "itoa", "lexical-core", "memchr", @@ -431,9 +431,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 +513,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 +542,7 @@ dependencies = [ "sha1", "sync_wrapper", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.29.0", "tower", "tower-layer", "tower-service", @@ -593,9 +593,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 +656,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", ] @@ -686,16 +686,16 @@ dependencies = [ [[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]] @@ -781,9 +781,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 +828,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -894,9 +894,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 +917,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 +929,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 +961,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 +976,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" @@ -1068,6 +1068,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 +1088,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 +1195,24 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[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", ] @@ -1335,15 +1355,15 @@ 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 = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9f8117889ba9503440f1dd79ebab32ba52ccf1720bb83cd718a29d4edc0d16" +checksum = "93db0e623840612f7f2cd757f7e8a8922064192363732c88692e0870016e141b" dependencies = [ "arrow", "arrow-schema", @@ -1384,7 +1404,7 @@ dependencies = [ "object_store", "parking_lot", "parquet", - "rand 0.9.2", + "rand 0.9.4", "regex", "sqlparser", "tempfile", @@ -1396,9 +1416,9 @@ dependencies = [ [[package]] name = "datafusion-catalog" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be893b73a13671f310ffcc8da2c546b81efcc54c22e0382c0a28aa3537017137" +checksum = "37cefde60b26a7f4ff61e9d2ff2833322f91df2b568d7238afe67bde5bdffb66" dependencies = [ "arrow", "async-trait", @@ -1421,9 +1441,9 @@ dependencies = [ [[package]] name = "datafusion-catalog-listing" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830487b51ed83807d6b32d6325f349c3144ae0c9bf772cf2a712db180c31d5e6" +checksum = "17e112307715d6a7a331111a4c2330ff54bc237183511c319e3708a4cff431fb" dependencies = [ "arrow", "async-trait", @@ -1444,9 +1464,9 @@ dependencies = [ [[package]] name = "datafusion-common" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d7663f3af955292f8004e74bcaf8f7ea3d66cc38438749615bb84815b61a293" +checksum = "d72a11ca44a95e1081870d3abb80c717496e8a7acb467a1d3e932bb636af5cc2" dependencies = [ "ahash", "arrow", @@ -1454,7 +1474,7 @@ dependencies = [ "chrono", "half", "hashbrown 0.16.1", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools", "libc", "log", @@ -1469,9 +1489,9 @@ dependencies = [ [[package]] name = "datafusion-common-runtime" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f590205c7e32fe1fea48dd53ffb406e56ae0e7a062213a3ac848db8771641bd" +checksum = "89f4afaed29670ec4fd6053643adc749fe3f4bc9d1ce1b8c5679b22c67d12def" dependencies = [ "futures", "log", @@ -1480,9 +1500,9 @@ dependencies = [ [[package]] name = "datafusion-datasource" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde1e030a9dc87b743c806fbd631f5ecfa2ccaa4ffb61fa19144a07fea406b79" +checksum = "e9fb386e1691355355a96419978a0022b7947b44d4a24a6ea99f00b6b485cbb6" dependencies = [ "arrow", "async-compression", @@ -1506,7 +1526,7 @@ dependencies = [ "liblzma", "log", "object_store", - "rand 0.9.2", + "rand 0.9.4", "tokio", "tokio-util", "url", @@ -1515,9 +1535,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-arrow" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331ebae7055dc108f9b54994b93dff91f3a17445539efe5b74e89264f7b36e15" +checksum = "ffa6c52cfed0734c5f93754d1c0175f558175248bf686c944fb05c373e5fc096" dependencies = [ "arrow", "arrow-ipc", @@ -1539,9 +1559,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-csv" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e0d475088325e2986876aa27bb30d0574f72a22955a527d202f454681d55c5c" +checksum = "503f29e0582c1fc189578d665ff57d9300da1f80c282777d7eb67bb79fb8cdca" dependencies = [ "arrow", "async-trait", @@ -1562,9 +1582,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-json" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea1520d81f31770f3ad6ee98b391e75e87a68a5bb90de70064ace5e0a7182fe8" +checksum = "e33804749abc8d0c8cb7473228483cb8070e524c6f6086ee1b85a64debe2b3d2" dependencies = [ "arrow", "async-trait", @@ -1586,9 +1606,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-parquet" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95be805d0742ab129720f4c51ad9242cd872599cdb076098b03f061fcdc7f946" +checksum = "32a8e0365e0e08e8ff94d912f0ababcf9065a1a304018ba90b1fc83c855b4997" dependencies = [ "arrow", "async-trait", @@ -1616,15 +1636,15 @@ dependencies = [ [[package]] name = "datafusion-doc" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c93ad9e37730d2c7196e68616f3f2dd3b04c892e03acd3a8eeca6e177f3c06a" +checksum = "8de6ac0df1662b9148ad3c987978b32cbec7c772f199b1d53520c8fa764a87ee" [[package]] name = "datafusion-execution" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9437d3cd5d363f9319f8122182d4d233427de79c7eb748f23054c9aaa0fdd8df" +checksum = "c03c7fbdaefcca4ef6ffe425a5fc2325763bfb426599bb0bf4536466efabe709" dependencies = [ "arrow", "arrow-buffer", @@ -1638,16 +1658,16 @@ 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 = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67164333342b86521d6d93fa54081ee39839894fb10f7a700c099af96d7552cf" +checksum = "574b9b6977fedbd2a611cbff12e5caf90f31640ad9dc5870f152836d94bad0dd" dependencies = [ "arrow", "async-trait", @@ -1658,7 +1678,7 @@ dependencies = [ "datafusion-functions-aggregate-common", "datafusion-functions-window-common", "datafusion-physical-expr-common", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools", "paste", "recursive", @@ -1668,22 +1688,22 @@ dependencies = [ [[package]] name = "datafusion-expr-common" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab05fdd00e05d5a6ee362882546d29d6d3df43a6c55355164a7fbee12d163bc9" +checksum = "7d7c3adf3db8bf61e92eb90cb659c8e8b734593a8f7c8e12a843c7ddba24b87e" dependencies = [ "arrow", "datafusion-common", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools", "paste", ] [[package]] name = "datafusion-functions" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04fb863482d987cf938db2079e07ab0d3bb64595f28907a6c2f8671ad71cca7e" +checksum = "f28aa4e10384e782774b10e72aca4d93ef7b31aa653095d9d4536b0a3dbc51b6" dependencies = [ "arrow", "arrow-buffer", @@ -1704,7 +1724,7 @@ dependencies = [ "md-5", "memchr", "num-traits", - "rand 0.9.2", + "rand 0.9.4", "regex", "sha2", "unicode-segmentation", @@ -1713,9 +1733,9 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829856f4e14275fb376c104f27cbf3c3b57a9cfe24885d98677525f5e43ce8d6" +checksum = "00aa6217e56098ba84e0a338176fe52f0a84cca398021512c6c8c5eff806d0ad" dependencies = [ "ahash", "arrow", @@ -1735,9 +1755,9 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate-common" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08af79cc3d2aa874a362fb97decfcbd73d687190cb096f16a6c85a7780cce311" +checksum = "b511250349407db7c43832ab2de63f5557b19a20dfd236b39ca2c04468b50d47" dependencies = [ "ahash", "arrow", @@ -1748,9 +1768,9 @@ dependencies = [ [[package]] name = "datafusion-functions-nested" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465ae3368146d49c2eda3e2c0ef114424c87e8a6b509ab34c1026ace6497e790" +checksum = "ef13a858e20d50f0a9bb5e96e7ac82b4e7597f247515bccca4fdd2992df0212a" dependencies = [ "arrow", "arrow-ord", @@ -1773,9 +1793,9 @@ dependencies = [ [[package]] name = "datafusion-functions-table" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6156e6b22fcf1784112fc0173f3ae6e78c8fdb4d3ed0eace9543873b437e2af6" +checksum = "72b40d3f5bbb3905f9ccb1ce9485a9595c77b69758a7c24d3ba79e334ff51e7e" dependencies = [ "arrow", "async-trait", @@ -1789,9 +1809,9 @@ dependencies = [ [[package]] name = "datafusion-functions-window" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca7baec14f866729012efb89011a6973f3a346dc8090c567bfcd328deff551c1" +checksum = "d4e88ec9d57c9b685d02f58bfee7be62d72610430ddcedb82a08e5d9925dbfb6" dependencies = [ "arrow", "datafusion-common", @@ -1807,9 +1827,9 @@ dependencies = [ [[package]] name = "datafusion-functions-window-common" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "159228c3280d342658466bb556dc24de30047fe1d7e559dc5d16ccc5324166f9" +checksum = "8307bb93519b1a91913723a1130cfafeee3f72200d870d88e91a6fc5470ede5c" dependencies = [ "datafusion-common", "datafusion-physical-expr-common", @@ -1817,9 +1837,9 @@ dependencies = [ [[package]] name = "datafusion-macros" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5427e5da5edca4d21ea1c7f50e1c9421775fe33d7d5726e5641a833566e7578" +checksum = "2e367e6a71051d0ebdd29b2f85d12059b38b1d1f172c6906e80016da662226bd" dependencies = [ "datafusion-doc", "quote", @@ -1828,9 +1848,9 @@ dependencies = [ [[package]] name = "datafusion-optimizer" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89099eefcd5b223ec685c36a41d35c69239236310d71d339f2af0fa4383f3f46" +checksum = "e929015451a67f77d9d8b727b2bf3a40c4445fdef6cdc53281d7d97c76888ace" dependencies = [ "arrow", "chrono", @@ -1838,7 +1858,7 @@ dependencies = [ "datafusion-expr", "datafusion-expr-common", "datafusion-physical-expr", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools", "log", "recursive", @@ -1848,9 +1868,9 @@ dependencies = [ [[package]] name = "datafusion-physical-expr" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f222df5195d605d79098ef37bdd5323bff0131c9d877a24da6ec98dfca9fe36" +checksum = "4b1e68aba7a4b350401cfdf25a3d6f989ad898a7410164afe9ca52080244cb59" dependencies = [ "ahash", "arrow", @@ -1861,7 +1881,7 @@ dependencies = [ "datafusion-physical-expr-common", "half", "hashbrown 0.16.1", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools", "parking_lot", "paste", @@ -1872,9 +1892,9 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-adapter" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40838625d63d9c12549d81979db3dd675d159055eb9135009ba272ab0e8d0f64" +checksum = "ea22315f33cf2e0adc104e8ec42e285f6ed93998d565c65e82fec6a9ee9f9db4" dependencies = [ "arrow", "datafusion-common", @@ -1887,9 +1907,9 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-common" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eacbcc4cfd502558184ed58fa3c72e775ec65bf077eef5fd2b3453db676f893c" +checksum = "b04b45ea8ad3ac2d78f2ea2a76053e06591c9629c7a603eda16c10649ecf4362" dependencies = [ "ahash", "arrow", @@ -1897,16 +1917,16 @@ dependencies = [ "datafusion-common", "datafusion-expr-common", "hashbrown 0.16.1", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools", "parking_lot", ] [[package]] name = "datafusion-physical-optimizer" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d501d0e1d0910f015677121601ac177ec59272ef5c9324d1147b394988f40941" +checksum = "7cb13397809a425918f608dfe8653f332015a3e330004ab191b4404187238b95" dependencies = [ "arrow", "datafusion-common", @@ -1923,9 +1943,9 @@ dependencies = [ [[package]] name = "datafusion-physical-plan" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463c88ad6f1ecab1810f4c9f046898bee035b370137eb79b2b2db925e270631d" +checksum = "5edc023675791af9d5fb4cc4c24abf5f7bd3bd4dcf9e5bd90ea1eff6976dcc79" dependencies = [ "ahash", "arrow", @@ -1944,7 +1964,7 @@ dependencies = [ "futures", "half", "hashbrown 0.16.1", - "indexmap 2.13.0", + "indexmap 2.14.0", "itertools", "log", "num-traits", @@ -1955,9 +1975,9 @@ dependencies = [ [[package]] name = "datafusion-proto" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677ee4448a010ed5faeff8d73ff78972c2ace59eff3cd7bd15833a1dafa00492" +checksum = "6a387aaef949dc16bb6abc81bd1af850ec7449183aef011214f9724957495738" dependencies = [ "arrow", "chrono", @@ -1978,14 +1998,14 @@ dependencies = [ "datafusion-proto-common", "object_store", "prost", - "rand 0.9.2", + "rand 0.9.4", ] [[package]] name = "datafusion-proto-common" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965eca01edc8259edbbd95883a00b6d81e329fd44a019cfac3a03b026a83eade" +checksum = "16e614c7c53a9c304c6a850b821010bb492e57300311835f1180613f9d2c63d9" dependencies = [ "arrow", "datafusion-common", @@ -1994,9 +2014,9 @@ dependencies = [ [[package]] name = "datafusion-pruning" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2857618a0ecbd8cd0cf29826889edd3a25774ec26b2995fc3862095c95d88fc6" +checksum = "ac8c76860e355616555081cab5968cec1af7a80701ff374510860bcd567e365a" dependencies = [ "arrow", "datafusion-common", @@ -2011,9 +2031,9 @@ dependencies = [ [[package]] name = "datafusion-session" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8637e35022c5c775003b3ab1debc6b4a8f0eb41b069bdd5475dd3aa93f6eba" +checksum = "5412111aa48e2424ba926112e192f7a6b7e4ccb450145d25ce5ede9f19dc491e" dependencies = [ "async-trait", "datafusion-common", @@ -2025,9 +2045,9 @@ dependencies = [ [[package]] name = "datafusion-sql" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12d9e9f16a1692a11c94bcc418191fa15fd2b4d72a0c1a0c607db93c0b84dd81" +checksum = "fa0d133ddf8b9b3b872acac900157f783e7b879fe9a6bccf389abebbfac45ec1" dependencies = [ "arrow", "bigdecimal", @@ -2035,7 +2055,7 @@ dependencies = [ "datafusion-common", "datafusion-expr", "datafusion-functions-nested", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "recursive", "regex", @@ -2114,9 +2134,9 @@ dependencies = [ [[package]] name = "dioxus" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d5b0aec58753daee127a5fe2d1a40b0db8cebc0b8a7f97b34df2492cb90d78e" +checksum = "14b5fd24c3d394e5a8cfc15b8319da5f853c9d27471a9f46ace0653761499740" dependencies = [ "dioxus-asset-resolver", "dioxus-cli-config", @@ -2147,9 +2167,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 +2188,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 +2207,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 +2224,7 @@ dependencies = [ "futures-util", "generational-box", "longest-increasing-subsequence", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustversion", "serde", "slab", @@ -2215,9 +2235,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 +2248,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 +2274,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 +2285,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 +2304,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 +2352,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tokio-stream", - "tokio-tungstenite", + "tokio-tungstenite 0.28.0", "tokio-util", "tower", "tower-http", @@ -2349,9 +2369,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 +2397,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 +2411,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 +2421,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 +2437,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 +2464,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 +2476,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 +2495,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 +2510,7 @@ dependencies = [ "futures-channel", "futures-util", "generational-box", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "serde_json", "slab", @@ -2503,9 +2523,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 +2535,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,9 +2556,9 @@ 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", @@ -2551,9 +2571,9 @@ dependencies = [ [[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 +2584,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 +2623,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 +2642,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 +2682,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 +2694,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 +2714,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", @@ -2712,7 +2732,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", ] @@ -2878,14 +2898,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", @@ -2941,7 +2961,7 @@ dependencies = [ "fastrace-macro", "parking_lot", "pin-project", - "rand 0.9.2", + "rand 0.9.4", "rtrb", "serde", ] @@ -2985,9 +3005,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 +3039,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 +3083,11 @@ dependencies = [ [[package]] name = "fsst-rs" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf65e16e100438be0030d113042e07a62bed67203998640ca6fae0404eed71e" +checksum = "3bf53d7c403a2b76873d4d66ba7d79c54bde2784cdaba6083f223d6e33270708" dependencies = [ - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", ] [[package]] @@ -3166,29 +3186,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 +3240,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "rand_core 0.10.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -3321,7 +3326,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.13.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -3372,6 +3377,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "hdrhistogram" version = "7.5.4" @@ -3496,9 +3507,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[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 +3522,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -3519,16 +3529,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 +3609,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 +3623,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 +3636,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 +3650,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 +3670,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 +3714,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 +3734,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.0", "serde", "serde_core", ] @@ -3750,7 +3760,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 +3791,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", ] @@ -3807,9 +3817,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ "memchr", "serde", @@ -3823,7 +3833,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3843,15 +3853,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 +3869,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 +3907,7 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", @@ -3906,9 +3916,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 = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +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 = "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,10 +3954,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -3947,15 +3981,30 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "serde", ] +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "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 +4077,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 +4118,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 +4135,11 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libmimalloc-sys" -version = "0.1.44" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" +checksum = "2d1eacfa31c33ec25e873c136ba5669f00f9866d0688bea7be4d3f7e43067df6" dependencies = [ "cc", - "libc", ] [[package]] @@ -4131,7 +4179,7 @@ dependencies = [ "object_store", "parquet", "parquet-variant-compute", - "rand 0.10.0", + "rand 0.10.1", "serde", "serde_json", "shuttle", @@ -4171,7 +4219,7 @@ dependencies = [ "perf-event2", "pprof", "regex", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde", "serde_json", "sysinfo", @@ -4218,10 +4266,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", @@ -4314,9 +4360,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" @@ -4408,9 +4454,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 +4489,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 +4505,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 +4519,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", @@ -4545,9 +4591,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.48" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" +checksum = "b3627c4272df786b9260cabaa46aec1d59c93ede723d4c3ef646c503816b0640" dependencies = [ "libmimalloc-sys", ] @@ -4586,9 +4632,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 +4664,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 +4685,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]] @@ -4659,7 +4705,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -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", @@ -4780,20 +4826,20 @@ dependencies = [ [[package]] name = "objc2-cloud-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-foundation", ] [[package]] name = "objc2-core-data" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ "objc2", "objc2-foundation", @@ -4805,18 +4851,18 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "dispatch2", "objc2", ] [[package]] name = "objc2-core-graphics" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -4825,9 +4871,9 @@ dependencies = [ [[package]] name = "objc2-core-image" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ "objc2", "objc2-foundation", @@ -4835,14 +4881,26 @@ dependencies = [ [[package]] name = "objc2-core-location" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac0f75792558aa9d618443bbb5db7426a7a0b6fddf96903f86ef9ad02e135740" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" dependencies = [ "objc2", "objc2-foundation", ] +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -4851,11 +4909,11 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2", "libc", "objc2", @@ -4874,22 +4932,22 @@ dependencies = [ [[package]] name = "objc2-io-surface" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-core-foundation", ] [[package]] name = "objc2-quartz-core" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -4897,11 +4955,11 @@ dependencies = [ [[package]] name = "objc2-ui-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2", "objc2", "objc2-cloud-kit", @@ -4910,6 +4968,7 @@ dependencies = [ "objc2-core-graphics", "objc2-core-image", "objc2-core-location", + "objc2-core-text", "objc2-foundation", "objc2-quartz-core", "objc2-user-notifications", @@ -4917,9 +4976,9 @@ dependencies = [ [[package]] name = "objc2-user-notifications" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a3f5ec77a81d9e0c5a0b32159b0cb143d7086165e79708351e02bf37dfc65cd" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ "objc2", "objc2-foundation", @@ -4957,7 +5016,7 @@ dependencies = [ "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 +5119,7 @@ dependencies = [ "futures-util", "opentelemetry", "percent-encoding", - "rand 0.9.2", + "rand 0.9.4", "thiserror 2.0.18", ] @@ -5165,7 +5224,7 @@ dependencies = [ "arrow-schema", "chrono", "half", - "indexmap 2.13.0", + "indexmap 2.14.0", "simdutf8", "uuid", ] @@ -5180,7 +5239,7 @@ dependencies = [ "arrow-schema", "chrono", "half", - "indexmap 2.13.0", + "indexmap 2.14.0", "parquet-variant", "parquet-variant-json", "serde_json", @@ -5219,7 +5278,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 +5299,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", @@ -5299,7 +5358,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap 2.14.0", "serde", ] @@ -5347,17 +5406,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 +5432,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", ] @@ -5434,9 +5487,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" @@ -5540,9 +5593,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 +5641,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 +5659,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", @@ -5629,7 +5682,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -5661,9 +5714,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 +5725,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 +5735,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 +5784,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,7 +5829,7 @@ 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]] @@ -5863,9 +5916,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64 0.22.1", "bytes", @@ -5917,9 +5970,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 +5988,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 +6007,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 +6042,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 +6052,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 +6084,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 +6129,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 +6148,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" @@ -6282,17 +6335,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 +6356,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" @@ -6442,15 +6496,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 +6521,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 +6540,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 +6555,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 +6571,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 +6591,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 +6602,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 +6613,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 +6633,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 +6645,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 +6705,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 +6722,22 @@ dependencies = [ [[package]] name = "t4" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586231b4196aa1217287f36db73b82afaf2e428d82824b395a806e0e7ae2b76" +checksum = "dea97b3f25826306b821d1c0b070709ef61d28e98c3ef1153de1048c33acf381" dependencies = [ "io-uring", "libc", "shuttle", "t4-verified", + "vstd", ] [[package]] name = "t4-verified" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27bc679002d08d84ba2ee9e644ee2da132db2540e454b7a1f5b024e3e668089" +checksum = "a729c7595d4771ff781dcd1fdd5f65d14c8225dce6341ddf28792b292e6f5cdb" dependencies = [ "vstd", ] @@ -6703,17 +6758,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 +6883,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 +6908,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -6868,9 +6923,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 +6977,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,32 +7006,32 @@ 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]] @@ -7015,7 +7082,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.13.0", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -7032,7 +7099,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", "futures-core", "futures-util", @@ -7166,7 +7233,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.2", + "rand 0.9.4", "sha1", "thiserror 2.0.18", "utf-8", @@ -7183,12 +7250,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 +7286,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 +7310,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 +7426,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -7403,15 +7486,15 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "verus_builtin" -version = "0.0.0-2026-02-08-0120" +version = "0.0.0-2026-04-12-0118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1bd13abb198fff161cf4be16cf8c1723f09941d03e323af71146fa4cfb0de0f" +checksum = "a46cb431066009ad2035f6bca936b1c2b7e293bffec93a2090fead0f35ab4276" [[package]] name = "verus_builtin_macros" -version = "0.0.0-2026-02-22-0103" +version = "0.0.0-2026-04-20-1748" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe750a656777699bfdcfee3894cc24f04b6f276e1240b2c9dbdeccdae4e5d94e" +checksum = "3131a0e0b0dc9272cb574ad1f8506d8e25ba33c9f9972151866c34cad12bb076" dependencies = [ "proc-macro2", "quote", @@ -7423,9 +7506,9 @@ dependencies = [ [[package]] name = "verus_prettyplease" -version = "0.0.0-2026-02-15-0106" +version = "0.0.0-2026-04-12-0118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa1c9a8d5635aac38528868312f72ed3f0e76f58ca25ac5c407d1064bdc33430" +checksum = "4b246e61b068e807cb05a030fc1d7efa83d2a0d227eaf67921661419edfe8e83" dependencies = [ "proc-macro2", "verus_syn", @@ -7433,9 +7516,9 @@ dependencies = [ [[package]] name = "verus_state_machines_macros" -version = "0.0.0-2026-02-15-0106" +version = "0.0.0-2026-04-20-1748" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28cb0ec7d66fc27adccbd5aa9d96acf3ab5508cd97f1ca8930fed4f272f6ba8" +checksum = "ba2497778c0b23d5cbd93f43cce512438af5cb01b1f1435aa7e411da8495efb5" dependencies = [ "indexmap 1.9.3", "proc-macro2", @@ -7445,9 +7528,9 @@ dependencies = [ [[package]] name = "verus_syn" -version = "0.0.0-2026-02-15-0106" +version = "0.0.0-2026-04-05-0114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fbe9986fe3ffe05a07a87834a6a2400792e52969813fa9660e313e1c9eda6b7" +checksum = "285b554a87b470ee705634ea1cdc92c14ba088a7b8bdacbb9116b32e832fe272" dependencies = [ "proc-macro2", "quote", @@ -7456,9 +7539,9 @@ dependencies = [ [[package]] name = "vstd" -version = "0.0.0-2026-02-22-0103" +version = "0.0.0-2026-04-20-1748" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b74abf6d04d21b8898894960d11dbe2489ab48be1f24eca92821b865616910c" +checksum = "8d03a77a2ee1b91bc776f8e9ee716d83b519d4338912116d7e855edfa3a8b976" dependencies = [ "verus_builtin", "verus_builtin_macros", @@ -7514,11 +7597,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 +7610,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.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -7545,23 +7628,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" 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.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7569,9 +7648,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -7582,9 +7661,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] @@ -7606,7 +7685,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 +7709,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.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", @@ -7658,9 +7737,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 +7766,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]] @@ -8059,9 +8138,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 +8154,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 +8179,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.13.0", + "indexmap 2.14.0", "prettyplease", "syn", "wasm-metadata", @@ -8124,8 +8209,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 +8229,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "semver", "serde", @@ -8156,9 +8241,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 +8262,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 +8273,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 +8285,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 +8305,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 +8332,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 +8343,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 +8354,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..a6a31c64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,15 +43,15 @@ parquet = { version = "58.1.0", features = [ ] } 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" } +datafusion = { version = "53.1.0" } +datafusion-common = { version = "53.1.0" } +datafusion-expr-common = { version = "53.1.0" } +datafusion-physical-expr = { version = "53.1.0" } +datafusion-physical-expr-common = { version = "53.1.0" } +datafusion-proto = { version = "53.1.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"] } +tokio = { version = "1.52.1", features = ["rt-multi-thread"] } log = "0.4.29" tonic = { version = "0.14.5" } url = "2.5.8" @@ -62,12 +62,12 @@ 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.1", features = ["v4"] } fastrace = "0.7" fastrace-tonic = "0.2" congee = "0.4.1" insta = "1.47.2" -t4 = "0.1.3" +t4 = "0.1.6" [profile.dev.package] insta.opt-level = 3 diff --git a/benchmark/Cargo.toml b/benchmark/Cargo.toml index 6180a248..a95464fd 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.50" serde_json.workspace = true serde.workspace = true sysinfo = { version = "0.38.4", default-features = false, features = [ @@ -35,8 +35,8 @@ 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.3", default-features = false, features = ["json"] } +uuid = { version = "1.23.1", features = ["v4"] } pprof = { version = "0.15.0", features = ["flamegraph"] } anyhow = "1.0" usdt = "0.6" 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/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..ea855efc 100644 --- a/dev/dev-tools/Cargo.toml +++ b/dev/dev-tools/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] -dioxus = { version = "0.7.4", features = ["router", "fullstack"] } +dioxus = { version = "0.7.6", features = ["router", "fullstack"] } [features] default = ["web"] diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 7903fd7a..3c9f4aaa 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -41,5 +41,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/example_inprocess_cache_eviction.rs b/examples/example_inprocess_cache_eviction.rs index 8f3c493a..a0f4f13d 100644 --- a/examples/example_inprocess_cache_eviction.rs +++ b/examples/example_inprocess_cache_eviction.rs @@ -16,7 +16,7 @@ async fn main() -> Result<(), Box> { println!("{:?}", temp_dir); let (_ctx, storage) = LiquidCacheLocalBuilder::new() - .with_max_cache_bytes(1024 * 1024) // 1MB + .with_max_memory_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())) diff --git a/examples/example_inprocess_insertion.rs b/examples/example_inprocess_insertion.rs index 7605d50f..3caf0c01 100644 --- a/examples/example_inprocess_insertion.rs +++ b/examples/example_inprocess_insertion.rs @@ -14,7 +14,7 @@ async fn main() -> Result<(), Box> { let temp_dir = TempDir::new().unwrap(); let (_ctx, storage) = LiquidCacheLocalBuilder::new() - .with_max_cache_bytes(1024 * 1024) // 1MB + .with_max_memory_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())) diff --git a/examples/example_local.rs b/examples/example_local.rs index d060703c..38577fab 100644 --- a/examples/example_local.rs +++ b/examples/example_local.rs @@ -9,7 +9,7 @@ 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_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_server.rs b/examples/example_server.rs index f6b15303..84634e5d 100644 --- a/examples/example_server.rs +++ b/examples/example_server.rs @@ -10,7 +10,7 @@ use tonic::transport::Server; async fn main() -> Result<(), Box> { let liquid_cache = LiquidCacheService::new( SessionContext::new(), - Some(1024 * 1024 * 1024), // max memory cache size 1GB + Some(1024 * 1024 * 1024), // max memory size 1GB Some(tempfile::tempdir()?.keep()), // disk cache dir Box::new(LruPolicy::new()), Box::new(TranscodeSqueezeEvict), diff --git a/flake.lock b/flake.lock index 6aedd304..21cfad5e 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1774313767, - "narHash": "sha256-hy0XTQND6avzGEUFrJtYBBpFa/POiiaGBr2vpU6Y9tY=", + "lastModified": 1777335812, + "narHash": "sha256-bEg5xoAxAwsyfnGhkEX7RJViTIBIYPd8ISg4O1c0HFc=", "owner": "ipetkov", "repo": "crane", - "rev": "3d9df76e29656c679c744968b17fbaf28f0e923d", + "rev": "5e0fb2f64edff2822249f21293b8304dedaaf676", "type": "github" }, "original": { @@ -35,11 +35,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": { @@ -78,11 +78,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/src/core/Cargo.toml b/src/core/Cargo.toml index c7a6cca3..38cdad0b 100644 --- a/src/core/Cargo.toml +++ b/src/core/Cargo.toml @@ -27,7 +27,7 @@ arrow = { workspace = true } arrow-schema = { workspace = true } fastlanes = "0.5.0" num-traits = "0.2.19" -fsst-rs = "0.5.9" +fsst-rs = "0.5.10" ahash = { workspace = true } tempfile = { workspace = true } congee = { workspace = true } @@ -40,14 +40,14 @@ serde = { workspace = true } [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.50" +clap = { version = "4.6.1", features = ["derive"] } divan = "0.1.21" kani-verifier = "0.67.0" insta = { workspace = true } diff --git a/src/core/src/cache/budget.rs b/src/core/src/cache/budget.rs index 921b388f..097cb417 100644 --- a/src/core/src/cache/budget.rs +++ b/src/core/src/cache/budget.rs @@ -21,8 +21,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 +40,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, diff --git a/src/core/src/cache/builders.rs b/src/core/src/cache/builders.rs index f3d35862..8be3e42c 100644 --- a/src/core/src/cache/builders.rs +++ b/src/core/src/cache/builders.rs @@ -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,12 @@ use crate::sync::Arc; /// ``` pub struct LiquidCacheBuilder { batch_size: usize, - max_cache_bytes: usize, + max_memory_bytes: usize, cache_policy: Box, hydration_policy: Box, squeeze_policy: Box, io_context: Option>, + squeeze_victims_concurrently: bool, } impl Default for LiquidCacheBuilder { @@ -49,11 +50,12 @@ impl LiquidCacheBuilder { pub fn new() -> Self { Self { batch_size: 8192, - max_cache_bytes: 1024 * 1024 * 1024, + max_memory_bytes: 1024 * 1024 * 1024, cache_policy: Box::new(LiquidPolicy::new()), hydration_policy: Box::new(super::AlwaysHydrate::new()), squeeze_policy: Box::new(TranscodeSqueezeEvict), io_context: None, + squeeze_victims_concurrently: !cfg!(test), } } @@ -64,10 +66,10 @@ impl LiquidCacheBuilder { self } - /// Set the max cache bytes for the cache. + /// Set the max memory 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; + pub fn with_max_memory_bytes(mut self, max_memory_bytes: usize) -> Self { + self.max_memory_bytes = max_memory_bytes; self } @@ -99,6 +101,12 @@ impl LiquidCacheBuilder { 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. @@ -118,11 +126,12 @@ impl LiquidCacheBuilder { }; Arc::new(LiquidCache::new( self.batch_size, - self.max_cache_bytes, + self.max_memory_bytes, self.squeeze_policy, self.cache_policy, self.hydration_policy, io_worker, + self.squeeze_victims_concurrently, )) } } diff --git a/src/core/src/cache/core.rs b/src/core/src/cache/core.rs index 7a5d76df..4fdea624 100644 --- a/src/core/src/cache/core.rs +++ b/src/core/src/cache/core.rs @@ -58,6 +58,7 @@ pub struct LiquidCache { squeeze_policy: Box, observer: Arc, io_context: Arc, + squeeze_victims_concurrently: bool, } /// Builder returned by [`LiquidCache::insert`] for configuring cache writes. @@ -110,7 +111,7 @@ 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(), runtime, } } @@ -334,22 +335,24 @@ impl LiquidCache { /// Create a new instance of CacheStorage. pub(crate) fn new( batch_size: usize, - max_cache_bytes: usize, + max_memory_bytes: usize, squeeze_policy: Box, cache_policy: Box, hydration_policy: Box, io_worker: Arc, + squeeze_victims_concurrently: bool, ) -> Self { - let config = CacheConfig::new(batch_size, max_cache_bytes); + let config = CacheConfig::new(batch_size, max_memory_bytes); Self { index: ArtIndex::new(), - budget: BudgetAccounting::new(config.max_cache_bytes()), + budget: BudgetAccounting::new(config.max_memory_bytes()), config, cache_policy, hydration_policy, squeeze_policy, observer: Arc::new(Observer::new()), io_context: io_worker, + squeeze_victims_concurrently, } } @@ -403,15 +406,20 @@ impl LiquidCache { #[fastrace::trace] async fn squeeze_victims(&self, victims: Vec) { - // Run squeeze operations sequentially using async I/O self.trace(InternalEvent::SqueezeBegin { victims: victims.clone(), }); - futures::stream::iter(victims) - .for_each_concurrent(None, |victim| async move { + if self.squeeze_victims_concurrently { + futures::stream::iter(victims) + .for_each_concurrent(None, |victim| async move { + self.squeeze_victim_inner(victim).await; + }) + .await; + } else { + for victim in victims { self.squeeze_victim_inner(victim).await; - }) - .await; + } + } } async fn squeeze_victim_inner(&self, to_squeeze: EntryID) { @@ -985,13 +993,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")] @@ -1046,7 +1054,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; @@ -1062,7 +1070,7 @@ mod tests { 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; diff --git a/src/core/src/cache/observer/stats.rs b/src/core/src/cache/observer/stats.rs index f81d57f5..cfb74e8d 100644 --- a/src/core/src/cache/observer/stats.rs +++ b/src/core/src/cache/observer/stats.rs @@ -144,8 +144,8 @@ 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, /// Runtime counters snapshot. pub runtime: RuntimeStatsSnapshot, } diff --git a/src/core/src/cache/tests/policies.rs b/src/core/src/cache/tests/policies.rs index b0aba179..fc85129b 100644 --- a/src/core/src/cache/tests/policies.rs +++ b/src/core/src/cache/tests/policies.rs @@ -12,7 +12,7 @@ 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; @@ -40,7 +40,7 @@ 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; 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..1274941f 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,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=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 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..56e09ed8 100644 --- a/src/core/src/cache/tests/squeezed.rs +++ b/src/core/src/cache/tests/squeezed.rs @@ -27,7 +27,7 @@ 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_max_memory_bytes(array_size * 2) .with_io_context(Arc::new(DefaultIoContext::new( t4::mount(temp_dir.path().join("liquid_cache.t4")) .await @@ -92,7 +92,7 @@ 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_max_memory_bytes(array_size * 3 / 2) .with_io_context(Arc::new(DefaultIoContext::new( t4::mount(temp_dir.path().join("liquid_cache.t4")) .await @@ -154,7 +154,7 @@ 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_max_memory_bytes(array_size * 2) .with_io_context(Arc::new(DefaultIoContext::new( t4::mount(temp_dir.path().join("liquid_cache.t4")) .await diff --git a/src/core/src/cache/utils.rs b/src/core/src/cache/utils.rs index a430db48..f22f398d 100644 --- a/src/core/src/cache/utils.rs +++ b/src/core/src/cache/utils.rs @@ -8,14 +8,14 @@ use bytes::Bytes; #[derive(Debug)] pub struct CacheConfig { batch_size: usize, - max_cache_bytes: usize, + max_memory_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) -> Self { Self { batch_size, - max_cache_bytes, + max_memory_bytes, } } @@ -23,8 +23,8 @@ 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 } } @@ -46,7 +46,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 +55,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/study/cache_storage.rs b/src/core/study/cache_storage.rs index d9a819d3..66a0e598 100644 --- a/src/core/study/cache_storage.rs +++ b/src/core/study/cache_storage.rs @@ -51,7 +51,7 @@ fn main() { 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) diff --git a/src/datafusion-local/README.md b/src/datafusion-local/README.md index 142049b0..bc6e1454 100644 --- a/src/datafusion-local/README.md +++ b/src/datafusion-local/README.md @@ -19,7 +19,7 @@ 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())) .build(SessionConfig::new()) diff --git a/src/datafusion-local/src/lib.rs b/src/datafusion-local/src/lib.rs index 678ae75e..9f23d654 100644 --- a/src/datafusion-local/src/lib.rs +++ b/src/datafusion-local/src/lib.rs @@ -12,8 +12,7 @@ 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::cache_policies::{CachePolicy, LiquidPolicy}; use liquid_cache_datafusion::optimizers::{LineageOptimizer, 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,8 @@ 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, /// Directory for disk cache cache_dir: PathBuf, /// Cache policy @@ -69,21 +68,18 @@ pub struct LiquidCacheLocalBuilder { /// Hydration policy hydration_policy: Box, span: fastrace::Span, - - eager_shredding: bool, } impl Default for LiquidCacheLocalBuilder { fn default() -> Self { Self { batch_size: 8192, - max_cache_bytes: 1024 * 1024 * 1024, // 1GB + max_memory_bytes: 1024 * 1024 * 1024, // 1GB 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, } } } @@ -100,9 +96,9 @@ 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 + pub fn with_max_memory_bytes(mut self, max_memory_bytes: usize) -> Self { + self.max_memory_bytes = max_memory_bytes; self } @@ -136,12 +132,6 @@ impl LiquidCacheLocalBuilder { self } - /// Set enable shredding - pub fn with_eager_shredding(mut self, eager_shredding: bool) -> Self { - self.eager_shredding = eager_shredding; - self - } - /// Build a SessionContext with liquid cache configured /// Returns the SessionContext and the liquid cache reference pub async fn build( @@ -161,20 +151,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, + store, + self.cache_policy, + self.squeeze_policy, + self.hydration_policy, + ) + .await; + + #[cfg(test)] + let cache = LiquidCacheParquet::new_with_squeeze_victim_concurrency( + self.batch_size, + self.max_memory_bytes, store, self.cache_policy, self.squeeze_policy, self.hydration_policy, + false, ) .await; let cache_ref = Arc::new(cache); let date_extract_optimizer = Arc::new(LineageOptimizer::new()); - let optimizer = LocalModeOptimizer::new(cache_ref.clone(), self.eager_shredding); + let optimizer = LocalModeOptimizer::new(cache_ref.clone()); let state = datafusion::execution::SessionStateBuilder::new() .with_config(config) diff --git a/src/datafusion-local/src/tests/date_optimizer.rs b/src/datafusion-local/src/tests/date_optimizer.rs index ab421b2b..a3e1edb7 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())); 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__squeeze__basic_squeeze.snap b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__basic_squeeze.snap index f9e15ff4..a0356b48 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 @@ -23,9 +23,9 @@ 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=insert_failed entry=2 kind=MemoryArrow event=squeeze_begin victims=[262145,1] @@ -41,9 +41,9 @@ 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 @@ -67,15 +67,15 @@ event=insert_failed entry=1 kind=MemoryLiquid event=squeeze_begin victims=[262146,0,262144,2] event=squeeze_victim entry=262146 event=io_write entry=262146 kind=DiskLiquid bytes=17448 +event=insert_success entry=262146 kind=DiskLiquid event=squeeze_victim entry=0 event=io_write entry=0 kind=MemorySqueezedLiquid bytes=63528 +event=insert_success entry=0 kind=MemorySqueezedLiquid 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=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 @@ -88,12 +88,12 @@ 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=insert_success entry=1 kind=MemorySqueezedLiquid 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=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 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..9c40e641 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 @@ -30,9 +30,9 @@ 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,9 +45,9 @@ 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 diff --git a/src/datafusion-local/src/tests/squeeze.rs b/src/datafusion-local/src/tests/squeeze.rs index 6a12b4a7..73b6d686 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 @@ -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..556f21dd 100644 --- a/src/datafusion-server/Cargo.toml +++ b/src/datafusion-server/Cargo.toml @@ -23,7 +23,7 @@ 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"] } sysinfo = { version = "0.38.4", default-features = false, features = [ diff --git a/src/datafusion-server/src/admin_server/handlers.rs b/src/datafusion-server/src/admin_server/handlers.rs index 87e4e12e..55efd0a5 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, }) diff --git a/src/datafusion-server/src/lib.rs b/src/datafusion-server/src/lib.rs index abba734b..3a5ee2a1 100644 --- a/src/datafusion-server/src/lib.rs +++ b/src/datafusion-server/src/lib.rs @@ -124,11 +124,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 +145,7 @@ impl LiquidCacheService { Ok(Self { inner: LiquidCacheServiceInner::new( Arc::new(ctx), - max_cache_bytes, + max_memory_bytes, disk_cache_dir, cache_policy, squeeze_policy, diff --git a/src/datafusion-server/src/service.rs b/src/datafusion-server/src/service.rs index 1d9cbe46..f13596ca 100644 --- a/src/datafusion-server/src/service.rs +++ b/src/datafusion-server/src/service.rs @@ -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,7 @@ 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), store, cache_policy, squeeze_policy, @@ -150,7 +150,7 @@ impl LiquidCacheServiceInner { let cache = self.cache(); self.execution_plans.write().unwrap().insert( handle, - ExecutionPlanEntry::new(rewrite_data_source_plan(plan, cache, true)), + ExecutionPlanEntry::new(rewrite_data_source_plan(plan, cache)), ); } diff --git a/src/datafusion/Cargo.toml b/src/datafusion/Cargo.toml index 9a927edb..cbdadf53 100644 --- a/src/datafusion/Cargo.toml +++ b/src/datafusion/Cargo.toml @@ -23,8 +23,6 @@ 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 +30,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/bench/filter_pushdown.rs b/src/datafusion/bench/filter_pushdown.rs index 7488865a..6113928e 100644 --- a/src/datafusion/bench/filter_pushdown.rs +++ b/src/datafusion/bench/filter_pushdown.rs @@ -43,7 +43,7 @@ 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) 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..d16975e3 100644 --- a/src/datafusion/src/cache/column.rs +++ b/src/datafusion/src/cache/column.rs @@ -1,22 +1,17 @@ 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 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, - }, + optimizers::{DATE_MAPPING_METADATA_KEY, STRING_FINGERPRINT_METADATA_KEY}, }; use std::sync::Arc; @@ -49,17 +44,6 @@ fn infer_expression(field: &Field) -> Option { { 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 } @@ -180,17 +164,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,10 +188,6 @@ 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; @@ -220,64 +195,6 @@ impl CachedColumn { } } -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, @@ -285,88 +202,3 @@ fn is_string_type(data_type: &DataType) -> bool { _ => 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..27a29319 100644 --- a/src/datafusion/src/cache/mod.rs +++ b/src/datafusion/src/cache/mod.rs @@ -243,21 +243,45 @@ impl LiquidCacheParquet { /// Create a new cache for parquet files. pub async fn new( batch_size: usize, - max_cache_bytes: usize, + max_memory_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, + store, + cache_policy, + squeeze_policy, + hydration_policy, + !cfg!(test), + ) + .await + } + + /// Create a new cache for parquet files with explicit victim squeeze concurrency. + #[doc(hidden)] + pub async fn new_with_squeeze_victim_concurrency( + batch_size: usize, + max_memory_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 cache_storage = LiquidCacheBuilder::new() .with_batch_size(batch_size) - .with_max_cache_bytes(max_cache_bytes) + .with_max_memory_bytes(max_memory_bytes) .with_squeeze_policy(squeeze_policy) .with_cache_policy(cache_policy) .with_hydration_policy(hydration_policy) .with_io_context(io_context) + .with_squeeze_victims_concurrently(squeeze_victims_concurrently) .build() .await; @@ -292,9 +316,9 @@ 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 memory usage of the cache in bytes. 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 index 58ff79c0..b518012d 100644 --- a/src/datafusion/src/optimizers/lineage_opt.rs +++ b/src/datafusion/src/optimizers/lineage_opt.rs @@ -403,9 +403,7 @@ impl LineageAnalyzer { 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()) - { + for (input_column, output_column) in input_columns.iter().zip(alias.schema.columns()) { let key = ColumnKey::from_column(&output_column); let usages = input_map .get(&ColumnKey::from_column(input_column)) @@ -526,7 +524,7 @@ impl LineageAnalyzer { for (expr, column) in distinct_on .select_expr .iter() - .zip(distinct_on.schema.columns().into_iter()) + .zip(distinct_on.schema.columns()) { let usages = lineage_for_expr(expr, &input_map, schema.as_ref())?; output.insert(ColumnKey::from_column(&column), usages); @@ -1068,9 +1066,7 @@ fn part_to_unit(expr: &Expr) -> Option { #[cfg(test)] mod tests { - use crate::optimizers::{ - DATE_MAPPING_METADATA_KEY, LocalModeOptimizer, VARIANT_MAPPING_METADATA_KEY, - }; + use crate::optimizers::{DATE_MAPPING_METADATA_KEY, LocalModeOptimizer}; use crate::{LiquidCacheParquet, VariantGetUdf, VariantToJsonUdf}; use liquid_cache::cache::AlwaysHydrate; @@ -1087,7 +1083,6 @@ mod tests { use liquid_cache::cache_policies::LiquidPolicy; use parquet::arrow::ArrowWriter; use parquet::variant::{VariantArray, json_to_variant}; - use serde::Deserialize; use tempfile::TempDir; // ───────────────────────────────────────────────────────────────────────────── @@ -1312,35 +1307,12 @@ mod tests { 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 + /// Assert metadata on physical plan matches expected date extractions. async fn assert_metadata( ctx: &SessionContext, sql: &str, expected_date: Vec<(&str, &str)>, - expected_variant: Vec<&str>, + _expected_variant: Vec<&str>, ) { let df = ctx.sql(sql).await.unwrap(); let (state, plan) = df.into_parts(); @@ -1348,9 +1320,7 @@ mod tests { 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())) @@ -1360,31 +1330,6 @@ mod tests { "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 - ); - } } // ───────────────────────────────────────────────────────────────────────────── @@ -1739,7 +1684,7 @@ mod tests { // ───────────────────────────────────────────────────────────────────────────── #[tokio::test] - async fn variant_get_type_hint_propagated() { + async fn variant_get_type_hint_does_not_emit_metadata() { let (_dir, ctx, _) = setup_variant_table().await; let df = ctx @@ -1750,17 +1695,11 @@ mod tests { 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")); + let metadata = extract_field_metadata(&physical_plan, DATE_MAPPING_METADATA_KEY); + assert!( + !metadata.contains_key("data"), + "variant_get should not emit cache metadata" + ); } #[tokio::test] diff --git a/src/datafusion/src/optimizers/mod.rs b/src/datafusion/src/optimizers/mod.rs index cc1950f8..aeaccfba 100644 --- a/src/datafusion/src/optimizers/mod.rs +++ b/src/datafusion/src/optimizers/mod.rs @@ -2,9 +2,9 @@ mod lineage_opt; -use std::{str::FromStr, sync::Arc}; +use std::sync::Arc; -use arrow_schema::{DataType, Field, Fields, Schema, SchemaRef}; +use arrow_schema::{Field, Schema, SchemaRef}; use datafusion::{ catalog::memory::DataSourceExec, common::tree_node::{Transformed, TreeNode, TreeNodeRecursion}, @@ -19,79 +19,15 @@ use datafusion::{ 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); - } - - let data_type = metadata - .get(VARIANT_MAPPING_TYPE_METADATA_KEY) - .and_then(|spec| DataType::from_str(spec).ok()); - - Some(vec![VariantField { - path: raw.clone(), - data_type, - }]) -} - /// Physical optimizer rule for local mode liquid cache /// /// This optimizer rewrites DataSourceExec nodes that read Parquet files @@ -99,24 +35,17 @@ pub(crate) fn variant_mappings_from_field(field: &Field) -> Option Self { - Self { - cache, - eager_shredding, - } + pub fn new(cache: LiquidCacheParquetRef) -> Self { + Self { cache } } /// Create an optimizer with an existing cache instance pub fn with_cache(cache: LiquidCacheParquetRef) -> Self { - Self { - cache, - eager_shredding: true, - } + Self { cache } } } @@ -126,11 +55,7 @@ impl PhysicalOptimizerRule for LocalModeOptimizer { plan: Arc, _config: &ConfigOptions, ) -> Result, datafusion::error::DataFusionError> { - Ok(rewrite_data_source_plan( - plan, - &self.cache, - self.eager_shredding, - )) + Ok(rewrite_data_source_plan(plan, &self.cache)) } fn name(&self) -> &str { @@ -148,10 +73,9 @@ impl PhysicalOptimizerRule for LocalModeOptimizer { pub fn rewrite_data_source_plan( plan: Arc, cache: &LiquidCacheParquetRef, - eager_shredding: bool, ) -> Arc { let rewritten = plan - .transform_up(|node| try_optimize_parquet_source(node, cache, eager_shredding)) + .transform_up(|node| try_optimize_parquet_source(node, cache)) .unwrap(); rewritten.data } @@ -159,7 +83,6 @@ pub fn rewrite_data_source_plan( fn try_optimize_parquet_source( plan: Arc, cache: &LiquidCacheParquetRef, - eager_shredding: bool, ) -> Result>, datafusion::error::DataFusionError> { let any_plan = plan.as_any(); if let Some(data_source_exec) = any_plan.downcast_ref::() @@ -171,11 +94,8 @@ fn try_optimize_parquet_source( 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 new_schema = + enrich_source_schema(file_scan_config.file_schema(), expr_adapter_factory); 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()); @@ -198,12 +118,11 @@ fn try_optimize_parquet_source( 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)); + new_fields.push(process_field_annotation(field, annotation)); } else { new_fields.push(field.clone()); } @@ -211,13 +130,8 @@ fn enrich_source_schema( Schema::new(new_fields) } -fn process_field_annotation( - field: &Arc, - annotation: ColumnAnnotation, - eager_shredding: bool, -) -> Arc { +fn process_field_annotation(field: &Arc, annotation: ColumnAnnotation) -> 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( @@ -225,14 +139,7 @@ fn process_field_annotation( 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::VariantPaths(_) => {} ColumnAnnotation::SubstringSearch => { field_metadata.insert( STRING_FINGERPRINT_METADATA_KEY.to_string(), @@ -240,78 +147,7 @@ fn process_field_annotation( ); } } - 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) -} - -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)) -} - -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, - )) + Arc::new(Field::clone(field.as_ref()).with_metadata(field_metadata)) } #[cfg(test)] @@ -343,7 +179,7 @@ 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| { diff --git a/src/datafusion/src/reader/plantime/opener.rs b/src/datafusion/src/reader/plantime/opener.rs index 0bf5b7c1..7abe216e 100644 --- a/src/datafusion/src/reader/plantime/opener.rs +++ b/src/datafusion/src/reader/plantime/opener.rs @@ -1,441 +1,440 @@ -use std::sync::Arc; - +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 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 = 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, 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), + )) +} From de2a207b8f889e0e7a7bcd698c70109b3a08445a Mon Sep 17 00:00:00 2001 From: Xiangpeng Hao Date: Thu, 30 Apr 2026 13:37:52 -0500 Subject: [PATCH 02/13] Io refactor, remove unrelated stuff (#493) --- .github/workflows/ci.yml | 278 +++++++++--------- Cargo.lock | 205 +------------ flake.lock | 16 - flake.nix | 15 - src/core/Cargo.toml | 12 - src/core/src/cache/builders.rs | 41 ++- src/core/src/cache/core.rs | 50 ++-- src/core/src/cache/io_context.rs | 103 +++---- src/core/src/cache/mod.rs | 2 +- .../policies/cache/doubly_linked_list.rs | 62 ---- src/core/src/cache/tests/squeezed.rs | 16 +- src/core/study/cache_storage.rs | 4 +- src/datafusion/Cargo.toml | 1 - src/datafusion/src/cache/mod.rs | 7 +- src/datafusion/src/io/mod.rs | 91 ++---- 15 files changed, 257 insertions(+), 646 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 429bd6f0..6f3bdaa0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -301,143 +301,143 @@ jobs: files: codecov_stackoverflow.json fail_ci_if_error: true - benchmark: - name: Performance Benchmark - runs-on: pittsburgh - 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 + # benchmark: + # name: Performance Benchmark + # runs-on: pittsburgh + # 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: 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 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: 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); - } + # - 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 @@ -505,15 +505,3 @@ jobs: run: | # Run to populate cache and read arrow array env RUST_LOG=info cargo run --bin example_inprocess_read - - kani: - name: Run Kani proofs - runs-on: ubuntu-22.04 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - uses: ./.github/actions/free-disk-space - - name: Verify storage crate with Kani - uses: model-checking/kani-github-action@v1.1 - with: - working-directory: src/core diff --git a/Cargo.lock b/Cargo.lock index 2f874975..c8cfd5e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -707,15 +707,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - [[package]] name = "brotli" version = "8.0.2" @@ -2726,16 +2717,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.1", - "objc2", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -3439,15 +3420,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" @@ -3964,17 +3936,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "kani-verifier" -version = "0.67.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6225a7ec13037e984f6bebdda33d34390e643a1cd2a4522cb5d5e8e6ed85e2b" -dependencies = [ - "anyhow", - "home", - "os_info", -] - [[package]] name = "keyboard-types" version = "0.7.0" @@ -4171,7 +4132,6 @@ dependencies = [ "fsst-rs", "futures", "insta", - "kani-verifier", "liquid-cache-common", "log", "mimalloc", @@ -4255,7 +4215,6 @@ dependencies = [ "ahash", "arrow", "arrow-schema", - "async-trait", "bytes", "datafusion", "divan", @@ -4699,18 +4658,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.1", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "nom" version = "7.1.3" @@ -4824,27 +4771,6 @@ dependencies = [ "objc2-encode", ] -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.11.1", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "objc2", - "objc2-foundation", -] - [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -4852,53 +4778,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.11.1", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.11.1", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-location" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.11.1", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", ] [[package]] @@ -4907,19 +4786,6 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.11.1", - "block2", - "libc", - "objc2", - "objc2-core-foundation", -] - [[package]] name = "objc2-io-kit" version = "0.3.2" @@ -4930,60 +4796,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.11.1", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags 2.11.1", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" -dependencies = [ - "bitflags 2.11.1", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-location", - "objc2-core-text", - "objc2-foundation", - "objc2-quartz-core", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" -dependencies = [ - "objc2", - "objc2-foundation", -] - [[package]] name = "object" version = "0.37.3" @@ -5132,21 +4944,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" @@ -5467,7 +5264,7 @@ dependencies = [ "inferno", "libc", "log", - "nix 0.26.4", + "nix", "once_cell", "smallvec", "spin 0.10.0", diff --git a/flake.lock b/flake.lock index 21cfad5e..91d09a14 100644 --- a/flake.lock +++ b/flake.lock @@ -1,20 +1,5 @@ { "nodes": { - "crane": { - "locked": { - "lastModified": 1777335812, - "narHash": "sha256-bEg5xoAxAwsyfnGhkEX7RJViTIBIYPd8ISg4O1c0HFc=", - "owner": "ipetkov", - "repo": "crane", - "rev": "5e0fb2f64edff2822249f21293b8304dedaaf676", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, "flake-utils": { "inputs": { "systems": "systems" @@ -67,7 +52,6 @@ }, "root": { "inputs": { - "crane": "crane", "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay" diff --git a/flake.nix b/flake.nix index 327fbe8f..8e7b1419 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,18 +20,6 @@ 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"; @@ -52,7 +38,6 @@ pkg-config eza fd - kaniVerifier llvmPackages.bintools lldb cargo-fuzz diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml index 38cdad0b..214479a4 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 } @@ -49,17 +45,9 @@ serde_json = { workspace = true } mimalloc = "0.1.50" 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/builders.rs b/src/core/src/cache/builders.rs index 8be3e42c..0555a87d 100644 --- a/src/core/src/cache/builders.rs +++ b/src/core/src/cache/builders.rs @@ -8,7 +8,7 @@ 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 crate::sync::Arc; @@ -35,7 +35,8 @@ pub struct LiquidCacheBuilder { cache_policy: Box, hydration_policy: Box, squeeze_policy: Box, - io_context: Option>, + metadata: Option>, + store: Option, squeeze_victims_concurrently: bool, } @@ -54,7 +55,8 @@ impl LiquidCacheBuilder { 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), } } @@ -94,10 +96,17 @@ 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 } @@ -110,27 +119,29 @@ impl LiquidCacheBuilder { /// 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_memory_bytes, self.squeeze_policy, self.cache_policy, self.hydration_policy, - io_worker, + metadata, + store, self.squeeze_victims_concurrently, )) } diff --git a/src/core/src/cache/core.rs b/src/core/src/cache/core.rs index 4fdea624..8963a7c7 100644 --- a/src/core/src/cache/core.rs +++ b/src/core/src/cache/core.rs @@ -10,7 +10,7 @@ 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, @@ -57,7 +57,8 @@ pub struct LiquidCache { hydration_policy: Box, squeeze_policy: Box, observer: Arc, - io_context: Arc, + metadata: Arc, + store: t4::Store, squeeze_victims_concurrently: bool, } @@ -210,12 +211,12 @@ 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. @@ -266,13 +267,13 @@ impl LiquidCache { 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( batch, - self.io_context.get_compressor(&entry_id).as_ref(), + self.metadata.get_compressor(&entry_id).as_ref(), None, &squeeze_io, ); @@ -333,13 +334,15 @@ impl LiquidCache { } /// Create a new instance of CacheStorage. + #[allow(clippy::too_many_arguments)] pub(crate) fn new( batch_size: usize, max_memory_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_memory_bytes); @@ -351,7 +354,8 @@ impl LiquidCache { hydration_policy, squeeze_policy, observer: Arc::new(Observer::new()), - io_context: io_worker, + metadata, + store, squeeze_victims_concurrently, } } @@ -427,11 +431,11 @@ impl LiquidCache { return; }; 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(), )); @@ -474,7 +478,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, @@ -695,24 +699,28 @@ impl LiquidCache { bytes: bytes.len(), }); let len = bytes.len(); - self.io_context.write(&entry_id, bytes).await.unwrap(); + self.store + .put(entry_id_to_key(&entry_id), bytes.to_vec()) + .await + .expect("write failed"); self.budget.add_used_disk_bytes(len); } 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 } @@ -722,19 +730,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 _ } diff --git a/src/core/src/cache/io_context.rs b/src/core/src/cache/io_context.rs index 8f804fb2..d03a8184 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 @@ -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/mod.rs b/src/core/src/cache/mod.rs index 663f19fd..d2cda0df 100644 --- a/src/core/src/cache/mod.rs +++ b/src/core/src/cache/mod.rs @@ -19,7 +19,7 @@ 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; 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..9e4c7ece 100644 --- a/src/core/src/cache/policies/cache/doubly_linked_list.rs +++ b/src/core/src/cache/policies/cache/doubly_linked_list.rs @@ -121,65 +121,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/tests/squeezed.rs b/src/core/src/cache/tests/squeezed.rs index 56e09ed8..7dd84076 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, }; @@ -28,11 +28,11 @@ async fn read_squeezed_date_time() { .with_hydration_policy(Box::new(AlwaysHydrate::new())) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) .with_max_memory_bytes(array_size * 2) - .with_io_context(Arc::new(DefaultIoContext::new( + .with_store( t4::mount(temp_dir.path().join("liquid_cache.t4")) .await .unwrap(), - ))) + ) .build() .await; @@ -93,11 +93,11 @@ async fn read_squeezed_variant_path() { .with_hydration_policy(Box::new(AlwaysHydrate::new())) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) .with_max_memory_bytes(array_size * 3 / 2) - .with_io_context(Arc::new(DefaultIoContext::new( + .with_store( t4::mount(temp_dir.path().join("liquid_cache.t4")) .await .unwrap(), - ))) + ) .build() .await; @@ -155,11 +155,11 @@ async fn read_squeezed_int64_array() { .with_hydration_policy(Box::new(AlwaysHydrate::new())) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) .with_max_memory_bytes(array_size * 2) - .with_io_context(Arc::new(DefaultIoContext::new( + .with_store( t4::mount(temp_dir.path().join("liquid_cache.t4")) .await .unwrap(), - ))) + ) .build() .await; diff --git a/src/core/study/cache_storage.rs b/src/core/study/cache_storage.rs index 66a0e598..6141da97 100644 --- a/src/core/study/cache_storage.rs +++ b/src/core/study/cache_storage.rs @@ -10,7 +10,6 @@ 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; @@ -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_memory_bytes(500 * 1024 * 1024) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) .with_cache_policy(Box::new(FiloPolicy::new())) - .with_io_context(io_context) + .with_store(store) .build() .await }); diff --git a/src/datafusion/Cargo.toml b/src/datafusion/Cargo.toml index cbdadf53..0dfd8d59 100644 --- a/src/datafusion/Cargo.toml +++ b/src/datafusion/Cargo.toml @@ -21,7 +21,6 @@ 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 } serde_json = { workspace = true } diff --git a/src/datafusion/src/cache/mod.rs b/src/datafusion/src/cache/mod.rs index 27a29319..daeb1967 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; @@ -273,14 +273,15 @@ impl LiquidCacheParquet { 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_memory_bytes(max_memory_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; 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); } } From 3a83d13eb19887a88a9833d887ba381655fb3732 Mon Sep 17 00:00:00 2001 From: Xiangpeng Hao Date: Fri, 1 May 2026 19:52:01 -0500 Subject: [PATCH 03/13] fix dev tool build (#494) --- .github/workflows/ci.yml | 24 ++++++-- Cargo.lock | 126 ++++++++------------------------------- dev/dev-tools/Cargo.toml | 2 +- flake.nix | 10 ++-- 4 files changed, 52 insertions(+), 110 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f3bdaa0..c72baef1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,8 +35,8 @@ jobs: cd dev/dev-tools mkdir -p vendor npm install tailwindcss @tailwindcss/cli - wget https://github.com/saadeghi/daisyui/releases/latest/download/daisyui.mjs -O vendor/daisyui.mjs - wget https://github.com/saadeghi/daisyui/releases/latest/download/daisyui-theme.mjs -O vendor/daisyui-theme.mjs + 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 @@ -56,6 +56,22 @@ jobs: - 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 @@ -74,8 +90,8 @@ jobs: cd dev/dev-tools mkdir -p vendor npm install tailwindcss @tailwindcss/cli - wget https://github.com/saadeghi/daisyui/releases/latest/download/daisyui.mjs -O vendor/daisyui.mjs - wget https://github.com/saadeghi/daisyui/releases/latest/download/daisyui-theme.mjs -O vendor/daisyui-theme.mjs + 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 diff --git a/Cargo.lock b/Cargo.lock index c8cfd5e0..61dbc368 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2125,9 +2125,9 @@ dependencies = [ [[package]] name = "dioxus" -version = "0.7.6" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b5fd24c3d394e5a8cfc15b8319da5f853c9d27471a9f46ace0653761499740" +checksum = "a44c550c06b6785e16258ad620d5b559f5bbcbcc50e3c18c08aa6af2604a4c32" dependencies = [ "dioxus-asset-resolver", "dioxus-cli-config", @@ -2879,7 +2879,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3805,7 +3805,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3841,7 +3841,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3926,9 +3926,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.97" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "cfg-if", "futures-util", @@ -4683,7 +4683,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5479,7 +5479,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -5808,7 +5808,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6555,7 +6555,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6565,7 +6565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -7412,9 +7412,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.120" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -7425,9 +7425,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.70" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ "js-sys", "wasm-bindgen", @@ -7435,9 +7435,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.120" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7445,9 +7445,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.120" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -7458,9 +7458,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.120" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -7514,9 +7514,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.97" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -7563,7 +7563,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -7711,15 +7711,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" @@ -7753,30 +7744,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" @@ -7798,12 +7772,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" @@ -7816,12 +7784,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" @@ -7834,24 +7796,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" @@ -7864,12 +7814,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" @@ -7882,12 +7826,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" @@ -7900,12 +7838,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" @@ -7918,12 +7850,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" diff --git a/dev/dev-tools/Cargo.toml b/dev/dev-tools/Cargo.toml index ea855efc..387a086f 100644 --- a/dev/dev-tools/Cargo.toml +++ b/dev/dev-tools/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] -dioxus = { version = "0.7.6", features = ["router", "fullstack"] } +dioxus = { version = "=0.7.5", features = ["router", "fullstack"] } [features] default = ["web"] diff --git a/flake.nix b/flake.nix index 8e7b1419..ef8360ae 100644 --- a/flake.nix +++ b/flake.nix @@ -22,12 +22,12 @@ }; # 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 { @@ -49,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" ]; From 8be8185fb275caaa5e5dc83ede27bd939cb4ef8e Mon Sep 17 00:00:00 2001 From: Xiangpeng Hao Date: Mon, 4 May 2026 12:49:13 -0500 Subject: [PATCH 04/13] Allow setting limit to disk space (Phase 1/2) (#495) This is one of the most important yet very complicated refactor. Many things can go wrong, especially around the datafusion integration: concurrency, proper filter/projection/schema etc. This is the first phase of the 2, where we stop taking new entries when the cache is full (e.g. used up disk space). The second phase will allow properly evicting entries from disk. --- examples/example_inprocess_cache_eviction.rs | 3 +- examples/example_inprocess_insertion.rs | 3 +- examples/example_inprocess_read.rs | 4 +- src/core/src/cache/budget.rs | 65 ++- src/core/src/cache/builders.rs | 22 +- src/core/src/cache/core.rs | 166 +++++-- src/core/src/cache/mod.rs | 4 + src/core/src/cache/observer/mod.rs | 10 + src/core/src/cache/observer/stats.rs | 4 + src/core/src/cache/policies/cache/clock.rs | 20 +- src/core/src/cache/policies/cache/filo.rs | 20 +- src/core/src/cache/policies/cache/lru.rs | 15 +- src/core/src/cache/policies/cache/sieve.rs | 15 +- src/core/src/cache/tests/policies.rs | 12 +- src/core/src/cache/tests/squeezed.rs | 11 +- src/core/src/cache/utils.rs | 8 +- src/core/study/cache_storage.rs | 2 +- src/datafusion-local/src/lib.rs | 2 + ...datafusion_local__tests__os_selection.snap | 2 + ...afusion_local__tests__provide_schema2.snap | 6 + ...al__tests__provide_schema_with_filter.snap | 2 + ...usion_local__tests__referer_filtering.snap | 2 + ...ests__single_column_filter_projection.snap | 2 + ...on_local__tests__url_prefix_filtering.snap | 2 + ...al__tests__url_selection_and_ordering.snap | 2 + src/datafusion-server/src/service.rs | 1 + src/datafusion/bench/filter_pushdown.rs | 1 + src/datafusion/src/cache/column.rs | 12 +- src/datafusion/src/cache/mod.rs | 19 +- src/datafusion/src/cache/stats.rs | 1 + src/datafusion/src/optimizers/lineage_opt.rs | 1 + src/datafusion/src/optimizers/mod.rs | 1 + src/datafusion/src/reader/plantime/mod.rs | 2 + .../src/reader/runtime/liquid_cache_reader.rs | 1 + .../src/reader/runtime/liquid_stream.rs | 467 +++++++++++++++++- 35 files changed, 799 insertions(+), 111 deletions(-) diff --git a/examples/example_inprocess_cache_eviction.rs b/examples/example_inprocess_cache_eviction.rs index a0f4f13d..9ce476dc 100644 --- a/examples/example_inprocess_cache_eviction.rs +++ b/examples/example_inprocess_cache_eviction.rs @@ -30,7 +30,8 @@ async fn main() -> Result<(), Box> { storage .storage() .insert(entry_id, arrow_array.clone()) - .await; + .await + .unwrap(); let _ = storage.storage().get(&entry_id).await.unwrap(); } println!("{:?}", storage.storage().stats()); diff --git a/examples/example_inprocess_insertion.rs b/examples/example_inprocess_insertion.rs index 3caf0c01..9b4dcc95 100644 --- a/examples/example_inprocess_insertion.rs +++ b/examples/example_inprocess_insertion.rs @@ -28,7 +28,8 @@ async fn main() -> Result<(), Box> { storage .storage() .insert(entry_id, arrow_array.clone()) - .await; + .await + .unwrap(); assert!(storage.storage().is_cached(&entry_id)); diff --git a/examples/example_inprocess_read.rs b/examples/example_inprocess_read.rs index f2b874ed..7b5efda3 100644 --- a/examples/example_inprocess_read.rs +++ b/examples/example_inprocess_read.rs @@ -9,10 +9,10 @@ async fn main() -> Result<(), Box> { 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; + storage.insert(entry_id, arrow_array.clone()).await.unwrap(); // Move data to disk so the read will demonstrate async I/O - storage.flush_all_to_disk().await; + storage.flush_all_to_disk().await.unwrap(); // Read asynchronously let retrieved = storage.get(&entry_id).await.unwrap(); diff --git a/src/core/src/cache/budget.rs b/src/core/src/cache/budget.rs index 097cb417..b01239eb 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, } } @@ -66,8 +78,27 @@ 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), + } + } + + #[allow(dead_code)] + pub(super) fn release_disk(&self, bytes: usize) { + self.used_disk_bytes.fetch_sub(bytes, Ordering::Relaxed); } } @@ -76,9 +107,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 +146,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 +200,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 0555a87d..4d4f9cf7 100644 --- a/src/core/src/cache/builders.rs +++ b/src/core/src/cache/builders.rs @@ -10,7 +10,7 @@ use super::cached_batch::CacheEntry; use super::core::LiquidCache; 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]. @@ -32,6 +32,7 @@ use crate::sync::Arc; pub struct LiquidCacheBuilder { batch_size: usize, max_memory_bytes: usize, + max_disk_bytes: usize, cache_policy: Box, hydration_policy: Box, squeeze_policy: Box, @@ -52,6 +53,7 @@ impl LiquidCacheBuilder { Self { batch_size: 8192, max_memory_bytes: 1024 * 1024 * 1024, + max_disk_bytes: usize::MAX, cache_policy: Box::new(LiquidPolicy::new()), hydration_policy: Box::new(super::AlwaysHydrate::new()), squeeze_policy: Box::new(TranscodeSqueezeEvict), @@ -75,6 +77,13 @@ impl LiquidCacheBuilder { self } + /// Set the max disk bytes for the cache. + /// Default is unlimited. + pub fn with_max_disk_bytes(mut self, max_disk_bytes: usize) -> Self { + self.max_disk_bytes = max_disk_bytes; + self + } + /// Set the cache policy for the cache. /// Default is [LiquidPolicy]. pub fn with_cache_policy(mut self, policy: Box) -> Self { @@ -137,6 +146,7 @@ impl LiquidCacheBuilder { Arc::new(LiquidCache::new( self.batch_size, self.max_memory_bytes, + self.max_disk_bytes, self.squeeze_policy, self.cache_policy, self.hydration_policy, @@ -180,7 +190,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 { @@ -190,13 +200,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 }) @@ -429,7 +439,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/core.rs b/src/core/src/cache/core.rs index 8963a7c7..205aa7c7 100644 --- a/src/core/src/cache/core.rs +++ b/src/core/src/cache/core.rs @@ -19,7 +19,7 @@ use crate::cache::DefaultSqueezeIo; use crate::cache::policies::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, @@ -113,6 +113,7 @@ impl LiquidCache { memory_usage_bytes, disk_usage_bytes, max_memory_bytes: self.config.max_memory_bytes(), + max_disk_bytes: self.config.max_disk_bytes(), runtime, } } @@ -209,6 +210,11 @@ impl LiquidCache { &self.observer } + /// Record that a parquet row group bypassed the cache because it was full. + pub fn record_cache_full_bypass(&self) { + self.observer.on_cache_full_bypass(); + } + /// Get the compressor states of the cache. pub fn compressor_states(&self, entry_id: &EntryID) -> Arc { self.metadata.get_compressor(entry_id) @@ -220,7 +226,7 @@ impl LiquidCache { } /// 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())); @@ -229,14 +235,14 @@ 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.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"); } CacheEntry::MemoryLiquid(liquid_array) => { let liquid_bytes = liquid_array.to_bytes(); self.write_batch_to_disk(entry_id, &batch, Bytes::from(liquid_bytes)) - .await; + .await?; self.try_insert( entry_id, CacheEntry::disk_liquid(liquid_array.original_arrow_data_type()), @@ -254,6 +260,7 @@ impl LiquidCache { } } } + Ok(()) } } @@ -263,7 +270,7 @@ impl LiquidCache { &self, entry_id: EntryID, batch: CacheEntry, - ) -> CacheEntry { + ) -> Result { match &batch { batch @ CacheEntry::MemoryArrow(_) => { let squeeze_io: Arc = Arc::new(DefaultSqueezeIo::new( @@ -279,24 +286,27 @@ impl LiquidCache { ); 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()); 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(), + )) } 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 { + let entry = if backing == SqueezedBacking::Liquid { CacheEntry::disk_liquid(squeezed_array.original_arrow_data_type()) } else { CacheEntry::disk_arrow(squeezed_array.original_arrow_data_type()) - } + }; + Ok(entry) } CacheEntry::DiskLiquid(_) | CacheEntry::DiskArrow(_) => { unreachable!("Unexpected batch in write_in_memory_batch_to_disk") @@ -305,10 +315,14 @@ impl LiquidCache { } /// 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, @@ -322,11 +336,11 @@ impl LiquidCache { // 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(); @@ -338,6 +352,7 @@ impl LiquidCache { pub(crate) fn new( batch_size: usize, max_memory_bytes: usize, + max_disk_bytes: usize, squeeze_policy: Box, cache_policy: Box, hydration_policy: Box, @@ -345,15 +360,20 @@ impl LiquidCache { store: t4::Store, squeeze_victims_concurrently: bool, ) -> Self { - let config = CacheConfig::new(batch_size, max_memory_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_memory_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()), + observer, metadata, store, squeeze_victims_concurrently, @@ -409,26 +429,28 @@ impl LiquidCache { } #[fastrace::trace] - async fn squeeze_victims(&self, victims: Vec) { + async fn squeeze_victims(&self, victims: Vec) -> Result<(), CacheFull> { self.trace(InternalEvent::SqueezeBegin { victims: victims.clone(), }); if self.squeeze_victims_concurrently { - futures::stream::iter(victims) - .for_each_concurrent(None, |victim| async move { - self.squeeze_victim_inner(victim).await; - }) + 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; + 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.metadata.get_compressor(&to_squeeze); @@ -450,7 +472,7 @@ impl LiquidCache { if let Some(bytes_to_write) = bytes_to_write { self.write_batch_to_disk(to_squeeze, &new_batch, bytes_to_write) - .await; + .await?; } match self.try_insert(to_squeeze, new_batch) { Ok(()) => { @@ -461,6 +483,7 @@ impl LiquidCache { } } } + Ok(()) } fn disk_entry_from_squeezed(array: &LiquidSqueezedArrayRef) -> CacheEntry { @@ -493,7 +516,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; } } @@ -692,18 +715,24 @@ 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(); + self.budget.try_reserve_disk(len).map_err(|()| CacheFull)?; self.trace(InternalEvent::IoWrite { entry: entry_id, kind: CachedBatchType::from(batch), - bytes: bytes.len(), + bytes: len, }); - let len = bytes.len(); self.store .put(entry_id_to_key(&entry_id), bytes.to_vec()) .await .expect("write failed"); - self.budget.add_used_disk_bytes(len); + Ok(()) } async fn read_disk_arrow_array(&self, entry_id: &EntryID) -> ArrayRef { @@ -908,7 +937,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); @@ -921,13 +950,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()); @@ -948,7 +977,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 @@ -982,13 +1012,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:?}"), @@ -1036,7 +1072,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(); } }); })); @@ -1070,8 +1106,8 @@ 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(); @@ -1081,7 +1117,7 @@ mod tests { 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); @@ -1095,8 +1131,8 @@ mod tests { 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(_))); @@ -1120,8 +1156,9 @@ 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(_))); @@ -1134,4 +1171,41 @@ 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 flush_all_to_disk_returns_cache_full_on_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 err = cache.flush_all_to_disk().await; + + assert_eq!(err, Err(CacheFull)); + assert!(matches!( + cache.index().get(&entry_id).unwrap().as_ref(), + CacheEntry::MemoryArrow(_) + )); + } } diff --git a/src/core/src/cache/mod.rs b/src/core/src/cache/mod.rs index d2cda0df..daf9c46e 100644 --- a/src/core/src/cache/mod.rs +++ b/src/core/src/cache/mod.rs @@ -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/mod.rs b/src/core/src/cache/observer/mod.rs index cba1c300..08e632d9 100644 --- a/src/core/src/cache/observer/mod.rs +++ b/src/core/src/cache/observer/mod.rs @@ -99,6 +99,16 @@ impl Observer { self.runtime.incr_hit_date32_expression(); } + #[inline] + pub(crate) fn on_cache_full_bypass(&self) { + self.runtime.incr_cache_full_bypasses(); + } + + #[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(), diff --git a/src/core/src/cache/observer/stats.rs b/src/core/src/cache/observer/stats.rs index cfb74e8d..58e35097 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), + (cache_full_bypasses, "Number of row groups bypassed because the cache was full.", incr_cache_full_bypasses), + (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), @@ -146,6 +148,8 @@ pub struct CacheStats { pub disk_usage_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 index c9b40555..45d7fc03 100644 --- a/src/core/src/cache/policies/cache/clock.rs +++ b/src/core/src/cache/policies/cache/clock.rs @@ -244,12 +244,24 @@ mod tests { 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; + store + .insert(entry_id1, create_test_arrow_array(100)) + .await + .unwrap(); + store + .insert(entry_id2, create_test_arrow_array(100)) + .await + .unwrap(); + store + .insert(entry_id3, create_test_arrow_array(100)) + .await + .unwrap(); let entry_id4 = EntryID::from(4); - store.insert(entry_id4, create_test_arrow_array(100)).await; + store + .insert(entry_id4, create_test_arrow_array(100)) + .await + .unwrap(); let data = store.index().get(&entry_id1).unwrap(); assert!(matches!(data.as_ref(), CacheEntry::DiskLiquid(_))); diff --git a/src/core/src/cache/policies/cache/filo.rs b/src/core/src/cache/policies/cache/filo.rs index 41269f36..50f8981d 100644 --- a/src/core/src/cache/policies/cache/filo.rs +++ b/src/core/src/cache/policies/cache/filo.rs @@ -209,15 +209,27 @@ mod tests { 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_id1, create_test_arrow_array(100)) + .await + .unwrap(); 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; + store + .insert(entry_id2, create_test_arrow_array(100)) + .await + .unwrap(); + store + .insert(entry_id3, create_test_arrow_array(100)) + .await + .unwrap(); let entry_id4: EntryID = EntryID::from(4); - store.insert(entry_id4, create_test_arrow_array(100)).await; + store + .insert(entry_id4, create_test_arrow_array(100)) + .await + .unwrap(); assert!(store.index().get(&entry_id1).is_some()); assert!(store.index().get(&entry_id2).is_some()); diff --git a/src/core/src/cache/policies/cache/lru.rs b/src/core/src/cache/policies/cache/lru.rs index 794e8500..c61d6259 100644 --- a/src/core/src/cache/policies/cache/lru.rs +++ b/src/core/src/cache/policies/cache/lru.rs @@ -371,9 +371,18 @@ mod tests { 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; + store + .insert(entry_id1, create_test_arrow_array(100)) + .await + .unwrap(); + store + .insert(entry_id2, create_test_arrow_array(100)) + .await + .unwrap(); + store + .insert(entry_id3, create_test_arrow_array(100)) + .await + .unwrap(); assert!(store.index().get(&entry_id1).is_some()); assert!(store.index().get(&entry_id2).is_some()); diff --git a/src/core/src/cache/policies/cache/sieve.rs b/src/core/src/cache/policies/cache/sieve.rs index 6fe54638..de30dc12 100644 --- a/src/core/src/cache/policies/cache/sieve.rs +++ b/src/core/src/cache/policies/cache/sieve.rs @@ -285,9 +285,18 @@ mod tests { 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; + store + .insert(entry_id1, create_test_arrow_array(100)) + .await + .unwrap(); + store + .insert(entry_id2, create_test_arrow_array(100)) + .await + .unwrap(); + store + .insert(entry_id3, create_test_arrow_array(100)) + .await + .unwrap(); 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/tests/policies.rs b/src/core/src/cache/tests/policies.rs index fc85129b..29c84990 100644 --- a/src/core/src/cache/tests/policies.rs +++ b/src/core/src/cache/tests/policies.rs @@ -18,7 +18,7 @@ async fn default_policies() { 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 { @@ -43,11 +43,17 @@ async fn insert_wont_fit_cache() { .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/squeezed.rs b/src/core/src/cache/tests/squeezed.rs index 7dd84076..ec46a450 100644 --- a/src/core/src/cache/tests/squeezed.rs +++ b/src/core/src/cache/tests/squeezed.rs @@ -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 { @@ -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 @@ -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 f22f398d..a4d25343 100644 --- a/src/core/src/cache/utils.rs +++ b/src/core/src/cache/utils.rs @@ -9,13 +9,15 @@ use bytes::Bytes; pub struct CacheConfig { batch_size: usize, max_memory_bytes: usize, + max_disk_bytes: usize, } impl CacheConfig { - pub(super) fn new(batch_size: usize, max_memory_bytes: usize) -> Self { + pub(super) fn new(batch_size: usize, max_memory_bytes: usize, max_disk_bytes: usize) -> Self { Self { batch_size, max_memory_bytes, + max_disk_bytes, } } @@ -26,6 +28,10 @@ impl CacheConfig { pub fn max_memory_bytes(&self) -> usize { self.max_memory_bytes } + + pub fn max_disk_bytes(&self) -> usize { + self.max_disk_bytes + } } // Helper methods diff --git a/src/core/study/cache_storage.rs b/src/core/study/cache_storage.rs index 6141da97..3187dfe6 100644 --- a/src/core/study/cache_storage.rs +++ b/src/core/study/cache_storage.rs @@ -144,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/datafusion-local/src/lib.rs b/src/datafusion-local/src/lib.rs index 9f23d654..81a259aa 100644 --- a/src/datafusion-local/src/lib.rs +++ b/src/datafusion-local/src/lib.rs @@ -155,6 +155,7 @@ impl LiquidCacheLocalBuilder { let cache = LiquidCacheParquet::new( self.batch_size, self.max_memory_bytes, + usize::MAX, store, self.cache_policy, self.squeeze_policy, @@ -166,6 +167,7 @@ impl LiquidCacheLocalBuilder { let cache = LiquidCacheParquet::new_with_squeeze_victim_concurrency( self.batch_size, self.max_memory_bytes, + usize::MAX, store, self.cache_policy, self.squeeze_policy, 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..e8b3ca10 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 @@ -53,6 +53,8 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 1 write_io_count: 0 + cache_full_bypasses: 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..8e93a3c0 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 @@ -38,6 +38,8 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 0 write_io_count: 0 + cache_full_bypasses: 0 + disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_count: 0 squeezed_total_count: 0 @@ -181,6 +183,8 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 0 write_io_count: 0 + cache_full_bypasses: 0 + disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_count: 0 squeezed_total_count: 0 @@ -225,6 +229,8 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 0 write_io_count: 0 + cache_full_bypasses: 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..3b1e571c 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 @@ -54,6 +54,8 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 0 write_io_count: 0 + cache_full_bypasses: 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..ace806f6 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 @@ -52,6 +52,8 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 0 write_io_count: 0 + cache_full_bypasses: 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..875f395f 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 @@ -38,6 +38,8 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 0 write_io_count: 0 + cache_full_bypasses: 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_prefix_filtering.snap b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__url_prefix_filtering.snap index dc545bd8..8a1edd3a 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 @@ -68,6 +68,8 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 1 write_io_count: 0 + cache_full_bypasses: 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..f1ab048f 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 @@ -53,6 +53,8 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 4 write_io_count: 0 + cache_full_bypasses: 0 + disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_count: 4362 squeezed_total_count: 8814 diff --git a/src/datafusion-server/src/service.rs b/src/datafusion-server/src/service.rs index f13596ca..1ccfe76d 100644 --- a/src/datafusion-server/src/service.rs +++ b/src/datafusion-server/src/service.rs @@ -65,6 +65,7 @@ impl LiquidCacheServiceInner { LiquidCacheParquet::new( batch_size, max_memory_bytes.unwrap_or(usize::MAX), + usize::MAX, store, cache_policy, squeeze_policy, diff --git a/src/datafusion/bench/filter_pushdown.rs b/src/datafusion/bench/filter_pushdown.rs index 6113928e..897d8e2e 100644 --- a/src/datafusion/bench/filter_pushdown.rs +++ b/src/datafusion/bench/filter_pushdown.rs @@ -44,6 +44,7 @@ fn setup_cache() -> (Arc, tempfile::TempDir) { let cache = tokio_test::block_on(LiquidCacheParquet::new( BATCH_SIZE, 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 d16975e3..65870a78 100644 --- a/src/datafusion/src/cache/column.rs +++ b/src/datafusion/src/cache/column.rs @@ -5,7 +5,7 @@ use arrow::{ record_batch::RecordBatch, }; use arrow_schema::{ArrowError, DataType, Field, Schema}; -use liquid_cache::cache::{CacheExpression, LiquidCache, LiquidExpr}; +use liquid_cache::cache::{CacheExpression, CacheFull, LiquidCache, LiquidExpr}; use parquet::arrow::arrow_reader::ArrowPredicate; use crate::{ @@ -52,6 +52,14 @@ fn infer_expression(field: &Field) -> Option { 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 { @@ -190,7 +198,7 @@ impl CachedColumn { self.cache_store .insert(self.entry_id(batch_id).into(), array) - .await; + .await?; Ok(()) } } diff --git a/src/datafusion/src/cache/mod.rs b/src/datafusion/src/cache/mod.rs index daeb1967..cbe190a4 100644 --- a/src/datafusion/src/cache/mod.rs +++ b/src/datafusion/src/cache/mod.rs @@ -216,6 +216,10 @@ impl CachedFile { self.cache_store.config().batch_size() } + pub(crate) fn record_cache_full_bypass(&self) { + self.cache_store.record_cache_full_bypass(); + } + /// Return the full file schema tracked by the cache entry. pub fn schema(&self) -> SchemaRef { Arc::clone(&self.file_schema) @@ -244,6 +248,7 @@ impl LiquidCacheParquet { pub async fn new( batch_size: usize, max_memory_bytes: usize, + max_disk_bytes: usize, store: t4::Store, cache_policy: Box, squeeze_policy: Box, @@ -252,6 +257,7 @@ impl LiquidCacheParquet { Self::new_with_squeeze_victim_concurrency( batch_size, max_memory_bytes, + max_disk_bytes, store, cache_policy, squeeze_policy, @@ -263,9 +269,11 @@ impl LiquidCacheParquet { /// 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, @@ -277,6 +285,7 @@ impl LiquidCacheParquet { let cache_storage = LiquidCacheBuilder::new() .with_batch_size(batch_size) .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) @@ -322,6 +331,11 @@ impl LiquidCacheParquet { 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. pub fn memory_usage_bytes(&self) -> usize { self.cache_store.budget().memory_usage_bytes() @@ -365,8 +379,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. @@ -409,6 +423,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..f635ec12 100644 --- a/src/datafusion/src/cache/stats.rs +++ b/src/datafusion/src/cache/stats.rs @@ -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/optimizers/lineage_opt.rs b/src/datafusion/src/optimizers/lineage_opt.rs index b518012d..e8a3190b 100644 --- a/src/datafusion/src/optimizers/lineage_opt.rs +++ b/src/datafusion/src/optimizers/lineage_opt.rs @@ -1098,6 +1098,7 @@ mod tests { LiquidCacheParquet::new( 1024, 1024 * 1024 * 1024, + usize::MAX, store, Box::new(LiquidPolicy::new()), Box::new(TranscodeSqueezeEvict), diff --git a/src/datafusion/src/optimizers/mod.rs b/src/datafusion/src/optimizers/mod.rs index aeaccfba..b2490580 100644 --- a/src/datafusion/src/optimizers/mod.rs +++ b/src/datafusion/src/optimizers/mod.rs @@ -172,6 +172,7 @@ mod tests { LiquidCacheParquet::new( 8192, 1000000, + usize::MAX, store, Box::new(LiquidPolicy::new()), Box::new(TranscodeSqueezeEvict), 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/runtime/liquid_cache_reader.rs b/src/datafusion/src/reader/runtime/liquid_cache_reader.rs index 87c123ec..f612d094 100644 --- a/src/datafusion/src/reader/runtime/liquid_cache_reader.rs +++ b/src/datafusion/src/reader/runtime/liquid_cache_reader.rs @@ -297,6 +297,7 @@ mod tests { let cache = LiquidCacheParquet::new( batch_size, usize::MAX, + usize::MAX, store, Box::new(LiquidPolicy::new()), Box::new(Evict), diff --git a/src/datafusion/src/reader/runtime/liquid_stream.rs b/src/datafusion/src/reader/runtime/liquid_stream.rs index b93dfb2f..290a619a 100644 --- a/src/datafusion/src/reader/runtime/liquid_stream.rs +++ b/src/datafusion/src/reader/runtime/liquid_stream.rs @@ -4,8 +4,10 @@ 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::{FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream}; +use parquet::arrow::arrow_reader::{ + ArrowPredicate, ArrowReaderMetadata, ArrowReaderOptions, RowFilter, +}; use parquet::{ arrow::{ ParquetRecordBatchStreamBuilder, ProjectionMask, @@ -26,7 +28,12 @@ use super::liquid_cache_reader::LiquidCacheReader; use super::utils::{get_root_column_ids, limit_row_selection, offset_row_selection}; type PlanResult = Option; -type FillCacheResult = Result<(ReaderFactory, PlanningContext), ParquetError>; +type FillCacheResult = Result<(ReaderFactory, PlanningContext, FillOutcome), ParquetError>; + +enum FillOutcome { + Filled, + Bypass, +} struct ReaderFactory { metadata: Arc, @@ -146,7 +153,7 @@ impl ReaderFactory { 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)); + return Ok((self, context, FillOutcome::Filled)); } // Build row selection for the missing batches @@ -154,7 +161,7 @@ impl ReaderFactory { build_selection_for_batches(&context.missing_batches, cache_batch_size, row_count); if !backfill_selection.selects_any() { - return Ok((self, context)); + return Ok((self, context, FillOutcome::Filled)); } // Clone the reader for this operation (cheap since it's Arc-based) @@ -209,7 +216,7 @@ impl ReaderFactory { ); let batch_id = *batch_id; - insert_batch_into_cache( + let outcome = insert_batch_into_cache( &record_batch, &column_ids, batch_id, @@ -218,6 +225,9 @@ impl ReaderFactory { &context.cached_row_group, ) .await?; + if matches!(outcome, InsertBatchOutcome::CacheFull) { + return Ok((self, context, FillOutcome::Bypass)); + } processed_batches += 1; } @@ -230,7 +240,7 @@ impl ReaderFactory { ))); } - Ok((self, context)) + Ok((self, context, FillOutcome::Filled)) } } @@ -353,6 +363,11 @@ fn build_selection_for_batches( RowSelection::from(selectors) } +enum InsertBatchOutcome { + Ok, + CacheFull, +} + async fn insert_batch_into_cache( record_batch: &RecordBatch, column_ids: &[usize], @@ -360,9 +375,9 @@ async fn insert_batch_into_cache( batch_size: usize, row_count: usize, cached_row_group: &CachedRowGroupRef, -) -> Result<(), ParquetError> { +) -> Result { if column_ids.is_empty() || record_batch.num_rows() == 0 { - return Ok(()); + return Ok(InsertBatchOutcome::Ok); } debug_assert_eq!(record_batch.num_columns(), column_ids.len()); @@ -370,7 +385,7 @@ async fn insert_batch_into_cache( let batch_idx = usize::from(*batch_id); let start = batch_idx * batch_size; if start >= row_count { - return Ok(()); + return Ok(InsertBatchOutcome::Ok); } let end = ((batch_idx + 1) * batch_size).min(row_count); let len = end - start; @@ -389,18 +404,17 @@ async fn insert_batch_into_cache( 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 - ))); + match column.insert(batch_id, array).await { + Ok(()) | Err(InsertArrowArrayError::AlreadyCached) => { + debug_assert!(column.is_cached(batch_id)); + } + Err(InsertArrowArrayError::CacheFull) => { + return Ok(InsertBatchOutcome::CacheFull); + } } - debug_assert!(column.is_cached(batch_id)); } - Ok(()) + Ok(InsertBatchOutcome::Ok) } /// Context for planning what to read from cache vs parquet @@ -422,6 +436,11 @@ enum StreamState { FillCache(BoxFuture<'static, FillCacheResult>), /// Decoding a batch from cache ReadFromCache(LiquidCacheReader), + /// Reading a row group directly from parquet after cache fill hit disk budget. + BypassParquet { + stream: BoxStream<'static, Result>, + filter: Option, + }, } impl std::fmt::Debug for StreamState { @@ -430,6 +449,7 @@ impl std::fmt::Debug for StreamState { StreamState::Init => write!(f, "StreamState::Init"), StreamState::FillCache(_) => write!(f, "StreamState::FillingCache"), StreamState::ReadFromCache(_) => write!(f, "StreamState::Decoding"), + StreamState::BypassParquet { .. } => write!(f, "StreamState::BypassParquet"), } } } @@ -675,7 +695,7 @@ impl Stream for LiquidStream { return Poll::Pending; } Poll::Ready(result) => match result { - Ok((reader_factory, context)) => { + Ok((reader_factory, context, FillOutcome::Filled)) => { self.reader = Some(reader_factory); LocalSpan::add_event(Event::new("LiquidStream::read_from_cache")); let reader_factory = self.reader.as_mut().unwrap(); @@ -689,11 +709,71 @@ impl Stream for LiquidStream { ); self.state = StreamState::ReadFromCache(batch_reader); } + Ok((mut reader_factory, context, FillOutcome::Bypass)) => { + LocalSpan::add_event(Event::new("LiquidStream::bypass_parquet")); + reader_factory.cached_file.record_cache_full_bypass(); + let filter = reader_factory.filter.take(); + let reader_clone = reader_factory.input.clone(); + let reader_metadata = ArrowReaderMetadata::try_new( + Arc::clone(&reader_factory.metadata), + ArrowReaderOptions::new(), + ) + .unwrap(); + let mut builder = ParquetRecordBatchStreamBuilder::new_with_metadata( + reader_clone, + reader_metadata, + ) + .with_projection(self.projection.clone()) + .with_row_groups(vec![context.row_group_idx]) + .with_row_selection(context.selection) + .with_batch_size(context.batch_size); + // Push predicates into the parquet decoder so it + // decodes each predicate's own columns and applies + // the filter using its rebound column indices. + // Clones share metric counters with the originals + // (`metrics::Count`/`Time` are `Arc`-backed), so + // accumulated stats are preserved when we restore + // `filter` to `reader_factory` after this row group. + if let Some(f) = filter.as_ref() { + let predicates: Vec> = f + .predicates() + .iter() + .cloned() + .map(|p| Box::new(p) as Box) + .collect(); + builder = builder.with_row_filter(RowFilter::new(predicates)); + } + let stream = builder.build().unwrap().boxed(); + self.reader = Some(reader_factory); + self.state = StreamState::BypassParquet { stream, filter }; + } Err(e) => { panic!("Filling cache error: {e:?}"); } }, }, + StreamState::BypassParquet { mut stream, filter } => { + match Pin::new(&mut stream).poll_next(cx) { + Poll::Ready(Some(Ok(batch))) => { + self.state = StreamState::BypassParquet { stream, filter }; + if batch.num_rows() == 0 { + continue; + } + return Poll::Ready(Some(Ok(batch))); + } + Poll::Ready(Some(Err(err))) => { + self.state = StreamState::BypassParquet { stream, filter }; + return Poll::Ready(Some(Err(err))); + } + Poll::Ready(None) => { + self.reader.as_mut().unwrap().filter = filter; + } + Poll::Pending => { + self.state = StreamState::BypassParquet { stream, filter }; + return Poll::Pending; + } + } + } } } } @@ -702,13 +782,25 @@ impl Stream for LiquidStream { #[cfg(test)] mod tests { use super::*; - use crate::cache::LiquidCacheParquet; - use arrow::array::{ArrayRef, Int32Array}; + use crate::cache::{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 liquid_cache::cache::AlwaysHydrate; use liquid_cache::cache::squeeze_policies::Evict; use liquid_cache::cache_policies::LiquidPolicy; + use object_store::local::LocalFileSystem; + use parquet::arrow::ArrowWriter; use parquet::arrow::arrow_reader::RowSelection; + use std::fs::File; use std::sync::Arc; async fn make_cache(batch_size: usize, schema: SchemaRef) -> CachedRowGroupRef { @@ -719,6 +811,7 @@ mod tests { let cache = LiquidCacheParquet::new( batch_size, usize::MAX, + usize::MAX, store, Box::new(LiquidPolicy::new()), Box::new(Evict), @@ -729,6 +822,210 @@ mod tests { file.create_row_group(0, vec![]) } + 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(); + } + + 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 = 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) + } + + 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 insert_batches( row_group: &CachedRowGroupRef, column_id: usize, @@ -879,4 +1176,130 @@ mod tests { vec![RowSelector::skip(16), RowSelector::select(2),] ); } + + #[tokio::test] + async fn cache_full_bypasses_row_group_and_keeps_inserted_batches() { + 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; + + 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_eq!(cache.storage().stats().runtime.cache_full_bypasses, 1); + + let row_group0 = cached_file.create_row_group(0, vec![]); + let row_group1 = cached_file.create_row_group(1, vec![]); + assert!( + row_group0 + .get_column(0) + .unwrap() + .get_arrow_array_test_only(BatchID::from_raw(0)) + .await + .is_some() + ); + assert!( + row_group0 + .get_column(1) + .unwrap() + .get_arrow_array_test_only(BatchID::from_raw(0)) + .await + .is_some() + ); + assert!( + row_group1 + .get_column(0) + .unwrap() + .get_arrow_array_test_only(BatchID::from_raw(0)) + .await + .is_some() + ); + } + + #[tokio::test] + async fn cache_full_bypass_applies_row_filter_in_memory() { + 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(schema, 2); + let (stream, cache, _cached_file, _tmp_dir) = + make_liquid_stream(one_array_memory * 3, 0, Some(filter)).await; + + let (a, b) = collect_liquid_values(stream).await; + + assert_eq!(a, vec![3, 4, 5, 6, 7]); + assert_eq!(b, vec![13, 14, 15, 16, 17]); + assert_eq!(cache.storage().stats().runtime.cache_full_bypasses, 1); + } + + #[tokio::test] + async fn cache_full_bypasses_immediately_when_already_at_cap() { + let (stream, cache, cached_file, _tmp_dir) = make_liquid_stream(0, 0, None).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_eq!(cache.storage().stats().runtime.cache_full_bypasses, 2); + + let row_group0 = cached_file.create_row_group(0, vec![]); + assert!( + row_group0 + .get_column(0) + .unwrap() + .get_arrow_array_test_only(BatchID::from_raw(0)) + .await + .is_none() + ); + } + + /// Reproducer: predicate references a column that is NOT in the user + /// projection. On the bypass path the parquet stream is built with + /// `self.projection.clone()` only, so the decoded batch does not even + /// contain the predicate's column. `LiquidRowFilter::filter_batch` then + /// evaluates the predicate against whatever column happens to sit at + /// index 0 of the batch, producing wrong results. + /// + /// File data: a = [0..7], b = [10..17] + /// Query: SELECT a WHERE b > 13 + /// Correct: a = [4, 5, 6, 7] + /// Buggy: predicate is `b > 13` rebound to "column index 0 of + /// filter_schema"; the bypass batch is `[a]`, so the predicate + /// evaluates `a > 13` -> all false -> empty result. + #[tokio::test] + async fn cache_full_bypass_evaluates_predicate_against_wrong_column() { + 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 batches = stream + .map(|batch| batch.expect("valid liquid stream batch")) + .collect::>() + .await; + + let mut a_values = Vec::new(); + for batch in batches { + let arr = batch + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + a_values.extend(arr.iter().map(|value| value.unwrap())); + } + + // The bypass path was actually exercised. + assert!(cache.storage().stats().runtime.cache_full_bypasses >= 1); + + // SELECT a WHERE b > 13 -> a = [4, 5, 6, 7] + assert_eq!(a_values, vec![4, 5, 6, 7]); + } } From 12a05cd9a624de957ba9f2c0664d79a1fe02df51 Mon Sep 17 00:00:00 2001 From: Xiangpeng Hao Date: Mon, 4 May 2026 15:42:57 -0500 Subject: [PATCH 05/13] fix date squeeze (#496) When refactoring things, a few bugs surfaced. Fixing them. --- src/core/src/cache/core.rs | 8 +- .../src/liquid_array/squeezed_date32_array.rs | 106 +++++++++++++++--- 2 files changed, 93 insertions(+), 21 deletions(-) diff --git a/src/core/src/cache/core.rs b/src/core/src/cache/core.rs index 205aa7c7..9ef197a9 100644 --- a/src/core/src/cache/core.rs +++ b/src/core/src/cache/core.rs @@ -967,7 +967,7 @@ mod tests { let store = create_cache_store(1 << 20, Box::new(LruPolicy::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); @@ -993,10 +993,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] diff --git a/src/core/src/liquid_array/squeezed_date32_array.rs b/src/core/src/liquid_array/squeezed_date32_array.rs index 67e2b93a..a3f9657d 100644 --- a/src/core/src/liquid_array/squeezed_date32_array.rs +++ b/src/core/src/liquid_array/squeezed_date32_array.rs @@ -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, )), } @@ -645,8 +654,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 +718,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)); } } From f9ff2c3ecbe11456d0384f378be844fb40800a6f Mon Sep 17 00:00:00 2001 From: Xiangpeng Hao Date: Tue, 5 May 2026 14:48:36 -0500 Subject: [PATCH 06/13] Refactor cache miss handling, allowing disk space limit (2/3) (#497) This is a major refactoring of the liquid stream, so that it does not rely on the invariant that entires must be in the cache at streaming time. Instead, it will first try to read from liquid cache, and if non-exists, it will build parquet stream and read from parquet. --- src/core/src/cache/core.rs | 5 - src/core/src/cache/observer/mod.rs | 5 - src/core/src/cache/observer/stats.rs | 1 - ...datafusion_local__tests__os_selection.snap | 3 +- ...afusion_local__tests__provide_schema2.snap | 3 - ...al__tests__provide_schema_with_filter.snap | 1 - ...usion_local__tests__referer_filtering.snap | 11 +- ...ests__single_column_filter_projection.snap | 1 - ..._local__tests__squeeze__basic_squeeze.snap | 60 +- ...queeze__squeeze_distinct_search_phase.snap | 9 +- ...ocal__tests__squeeze__squeeze_strings.snap | 38 +- ...s__squeeze__squeeze_substrings_search.snap | 4 +- ...eeze__squeeze_substrings_search_title.snap | 6 +- ...on_local__tests__url_prefix_filtering.snap | 1 - ...al__tests__url_selection_and_ordering.snap | 15 +- src/datafusion/src/cache/mod.rs | 4 - .../src/reader/runtime/liquid_cache_reader.rs | 519 ++++++++--- .../src/reader/runtime/liquid_stream.rs | 837 +++++------------- 18 files changed, 670 insertions(+), 853 deletions(-) diff --git a/src/core/src/cache/core.rs b/src/core/src/cache/core.rs index 9ef197a9..63a5f033 100644 --- a/src/core/src/cache/core.rs +++ b/src/core/src/cache/core.rs @@ -210,11 +210,6 @@ impl LiquidCache { &self.observer } - /// Record that a parquet row group bypassed the cache because it was full. - pub fn record_cache_full_bypass(&self) { - self.observer.on_cache_full_bypass(); - } - /// Get the compressor states of the cache. pub fn compressor_states(&self, entry_id: &EntryID) -> Arc { self.metadata.get_compressor(entry_id) diff --git a/src/core/src/cache/observer/mod.rs b/src/core/src/cache/observer/mod.rs index 08e632d9..fbf20684 100644 --- a/src/core/src/cache/observer/mod.rs +++ b/src/core/src/cache/observer/mod.rs @@ -99,11 +99,6 @@ impl Observer { self.runtime.incr_hit_date32_expression(); } - #[inline] - pub(crate) fn on_cache_full_bypass(&self) { - self.runtime.incr_cache_full_bypasses(); - } - #[inline] pub(crate) fn on_disk_reservation_failure(&self) { self.runtime.incr_disk_reservation_failures(); diff --git a/src/core/src/cache/observer/stats.rs b/src/core/src/cache/observer/stats.rs index 58e35097..998639a4 100644 --- a/src/core/src/cache/observer/stats.rs +++ b/src/core/src/cache/observer/stats.rs @@ -103,7 +103,6 @@ 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), - (cache_full_bypasses, "Number of row groups bypassed because the cache was full.", incr_cache_full_bypasses), (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), 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 e8b3ca10..8dff9167 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 @@ -51,9 +51,8 @@ RuntimeStatsSnapshot: get_squeezed_needs_io: 1 try_read_liquid_calls: 0 hit_date32_expression_calls: 0 - read_io_count: 1 + read_io_count: 2 write_io_count: 0 - cache_full_bypasses: 0 disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_count: 2141 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 8e93a3c0..f8dc59e0 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 @@ -38,7 +38,6 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 0 write_io_count: 0 - cache_full_bypasses: 0 disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_count: 0 @@ -183,7 +182,6 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 0 write_io_count: 0 - cache_full_bypasses: 0 disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_count: 0 @@ -229,7 +227,6 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 0 write_io_count: 0 - cache_full_bypasses: 0 disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_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 3b1e571c..72b1d8c0 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 @@ -54,7 +54,6 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 0 write_io_count: 0 - cache_full_bypasses: 0 disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_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 ace806f6..20f7734c 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: 0 +entries.memory.liquid: 8 entries.memory.squeezed_liquid: 0 entries.disk.liquid: 0 entries.disk.arrow: 0 -usage.memory_bytes: 885884 -usage.disk_bytes: 729144 +usage.memory_bytes: 884561 +usage.disk_bytes: 877216 RuntimeStatsSnapshot: get: 2 get_with_selection: 2 @@ -50,9 +50,8 @@ 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 - cache_full_bypasses: 0 disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_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 875f395f..67775780 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 @@ -38,7 +38,6 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 0 write_io_count: 0 - cache_full_bypasses: 0 disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_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 a0356b48..a162c4af 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 @@ -27,6 +29,8 @@ event=insert_success entry=262144 kind=DiskLiquid event=squeeze_victim entry=0 event=insert_success entry=0 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 @@ -45,60 +49,8 @@ event=insert_success entry=262145 kind=DiskLiquid event=squeeze_victim entry=1 event=insert_success entry=1 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=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=squeeze_victim entry=262146 -event=io_write entry=262146 kind=DiskLiquid bytes=17448 -event=insert_success entry=262146 kind=DiskLiquid -event=squeeze_victim entry=0 -event=io_write entry=0 kind=MemorySqueezedLiquid bytes=63528 -event=insert_success entry=0 kind=MemorySqueezedLiquid -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=2 -event=insert_success entry=2 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=insert_success entry=1 kind=MemorySqueezedLiquid -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=0 -event=insert_success entry=0 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=eval_predicate entry=2 selection=true cached=MemoryLiquid +event=read entry=262146 selection=true expr=None cached=MemoryArrow event=insert_success entry=4294967296 kind=MemoryArrow event=insert_success entry=4295229440 kind=MemoryArrow event=eval_predicate entry=4294967296 selection=true 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..e41d5bba 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,11 +4,15 @@ expression: trace --- EventTrace: [ event=insert_success entry=2555904 kind=MemoryArrow +event=eval_predicate entry=2555904 selection=true cached=MemoryArrow +event=read entry=2555904 selection=true expr=None cached=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=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_victim entry=2555905 @@ -17,11 +21,6 @@ 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 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 9c40e641..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,15 +15,25 @@ 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 @@ -49,30 +59,14 @@ event=insert_success entry=851970 kind=DiskLiquid event=squeeze_victim entry=917505 event=insert_success entry=917505 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..14181814 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,11 +4,13 @@ expression: trace --- EventTrace: [ event=insert_success entry=2555904 kind=MemoryArrow +event=eval_predicate entry=2555904 selection=true cached=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=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_victim entry=2555905 @@ -17,8 +19,6 @@ 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..505745a0 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,11 +4,13 @@ expression: trace --- EventTrace: [ event=insert_success entry=131072 kind=MemoryArrow +event=eval_predicate entry=131072 selection=true cached=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=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_victim entry=131073 @@ -17,10 +19,6 @@ 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 8a1edd3a..1d5c5271 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 @@ -68,7 +68,6 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 1 write_io_count: 0 - cache_full_bypasses: 0 disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_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 f1ab048f..2ac292a5 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,26 +36,25 @@ values: stats: entries.total: 4 entries.after_first_run: 4 -entries.memory.arrow: 1 -entries.memory.liquid: 1 +entries.memory.arrow: 0 +entries.memory.liquid: 2 entries.memory.squeezed_liquid: 2 entries.disk.liquid: 0 entries.disk.arrow: 0 -usage.memory_bytes: 227389 +usage.memory_bytes: 226744 usage.disk_bytes: 564392 RuntimeStatsSnapshot: get: 3 get_with_selection: 3 eval_predicate: 4 get_squeezed_success: 0 - get_squeezed_needs_io: 4 + get_squeezed_needs_io: 2 try_read_liquid_calls: 0 hit_date32_expression_calls: 0 read_io_count: 4 - write_io_count: 0 - cache_full_bypasses: 0 + write_io_count: 2 disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 - squeezed_decompressed_count: 4362 - squeezed_total_count: 8814 + squeezed_decompressed_count: 2220 + squeezed_total_count: 4486 squeeze_io_saved: 0 diff --git a/src/datafusion/src/cache/mod.rs b/src/datafusion/src/cache/mod.rs index cbe190a4..d8af7a41 100644 --- a/src/datafusion/src/cache/mod.rs +++ b/src/datafusion/src/cache/mod.rs @@ -216,10 +216,6 @@ impl CachedFile { self.cache_store.config().batch_size() } - pub(crate) fn record_cache_full_bypass(&self) { - self.cache_store.record_cache_full_bypass(); - } - /// Return the full file schema tracked by the cache entry. pub fn schema(&self) -> SchemaRef { Arc::clone(&self.file_schema) diff --git a/src/datafusion/src/reader/runtime/liquid_cache_reader.rs b/src/datafusion/src/reader/runtime/liquid_cache_reader.rs index f612d094..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,17 +531,77 @@ 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(); @@ -304,8 +615,6 @@ mod tests { 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(); @@ -318,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 { @@ -378,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); @@ -393,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); @@ -407,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); @@ -429,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); @@ -457,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); @@ -500,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); @@ -524,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); @@ -551,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_stream.rs b/src/datafusion/src/reader/runtime/liquid_stream.rs index 290a619a..2933d5d2 100644 --- a/src/datafusion/src/reader/runtime/liquid_stream.rs +++ b/src/datafusion/src/reader/runtime/liquid_stream.rs @@ -1,17 +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, stream::BoxStream}; -use parquet::arrow::arrow_reader::{ - ArrowPredicate, ArrowReaderMetadata, ArrowReaderOptions, RowFilter, -}; +use futures::Stream; use parquet::{ arrow::{ - ParquetRecordBatchStreamBuilder, ProjectionMask, - arrow_reader::{RowSelection, RowSelector}, + ProjectionMask, + arrow_reader::{ArrowPredicate, RowSelection, RowSelector}, }, errors::ParquetError, file::metadata::ParquetMetaData, @@ -24,16 +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, FillOutcome), ParquetError>; - -enum FillOutcome { - Filled, - Bypass, -} struct ReaderFactory { metadata: Arc, @@ -106,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 { @@ -130,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, @@ -141,107 +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, FillOutcome::Filled)); - } - - // 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, FillOutcome::Filled)); - } - - // 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; - let outcome = insert_batch_into_cache( - &record_batch, - &column_ids, - batch_id, - cache_batch_size, - row_count, - &context.cached_row_group, - ) - .await?; - if matches!(outcome, InsertBatchOutcome::CacheFull) { - return Ok((self, context, FillOutcome::Bypass)); - } - - 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, FillOutcome::Filled)) - } } fn build_projection_schema(file_schema: &SchemaRef, projection_column_ids: &[usize]) -> SchemaRef { @@ -253,170 +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) -} - -enum InsertBatchOutcome { - Ok, - CacheFull, -} - -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 { - if column_ids.is_empty() || record_batch.num_rows() == 0 { - return Ok(InsertBatchOutcome::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(InsertBatchOutcome::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)); - - match column.insert(batch_id, array).await { - Ok(()) | Err(InsertArrowArrayError::AlreadyCached) => { - debug_assert!(column.is_cached(batch_id)); - } - Err(InsertArrowArrayError::CacheFull) => { - return Ok(InsertBatchOutcome::CacheFull); - } - } - } - - Ok(InsertBatchOutcome::Ok) -} - /// Context for planning what to read from cache vs parquet struct PlanningContext { row_group_idx: usize, @@ -426,30 +149,49 @@ 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), - /// Reading a row group directly from parquet after cache fill hit disk budget. - BypassParquet { - stream: BoxStream<'static, Result>, - filter: Option, - }, + 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"), - StreamState::BypassParquet { .. } => write!(f, "StreamState::BypassParquet"), } } } @@ -623,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))); @@ -632,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 @@ -663,114 +406,15 @@ 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); - } - } - 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, FillOutcome::Filled)) => { - self.reader = Some(reader_factory); 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 = 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); - } - Ok((mut reader_factory, context, FillOutcome::Bypass)) => { - LocalSpan::add_event(Event::new("LiquidStream::bypass_parquet")); - reader_factory.cached_file.record_cache_full_bypass(); - let filter = reader_factory.filter.take(); - let reader_clone = reader_factory.input.clone(); - let reader_metadata = ArrowReaderMetadata::try_new( - Arc::clone(&reader_factory.metadata), - ArrowReaderOptions::new(), - ) - .unwrap(); - let mut builder = ParquetRecordBatchStreamBuilder::new_with_metadata( - reader_clone, - reader_metadata, - ) - .with_projection(self.projection.clone()) - .with_row_groups(vec![context.row_group_idx]) - .with_row_selection(context.selection) - .with_batch_size(context.batch_size); - // Push predicates into the parquet decoder so it - // decodes each predicate's own columns and applies - // the filter using its rebound column indices. - // Clones share metric counters with the originals - // (`metrics::Count`/`Time` are `Arc`-backed), so - // accumulated stats are preserved when we restore - // `filter` to `reader_factory` after this row group. - if let Some(f) = filter.as_ref() { - let predicates: Vec> = f - .predicates() - .iter() - .cloned() - .map(|p| Box::new(p) as Box) - .collect(); - builder = builder.with_row_filter(RowFilter::new(predicates)); - } - let stream = builder.build().unwrap().boxed(); - self.reader = Some(reader_factory); - self.state = StreamState::BypassParquet { stream, filter }; - } - Err(e) => { - panic!("Filling cache error: {e:?}"); + let batch_reader = + build_liquid_cache_reader(reader_factory, context, schema); + self.state = StreamState::ReadFromCache(Box::new(batch_reader)); } - }, - }, - StreamState::BypassParquet { mut stream, filter } => { - match Pin::new(&mut stream).poll_next(cx) { - Poll::Ready(Some(Ok(batch))) => { - self.state = StreamState::BypassParquet { stream, filter }; - if batch.num_rows() == 0 { - continue; - } - return Poll::Ready(Some(Ok(batch))); - } - Poll::Ready(Some(Err(err))) => { - self.state = StreamState::BypassParquet { stream, filter }; - return Poll::Ready(Some(Err(err))); - } - Poll::Ready(None) => { - self.reader.as_mut().unwrap().filter = filter; - } - Poll::Pending => { - self.state = StreamState::BypassParquet { stream, filter }; - return Poll::Pending; + None => { + self.state = StreamState::Init; } } } @@ -782,7 +426,7 @@ impl Stream for LiquidStream { #[cfg(test)] mod tests { use super::*; - use crate::cache::{CachedFileRef, LiquidCacheParquet}; + use crate::cache::{BatchID, CachedFileRef, LiquidCacheParquet}; use crate::reader::plantime::{ CachedMetaReaderFactory, FilterCandidateBuilder, LiquidPredicate, }; @@ -794,34 +438,16 @@ mod tests { 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 object_store::local::LocalFileSystem; use parquet::arrow::ArrowWriter; - use parquet::arrow::arrow_reader::RowSelection; + 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 { - let tmp_dir = tempfile::tempdir().unwrap(); - 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 file = cache.register_or_get_file("test.parquet".to_string(), schema); - file.create_row_group(0, vec![]) - } - 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(); @@ -847,6 +473,19 @@ mod tests { 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, @@ -935,6 +574,23 @@ mod tests { (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) } @@ -1026,6 +682,62 @@ mod tests { (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( row_group: &CachedRowGroupRef, column_id: usize, @@ -1041,265 +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); - } - - #[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); - } - - #[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); - } - - #[tokio::test] - async fn compute_missing_batches_identifies_partial_columns() { - 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), - ])); - 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 missing_for_col0 = compute_missing_batches(&row_group, &[0], &selection_batches); - assert_eq!(missing_for_col0, vec![BatchID::from_raw(1)]); - - 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),] - ); - - 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), - ] - ); - } - - #[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), - ] - ); - } - - #[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![]); - } - - #[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![]); - } - - #[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),] - ); - } - - #[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),] - ); + 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() } #[tokio::test] - async fn cache_full_bypasses_row_group_and_keeps_inserted_batches() { + 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) = + let (stream, _cache, cached_file, _tmp_dir) = make_liquid_stream(one_array_memory * 3, 0, None).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_eq!(cache.storage().stats().runtime.cache_full_bypasses, 1); let row_group0 = cached_file.create_row_group(0, vec![]); let row_group1 = cached_file.create_row_group(1, vec![]); - assert!( - row_group0 - .get_column(0) - .unwrap() - .get_arrow_array_test_only(BatchID::from_raw(0)) - .await - .is_some() - ); - assert!( - row_group0 - .get_column(1) - .unwrap() - .get_arrow_array_test_only(BatchID::from_raw(0)) - .await - .is_some() - ); - assert!( - row_group1 - .get_column(0) - .unwrap() - .get_arrow_array_test_only(BatchID::from_raw(0)) - .await - .is_some() - ); + 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 cache_full_bypass_applies_row_filter_in_memory() { + async fn cache_full_with_row_filter_keeps_lookaside_results_correct() { 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(schema, 2); - let (stream, cache, _cached_file, _tmp_dir) = + let (stream, _cache, cached_file, _tmp_dir) = make_liquid_stream(one_array_memory * 3, 0, Some(filter)).await; let (a, b) = collect_liquid_values(stream).await; assert_eq!(a, vec![3, 4, 5, 6, 7]); assert_eq!(b, vec![13, 14, 15, 16, 17]); - assert_eq!(cache.storage().stats().runtime.cache_full_bypasses, 1); + + 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 cache_full_bypasses_immediately_when_already_at_cap() { - let (stream, cache, cached_file, _tmp_dir) = make_liquid_stream(0, 0, None).await; + async fn mid_scan_eviction_recovers() { + let (stream, _cache, cached_file, _tmp_dir) = make_liquid_stream(0, 0, None).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_eq!(cache.storage().stats().runtime.cache_full_bypasses, 2); let row_group0 = cached_file.create_row_group(0, vec![]); - assert!( - row_group0 - .get_column(0) - .unwrap() - .get_arrow_array_test_only(BatchID::from_raw(0)) - .await - .is_none() - ); + 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); } - /// Reproducer: predicate references a column that is NOT in the user - /// projection. On the bypass path the parquet stream is built with - /// `self.projection.clone()` only, so the decoded batch does not even - /// contain the predicate's column. `LiquidRowFilter::filter_batch` then - /// evaluates the predicate against whatever column happens to sit at - /// index 0 of the batch, producing wrong results. - /// - /// File data: a = [0..7], b = [10..17] - /// Query: SELECT a WHERE b > 13 - /// Correct: a = [4, 5, 6, 7] - /// Buggy: predicate is `b > 13` rebound to "column index 0 of - /// filter_schema"; the bypass batch is `[a]`, so the predicate - /// evaluates `a > 13` -> all false -> empty result. #[tokio::test] - async fn cache_full_bypass_evaluates_predicate_against_wrong_column() { + 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) = + let (stream, _cache, cached_file, _tmp_dir) = make_liquid_stream_with_projection(one_array_memory * 3, 0, Some(filter), vec![0]) .await; - let batches = stream - .map(|batch| batch.expect("valid liquid stream batch")) - .collect::>() - .await; + let a_values = collect_projected_a(stream).await; - let mut a_values = Vec::new(); - for batch in batches { - let arr = batch - .column(0) - .as_any() - .downcast_ref::() - .unwrap(); - a_values.extend(arr.iter().map(|value| value.unwrap())); - } + assert_eq!(a_values, vec![4, 5, 6, 7]); - // The bypass path was actually exercised. - assert!(cache.storage().stats().runtime.cache_full_bypasses >= 1); + 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); + } - // SELECT a WHERE b > 13 -> a = [4, 5, 6, 7] - assert_eq!(a_values, vec![4, 5, 6, 7]); + #[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); + } + + #[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); } } From 4597d1d1c434100031b00bd743c91d0c70b334d9 Mon Sep 17 00:00:00 2001 From: Xiangpeng Hao Date: Wed, 6 May 2026 00:17:14 -0500 Subject: [PATCH 07/13] Proper disk replacement (3/3) (#498) When disk is full, we will run replacement to evict old items. This pr also clean up the unnecessary policies (they don't really makes sense) --- examples/example_server.rs | 5 +- src/core/src/cache/budget.rs | 1 - src/core/src/cache/cached_batch.rs | 40 +- src/core/src/cache/core.rs | 294 ++++++++++--- src/core/src/cache/index.rs | 9 + .../src/cache/observer/internal_tracing.rs | 12 + src/core/src/cache/observer/mod.rs | 1 + src/core/src/cache/observer/stats.rs | 1 + src/core/src/cache/policies/cache/clock.rs | 373 ----------------- .../policies/cache/doubly_linked_list.rs | 2 + src/core/src/cache/policies/cache/filo.rs | 307 -------------- src/core/src/cache/policies/cache/lru.rs | 391 ------------------ src/core/src/cache/policies/cache/mod.rs | 26 +- src/core/src/cache/policies/cache/s3_fifo.rs | 379 ----------------- src/core/src/cache/policies/cache/sieve.rs | 304 -------------- .../src/cache/policies/cache/three_queue.rs | 69 +++- src/core/src/cache/policies/hydration.rs | 20 +- src/core/src/cache/policies/squeeze.rs | 299 +++++++++----- .../src/liquid_array/byte_view_array/mod.rs | 6 +- src/core/src/liquid_array/decimal_array.rs | 6 +- src/core/src/liquid_array/float_array.rs | 6 +- .../liquid_array/hybrid_primitive_array.rs | 10 +- src/core/src/liquid_array/mod.rs | 25 +- src/core/src/liquid_array/raw/fsst_buffer.rs | 4 + .../src/liquid_array/squeezed_date32_array.rs | 16 +- src/core/src/liquid_array/variant_array.rs | 7 +- src/core/study/cache_storage.rs | 4 +- src/datafusion-local/README.md | 4 +- ...datafusion_local__tests__os_selection.snap | 1 + ...afusion_local__tests__provide_schema2.snap | 3 + ...al__tests__provide_schema_with_filter.snap | 1 + ...usion_local__tests__referer_filtering.snap | 1 + ...ests__single_column_filter_projection.snap | 1 + ...on_local__tests__url_prefix_filtering.snap | 1 + ...al__tests__url_selection_and_ordering.snap | 1 + src/datafusion/src/cache/stats.rs | 8 +- 36 files changed, 645 insertions(+), 1993 deletions(-) delete mode 100644 src/core/src/cache/policies/cache/clock.rs delete mode 100644 src/core/src/cache/policies/cache/filo.rs delete mode 100644 src/core/src/cache/policies/cache/lru.rs delete mode 100644 src/core/src/cache/policies/cache/s3_fifo.rs delete mode 100644 src/core/src/cache/policies/cache/sieve.rs diff --git a/examples/example_server.rs b/examples/example_server.rs index 84634e5d..67872ac7 100644 --- a/examples/example_server.rs +++ b/examples/example_server.rs @@ -1,9 +1,8 @@ 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_local::storage::cache::{AlwaysHydrate, LiquidPolicy}; use liquid_cache_datafusion_server::LiquidCacheService; -use liquid_cache_datafusion_server::storage::cache_policies::LruPolicy; use tonic::transport::Server; #[tokio::main] @@ -12,7 +11,7 @@ async fn main() -> Result<(), Box> { SessionContext::new(), Some(1024 * 1024 * 1024), // max memory size 1GB Some(tempfile::tempdir()?.keep()), // disk cache dir - Box::new(LruPolicy::new()), + Box::new(LiquidPolicy::new()), Box::new(TranscodeSqueezeEvict), Box::new(AlwaysHydrate::new()), ) diff --git a/src/core/src/cache/budget.rs b/src/core/src/cache/budget.rs index b01239eb..2b7bc5d0 100644 --- a/src/core/src/cache/budget.rs +++ b/src/core/src/cache/budget.rs @@ -96,7 +96,6 @@ impl BudgetAccounting { } } - #[allow(dead_code)] pub(super) fn release_disk(&self, bytes: usize) { self.used_disk_bytes.fetch_sub(bytes, Ordering::Relaxed); } 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 63a5f033..84e0a795 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; @@ -16,7 +16,7 @@ use super::{ 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::{CacheFull, CacheStats, EventTrace}; @@ -92,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(); @@ -155,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, } } @@ -230,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 @@ -250,7 +268,7 @@ 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 } } @@ -273,12 +291,19 @@ impl LiquidCache { entry_id, self.observer.clone(), )); - let (new_batch, bytes_to_write) = self.squeeze_policy.squeeze( + let outcome = self.squeeze_policy.squeeze( batch, 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?; @@ -287,23 +312,24 @@ impl LiquidCache { } 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?; 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(); - let entry = 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") } } @@ -324,7 +350,7 @@ impl LiquidCache { 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, @@ -408,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() @@ -458,23 +524,34 @@ impl LiquidCache { )); 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; } } } @@ -482,11 +559,11 @@ impl LiquidCache { } 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( @@ -545,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 } @@ -564,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 { @@ -586,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 { @@ -717,7 +794,18 @@ impl LiquidCache { bytes: Bytes, ) -> Result<(), CacheFull> { let len = bytes.len(); - self.budget.try_reserve_disk(len).map_err(|()| CacheFull)?; + 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), @@ -801,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; @@ -823,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; @@ -878,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::{ @@ -912,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] @@ -923,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); @@ -959,7 +1047,7 @@ 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(2), Some(365 + 1), None, Some(365 + 100)]); @@ -1056,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 { @@ -1122,7 +1210,7 @@ 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); @@ -1130,7 +1218,7 @@ mod tests { 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"); @@ -1143,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(); @@ -1156,7 +1244,7 @@ mod tests { 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"); @@ -1184,7 +1272,82 @@ mod tests { } #[tokio::test] - async fn flush_all_to_disk_returns_cache_full_on_overflow() { + 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) @@ -1195,12 +1358,9 @@ mod tests { let array: ArrayRef = Arc::new(Int32Array::from_iter_values(0..16)); cache.insert(entry_id, array).await.unwrap(); - let err = cache.flush_all_to_disk().await; + let result = cache.flush_all_to_disk().await; - assert_eq!(err, Err(CacheFull)); - assert!(matches!( - cache.index().get(&entry_id).unwrap().as_ref(), - CacheEntry::MemoryArrow(_) - )); + assert_eq!(result, Ok(())); + assert!(!cache.is_cached(&entry_id)); } } 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/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 fbf20684..38de5fc4 100644 --- a/src/core/src/cache/observer/mod.rs +++ b/src/core/src/cache/observer/mod.rs @@ -107,6 +107,7 @@ impl Observer { 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 998639a4..fa0c3d9a 100644 --- a/src/core/src/cache/observer/stats.rs +++ b/src/core/src/cache/observer/stats.rs @@ -103,6 +103,7 @@ 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), 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 45d7fc03..00000000 --- a/src/core/src/cache/policies/cache/clock.rs +++ /dev/null @@ -1,373 +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 - .unwrap(); - store - .insert(entry_id2, create_test_arrow_array(100)) - .await - .unwrap(); - store - .insert(entry_id3, create_test_arrow_array(100)) - .await - .unwrap(); - - let entry_id4 = EntryID::from(4); - store - .insert(entry_id4, create_test_arrow_array(100)) - .await - .unwrap(); - - 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 9e4c7ece..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); 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 50f8981d..00000000 --- a/src/core/src/cache/policies/cache/filo.rs +++ /dev/null @@ -1,307 +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 - .unwrap(); - - 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 - .unwrap(); - store - .insert(entry_id3, create_test_arrow_array(100)) - .await - .unwrap(); - - let entry_id4: EntryID = EntryID::from(4); - store - .insert(entry_id4, create_test_arrow_array(100)) - .await - .unwrap(); - - 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 c61d6259..00000000 --- a/src/core/src/cache/policies/cache/lru.rs +++ /dev/null @@ -1,391 +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 - .unwrap(); - store - .insert(entry_id2, create_test_arrow_array(100)) - .await - .unwrap(); - store - .insert(entry_id3, create_test_arrow_array(100)) - .await - .unwrap(); - - 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 de30dc12..00000000 --- a/src/core/src/cache/policies/cache/sieve.rs +++ /dev/null @@ -1,304 +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 - .unwrap(); - store - .insert(entry_id2, create_test_arrow_array(100)) - .await - .unwrap(); - store - .insert(entry_id3, create_test_arrow_array(100)) - .await - .unwrap(); - 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/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/decimal_array.rs b/src/core/src/liquid_array/decimal_array.rs index 354f564e..050a746e 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, diff --git a/src/core/src/liquid_array/float_array.rs b/src/core/src/liquid_array/float_array.rs index b3985aa8..f6dea20e 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, diff --git a/src/core/src/liquid_array/hybrid_primitive_array.rs b/src/core/src/liquid_array/hybrid_primitive_array.rs index 15dcb5cb..7f05f934 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)] @@ -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()); @@ -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, 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 a3f9657d..aa3aa01d 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; @@ -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 } @@ -459,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()); 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 3187dfe6..0c69f448 100644 --- a/src/core/study/cache_storage.rs +++ b/src/core/study/cache_storage.rs @@ -14,8 +14,8 @@ 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; @@ -51,7 +51,7 @@ fn main() { LiquidCacheBuilder::new() .with_max_memory_bytes(500 * 1024 * 1024) .with_squeeze_policy(Box::new(TranscodeSqueezeEvict)) - .with_cache_policy(Box::new(FiloPolicy::new())) + .with_cache_policy(Box::new(LiquidPolicy::new())) .with_store(store) .build() .await diff --git a/src/datafusion-local/README.md b/src/datafusion-local/README.md index bc6e1454..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; @@ -21,7 +21,7 @@ async fn main() -> Result<(), Box> { let (ctx, _cache) = LiquidCacheLocalBuilder::new() .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/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 8dff9167..e8cff162 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 @@ -53,6 +53,7 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 2 write_io_count: 0 + disk_evictions: 0 disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_count: 2141 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 f8dc59e0..56fb14bb 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 @@ -38,6 +38,7 @@ 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 @@ -182,6 +183,7 @@ 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 @@ -227,6 +229,7 @@ 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 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 72b1d8c0..1cef51ab 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 @@ -54,6 +54,7 @@ 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 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 20f7734c..7ac42fc1 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 @@ -52,6 +52,7 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 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 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 67775780..4f70d628 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 @@ -38,6 +38,7 @@ 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 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 1d5c5271..d4817712 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 @@ -68,6 +68,7 @@ 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 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 2ac292a5..304f9894 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 @@ -53,6 +53,7 @@ RuntimeStatsSnapshot: hit_date32_expression_calls: 0 read_io_count: 4 write_io_count: 2 + disk_evictions: 0 disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 squeezed_decompressed_count: 2220 diff --git a/src/datafusion/src/cache/stats.rs b/src/datafusion/src/cache/stats.rs index f635ec12..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); From 44b4219a58436454e5e1c6ba62d1f02b849744fb Mon Sep 17 00:00:00 2001 From: Xiangpeng Hao Date: Fri, 22 May 2026 16:13:52 -0400 Subject: [PATCH 08/13] Bump t4 to latest (#501) With the latest t4, we will have real deletion and real disk replacement --- .github/workflows/ci.yml | 266 +++++++++++++++++++-------------------- Cargo.lock | 51 ++++---- Cargo.toml | 2 +- 3 files changed, 161 insertions(+), 158 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c72baef1..e341ff13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -317,143 +317,143 @@ jobs: files: codecov_stackoverflow.json fail_ci_if_error: true - # benchmark: - # name: Performance Benchmark - # runs-on: pittsburgh - # 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 + 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: Build benchmark binary - # run: cargo build --release --bin in_process + - 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: 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: Build benchmark binary + run: cargo build --release --bin in_process - # - 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: 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: 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); - # } + - 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 diff --git a/Cargo.lock b/Cargo.lock index 61dbc368..636823e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2879,7 +2879,7 @@ 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]] @@ -3805,7 +3805,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3841,7 +3841,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4683,7 +4683,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]] @@ -5808,7 +5808,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6519,10 +6519,13 @@ dependencies = [ [[package]] name = "t4" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea97b3f25826306b821d1c0b070709ef61d28e98c3ef1153de1048c33acf381" +checksum = "d3105026c5fb7e1dcf8864c2193387439aa650b690e1a5513ab27ae7de7999a9" dependencies = [ + "cfg-if", + "crossbeam-channel", + "futures-util", "io-uring", "libc", "shuttle", @@ -6532,9 +6535,9 @@ dependencies = [ [[package]] name = "t4-verified" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a729c7595d4771ff781dcd1fdd5f65d14c8225dce6341ddf28792b292e6f5cdb" +checksum = "494c2f2c4cfb21784ef9b380c378fbfdbbe2f19f1ad606e6729816ea2a0f4297" dependencies = [ "vstd", ] @@ -6555,7 +6558,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6565,7 +6568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7283,15 +7286,15 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "verus_builtin" -version = "0.0.0-2026-04-12-0118" +version = "0.0.0-2026-05-06-1803" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a46cb431066009ad2035f6bca936b1c2b7e293bffec93a2090fead0f35ab4276" +checksum = "650bcc71ef90cbc79bc2a2494c54dd159edad63a72677be791954edc993883fe" [[package]] name = "verus_builtin_macros" -version = "0.0.0-2026-04-20-1748" +version = "0.0.0-2026-05-10-0145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3131a0e0b0dc9272cb574ad1f8506d8e25ba33c9f9972151866c34cad12bb076" +checksum = "7ef946d78c84f284991d59035ca252f29cd8071526003e794be80f0beee18f2d" dependencies = [ "proc-macro2", "quote", @@ -7303,9 +7306,9 @@ dependencies = [ [[package]] name = "verus_prettyplease" -version = "0.0.0-2026-04-12-0118" +version = "0.0.0-2026-05-10-0145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b246e61b068e807cb05a030fc1d7efa83d2a0d227eaf67921661419edfe8e83" +checksum = "6a227e7eaa03f51ac4d659641192bc1b9fe4eda0a509a6734752cf72f5d7daaf" dependencies = [ "proc-macro2", "verus_syn", @@ -7313,9 +7316,9 @@ dependencies = [ [[package]] name = "verus_state_machines_macros" -version = "0.0.0-2026-04-20-1748" +version = "0.0.0-2026-05-10-0145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2497778c0b23d5cbd93f43cce512438af5cb01b1f1435aa7e411da8495efb5" +checksum = "3ff46046bffd2d55757503feef0b72a74a60ead2b58aa211861267485b955136" dependencies = [ "indexmap 1.9.3", "proc-macro2", @@ -7325,9 +7328,9 @@ dependencies = [ [[package]] name = "verus_syn" -version = "0.0.0-2026-04-05-0114" +version = "0.0.0-2026-05-10-0145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "285b554a87b470ee705634ea1cdc92c14ba088a7b8bdacbb9116b32e832fe272" +checksum = "a82174e474e1f06418dd304f714785ae1c3e4d2fbff415a0d0c05a02cd995819" dependencies = [ "proc-macro2", "quote", @@ -7336,9 +7339,9 @@ dependencies = [ [[package]] name = "vstd" -version = "0.0.0-2026-04-20-1748" +version = "0.0.0-2026-05-10-0145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d03a77a2ee1b91bc776f8e9ee716d83b519d4338912116d7e855edfa3a8b976" +checksum = "6958222eaf7bbe565c93890676bb1f96c4f5e25d90a81bb9993fdc19fc0d3f72" dependencies = [ "verus_builtin", "verus_builtin_macros", @@ -7563,7 +7566,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]] diff --git a/Cargo.toml b/Cargo.toml index a6a31c64..53991aea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,7 +67,7 @@ fastrace = "0.7" fastrace-tonic = "0.2" congee = "0.4.1" insta = "1.47.2" -t4 = "0.1.6" +t4 = "0.1.7" [profile.dev.package] insta.opt-level = 3 From bc13d43467324a636ff31074a46f5f092b935a94 Mon Sep 17 00:00:00 2001 From: Xiangpeng Hao Date: Fri, 22 May 2026 22:27:12 -0400 Subject: [PATCH 09/13] chore: release v0.1.13 (#502) ## Release v0.1.13 This PR bumps all workspace crate versions to `0.1.13`. --- **On merge**, the `publish.yml` workflow will automatically: 1. Publish all crates to crates.io 2. Create a GitHub Release with tag `v0.1.13` Co-authored-by: github-actions[bot] --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 14 +++++++------- dev/dev-tools/Cargo.toml | 2 +- fuzz/Cargo.toml | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 636823e7..cc41fdf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2107,7 +2107,7 @@ dependencies = [ [[package]] name = "dev-tools" -version = "0.1.2" +version = "0.1.3" dependencies = [ "dioxus", ] @@ -4111,7 +4111,7 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "liquid-cache" -version = "0.1.12" +version = "0.1.13" dependencies = [ "ahash", "arrow", @@ -4192,7 +4192,7 @@ dependencies = [ [[package]] name = "liquid-cache-common" -version = "0.1.12" +version = "0.1.13" dependencies = [ "arrow-flight", "async-trait", @@ -4210,7 +4210,7 @@ dependencies = [ [[package]] name = "liquid-cache-datafusion" -version = "0.1.12" +version = "0.1.13" dependencies = [ "ahash", "arrow", @@ -4237,7 +4237,7 @@ dependencies = [ [[package]] name = "liquid-cache-datafusion-client" -version = "0.1.12" +version = "0.1.13" dependencies = [ "arrow", "arrow-flight", @@ -4258,7 +4258,7 @@ dependencies = [ [[package]] name = "liquid-cache-datafusion-local" -version = "0.1.12" +version = "0.1.13" dependencies = [ "arrow", "arrow-schema", @@ -4276,7 +4276,7 @@ dependencies = [ [[package]] name = "liquid-cache-datafusion-server" -version = "0.1.12" +version = "0.1.13" dependencies = [ "anyhow", "arrow", @@ -4309,7 +4309,7 @@ dependencies = [ [[package]] name = "liquid-cache-fuzz" -version = "0.1.12" +version = "0.1.13" dependencies = [ "arbitrary", "arrow", diff --git a/Cargo.toml b/Cargo.toml index 53991aea..a8e40a42 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,12 +24,12 @@ 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" } +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.1.0", default-features = false, features = [ "prettyprint", "ipc", diff --git a/dev/dev-tools/Cargo.toml b/dev/dev-tools/Cargo.toml index 387a086f..879412d3 100644 --- a/dev/dev-tools/Cargo.toml +++ b/dev/dev-tools/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dev-tools" -version = "0.1.2" +version = "0.1.3" authors = ["XiangpengHao "] edition = "2024" 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" From f8e362e68e10812841b62ab3558531f368c3cef3 Mon Sep 17 00:00:00 2001 From: Shefeek Date: Wed, 3 Jun 2026 21:58:22 +0530 Subject: [PATCH 10/13] Propagate file_size to ParquetObjectReader in CachedMetaReaderFactory (#504) CachedMetaReaderFactory::create_liquid_reader constructed its inner ParquetObjectReader without the known file size, forcing parquet 58.x onto the suffix-fetch metadata path. With a large metadata_size_hint and a file containing page indexes, that path can slice page-index ranges against a retained suffix using the wrong base offset, panicking with `assertion failed: end <= remainder.len()`. Forward the already-known object_meta.size via with_file_size() so metadata loading uses the size-aware bounded-range path, matching DataFusion's parquet reader factories. Fixes #503 --- src/datafusion/src/reader/plantime/source.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/datafusion/src/reader/plantime/source.rs b/src/datafusion/src/reader/plantime/source.rs index 6c193c3f..6368ec1a 100644 --- a/src/datafusion/src/reader/plantime/source.rs +++ b/src/datafusion/src/reader/plantime/source.rs @@ -59,7 +59,8 @@ impl CachedMetaReaderFactory { ) -> ParquetMetadataCacheReader { let path = partitioned_file.object_meta.location.clone(); let store = Arc::clone(&self.store); - let mut inner = ParquetObjectReader::new(store, path.clone()); + let mut inner = ParquetObjectReader::new(store, path.clone()) + .with_file_size(partitioned_file.object_meta.size); if let Some(hint) = metadata_size_hint { inner = inner.with_footer_size_hint(hint); From ba4b884818e283134092550c92fc6d75856349bb Mon Sep 17 00:00:00 2001 From: Xiangpeng Hao Date: Tue, 9 Jun 2026 14:16:10 -0400 Subject: [PATCH 11/13] bump to df v54, simplify some examples (#507) closes #506 --- .github/workflows/ci.yml | 130 ++--- Cargo.lock | 452 ++++++++++-------- Cargo.toml | 34 +- README.md | 18 +- benchmark/Cargo.toml | 8 +- benchmark/src/lib.rs | 12 +- benchmark/src/observability.rs | 4 +- benchmark/src/utils.rs | 2 +- examples/Cargo.toml | 30 +- examples/README.md | 32 +- examples/core.rs | 31 ++ examples/datafusion-client-server.rs | 101 ++++ .../{example_local.rs => datafusion-local.rs} | 1 + examples/example_client.rs | 59 --- examples/example_inprocess_cache_eviction.rs | 40 -- examples/example_inprocess_insertion.rs | 37 -- examples/example_inprocess_read.rs | 22 - examples/example_projection_pushdown.rs | 59 --- examples/example_server.rs | 28 -- src/common/Cargo.toml | 2 +- src/core/Cargo.toml | 7 +- src/core/src/cache/builders.rs | 21 +- src/core/src/cache/liquid_expr.rs | 25 +- src/core/src/cache/mod.rs | 2 +- .../liquid_array/byte_view_array/operator.rs | 14 +- src/core/src/liquid_array/decimal_array.rs | 13 +- src/core/src/liquid_array/float_array.rs | 4 +- .../liquid_array/hybrid_primitive_array.rs | 16 +- src/core/study/squeeze_integer.rs | 4 +- src/datafusion-client/src/client_exec.rs | 83 +++- src/datafusion-client/src/lib.rs | 13 + src/datafusion-client/src/optimizer.rs | 31 +- ...che_datafusion_client__tests__tpch_q1.snap | 24 +- ...he_datafusion_client__tests__tpch_q10.snap | 97 ++-- ...he_datafusion_client__tests__tpch_q11.snap | 203 ++++---- ...he_datafusion_client__tests__tpch_q12.snap | 4 +- ...he_datafusion_client__tests__tpch_q14.snap | 77 +-- ...he_datafusion_client__tests__tpch_q16.snap | 59 +-- ...he_datafusion_client__tests__tpch_q17.snap | 126 ++--- ...he_datafusion_client__tests__tpch_q18.snap | 8 +- ...he_datafusion_client__tests__tpch_q19.snap | 62 ++- ...che_datafusion_client__tests__tpch_q2.snap | 157 ++---- ...he_datafusion_client__tests__tpch_q20.snap | 7 +- ...he_datafusion_client__tests__tpch_q21.snap | 8 +- ...he_datafusion_client__tests__tpch_q22.snap | 185 ++++--- ...che_datafusion_client__tests__tpch_q3.snap | 16 +- ...che_datafusion_client__tests__tpch_q5.snap | 103 ++-- ...che_datafusion_client__tests__tpch_q7.snap | 129 ++--- ...che_datafusion_client__tests__tpch_q8.snap | 231 +++------ ...che_datafusion_client__tests__tpch_q9.snap | 135 ++---- src/datafusion-local/src/lib.rs | 23 +- .../src/tests/date_optimizer.rs | 8 +- ...datafusion_local__tests__os_selection.snap | 6 +- ...afusion_local__tests__provide_schema2.snap | 6 +- ...al__tests__provide_schema_with_filter.snap | 2 +- ...usion_local__tests__referer_filtering.snap | 6 +- ...ests__single_column_filter_projection.snap | 2 +- ..._local__tests__squeeze__basic_squeeze.snap | 18 +- ...queeze__squeeze_distinct_search_phase.snap | 16 +- ...s__squeeze__squeeze_substrings_search.snap | 11 +- ...eeze__squeeze_substrings_search_title.snap | 11 +- ...on_local__tests__url_prefix_filtering.snap | 2 +- ...al__tests__url_selection_and_ordering.snap | 16 +- src/datafusion-local/src/tests/squeeze.rs | 2 +- src/datafusion-server/Cargo.toml | 2 +- .../src/admin_server/handlers.rs | 4 +- src/datafusion-server/src/errors.rs | 23 +- src/datafusion/src/optimizers/lineage_opt.rs | 51 +- src/datafusion/src/optimizers/mod.rs | 14 +- src/datafusion/src/reader/plantime/opener.rs | 28 +- .../src/reader/plantime/row_filter.rs | 6 +- .../src/reader/plantime/row_group_filter.rs | 18 +- src/datafusion/src/reader/plantime/source.rs | 5 - .../src/reader/runtime/liquid_predicate.rs | 24 +- src/datafusion/src/reader/variant_udf.rs | 12 - src/datafusion/src/utils.rs | 3 +- 76 files changed, 1503 insertions(+), 1782 deletions(-) create mode 100644 examples/core.rs create mode 100644 examples/datafusion-client-server.rs rename examples/{example_local.rs => datafusion-local.rs} (93%) delete mode 100644 examples/example_client.rs delete mode 100644 examples/example_inprocess_cache_eviction.rs delete mode 100644 examples/example_inprocess_insertion.rs delete mode 100644 examples/example_inprocess_read.rs delete mode 100644 examples/example_projection_pushdown.rs delete mode 100644 examples/example_server.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e341ff13..11e509b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,7 +127,6 @@ jobs: # Sanitizers can only run on nightly - uses: dtolnay/rust-toolchain@nightly with: - toolchain: nightly-2025-08-01 components: rust-src # Address sanitizers can't be cached: https://github.com/Swatinem/rust-cache/issues/161 @@ -221,46 +220,45 @@ jobs: files: codecov_tpch.json fail_ci_if_error: true - # Diable for now, because the upstream datafusion has a bug. - # 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 + 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 @@ -466,38 +464,25 @@ jobs: shared-key: ci-${{ runner.os }} save-if: ${{ github.ref == 'refs/heads/main' }} - - name: Build LiquidCache server - run: cargo build --bin example_server - - name: Build LiquidCache client - run: cargo build --bin example_client - - name: Build LiquidCache client projection pushdown - run: cargo build --bin example_projection_pushdown - - name: Build LiquidCache in process eviction - run: cargo build --bin example_inprocess_cache_eviction - - name: Build LiquidCache in process insertion - run: cargo build --bin example_inprocess_insertion - - name: Build LiquidCache in process read - run: cargo build --bin example_inprocess_read + - 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 example_server -- --abort-on-panic &> server.log & + 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: Start LiquidCache client - run: | - # First run to populate the cache - env RUST_LOG=info cargo run --bin example_client - # Run twice to test the cache - env RUST_LOG=info cargo run --bin example_client - - - name: Start LiquidCache client projection pushdown + - name: Run datafusion client run: | # First run to populate the cache - env RUST_LOG=info cargo run --bin example_projection_pushdown + 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 example_projection_pushdown + env RUST_LOG=info cargo run --bin datafusion-client-server -- --mode client - name: Kill LiquidCache server and show logs if: always() @@ -507,17 +492,8 @@ jobs: pkill -F server.pid || true rm -f server.pid - - name: Start LiquidCache in process projection pushdown - run: | - # Run to populate to evict cache - env RUST_LOG=info cargo run --bin example_inprocess_cache_eviction - - - name: Start LiquidCache in process insert - run: | - # Run to populate cache with arrow array - env RUST_LOG=info cargo run --bin example_inprocess_insertion + - name: Run datafusion-local example + run: env RUST_LOG=info cargo run --bin datafusion-local - - name: Start LiquidCache in process read arrow array - run: | - # Run to populate cache and read arrow array - env RUST_LOG=info cargo run --bin example_inprocess_read + - name: Run core example + run: env RUST_LOG=info cargo run --bin core diff --git a/Cargo.lock b/Cargo.lock index cc41fdf2..b8a9d81b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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,15 +327,16 @@ 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.14.0", @@ -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", @@ -681,7 +682,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -707,6 +708,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "brotli" version = "8.0.2" @@ -834,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", @@ -997,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" @@ -1186,6 +1202,12 @@ 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" @@ -1275,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" @@ -1352,14 +1383,13 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "datafusion" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93db0e623840612f7f2cd757f7e8a8922064192363732c88692e0870016e141b" +checksum = "997a31e15872606a49478e670c58302094c97cb96abb0a7d60720f8e92170040" dependencies = [ "arrow", "arrow-schema", "async-trait", - "bytes", "bzip2", "chrono", "datafusion-catalog", @@ -1389,14 +1419,13 @@ dependencies = [ "datafusion-sql", "flate2", "futures", + "indexmap 2.14.0", "itertools", "liblzma", "log", "object_store", "parking_lot", "parquet", - "rand 0.9.4", - "regex", "sqlparser", "tempfile", "tokio", @@ -1407,9 +1436,9 @@ dependencies = [ [[package]] name = "datafusion-catalog" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37cefde60b26a7f4ff61e9d2ff2833322f91df2b568d7238afe67bde5bdffb66" +checksum = "f7dd61161508f8f5fa1107774ea687bd753c22d83a32eebf963549f89de14139" dependencies = [ "arrow", "async-trait", @@ -1432,9 +1461,9 @@ dependencies = [ [[package]] name = "datafusion-catalog-listing" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17e112307715d6a7a331111a4c2330ff54bc237183511c319e3708a4cff431fb" +checksum = "897c70f871277f9ce99aa38347be0d679bbe3e617156c4d2a8378cec8a2a0891" dependencies = [ "arrow", "async-trait", @@ -1455,34 +1484,35 @@ dependencies = [ [[package]] name = "datafusion-common" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d72a11ca44a95e1081870d3abb80c717496e8a7acb467a1d3e932bb636af5cc2" +checksum = "121c9ded5d87d9172319e006f2afdb9928d72dbacd6a90a458d8acb1e3b43a65" dependencies = [ - "ahash", "arrow", "arrow-ipc", + "arrow-schema", "chrono", + "foldhash 0.2.0", "half", - "hashbrown 0.16.1", + "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.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89f4afaed29670ec4fd6053643adc749fe3f4bc9d1ce1b8c5679b22c67d12def" +checksum = "981b9dae74f78ee3d9f714fb49b01919eab975461b56149510c3ba9ea11287d1" dependencies = [ "futures", "log", @@ -1491,9 +1521,9 @@ dependencies = [ [[package]] name = "datafusion-datasource" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9fb386e1691355355a96419978a0022b7947b44d4a24a6ea99f00b6b485cbb6" +checksum = "ffd7d295b2ec7c00d8a56562f41ed41062cf0af75549ed891c12a0a09eddfefe" dependencies = [ "arrow", "async-compression", @@ -1517,6 +1547,7 @@ dependencies = [ "liblzma", "log", "object_store", + "parking_lot", "rand 0.9.4", "tokio", "tokio-util", @@ -1526,9 +1557,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-arrow" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffa6c52cfed0734c5f93754d1c0175f558175248bf686c944fb05c373e5fc096" +checksum = "552b0b3f342f7ec41b3fbd70f6339dc82a30cfd0349e7f280e7852528085349f" dependencies = [ "arrow", "arrow-ipc", @@ -1550,9 +1581,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-csv" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503f29e0582c1fc189578d665ff57d9300da1f80c282777d7eb67bb79fb8cdca" +checksum = "68850aa426b897e879c8b87e512ea8124f1d0a2869a4e51808ddaaddf1bc0ada" dependencies = [ "arrow", "async-trait", @@ -1573,9 +1604,9 @@ dependencies = [ [[package]] name = "datafusion-datasource-json" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33804749abc8d0c8cb7473228483cb8070e524c6f6086ee1b85a64debe2b3d2" +checksum = "402f93242ae08ef99139ee2c528a49d087efe88d5c7b2c3ff5480855a40ce54f" dependencies = [ "arrow", "async-trait", @@ -1590,16 +1621,15 @@ dependencies = [ "datafusion-session", "futures", "object_store", - "serde_json", "tokio", "tokio-stream", ] [[package]] name = "datafusion-datasource-parquet" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a8e0365e0e08e8ff94d912f0ababcf9065a1a304018ba90b1fc83c855b4997" +checksum = "ffd2499c1bee0eeccf6a57156105700eeeb17bc701899ac719183c4e74231450" dependencies = [ "arrow", "async-trait", @@ -1609,6 +1639,7 @@ dependencies = [ "datafusion-datasource", "datafusion-execution", "datafusion-expr", + "datafusion-functions", "datafusion-functions-aggregate-common", "datafusion-physical-expr", "datafusion-physical-expr-adapter", @@ -1627,20 +1658,19 @@ dependencies = [ [[package]] name = "datafusion-doc" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de6ac0df1662b9148ad3c987978b32cbec7c772f199b1d53520c8fa764a87ee" +checksum = "cb9e7e5d11130c48c8bd4e80c79a9772dd28ce6dc330baca9246205d245b9e2e" [[package]] name = "datafusion-execution" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03c7fbdaefcca4ef6ffe425a5fc2325763bfb426599bb0bf4536466efabe709" +checksum = "37a8643ab852eb68864e1b72ae789e8066282dce48eea6347ffb0aee33d1ccc0" dependencies = [ "arrow", "arrow-buffer", "async-trait", - "chrono", "dashmap", "datafusion-common", "datafusion-expr", @@ -1656,11 +1686,12 @@ dependencies = [ [[package]] name = "datafusion-expr" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "574b9b6977fedbd2a611cbff12e5caf90f31640ad9dc5870f152836d94bad0dd" +checksum = "6932f4d71eed9c8d9341476a2b845aadfabde5495d08dbcd8fc23881f49fa7a0" dependencies = [ "arrow", + "arrow-schema", "async-trait", "chrono", "datafusion-common", @@ -1671,7 +1702,6 @@ dependencies = [ "datafusion-physical-expr-common", "indexmap 2.14.0", "itertools", - "paste", "recursive", "serde_json", "sqlparser", @@ -1679,22 +1709,21 @@ dependencies = [ [[package]] name = "datafusion-expr-common" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d7c3adf3db8bf61e92eb90cb659c8e8b734593a8f7c8e12a843c7ddba24b87e" +checksum = "0225491839a31b1f7d2cb8092c2d50792e2fe1c1724e4e6d08e011f5feaf4ed2" dependencies = [ "arrow", "datafusion-common", "indexmap 2.14.0", "itertools", - "paste", ] [[package]] name = "datafusion-functions" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28aa4e10384e782774b10e72aca4d93ef7b31aa653095d9d4536b0a3dbc51b6" +checksum = "14872c47bfc3d21e53ec82f57074e6987a15941c1e2f43cde4ac6ae2746634e3" dependencies = [ "arrow", "arrow-buffer", @@ -1709,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.4", "regex", - "sha2", - "unicode-segmentation", + "sha2 0.11.0", "uuid", ] [[package]] name = "datafusion-functions-aggregate" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00aa6217e56098ba84e0a338176fe52f0a84cca398021512c6c8c5eff806d0ad" +checksum = "75a2ca14e1b609be21e657e2d3130b2f446456b08393b377bb721a33952d2e09" dependencies = [ - "ahash", "arrow", "datafusion-common", "datafusion-doc", @@ -1738,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.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b511250349407db7c43832ab2de63f5557b19a20dfd236b39ca2c04468b50d47" +checksum = "1ece74ba09092d2ef9c9b54a38445450aea292a1f8b04faf531936b723a24b3c" dependencies = [ - "ahash", "arrow", "datafusion-common", "datafusion-expr-common", @@ -1759,9 +1786,9 @@ dependencies = [ [[package]] name = "datafusion-functions-nested" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef13a858e20d50f0a9bb5e96e7ac82b4e7597f247515bccca4fdd2992df0212a" +checksum = "3f3e3f9ee8ca59bf70518802107de6f1b88a9509efdc629fadc5de9d6b2d5ef5" dependencies = [ "arrow", "arrow-ord", @@ -1775,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.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b40d3f5bbb3905f9ccb1ce9485a9595c77b69758a7c24d3ba79e334ff51e7e" +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.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e88ec9d57c9b685d02f58bfee7be62d72610430ddcedb82a08e5d9925dbfb6" +checksum = "d7339345b226b3874037708bf5023ba1c2de705128f8457a095aae5ae9cb9c78" dependencies = [ "arrow", "datafusion-common", @@ -1813,14 +1840,13 @@ dependencies = [ "datafusion-physical-expr", "datafusion-physical-expr-common", "log", - "paste", ] [[package]] name = "datafusion-functions-window-common" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8307bb93519b1a91913723a1130cfafeee3f72200d870d88e91a6fc5470ede5c" +checksum = "fa84836dc2392df6f43d6a29d37fb56a8ebdc8b3f4e10ae8dc15861fd20278fb" dependencies = [ "datafusion-common", "datafusion-physical-expr-common", @@ -1828,9 +1854,9 @@ dependencies = [ [[package]] name = "datafusion-macros" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e367e6a71051d0ebdd29b2f85d12059b38b1d1f172c6906e80016da662226bd" +checksum = "587164e03ad68732aa9e7bfe5686e3f25970d4c64fd4bd80790749840892dae5" dependencies = [ "datafusion-doc", "quote", @@ -1839,9 +1865,9 @@ dependencies = [ [[package]] name = "datafusion-optimizer" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e929015451a67f77d9d8b727b2bf3a40c4445fdef6cdc53281d7d97c76888ace" +checksum = "77f20e8cf9e8654d92f4c16b24c487353ee5bf153ffc12d5772cd399ab8cd281" dependencies = [ "arrow", "chrono", @@ -1859,11 +1885,10 @@ dependencies = [ [[package]] name = "datafusion-physical-expr" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b1e68aba7a4b350401cfdf25a3d6f989ad898a7410164afe9ca52080244cb59" +checksum = "f015a4a82f6f7ff7e1d8d4bf3870a936752fa38b17705dfcc14adef95aa8922c" dependencies = [ - "ahash", "arrow", "datafusion-common", "datafusion-expr", @@ -1871,11 +1896,10 @@ dependencies = [ "datafusion-functions-aggregate-common", "datafusion-physical-expr-common", "half", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "indexmap 2.14.0", "itertools", "parking_lot", - "paste", "petgraph", "recursive", "tokio", @@ -1883,9 +1907,9 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-adapter" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea22315f33cf2e0adc104e8ec42e285f6ed93998d565c65e82fec6a9ee9f9db4" +checksum = "51e6ffff8acdfe54e0ea15ccf38115c4a9184433b0439f42907637928d00a235" dependencies = [ "arrow", "datafusion-common", @@ -1898,26 +1922,26 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-common" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b04b45ea8ad3ac2d78f2ea2a76053e06591c9629c7a603eda16c10649ecf4362" +checksum = "7967a3e171c6a4bf09474b3f7a14f1a3db13ed1714ba12156f33fcce2bba54e8" dependencies = [ - "ahash", "arrow", "chrono", "datafusion-common", "datafusion-expr-common", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "indexmap 2.14.0", "itertools", "parking_lot", + "pin-project", ] [[package]] name = "datafusion-physical-optimizer" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cb13397809a425918f608dfe8653f332015a3e330004ab191b4404187238b95" +checksum = "59ff803e2a96054cb6d83f35f9e60fd4f42eac515e1932bd1b2dbc91d5fcbf36" dependencies = [ "arrow", "datafusion-common", @@ -1934,12 +1958,13 @@ dependencies = [ [[package]] name = "datafusion-physical-plan" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5edc023675791af9d5fb4cc4c24abf5f7bd3bd4dcf9e5bd90ea1eff6976dcc79" +checksum = "776ee54d47d15bdb126452f9ca17b03761e3b004682914beaedd3f86eb507fbc" dependencies = [ - "ahash", "arrow", + "arrow-data", + "arrow-ipc", "arrow-ord", "arrow-schema", "async-trait", @@ -1954,7 +1979,7 @@ dependencies = [ "datafusion-physical-expr-common", "futures", "half", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "indexmap 2.14.0", "itertools", "log", @@ -1966,9 +1991,9 @@ dependencies = [ [[package]] name = "datafusion-proto" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a387aaef949dc16bb6abc81bd1af850ec7449183aef011214f9724957495738" +checksum = "9dd15a1ba5d3af93808241065c6c44dbca8296a189845e8a587c45c07bf0ffae" dependencies = [ "arrow", "chrono", @@ -1989,14 +2014,13 @@ dependencies = [ "datafusion-proto-common", "object_store", "prost", - "rand 0.9.4", ] [[package]] name = "datafusion-proto-common" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e614c7c53a9c304c6a850b821010bb492e57300311835f1180613f9d2c63d9" +checksum = "90042982cf9462eb06a0b81f92efa4188dae871e7ea3ab8dc61aa9c9349b2530" dependencies = [ "arrow", "datafusion-common", @@ -2005,9 +2029,9 @@ dependencies = [ [[package]] name = "datafusion-pruning" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac8c76860e355616555081cab5968cec1af7a80701ff374510860bcd567e365a" +checksum = "d5fb9e5774660aa69c3ba93c610f175f75b65cb8c3776edb3626de8f3a4f4ee3" dependencies = [ "arrow", "datafusion-common", @@ -2016,15 +2040,14 @@ dependencies = [ "datafusion-physical-expr", "datafusion-physical-expr-common", "datafusion-physical-plan", - "itertools", "log", ] [[package]] name = "datafusion-session" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5412111aa48e2424ba926112e192f7a6b7e4ccb450145d25ce5ede9f19dc491e" +checksum = "15ce715fa2a61f4623cc234bcc14a3ef6a91f189128d5b14b468a6a17cdfc417" dependencies = [ "async-trait", "datafusion-common", @@ -2036,9 +2059,9 @@ dependencies = [ [[package]] name = "datafusion-sql" -version = "53.1.0" +version = "54.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0d133ddf8b9b3b872acac900157f783e7b879fe9a6bccf389abebbfac45ec1" +checksum = "6094ad36a3ed6d7ac87b20b479b2d0b118250f66cf997603829fdc65b44a7099" dependencies = [ "arrow", "bigdecimal", @@ -2118,11 +2141,22 @@ 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.5" @@ -2552,10 +2586,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b525ab775585f1dc4850178de9d0cb6bb37f7b9c1a1a0c121eab7449d7021480" dependencies = [ "base16", - "digest", + "digest 0.10.7", "proc-macro2", "quote", - "sha2", + "sha2 0.10.9", "slab", "syn", ] @@ -2896,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", @@ -2921,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", @@ -3064,9 +3101,9 @@ dependencies = [ [[package]] name = "fsst-rs" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf53d7c403a2b76873d4d66ba7d79c54bde2784cdaba6083f223d6e33270708" +checksum = "9b13ac798afc0d9194eb4efefef8b9332efbd80b43f302a968cb8cb23b9d5360" dependencies = [ "rustc-hash 2.1.2", ] @@ -3360,9 +3397,14 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "hdrhistogram" @@ -3477,6 +3519,15 @@ 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.9.0" @@ -3711,7 +3762,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -3787,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.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-terminal" version = "0.4.17" @@ -4096,9 +4137,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libmimalloc-sys" -version = "0.1.47" +version = "0.1.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1eacfa31c33ec25e873c136ba5669f00f9866d0688bea7be4d3f7e43067df6" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" dependencies = [ "cc", ] @@ -4143,6 +4184,7 @@ dependencies = [ "serde", "serde_json", "shuttle", + "sysinfo", "t4", "tempfile", "tokio", @@ -4179,7 +4221,7 @@ dependencies = [ "perf-event2", "pprof", "regex", - "reqwest 0.13.3", + "reqwest 0.13.4", "serde", "serde_json", "sysinfo", @@ -4340,9 +4382,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", @@ -4512,7 +4554,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]] @@ -4550,9 +4602,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.50" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3627c4272df786b9260cabaa46aec1d59c93ede723d4c3ef646c503816b0640" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" dependencies = [ "libmimalloc-sys", ] @@ -4824,7 +4876,7 @@ dependencies = [ "humantime", "hyper", "itertools", - "md-5", + "md-5 0.10.6", "parking_lot", "percent-encoding", "quick-xml 0.39.2", @@ -4975,9 +5027,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", @@ -4993,7 +5045,7 @@ dependencies = [ "flate2", "futures", "half", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "lz4_flex", "num-bigint", "num-integer", @@ -5014,23 +5066,25 @@ 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.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", @@ -5045,9 +5099,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", @@ -5144,7 +5198,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -5352,9 +5406,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", @@ -5362,9 +5416,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", @@ -5631,9 +5685,9 @@ dependencies = [ [[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", @@ -5660,9 +5714,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" @@ -5713,9 +5767,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", @@ -6101,7 +6155,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -6112,7 +6166,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]] @@ -6265,9 +6330,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", @@ -6708,9 +6773,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -6836,9 +6901,9 @@ dependencies = [ [[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", @@ -6895,9 +6960,9 @@ 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.1", "bytes", @@ -6908,7 +6973,6 @@ dependencies = [ "http-body-util", "http-range-header", "httpdate", - "iri-string", "mime", "mime_guess", "percent-encoding", @@ -6918,7 +6982,7 @@ dependencies = [ "tower", "tower-layer", "tower-service", - "tracing", + "url", ] [[package]] @@ -7226,9 +7290,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ "getrandom 0.4.2", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index a8e40a42..9fcc379c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,39 +30,39 @@ 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.1.0", default-features = false, features = [ +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.1.0" } -datafusion-common = { version = "53.1.0" } -datafusion-expr-common = { version = "53.1.0" } -datafusion-physical-expr = { version = "53.1.0" } -datafusion-physical-expr-common = { version = "53.1.0" } -datafusion-proto = { version = "53.1.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.52.1", 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.1", features = ["v4"] } +uuid = { version = "1.23.3", features = ["v4"] } fastrace = "0.7" fastrace-tonic = "0.2" congee = "0.4.1" 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 a95464fd..25eb33de 100644 --- a/benchmark/Cargo.toml +++ b/benchmark/Cargo.toml @@ -18,7 +18,7 @@ arrow-flight = { workspace = true } tonic = { workspace = true } clap = { version = "4.6.1", features = ["derive"] } url = { workspace = true } -mimalloc = "0.1.50" +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.3", default-features = false, features = ["json"] } -uuid = { version = "1.23.1", 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/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/examples/Cargo.toml b/examples/Cargo.toml index 3c9f4aaa..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 } 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 93% rename from examples/example_local.rs rename to examples/datafusion-local.rs index 38577fab..1908ad81 100644 --- a/examples/example_local.rs +++ b/examples/datafusion-local.rs @@ -10,6 +10,7 @@ async fn main() -> Result<(), Box> { let (ctx, _) = LiquidCacheLocalBuilder::new() .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 9ce476dc..00000000 --- a/examples/example_inprocess_cache_eviction.rs +++ /dev/null @@ -1,40 +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_memory_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 - .unwrap(); - 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 9b4dcc95..00000000 --- a/examples/example_inprocess_insertion.rs +++ /dev/null @@ -1,37 +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_memory_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 - .unwrap(); - - 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 7b5efda3..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.unwrap(); - - // Move data to disk so the read will demonstrate async I/O - storage.flush_all_to_disk().await.unwrap(); - - // 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 67872ac7..00000000 --- a/examples/example_server.rs +++ /dev/null @@ -1,28 +0,0 @@ -use arrow_flight::flight_service_server::FlightServiceServer; -use datafusion::prelude::SessionContext; -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 tonic::transport::Server; - -#[tokio::main] -async fn main() -> 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(()) -} 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/core/Cargo.toml b/src/core/Cargo.toml index 214479a4..743c5076 100644 --- a/src/core/Cargo.toml +++ b/src/core/Cargo.toml @@ -21,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.10" +fsst-rs = "0.5.11" ahash = { workspace = true } tempfile = { workspace = true } congee = { workspace = true } @@ -33,6 +33,7 @@ parquet = { workspace = true } parquet-variant-compute = { workspace = true } fastrace = { workspace = true } serde = { workspace = true } +sysinfo = { version = "0.38.4", default-features = false, features = ["system"] } [dev-dependencies] tempfile = { workspace = true } @@ -42,7 +43,7 @@ tracing-subscriber = "0.3.23" rand = "0.10.1" serde.workspace = true serde_json = { workspace = true } -mimalloc = "0.1.50" +mimalloc = "0.1.52" clap = { version = "4.6.1", features = ["derive"] } divan = "0.1.21" insta = { workspace = true } diff --git a/src/core/src/cache/builders.rs b/src/core/src/cache/builders.rs index 4d4f9cf7..18db151f 100644 --- a/src/core/src/cache/builders.rs +++ b/src/core/src/cache/builders.rs @@ -50,10 +50,12 @@ 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_memory_bytes: 1024 * 1024 * 1024, - max_disk_bytes: usize::MAX, + max_memory_bytes, + max_disk_bytes, cache_policy: Box::new(LiquidPolicy::new()), hydration_policy: Box::new(super::AlwaysHydrate::new()), squeeze_policy: Box::new(TranscodeSqueezeEvict), @@ -71,14 +73,14 @@ impl LiquidCacheBuilder { } /// Set the max memory bytes for the cache. - /// Default is 1GB. + /// 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 unlimited. + /// 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 @@ -157,6 +159,17 @@ impl LiquidCacheBuilder { } } +/// 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> { 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 daf9c46e..4f52abe5 100644 --- a/src/core/src/cache/mod.rs +++ b/src/core/src/cache/mod.rs @@ -13,7 +13,7 @@ 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}; 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 050a746e..374ff9cc 100644 --- a/src/core/src/liquid_array/decimal_array.rs +++ b/src/core/src/liquid_array/decimal_array.rs @@ -554,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); }; @@ -621,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 f6dea20e..683dece8 100644 --- a/src/core/src/liquid_array/float_array.rs +++ b/src/core/src/liquid_array/float_array.rs @@ -998,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 7f05f934..f2d0845b 100644 --- a/src/core/src/liquid_array/hybrid_primitive_array.rs +++ b/src/core/src/liquid_array/hybrid_primitive_array.rs @@ -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); } @@ -348,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); }; @@ -356,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); }; @@ -711,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); }; @@ -719,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/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/src/client_exec.rs b/src/datafusion-client/src/client_exec.rs index 67ba8851..a6a88c74 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::{ @@ -114,10 +121,6 @@ impl DisplayAs for LiquidCacheClientExec { } impl ExecutionPlan for LiquidCacheClientExec { - fn as_any(&self) -> &dyn Any { - self - } - fn name(&self) -> &str { "LiquidCacheClientExec" } @@ -232,6 +235,12 @@ async fn flight_stream( partition: usize, object_stores: Vec<(ObjectStoreUrl, HashMap)>, ) -> 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?; @@ -300,6 +309,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..a99a7138 100644 --- a/src/datafusion-client/src/optimizer.rs +++ b/src/datafusion-client/src/optimizer.rs @@ -39,11 +39,7 @@ impl PushdownOptimizer { /// Apply the optimization by finding nodes to push down and wrapping them fn optimize_plan(&self, plan: Arc) -> 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); } @@ -83,48 +79,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()); } 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/src/lib.rs b/src/datafusion-local/src/lib.rs index 81a259aa..900b4c24 100644 --- a/src/datafusion-local/src/lib.rs +++ b/src/datafusion-local/src/lib.rs @@ -11,7 +11,7 @@ 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::{AlwaysHydrate, HydrationPolicy, default_max_memory_bytes}; use liquid_cache::cache_policies::{CachePolicy, LiquidPolicy}; use liquid_cache_datafusion::optimizers::{LineageOptimizer, LocalModeOptimizer}; use liquid_cache_datafusion::{ @@ -59,6 +59,8 @@ pub struct LiquidCacheLocalBuilder { batch_size: 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 @@ -72,9 +74,12 @@ pub struct LiquidCacheLocalBuilder { 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_memory_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), @@ -96,12 +101,20 @@ impl LiquidCacheLocalBuilder { self } - /// Set maximum memory size in 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 + } + /// Set cache directory pub fn with_cache_dir(mut self, cache_dir: PathBuf) -> Self { self.cache_dir = cache_dir; @@ -155,7 +168,7 @@ impl LiquidCacheLocalBuilder { let cache = LiquidCacheParquet::new( self.batch_size, self.max_memory_bytes, - usize::MAX, + self.max_disk_bytes, store, self.cache_policy, self.squeeze_policy, @@ -167,7 +180,7 @@ impl LiquidCacheLocalBuilder { let cache = LiquidCacheParquet::new_with_squeeze_victim_concurrency( self.batch_size, self.max_memory_bytes, - usize::MAX, + self.max_disk_bytes, store, self.cache_policy, self.squeeze_policy, diff --git a/src/datafusion-local/src/tests/date_optimizer.rs b/src/datafusion-local/src/tests/date_optimizer.rs index a3e1edb7..2d5fb998 100644 --- a/src/datafusion-local/src/tests/date_optimizer.rs +++ b/src/datafusion-local/src/tests/date_optimizer.rs @@ -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/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 e8cff162..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,7 +51,7 @@ RuntimeStatsSnapshot: get_squeezed_needs_io: 1 try_read_liquid_calls: 0 hit_date32_expression_calls: 0 - read_io_count: 2 + read_io_count: 3 write_io_count: 0 disk_evictions: 0 disk_reservation_failures: 0 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 56fb14bb..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 @@ -171,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 @@ -217,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 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 1cef51ab..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 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 7ac42fc1..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,12 +35,12 @@ values: stats: entries.total: 8 entries.after_first_run: 8 -entries.memory.arrow: 0 -entries.memory.liquid: 8 +entries.memory.arrow: 1 +entries.memory.liquid: 7 entries.memory.squeezed_liquid: 0 entries.disk.liquid: 0 entries.disk.arrow: 0 -usage.memory_bytes: 884561 +usage.memory_bytes: 917947 usage.disk_bytes: 877216 RuntimeStatsSnapshot: get: 2 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 4f70d628..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 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 a162c4af..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 @@ -51,8 +51,24 @@ event=insert_success entry=1 kind=DiskLiquid event=insert_success entry=262146 kind=MemoryArrow 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=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=insert_success entry=262146 kind=DiskLiquid +event=squeeze_victim entry=2 +event=insert_success entry=2 kind=DiskLiquid 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 e41d5bba..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 @@ -6,29 +6,23 @@ EventTrace: [ event=insert_success entry=2555904 kind=MemoryArrow event=eval_predicate entry=2555904 selection=true cached=MemoryArrow event=read entry=2555904 selection=true expr=None cached=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=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=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_substrings_search.snap b/src/datafusion-local/src/tests/snapshots/liquid_cache_datafusion_local__tests__squeeze__squeeze_substrings_search.snap index 14181814..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 @@ -5,19 +5,14 @@ expression: trace EventTrace: [ event=insert_success entry=2555904 kind=MemoryArrow event=eval_predicate entry=2555904 selection=true cached=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=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=2555906 selection=true cached=MemoryArrow event=insert_success entry=4297523200 kind=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 505745a0..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 @@ -5,19 +5,14 @@ expression: trace EventTrace: [ event=insert_success entry=131072 kind=MemoryArrow event=eval_predicate entry=131072 selection=true cached=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=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=131074 selection=true cached=MemoryArrow event=insert_success entry=4295098368 kind=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 d4817712..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 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 304f9894..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 @@ -38,24 +38,24 @@ entries.total: 4 entries.after_first_run: 4 entries.memory.arrow: 0 entries.memory.liquid: 2 -entries.memory.squeezed_liquid: 2 -entries.disk.liquid: 0 +entries.memory.squeezed_liquid: 1 +entries.disk.liquid: 1 entries.disk.arrow: 0 -usage.memory_bytes: 226744 -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: 2 + get_squeezed_needs_io: 0 try_read_liquid_calls: 0 hit_date32_expression_calls: 0 read_io_count: 4 - write_io_count: 2 + write_io_count: 4 disk_evictions: 0 disk_reservation_failures: 0 eval_predicate_on_liquid_failed: 0 - squeezed_decompressed_count: 2220 - squeezed_total_count: 4486 + 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 73b6d686..89b2eba8 100644 --- a/src/datafusion-local/src/tests/squeeze.rs +++ b/src/datafusion-local/src/tests/squeeze.rs @@ -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(); diff --git a/src/datafusion-server/Cargo.toml b/src/datafusion-server/Cargo.toml index 556f21dd..192d1563 100644 --- a/src/datafusion-server/Cargo.toml +++ b/src/datafusion-server/Cargo.toml @@ -25,7 +25,7 @@ liquid-cache-common = { workspace = true } tempfile = { workspace = true } 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 55efd0a5..8fcdf9c5 100644 --- a/src/datafusion-server/src/admin_server/handlers.rs +++ b/src/datafusion-server/src/admin_server/handlers.rs @@ -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/src/optimizers/lineage_opt.rs b/src/datafusion/src/optimizers/lineage_opt.rs index e8a3190b..a339ff4c 100644 --- a/src/datafusion/src/optimizers/lineage_opt.rs +++ b/src/datafusion/src/optimizers/lineage_opt.rs @@ -256,6 +256,9 @@ struct LineageAnalyzer { impl LineageAnalyzer { fn analyze_plan(&mut self, plan: &LogicalPlan) -> Result { + // Subqueries live inside expressions, not in `inputs()`, so the match + // below never reaches them. Analyze their plans explicitly. + self.analyze_subquery_plans(plan)?; match plan { LogicalPlan::TableScan(scan) => self.analyze_table_scan(scan), LogicalPlan::Projection(projection) => self.analyze_projection(projection), @@ -280,6 +283,40 @@ impl LineageAnalyzer { } } + /// Analyze the plans of any subqueries embedded in this node's expressions. + /// + /// DataFusion 54 keeps scalar subqueries as dedicated operators rather than + /// decorrelating them into joins before our optimizer runs, so the columns + /// they use (e.g. via `EXTRACT` or `variant_get`) are no longer part of the + /// main plan tree. Recurse into each subquery plan so those usages are still + /// recorded; `analyze_plan` calls this for every node, so nested subqueries + /// are covered too. + fn analyze_subquery_plans(&mut self, plan: &LogicalPlan) -> Result<()> { + let mut subqueries: Vec> = Vec::new(); + plan.apply_expressions(|expr| { + expr.apply(|nested| { + match nested { + Expr::ScalarSubquery(subquery) => { + subqueries.push(Arc::clone(&subquery.subquery)); + } + Expr::Exists(exists) => { + subqueries.push(Arc::clone(&exists.subquery.subquery)); + } + Expr::InSubquery(in_subquery) => { + subqueries.push(Arc::clone(&in_subquery.subquery.subquery)); + } + _ => {} + } + Ok(TreeNodeRecursion::Continue) + }) + })?; + + for subquery in subqueries { + self.analyze_plan(&subquery)?; + } + Ok(()) + } + fn analyze_table_scan(&mut self, scan: &TableScan) -> Result { let schema = scan.projected_schema.as_ref(); let mut map = LineageMap::new(); @@ -723,7 +760,10 @@ fn annotate_plan_with_extractions( return Ok(Transformed::no(plan)); } - plan.transform_up(|logical_plan| match logical_plan { + // `transform_up` alone skips subqueries (they live in expressions, not in + // `inputs()`); use the subquery-aware variant so scans inside scalar/IN/ + // EXISTS subqueries get annotated too. + plan.transform_up_with_subqueries(|logical_plan| match logical_plan { LogicalPlan::TableScan(mut scan) => { let table_annotations = annotations_for_table_scan(&scan, annotations); let mut changed = false; @@ -781,7 +821,7 @@ fn annotate_listing_table_source( Err(_) => return Ok(None), }; - let Some(listing) = provider.as_any().downcast_ref::() else { + let Some(listing) = provider.downcast_ref::() else { return Ok(None); }; @@ -1285,13 +1325,10 @@ mod tests { let mut field_metadata_map = HashMap::new(); 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 Some(file_scan_config) = data_source - .data_source() - .as_any() - .downcast_ref::() + let Some(file_scan_config) = data_source.data_source().downcast_ref::() else { return Ok(TreeNodeRecursion::Continue); }; diff --git a/src/datafusion/src/optimizers/mod.rs b/src/datafusion/src/optimizers/mod.rs index b2490580..6125918d 100644 --- a/src/datafusion/src/optimizers/mod.rs +++ b/src/datafusion/src/optimizers/mod.rs @@ -84,8 +84,7 @@ fn try_optimize_parquet_source( plan: Arc, cache: &LiquidCacheParquetRef, ) -> Result>, datafusion::error::DataFusionError> { - let any_plan = plan.as_any(); - if let Some(data_source_exec) = any_plan.downcast_ref::() + if let Some(data_source_exec) = plan.downcast_ref::() && let Some((file_scan_config, parquet_source)) = data_source_exec.downcast_to_file_source::() { @@ -184,15 +183,12 @@ mod tests { 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()); } diff --git a/src/datafusion/src/reader/plantime/opener.rs b/src/datafusion/src/reader/plantime/opener.rs index 7abe216e..e737ef84 100644 --- a/src/datafusion/src/reader/plantime/opener.rs +++ b/src/datafusion/src/reader/plantime/opener.rs @@ -120,7 +120,7 @@ fn transfer_lineage_metadata_to_file_schema( 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 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); @@ -281,7 +281,7 @@ impl FileOpener for LiquidParquetOpener { 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 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() { @@ -383,23 +383,19 @@ impl FileOpener for LiquidParquetOpener { fn create_initial_plan( file_name: &str, - extensions: Option>, + access_plan: 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}"); + 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 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 6368ec1a..46571e69 100644 --- a/src/datafusion/src/reader/plantime/source.rs +++ b/src/datafusion/src/reader/plantime/source.rs @@ -32,7 +32,6 @@ use parquet::{ file::metadata::{PageIndexPolicy, ParquetMetaData, ParquetMetaDataReader}, }; use std::{ - any::Any, ops::Range, sync::{Arc, LazyLock}, }; @@ -273,10 +272,6 @@ impl LiquidParquetSource { } impl FileSource for LiquidParquetSource { - fn as_any(&self) -> &dyn Any { - self - } - fn create_file_opener( &self, object_store: Arc, 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/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 From 746d6399e673570eebc0c857b20525c736f7d973 Mon Sep 17 00:00:00 2001 From: Xiangpeng Hao Date: Sat, 13 Jun 2026 13:32:13 -0400 Subject: [PATCH 12/13] Refactor lineage pushdown (#508) Remove the hack --- Cargo.lock | 1 + src/common/src/rpc.rs | 23 + src/core/Cargo.toml | 1 + src/core/src/cache/core.rs | 4 +- src/core/src/cache/expressions.rs | 244 ++- src/core/src/cache/io_context.rs | 2 +- ...ts__squeezed__read_squeezed_date_time.snap | 14 +- .../src/liquid_array/squeezed_date32_array.rs | 2 +- src/datafusion-client/Cargo.toml | 1 + src/datafusion-client/src/client_exec.rs | 24 +- src/datafusion-client/src/optimizer.rs | 23 +- src/datafusion-local/src/lib.rs | 5 +- src/datafusion-server/src/lib.rs | 14 +- src/datafusion-server/src/service.rs | 19 +- src/datafusion-server/src/tests/mod.rs | 2 +- src/datafusion/README.md | 35 + src/datafusion/src/cache/column.rs | 53 +- src/datafusion/src/cache/mod.rs | 49 +- src/datafusion/src/optimizers/lineage_opt.rs | 1848 ----------------- src/datafusion/src/optimizers/mod.rs | 154 +- src/datafusion/src/optimizers/squeeze_hint.rs | 800 +++++++ src/datafusion/src/reader/plantime/opener.rs | 55 +- src/datafusion/src/reader/plantime/source.rs | 19 +- 23 files changed, 1315 insertions(+), 2077 deletions(-) delete mode 100644 src/datafusion/src/optimizers/lineage_opt.rs create mode 100644 src/datafusion/src/optimizers/squeeze_hint.rs diff --git a/Cargo.lock b/Cargo.lock index b8a9d81b..20b7f2ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4291,6 +4291,7 @@ dependencies = [ "futures", "insta", "liquid-cache-common", + "liquid-cache-datafusion", "object_store", "tokio", "tonic", 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 743c5076..d8e2b9f4 100644 --- a/src/core/Cargo.toml +++ b/src/core/Cargo.toml @@ -33,6 +33,7 @@ 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] diff --git a/src/core/src/cache/core.rs b/src/core/src/cache/core.rs index 84e0a795..f26f2877 100644 --- a/src/core/src/cache/core.rs +++ b/src/core/src/cache/core.rs @@ -728,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(); 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/io_context.rs b/src/core/src/cache/io_context.rs index d03a8184..a9c8974f 100644 --- a/src/core/src/cache/io_context.rs +++ b/src/core/src/cache/io_context.rs @@ -27,7 +27,7 @@ pub trait EntryMetadata: 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> { 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 1274941f..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 @@ -23,12 +23,12 @@ event=squeeze_victim entry=1 event=io_write entry=1 kind=MemorySqueezedLiquid bytes=6184 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/liquid_array/squeezed_date32_array.rs b/src/core/src/liquid_array/squeezed_date32_array.rs index aa3aa01d..e2497e55 100644 --- a/src/core/src/liquid_array/squeezed_date32_array.rs +++ b/src/core/src/liquid_array/squeezed_date32_array.rs @@ -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, 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 a6a88c74..658c86dc 100644 --- a/src/datafusion-client/src/client_exec.rs +++ b/src/datafusion-client/src/client_exec.rs @@ -37,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; @@ -61,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 { @@ -74,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 @@ -90,6 +96,7 @@ impl LiquidCacheClientExec { uuid, metrics: ExecutionPlanMetricsSet::new(), properties, + squeeze_hints, } } @@ -145,6 +152,7 @@ impl ExecutionPlan for LiquidCacheClientExec { metrics: self.metrics.clone(), uuid: self.uuid, properties: self.properties.clone(), + squeeze_hints: self.squeeze_hints.clone(), })) } @@ -171,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)), @@ -227,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, @@ -234,6 +254,7 @@ 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 @@ -273,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 diff --git a/src/datafusion-client/src/optimizer.rs b/src/datafusion-client/src/optimizer.rs index a99a7138..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,7 +39,11 @@ 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.is::() { return Ok(plan); @@ -47,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, ))); } } @@ -60,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; } @@ -131,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-local/src/lib.rs b/src/datafusion-local/src/lib.rs index 900b4c24..073864d6 100644 --- a/src/datafusion-local/src/lib.rs +++ b/src/datafusion-local/src/lib.rs @@ -13,7 +13,7 @@ use datafusion::prelude::{SessionConfig, SessionContext}; use liquid_cache::cache::squeeze_policies::{SqueezePolicy, TranscodeSqueezeEvict}; use liquid_cache::cache::{AlwaysHydrate, HydrationPolicy, default_max_memory_bytes}; use liquid_cache::cache_policies::{CachePolicy, LiquidPolicy}; -use liquid_cache_datafusion::optimizers::{LineageOptimizer, LocalModeOptimizer}; +use liquid_cache_datafusion::optimizers::LocalModeOptimizer; use liquid_cache_datafusion::{ LiquidCacheParquet, LiquidCacheParquetRef, VariantGetUdf, VariantPretty, VariantToJsonUdf, }; @@ -190,14 +190,11 @@ impl LiquidCacheLocalBuilder { .await; let cache_ref = Arc::new(cache); - let date_extract_optimizer = Arc::new(LineageOptimizer::new()); - let optimizer = LocalModeOptimizer::new(cache_ref.clone()); 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-server/src/lib.rs b/src/datafusion-server/src/lib.rs index 3a5ee2a1..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; @@ -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 1ccfe76d..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; @@ -147,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)), + 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/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/src/cache/column.rs b/src/datafusion/src/cache/column.rs index 65870a78..1f582fd8 100644 --- a/src/datafusion/src/cache/column.rs +++ b/src/datafusion/src/cache/column.rs @@ -4,14 +4,13 @@ use arrow::{ compute::prep_null_mask_filter, record_batch::RecordBatch, }; -use arrow_schema::{ArrowError, DataType, Field, Schema}; +use arrow_schema::{ArrowError, Field, Schema}; use liquid_cache::cache::{CacheExpression, CacheFull, LiquidCache, LiquidExpr}; use parquet::arrow::arrow_reader::ArrowPredicate; use crate::{ LiquidPredicate, cache::{BatchID, ColumnAccessPath, ParquetArrayID}, - optimizers::{DATE_MAPPING_METADATA_KEY, STRING_FINGERPRINT_METADATA_KEY}, }; use std::sync::Arc; @@ -27,26 +26,6 @@ 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()); - } - None -} - /// Error type for inserting an arrow array into the cache. #[derive(Debug)] pub enum InsertArrowArrayError { @@ -67,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, @@ -202,11 +189,3 @@ impl CachedColumn { Ok(()) } } - -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, - } -} diff --git a/src/datafusion/src/cache/mod.rs b/src/datafusion/src/cache/mod.rs index d8af7a41..7c2fda5b 100644 --- a/src/datafusion/src/cache/mod.rs +++ b/src/datafusion/src/cache/mod.rs @@ -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(); @@ -303,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 @@ -314,6 +350,7 @@ impl LiquidCacheParquet { self.cache_store.clone(), file_id, full_file_schema, + squeeze_hints, )) } diff --git a/src/datafusion/src/optimizers/lineage_opt.rs b/src/datafusion/src/optimizers/lineage_opt.rs deleted file mode 100644 index a339ff4c..00000000 --- a/src/datafusion/src/optimizers/lineage_opt.rs +++ /dev/null @@ -1,1848 +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 { - // Subqueries live inside expressions, not in `inputs()`, so the match - // below never reaches them. Analyze their plans explicitly. - self.analyze_subquery_plans(plan)?; - 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) - } - } - } - - /// Analyze the plans of any subqueries embedded in this node's expressions. - /// - /// DataFusion 54 keeps scalar subqueries as dedicated operators rather than - /// decorrelating them into joins before our optimizer runs, so the columns - /// they use (e.g. via `EXTRACT` or `variant_get`) are no longer part of the - /// main plan tree. Recurse into each subquery plan so those usages are still - /// recorded; `analyze_plan` calls this for every node, so nested subqueries - /// are covered too. - fn analyze_subquery_plans(&mut self, plan: &LogicalPlan) -> Result<()> { - let mut subqueries: Vec> = Vec::new(); - plan.apply_expressions(|expr| { - expr.apply(|nested| { - match nested { - Expr::ScalarSubquery(subquery) => { - subqueries.push(Arc::clone(&subquery.subquery)); - } - Expr::Exists(exists) => { - subqueries.push(Arc::clone(&exists.subquery.subquery)); - } - Expr::InSubquery(in_subquery) => { - subqueries.push(Arc::clone(&in_subquery.subquery.subquery)); - } - _ => {} - } - Ok(TreeNodeRecursion::Continue) - }) - })?; - - for subquery in subqueries { - self.analyze_plan(&subquery)?; - } - Ok(()) - } - - 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()) { - 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()) - { - 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)); - } - - // `transform_up` alone skips subqueries (they live in expressions, not in - // `inputs()`); use the subquery-aware variant so scans inside scalar/IN/ - // EXISTS subqueries get annotated too. - plan.transform_up_with_subqueries(|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.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}; - 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 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, - usize::MAX, - 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.downcast_ref::() else { - return Ok(TreeNodeRecursion::Continue); - }; - let Some(file_scan_config) = data_source.data_source().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 - } - - /// Assert metadata on physical plan matches expected date 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 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 - ); - } - - // ───────────────────────────────────────────────────────────────────────────── - // 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_does_not_emit_metadata() { - 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, DATE_MAPPING_METADATA_KEY); - assert!( - !metadata.contains_key("data"), - "variant_get should not emit cache metadata" - ); - } - - #[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 6125918d..47b842bb 100644 --- a/src/datafusion/src/optimizers/mod.rs +++ b/src/datafusion/src/optimizers/mod.rs @@ -1,37 +1,28 @@ //! Optimizers for the Parquet module -mod lineage_opt; +mod squeeze_hint; use std::sync::Arc; -use arrow_schema::{Field, 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; -use crate::{ - LiquidCacheParquetRef, LiquidParquetSource, - optimizers::lineage_opt::{ColumnAnnotation, metadata_from_factory, serialize_date_part}, -}; +pub(crate) use squeeze_hint::HintAnalyzer; +pub use squeeze_hint::SqueezeHintMap; -pub(crate) const DATE_MAPPING_METADATA_KEY: &str = "liquid.cache.date_mapping"; -pub(crate) const STRING_FINGERPRINT_METADATA_KEY: &str = "liquid.cache.string_fingerprint"; +use crate::{LiquidCacheParquetRef, LiquidParquetSource, cache::ColumnSqueezeHints}; -/// 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, @@ -55,7 +46,16 @@ impl PhysicalOptimizerRule for LocalModeOptimizer { plan: Arc, _config: &ConfigOptions, ) -> Result, datafusion::error::DataFusionError> { - Ok(rewrite_data_source_plan(plan, &self.cache)) + let analysis = HintAnalyzer::analyze(&plan); + let cache = self.cache.clone(); + let mut convert = |node: &Arc, hints: ColumnSqueezeHints| { + convert_parquet_scan(node, &cache, hints) + }; + Ok(squeeze_hint::rewrite_with_hints( + plan, + &mut convert, + &analysis, + )) } fn name(&self) -> &str { @@ -63,90 +63,62 @@ 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, + hints: &ColumnSqueezeHints, ) -> Arc { - let rewritten = plan - .transform_up(|node| try_optimize_parquet_source(node, cache)) - .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, -) -> Result>, datafusion::error::DataFusionError> { - if let Some(data_source_exec) = plan.downcast_ref::() - && let Some((file_scan_config, parquet_source)) = - data_source_exec.downcast_to_file_source::() - { - 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); - 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, -) -> 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)); - } else { - new_fields.push(field.clone()); - } - } - Schema::new(new_fields) +) -> Arc { + rewrite_data_source_plan_with_hints(plan, cache, &ColumnSqueezeHints::default()) } -fn process_field_annotation(field: &Arc, annotation: ColumnAnnotation) -> Arc { - let mut field_metadata = field.metadata().clone(); - match annotation { - ColumnAnnotation::DatePart(unit) => { - field_metadata.insert( - DATE_MAPPING_METADATA_KEY.to_string(), - serialize_date_part(&unit), - ); - } - ColumnAnnotation::VariantPaths(_) => {} - ColumnAnnotation::SubstringSearch => { - field_metadata.insert( - STRING_FINGERPRINT_METADATA_KEY.to_string(), - "substring".into(), - ); - } - } - Arc::new(Field::clone(field.as_ref()).with_metadata(field_metadata)) +/// 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)] 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/opener.rs b/src/datafusion/src/reader/plantime/opener.rs index e737ef84..b1b7e836 100644 --- a/src/datafusion/src/reader/plantime/opener.rs +++ b/src/datafusion/src/reader/plantime/opener.rs @@ -1,15 +1,14 @@ use std::sync::Arc; -use crate::{ - cache::LiquidCacheParquetRef, - reader::{ - plantime::{row_filter::build_row_filter, row_group_filter::RowGroupAccessPlanFilter}, - runtime::LiquidStreamBuilder, +use crate::{ + cache::{ColumnSqueezeHints, LiquidCacheParquetRef}, + 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 arrow_schema::SchemaRef; use datafusion::{ common::exec_err, datasource::{ @@ -56,6 +55,7 @@ pub struct LiquidParquetOpener { liquid_cache: LiquidCacheParquetRef, expr_adapter_factory: Arc, span: Option>, + squeeze_hints: Arc, } impl LiquidParquetOpener { @@ -73,6 +73,7 @@ impl LiquidParquetOpener { reorder_filters: bool, expr_adapter_factory: Arc, span: Option>, + squeeze_hints: Arc, ) -> Self { Self { partition_index, @@ -87,36 +88,11 @@ impl LiquidParquetOpener { reorder_filters, expr_adapter_factory, span, + squeeze_hints, } } } -// 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(); @@ -169,6 +145,7 @@ impl FileOpener for LiquidParquetOpener { 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 @@ -217,11 +194,7 @@ impl FileOpener for LiquidParquetOpener { // 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 = Arc::clone(&physical_file_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)?; @@ -346,7 +319,11 @@ impl FileOpener for LiquidParquetOpener { liquid_builder = liquid_builder.with_span(span); } - let liquid_cache = lc.register_or_get_file(file_loc, Arc::clone(&cache_full_schema)); + 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)?; diff --git a/src/datafusion/src/reader/plantime/source.rs b/src/datafusion/src/reader/plantime/source.rs index 46571e69..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; @@ -175,6 +175,7 @@ pub struct LiquidParquetSource { projection: ProjectionExprs, table_schema: TableSchema, span: Option>, + squeeze_hints: Arc, } impl LiquidParquetSource { @@ -198,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, @@ -256,6 +271,7 @@ impl LiquidParquetSource { pruning_predicate: None, page_pruning_predicate: None, span: None, + squeeze_hints: Arc::default(), }; if let Some(predicate) = predicate { @@ -303,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)) From 0ed51a8d1f2ba688d9015d6ea3e312ce19b3b65d Mon Sep 17 00:00:00 2001 From: Shefeek Jinnah Date: Fri, 3 Jul 2026 15:02:28 +0530 Subject: [PATCH 13/13] feat: table-size based cache pass-through LocalModeOptimizer gains an optional max_scan_bytes; when set, parquet scans whose total file size exceeds the threshold are left as vanilla DataFusion reads instead of being wrapped by LiquidCache. Exposed via LiquidCacheLocalBuilder::with_max_scan_bytes. Ported onto the DataFusion 54 optimizer API (convert closure + squeeze hints), and covered by a test asserting the above/below-threshold split. --- src/datafusion-local/src/lib.rs | 16 +++- src/datafusion/src/optimizers/mod.rs | 126 ++++++++++++++++++++++++++- 2 files changed, 139 insertions(+), 3 deletions(-) diff --git a/src/datafusion-local/src/lib.rs b/src/datafusion-local/src/lib.rs index 073864d6..2bb2485f 100644 --- a/src/datafusion-local/src/lib.rs +++ b/src/datafusion-local/src/lib.rs @@ -69,6 +69,8 @@ pub struct LiquidCacheLocalBuilder { squeeze_policy: Box, /// Hydration policy hydration_policy: Box, + /// Maximum total file size for a scan to be routed through LiquidCache. + max_scan_bytes: Option, span: fastrace::Span, } @@ -84,6 +86,7 @@ impl Default for LiquidCacheLocalBuilder { cache_policy: Box::new(LiquidPolicy::new()), squeeze_policy: Box::new(TranscodeSqueezeEvict), hydration_policy: Box::new(AlwaysHydrate::new()), + max_scan_bytes: None, span: fastrace::Span::enter_with_local_parent("liquid_cache_datafusion_local_builder"), } } @@ -145,6 +148,14 @@ impl LiquidCacheLocalBuilder { self } + /// 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 + } + /// Build a SessionContext with liquid cache configured /// Returns the SessionContext and the liquid cache reference pub async fn build( @@ -190,7 +201,10 @@ impl LiquidCacheLocalBuilder { .await; let cache_ref = Arc::new(cache); - let optimizer = LocalModeOptimizer::new(cache_ref.clone()); + 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); + } let state = datafusion::execution::SessionStateBuilder::new() .with_config(config) diff --git a/src/datafusion/src/optimizers/mod.rs b/src/datafusion/src/optimizers/mod.rs index 47b842bb..804a5222 100644 --- a/src/datafusion/src/optimizers/mod.rs +++ b/src/datafusion/src/optimizers/mod.rs @@ -26,17 +26,35 @@ use crate::{LiquidCacheParquetRef, LiquidParquetSource, cache::ColumnSqueezeHint #[derive(Debug)] pub struct LocalModeOptimizer { cache: LiquidCacheParquetRef, + /// 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. + max_scan_bytes: Option, } impl LocalModeOptimizer { /// Create an optimizer with an existing cache instance pub fn new(cache: LiquidCacheParquetRef) -> Self { - Self { cache } + Self { + cache, + max_scan_bytes: None, + } } /// Create an optimizer with an existing cache instance pub fn with_cache(cache: LiquidCacheParquetRef) -> Self { - Self { cache } + Self { + cache, + max_scan_bytes: None, + } + } + + /// 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, bypassing the cache. + pub fn with_max_scan_bytes(mut self, max_bytes: u64) -> Self { + self.max_scan_bytes = Some(max_bytes); + self } } @@ -48,7 +66,14 @@ impl PhysicalOptimizerRule for LocalModeOptimizer { ) -> Result, datafusion::error::DataFusionError> { let analysis = HintAnalyzer::analyze(&plan); let cache = self.cache.clone(); + let max_scan_bytes = self.max_scan_bytes; 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( @@ -100,6 +125,21 @@ pub fn rewrite_data_source_plan( rewrite_data_source_plan_with_hints(plan, cache, &ColumnSqueezeHints::default()) } +/// 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(), + ) +} + /// If `node` is a `DataSourceExec` over a `ParquetSource`, return an equivalent /// node backed by [`LiquidParquetSource`] carrying `hints`. fn convert_parquet_scan( @@ -186,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" + ); + } }