diff --git a/.github/runner-scale-sets/query-regression/README.md b/.github/runner-scale-sets/query-regression/README.md
index 3e0159af60b..fa8d436d2c2 100644
--- a/.github/runner-scale-sets/query-regression/README.md
+++ b/.github/runner-scale-sets/query-regression/README.md
@@ -51,10 +51,10 @@ Configuration lives in repository variables/secrets:
| vars | `QUERY_REGRESSION_ECS_IMAGE_ID` | Custom image built by `ecs-image/build-ecs-image.py`. |
| vars | `QUERY_REGRESSION_COMMENT_ALLOWLIST` | Comma/whitespace-separated GitHub logins allowed to comment `/query-regression` on a PR. Each login must also have repository `admin` permission. Empty denies all comment commands. |
-The system disk is 50 GiB, which covers the image, a 16 GiB swapfile, the
-checkout, and cold build caches (target dir, cargo registry, sccache). ENOSPC
-stops the runner itself from writing logs, which GitHub reports as `The
-operation was canceled` with no telemetry, indistinguishable from a
+The system disk is 80 GiB, providing capacity headroom for the image, a 16 GiB
+swapfile, the checkout, and cold build caches (target dir, cargo registry,
+sccache). ENOSPC stops the runner itself from writing logs, which GitHub reports
+as `The operation was canceled` with no telemetry, indistinguishable from a
platform-side cancellation.
cloud-init masks `systemd-oomd`, disables `unattended-upgrades` /
`apt-daily-upgrade`, creates `/swapfile`, and sets `OOMPolicy=continue`
@@ -108,7 +108,7 @@ agent, which Aliyun public Ubuntu images include.
An allowlisted repository admin commenting `/query-regression` on the PR is
**trust admission for that exact PR revision**. `/query-regression` runs the
-six routine default cases; `/query-regression heavy` runs only the
+nine routine default cases; `/query-regression heavy` runs only the
high-cardinality `prom_remote_write_7913` remote-write case.
`slash-command-dispatch.yml` (`issue_comment` on the default branch) parses
the command and `repository_dispatch`es; `query-regression-slash.yml` admits
@@ -180,7 +180,7 @@ reintroduce those actions unless the image contract changes.
Each run provisions its own ECS instance, so overlapping dispatches proceed
in parallel. There is no workflow `concurrency` group. All Cargo state
(`CARGO_HOME` including registry/git, `CARGO_TARGET_DIR`, sccache, cache
-metadata) lives on the 50 GiB system disk and is discarded with the VM.
+metadata) lives on the 80 GiB system disk and is discarded with the VM.
`RUSTUP_HOME=/opt/rustup` and `/opt/cargo/bin` are image-owned. The runner
sets `RUSTC_WRAPPER=/usr/local/bin/sccache`,
`SCCACHE_DIR=/home/runner/.cache/sccache`, `SCCACHE_CACHE_SIZE=10G`, and
@@ -189,7 +189,7 @@ at 10G. Base and candidate builds share the target on that disk; Cargo
fingerprints invalidate source and dependency changes.
The workflow reports `du` / `df` in telemetry. It does not try to reclaim
-space across runs: a cold 50 GiB disk that fills up fails the build.
+space across runs: a cold 80 GiB disk that fills up fails the build.
## Future optional phases
diff --git a/.github/scripts/aliyun-ecs-runner-provision.py b/.github/scripts/aliyun-ecs-runner-provision.py
index 307ee336ebf..b5dee7f51fe 100644
--- a/.github/scripts/aliyun-ecs-runner-provision.py
+++ b/.github/scripts/aliyun-ecs-runner-provision.py
@@ -81,8 +81,8 @@ POLL_INTERVAL_SECONDS = 5
SWAP_FILE = "/swapfile"
SWAP_SIZE_GIB = 16
# Image (~12G), 16G swap, checkout, base+candidate data homes, and cold
-# build caches. Deleted with the instance.
-SYSTEM_DISK_GIB = 50
+# build caches. 80 GiB provides capacity headroom. Deleted with the instance.
+SYSTEM_DISK_GIB = 80
@dataclass(frozen=True)
diff --git a/.github/scripts/query-regression-run.py b/.github/scripts/query-regression-run.py
index 268cb0bae6c..6da7c896d67 100644
--- a/.github/scripts/query-regression-run.py
+++ b/.github/scripts/query-regression-run.py
@@ -33,12 +33,14 @@ from typing import Any
DEFAULT_CASES = [
"tests/perf/query_cases/smoke_direct_sst/case.toml",
+ "tests/perf/query_cases/sst_float_bss/case.toml",
"tests/perf/query_cases/prom_remote_write_seeded_random/case.toml",
"tests/perf/query_cases/prom_remote_write_run_heavy/case.toml",
"tests/perf/query_cases/prom_remote_write_mixed_every/case.toml",
"tests/perf/query_cases/prom_remote_write_integer_counter/case.toml",
"tests/perf/query_cases/promql_range_boundary/case.toml",
"tests/perf/query_cases/promql_instant_last_row_9034/case.toml",
+ "tests/perf/query_cases/mito_prefilter_all_match/case.toml",
]
HEAVY_CASES = [
diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml
index 8cf2da32fcb..013aa320b20 100644
--- a/.github/workflows/backport.yml
+++ b/.github/workflows/backport.yml
@@ -45,6 +45,7 @@ jobs:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
AUTHOR: ${{ github.event.pull_request.merged_by.login }}
+ PR_AUTHOR: ${{ github.event.pull_request.user.login }}
BASE_BRANCH: ${{ github.event.pull_request.base.ref }}
run: |
git config user.name "github-actions[bot]"
@@ -76,6 +77,7 @@ jobs:
git branch -D "${head_branch}"
gh issue create \
--title "Backport failure: PR #${PR_NUMBER} to ${release_branch}" \
+ --assignee "${PR_AUTHOR}" \
--body "Cherry-pick of ${MERGE_COMMIT} (PR #${PR_NUMBER}) failed on ${release_branch}. Manual backport required." \
|| true
continue
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 6d12566b684..c9bba9c7726 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -122,6 +122,35 @@ jobs:
- name: Run cargo check for riscv64
run: cargo check --locked --workspace --all-targets --features servers/dashboard --target riscv64gc-unknown-linux-gnu
+ check-windows:
+ if: ${{ github.repository == 'GreptimeTeam/greptimedb' }}
+ name: Check Windows build
+ runs-on: windows-2022
+ timeout-minutes: 60
+ steps:
+ - run: git config --global core.autocrlf false
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+ - uses: ./.github/actions/setup-cyborg
+ - uses: arduino/setup-protoc@v3
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ - uses: KyleMayes/install-llvm-action@v1
+ with:
+ version: "14.0"
+ - uses: actions-rust-lang/setup-rust-toolchain@v1
+ - name: Rust Cache
+ uses: Swatinem/rust-cache@v2
+ with:
+ shared-key: "check-windows"
+ cache-all-crates: "true"
+ save-if: ${{ github.ref == 'refs/heads/main' }}
+ - name: Run cargo check for Windows
+ run: cargo check --locked --workspace --all-targets
+ env:
+ RUSTFLAGS: "-D warnings"
+
conflict-check:
if: ${{ github.repository == 'GreptimeTeam/greptimedb' }}
name: Check for conflict
diff --git a/AGENTS.md b/AGENTS.md
index 819679f3048..e8feb4abe4b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -69,6 +69,11 @@ a module map, read/write paths, change-coupling points, and gotchas:
- [`tests/compatibility/AGENTS.md`](tests/compatibility/AGENTS.md)
- [`tests/perf/AGENTS.md`](tests/perf/AGENTS.md)
+## Rust imports
+
+Prefer crate-rooted imports (`use crate::...`) over `use super::...` in
+production code. Relative imports using `super` are allowed in tests.
+
## Read before changing code
- [`.agents/architecture-invariants.md`](.agents/architecture-invariants.md) —
diff --git a/Cargo.lock b/Cargo.lock
index 00e3d854421..74617c08cbb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1172,7 +1172,7 @@ dependencies = [
"cfg-if",
"libc",
"miniz_oxide",
- "object",
+ "object 0.36.7",
"rustc-demangle",
"windows-targets 0.52.6",
]
@@ -1623,7 +1623,7 @@ dependencies = [
"maybe-owned",
"rustix 1.0.7",
"rustix-linux-procfs",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
"winx",
]
@@ -2271,7 +2271,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
- "windows-sys 0.59.0",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -3768,7 +3768,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]]
name = "datafusion"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"arrow-schema 58.3.0",
@@ -3822,7 +3822,7 @@ dependencies = [
[[package]]
name = "datafusion-catalog"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"async-trait",
@@ -3846,7 +3846,7 @@ dependencies = [
[[package]]
name = "datafusion-catalog-listing"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"async-trait",
@@ -3868,7 +3868,7 @@ dependencies = [
[[package]]
name = "datafusion-common"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"ahash 0.8.12",
"arrow 58.3.0",
@@ -3892,7 +3892,7 @@ dependencies = [
[[package]]
name = "datafusion-common-runtime"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"futures",
"log",
@@ -3902,7 +3902,7 @@ dependencies = [
[[package]]
name = "datafusion-datasource"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"async-compression",
@@ -3936,7 +3936,7 @@ dependencies = [
[[package]]
name = "datafusion-datasource-arrow"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"arrow-ipc 58.3.0",
@@ -3959,7 +3959,7 @@ dependencies = [
[[package]]
name = "datafusion-datasource-csv"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"async-trait",
@@ -3981,7 +3981,7 @@ dependencies = [
[[package]]
name = "datafusion-datasource-json"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"async-trait",
@@ -4004,7 +4004,7 @@ dependencies = [
[[package]]
name = "datafusion-datasource-parquet"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"async-trait",
@@ -4033,12 +4033,12 @@ dependencies = [
[[package]]
name = "datafusion-doc"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
[[package]]
name = "datafusion-execution"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"arrow-buffer 58.3.0",
@@ -4060,7 +4060,7 @@ dependencies = [
[[package]]
name = "datafusion-expr"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"async-trait",
@@ -4082,7 +4082,7 @@ dependencies = [
[[package]]
name = "datafusion-expr-common"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"datafusion-common",
@@ -4094,7 +4094,7 @@ dependencies = [
[[package]]
name = "datafusion-functions"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"arrow-buffer 58.3.0",
@@ -4125,7 +4125,7 @@ dependencies = [
[[package]]
name = "datafusion-functions-aggregate"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"ahash 0.8.12",
"arrow 58.3.0",
@@ -4146,7 +4146,7 @@ dependencies = [
[[package]]
name = "datafusion-functions-aggregate-common"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"ahash 0.8.12",
"arrow 58.3.0",
@@ -4158,7 +4158,7 @@ dependencies = [
[[package]]
name = "datafusion-functions-nested"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"arrow-ord 58.3.0",
@@ -4182,7 +4182,7 @@ dependencies = [
[[package]]
name = "datafusion-functions-table"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"async-trait",
@@ -4197,7 +4197,7 @@ dependencies = [
[[package]]
name = "datafusion-functions-window"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"datafusion-common",
@@ -4214,7 +4214,7 @@ dependencies = [
[[package]]
name = "datafusion-functions-window-common"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"datafusion-common",
"datafusion-physical-expr-common",
@@ -4223,7 +4223,7 @@ dependencies = [
[[package]]
name = "datafusion-macros"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"datafusion-doc",
"quote",
@@ -4233,7 +4233,7 @@ dependencies = [
[[package]]
name = "datafusion-optimizer"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"chrono",
@@ -4282,7 +4282,7 @@ dependencies = [
[[package]]
name = "datafusion-physical-expr"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"ahash 0.8.12",
"arrow 58.3.0",
@@ -4305,7 +4305,7 @@ dependencies = [
[[package]]
name = "datafusion-physical-expr-adapter"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"datafusion-common",
@@ -4319,7 +4319,7 @@ dependencies = [
[[package]]
name = "datafusion-physical-expr-common"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"ahash 0.8.12",
"arrow 58.3.0",
@@ -4335,7 +4335,7 @@ dependencies = [
[[package]]
name = "datafusion-physical-optimizer"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"datafusion-common",
@@ -4353,7 +4353,7 @@ dependencies = [
[[package]]
name = "datafusion-physical-plan"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"ahash 0.8.12",
"arrow 58.3.0",
@@ -4384,7 +4384,7 @@ dependencies = [
[[package]]
name = "datafusion-proto"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"chrono",
@@ -4411,7 +4411,7 @@ dependencies = [
[[package]]
name = "datafusion-proto-common"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"datafusion-common",
@@ -4421,7 +4421,7 @@ dependencies = [
[[package]]
name = "datafusion-pruning"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"datafusion-common",
@@ -4437,7 +4437,7 @@ dependencies = [
[[package]]
name = "datafusion-session"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"async-trait",
"datafusion-common",
@@ -4450,7 +4450,7 @@ dependencies = [
[[package]]
name = "datafusion-sql"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"arrow 58.3.0",
"bigdecimal 0.4.8",
@@ -4468,7 +4468,7 @@ dependencies = [
[[package]]
name = "datafusion-substrait"
version = "53.1.0"
-source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a#a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a"
+source = "git+https://github.com/GreptimeTeam/datafusion.git?rev=bb531e754b3b36d7a4d3222f35ebc88204dafd2b#bb531e754b3b36d7a4d3222f35ebc88204dafd2b"
dependencies = [
"async-recursion",
"async-trait",
@@ -5177,7 +5177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
- "windows-sys 0.60.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -5265,6 +5265,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
+[[package]]
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
@@ -5601,6 +5607,20 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
+[[package]]
+name = "framehop"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09a5a3f0acb82df800ca3aa50c0d60d286c5d13d4cfc3114b3a9663f13b032fe"
+dependencies = [
+ "arrayvec",
+ "cfg-if",
+ "fallible-iterator 0.3.0",
+ "gimli",
+ "macho-unwind-info",
+ "pe-unwind-info",
+]
+
[[package]]
name = "frontend"
version = "1.3.0-alpha.1"
@@ -5746,7 +5766,7 @@ checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a"
dependencies = [
"io-lifetimes 2.0.4",
"rustix 1.0.7",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -6062,6 +6082,10 @@ name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+dependencies = [
+ "fallible-iterator 0.3.0",
+ "stable_deref_trait",
+]
[[package]]
name = "git2"
@@ -6097,7 +6121,7 @@ dependencies = [
[[package]]
name = "greptime-proto"
version = "0.1.0"
-source = "git+https://github.com/GreptimeTeam/greptime-proto.git?rev=32f467fa2ba2b3588a58381a24af83de09fbb00a#32f467fa2ba2b3588a58381a24af83de09fbb00a"
+source = "git+https://github.com/GreptimeTeam/greptime-proto.git?rev=0c4beffaa2b3968034915867593a717c6ab0c6b4#0c4beffaa2b3968034915867593a717c6ab0c6b4"
dependencies = [
"prost 0.14.1",
"prost-types 0.14.1",
@@ -7143,7 +7167,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f"
dependencies = [
"io-lifetimes 3.0.1",
- "windows-sys 0.60.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -7181,7 +7205,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -7884,7 +7908,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
- "windows-targets 0.53.5",
+ "windows-targets 0.48.5",
]
[[package]]
@@ -8230,6 +8254,17 @@ dependencies = [
"thiserror 1.0.69",
]
+[[package]]
+name = "macho-unwind-info"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb4bdc8b0ce69932332cf76d24af69c3a155242af95c226b2ab6c2e371ed1149"
+dependencies = [
+ "thiserror 2.0.17",
+ "zerocopy",
+ "zerocopy-derive",
+]
+
[[package]]
name = "mappings"
version = "0.7.1"
@@ -8346,6 +8381,15 @@ dependencies = [
"thiserror 1.0.69",
]
+[[package]]
+name = "memmap2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "memmap2"
version = "0.9.5"
@@ -9175,7 +9219,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -9443,6 +9487,16 @@ dependencies = [
"objc2-core-foundation",
]
+[[package]]
+name = "object"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
+dependencies = [
+ "flate2",
+ "memchr",
+]
+
[[package]]
name = "object"
version = "0.36.7"
@@ -10003,6 +10057,7 @@ dependencies = [
"arrow-ipc 58.3.0",
"async-stream",
"async-trait",
+ "axum 0.8.4",
"bytes",
"catalog",
"chrono",
@@ -10063,6 +10118,7 @@ dependencies = [
"tokio-util",
"tonic 0.14.2",
"tracing",
+ "url",
]
[[package]]
@@ -10484,6 +10540,19 @@ dependencies = [
"hmac 0.12.1",
]
+[[package]]
+name = "pe-unwind-info"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "500fa4cdeacd98997c5865e3d0d1cb8fe7e9d7d75ecc775e07989a433a9a9a59"
+dependencies = [
+ "arrayvec",
+ "bitflags 2.12.1",
+ "thiserror 2.0.17",
+ "zerocopy",
+ "zerocopy-derive",
+]
+
[[package]]
name = "peeking_take_while"
version = "1.0.0"
@@ -10997,7 +11066,7 @@ dependencies = [
"base64 0.22.1",
"byteorder",
"bytes",
- "fallible-iterator",
+ "fallible-iterator 0.2.0",
"hmac 0.13.0",
"md-5 0.11.0",
"memchr",
@@ -11015,7 +11084,7 @@ dependencies = [
"array-init",
"bytes",
"chrono",
- "fallible-iterator",
+ "fallible-iterator 0.2.0",
"postgres-protocol",
"serde_core",
"serde_json",
@@ -11038,30 +11107,33 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "pprof"
-version = "0.14.0"
+version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebbe2f8898beba44815fdc9e5a4ae9c929e21c5dc29b0c774a15555f7f58d6d0"
+checksum = "38a01da47675efa7673b032bf8efd8214f1917d89685e07e395ab125ea42b187"
dependencies = [
"aligned-vec",
"backtrace",
"cfg-if",
"criterion 0.5.1",
"findshlibs",
+ "framehop",
"inferno 0.11.21",
"libc",
"log",
+ "memmap2 0.5.10",
"nix 0.26.4",
+ "object 0.29.0",
"once_cell",
- "parking_lot 0.12.4",
"prost 0.12.6",
"prost-build 0.12.6",
"prost-derive 0.12.6",
- "protobuf 2.28.0",
+ "protobuf 3.7.2",
"sha2 0.10.9",
"smallvec",
+ "spin 0.10.1",
"symbolic-demangle",
"tempfile",
- "thiserror 1.0.69",
+ "thiserror 2.0.17",
]
[[package]]
@@ -11383,7 +11455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
dependencies = [
"bytes",
- "heck 0.5.0",
+ "heck 0.4.1",
"itertools 0.12.1",
"log",
"multimap",
@@ -11403,7 +11475,7 @@ version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1"
dependencies = [
- "heck 0.5.0",
+ "heck 0.4.1",
"itertools 0.14.0",
"log",
"multimap",
@@ -11922,7 +11994,7 @@ dependencies = [
"once_cell",
"socket2 0.5.10",
"tracing",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -12893,7 +12965,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -12906,7 +12978,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.9.4",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -12997,7 +13069,7 @@ dependencies = [
"security-framework 3.7.0",
"security-framework-sys",
"webpki-root-certs",
- "windows-sys 0.60.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -13584,6 +13656,7 @@ dependencies = [
"table",
"tempfile",
"tikv-jemalloc-ctl",
+ "tikv-jemallocator",
"tokio",
"tokio-postgres",
"tokio-postgres-rustls",
@@ -13882,7 +13955,7 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1961e2ef424c1424204d3a5d6975f934f56b6d50ff5732382d84ebf460e147f7"
dependencies = [
- "heck 0.5.0",
+ "heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.117",
@@ -13911,7 +13984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
dependencies = [
"libc",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -13952,6 +14025,9 @@ name = "spin"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3"
+dependencies = [
+ "lock_api",
+]
[[package]]
name = "spki"
@@ -14303,7 +14379,7 @@ dependencies = [
"cfg-if",
"libc",
"psm",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -14582,7 +14658,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a1150bdda9314f6cfeeea801c23f5593c6e6a6c72e64f67e48d723a12b8efdb"
dependencies = [
"debugid",
- "memmap2",
+ "memmap2 0.9.5",
"stable_deref_trait",
"uuid",
]
@@ -14751,7 +14827,7 @@ dependencies = [
"lru 0.16.4",
"lz4_flex 0.13.1",
"measure_time",
- "memmap2",
+ "memmap2 0.9.5",
"once_cell",
"oneshot",
"rayon",
@@ -14920,7 +14996,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix 1.0.7",
- "windows-sys 0.60.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -15179,9 +15255,9 @@ dependencies = [
[[package]]
name = "tikv-jemalloc-ctl"
-version = "0.6.0"
+version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f21f216790c8df74ce3ab25b534e0718da5a1916719771d3fec23315c99e468b"
+checksum = "3a184c43b8ab2f41df2733b55556e3f5f632f4aeaa205b1bb018f574b7f5f142"
dependencies = [
"libc",
"paste",
@@ -15190,9 +15266,8 @@ dependencies = [
[[package]]
name = "tikv-jemalloc-sys"
-version = "0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd3c60906412afa9c2b5b5a48ca6a5abe5736aec9eb48ad05037a677e52e4e2d"
+version = "0.7.0+5.3.1-161-gff80bf2d0c8f162ea445533354cad8b7e5830f5c"
+source = "git+https://github.com/GreptimeTeam/jemallocator?rev=e254a7eaf13a9ede04953ddefd173b5303018530#e254a7eaf13a9ede04953ddefd173b5303018530"
dependencies = [
"cc",
"libc",
@@ -15200,9 +15275,9 @@ dependencies = [
[[package]]
name = "tikv-jemallocator"
-version = "0.6.0"
+version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cec5ff18518d81584f477e9bfdf957f5bb0979b0bac3af4ca30b5b3ae2d2865"
+checksum = "249f09e49ab1609436f34c776e84231bead18d6a955f119f939bdc1d847561bd"
dependencies = [
"libc",
"tikv-jemalloc-sys",
@@ -15379,7 +15454,7 @@ dependencies = [
"async-trait",
"byteorder",
"bytes",
- "fallible-iterator",
+ "fallible-iterator 0.2.0",
"futures-channel",
"futures-util",
"log",
@@ -16728,7 +16803,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -17178,7 +17253,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
dependencies = [
"bitflags 2.12.1",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 55181077f8d..b39064706c4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -159,7 +159,7 @@ fs2 = "0.4"
fst = "0.4.7"
futures = "0.3"
futures-util = "0.3"
-greptime-proto = { git = "https://github.com/GreptimeTeam/greptime-proto.git", rev = "32f467fa2ba2b3588a58381a24af83de09fbb00a" }
+greptime-proto = { git = "https://github.com/GreptimeTeam/greptime-proto.git", rev = "0c4beffaa2b3968034915867593a717c6ab0c6b4" }
hex = "0.4"
hostname = "0.4.0"
http = "1"
@@ -353,23 +353,30 @@ git = "https://github.com/GreptimeTeam/greptime-meter.git"
rev = "5618e779cf2bb4755b499c630fba4c35e91898cb"
[patch.crates-io]
-datafusion = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a" }
-datafusion-common = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a" }
-datafusion-datasource = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a" }
-datafusion-expr = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a" }
-datafusion-expr-common = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a" }
-datafusion-functions = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a" }
-datafusion-functions-aggregate-common = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a" }
-datafusion-functions-window-common = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a" }
-datafusion-optimizer = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a" }
-datafusion-physical-expr = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a" }
-datafusion-physical-expr-common = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a" }
-datafusion-physical-plan = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a" }
-datafusion-proto = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a" }
-datafusion-sql = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a" }
-datafusion-substrait = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "a281d9f0ea4b3fb2ec88bc3553d8dcef307e072a" }
+datafusion = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "bb531e754b3b36d7a4d3222f35ebc88204dafd2b" }
+datafusion-common = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "bb531e754b3b36d7a4d3222f35ebc88204dafd2b" }
+datafusion-datasource = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "bb531e754b3b36d7a4d3222f35ebc88204dafd2b" }
+datafusion-expr = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "bb531e754b3b36d7a4d3222f35ebc88204dafd2b" }
+datafusion-expr-common = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "bb531e754b3b36d7a4d3222f35ebc88204dafd2b" }
+datafusion-functions = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "bb531e754b3b36d7a4d3222f35ebc88204dafd2b" }
+datafusion-functions-aggregate-common = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "bb531e754b3b36d7a4d3222f35ebc88204dafd2b" }
+datafusion-functions-window-common = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "bb531e754b3b36d7a4d3222f35ebc88204dafd2b" }
+datafusion-optimizer = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "bb531e754b3b36d7a4d3222f35ebc88204dafd2b" }
+datafusion-physical-expr = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "bb531e754b3b36d7a4d3222f35ebc88204dafd2b" }
+datafusion-physical-expr-common = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "bb531e754b3b36d7a4d3222f35ebc88204dafd2b" }
+datafusion-physical-plan = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "bb531e754b3b36d7a4d3222f35ebc88204dafd2b" }
+datafusion-proto = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "bb531e754b3b36d7a4d3222f35ebc88204dafd2b" }
+datafusion-sql = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "bb531e754b3b36d7a4d3222f35ebc88204dafd2b" }
+datafusion-substrait = { git = "https://github.com/GreptimeTeam/datafusion.git", rev = "bb531e754b3b36d7a4d3222f35ebc88204dafd2b" }
sqlparser = { git = "https://github.com/GreptimeTeam/sqlparser-rs.git", rev = "2aefa08a8d69c96eec2d6d6703598a009bba6e4c" } # on branch v0.61.x
+# Temporary: use the GreptimeTeam fork of tikv-jemalloc-sys embedding
+# jemalloc 5.3.1 + backport of 54f22c83 ("Initialize TSD tcache before
+# enabling it"), fixing the TSD/tcache enable-before-init reentrancy
+# window. Remove once tikv/jemallocator ships a jemalloc snapshot that
+# includes the fix. See GreptimeTeam/jemalloc#1.
+tikv-jemalloc-sys = { git = "https://github.com/GreptimeTeam/jemallocator", rev = "e254a7eaf13a9ede04953ddefd173b5303018530" }
+
[profile.release]
debug = 1
diff --git a/Makefile b/Makefile
index 0f280b7b9be..9bed2ddc9b8 100644
--- a/Makefile
+++ b/Makefile
@@ -4,6 +4,8 @@ FEATURES ?=
TARGET_DIR ?=
TARGET ?=
BUILD_BIN ?= greptime
+BUILD_PACKAGE ?= $(if $(filter greptime,$(strip $(BUILD_BIN))),cmd)
+BUILD_PACKAGE_OPT = $(if $(strip $(BUILD_PACKAGE)),-p ${BUILD_PACKAGE})
CARGO_BUILD_OPTS := --locked
IMAGE_REGISTRY ?= docker.io
IMAGE_NAMESPACE ?= greptime
@@ -84,7 +86,7 @@ endif
.PHONY: build
build: ## Build debug version greptime.
- cargo ${CARGO_EXTENSION} build ${CARGO_BUILD_OPTS}
+ cargo ${CARGO_EXTENSION} build ${BUILD_PACKAGE_OPT} ${CARGO_BUILD_OPTS}
.PHONY: build-by-dev-builder
build-by-dev-builder: ## Build greptime by dev-builder.
@@ -93,6 +95,8 @@ build-by-dev-builder: ## Build greptime by dev-builder.
-v ${PWD}:/greptimedb -v ${CARGO_REGISTRY_CACHE}:/root/.cargo/registry -v ${CARGO_GIT_CACHE}:/root/.cargo/git \
-w /greptimedb ${IMAGE_REGISTRY}/${IMAGE_NAMESPACE}/dev-builder-${BASE_IMAGE}:${DEV_BUILDER_IMAGE_TAG} \
make build \
+ BUILD_PACKAGE="${BUILD_PACKAGE}" \
+ BUILD_BIN="${BUILD_BIN}" \
CARGO_EXTENSION="${CARGO_EXTENSION}" \
CARGO_PROFILE=${CARGO_PROFILE} \
FEATURES=${FEATURES} \
@@ -108,6 +112,8 @@ build-riscv64-bin: ## Build greptime binary for riscv64 (linux-gnu) by the riscv
-v ${PWD}:/greptimedb -v ${CARGO_REGISTRY_CACHE}:/root/.cargo/registry -v ${CARGO_GIT_CACHE}:/root/.cargo/git \
-w /greptimedb ${IMAGE_REGISTRY}/${IMAGE_NAMESPACE}/dev-builder-riscv64:${DEV_BUILDER_RISCV64_IMAGE_TAG} \
make build \
+ BUILD_PACKAGE="${BUILD_PACKAGE}" \
+ BUILD_BIN="${BUILD_BIN}" \
CARGO_EXTENSION="${CARGO_EXTENSION}" \
CARGO_PROFILE=${CARGO_PROFILE} \
FEATURES=${FEATURES} \
@@ -122,6 +128,8 @@ build-android-bin: ## Build greptime binary for android.
-v ${PWD}:/greptimedb -v ${CARGO_REGISTRY_CACHE}:/root/.cargo/registry -v ${CARGO_GIT_CACHE}:/root/.cargo/git \
-w /greptimedb ${IMAGE_REGISTRY}/${IMAGE_NAMESPACE}/dev-builder-android:${DEV_BUILDER_IMAGE_TAG} \
make build \
+ BUILD_PACKAGE="${BUILD_PACKAGE}" \
+ BUILD_BIN="${BUILD_BIN}" \
CARGO_EXTENSION="ndk --platform 23 -t aarch64-linux-android" \
CARGO_PROFILE=release \
FEATURES="${FEATURES}" \
@@ -278,6 +286,7 @@ dashboards: ## Generate the Grafana dashboards for standalone mode and intermedi
@./grafana/scripts/gen-dashboards.sh
##@ Docs
+.PHONY: config-docs
config-docs: ## Generate configuration documentation from toml files.
docker run --rm \
-v ${PWD}:/greptimedb \
diff --git a/README.md b/README.md
index e95f7849e4b..1c8ed985e1d 100644
--- a/README.md
+++ b/README.md
@@ -13,95 +13,65 @@ One engine, on your infrastructure.
> Apache-2.0 licensed core.
-- [Introduction](#introduction)
-- [Why You Might Use It](#why-you-might-use-it)
-- [Overview](#overview)
-- [What's Supported](#whats-supported)
-- [Compatibility and Migration](#compatibility-and-migration)
-- [Limitations and Edition Boundary](#limitations-and-edition-boundary)
-- [Architecture](#architecture)
-- [Try GreptimeDB](#try-greptimedb)
-- [Getting Started](#getting-started)
-- [Build From Source](#build-from-source)
-- [Tools & Extensions](#tools--extensions)
-- [Project Status](#project-status)
-- [Community](#community)
-- [License](#license)
-- [Commercial Support](#commercial-support)
-- [Contributing](#contributing)
-- [Acknowledgement](#acknowledgement)
-
## Introduction
**GreptimeDB** is an open-source observability database. Metrics, logs, and traces run on one columnar engine over object storage and share one [table model](https://docs.greptime.com/user-guide/concepts/data-model/): tags, timestamp, and fields. When signals carry common identifiers such as service, host, or trace ID, you can correlate them in SQL without moving data between databases.
Ingest through OpenTelemetry, Prometheus Remote Write, Loki Push, or Elasticsearch Bulk. Use SQL across observability data and PromQL for metrics. Migrate ingestion one signal at a time without rebuilding your collectors.
+## One Query Across Signals
+
+OpenTelemetry ingestion writes spans to `opentelemetry_traces` and log records to
+`opentelemetry_logs`. Both tables carry `trace_id`, so correlating them is a join:
+
+```sql
+-- The slowest failed spans in the last hour,
+-- with the log lines emitted inside those same traces.
+SELECT
+ t.service_name,
+ t.span_name,
+ t.duration_nano / 1000000 AS duration_ms,
+ l.timestamp AS log_time,
+ l.severity_text,
+ l.body
+FROM opentelemetry_traces t
+JOIN opentelemetry_logs l ON l.trace_id = t.trace_id
+WHERE t.timestamp > now() - INTERVAL '1' HOUR
+ AND t.span_status_code = 'STATUS_CODE_ERROR'
+ORDER BY t.duration_nano DESC
+LIMIT 20;
+```
+
+Metrics join the same way, on any tag the tables share, such as `service`,
+`host`, or `pod`.
+
## Why You Might Use It
- You run Prometheus plus Loki or Elasticsearch and want one backend instead of three
- You have outgrown Prometheus on cardinality or retention and don't want the Thanos/Mimir operational surface
+- You are hitting Loki's query performance limits as log volume grows
- You need long retention on object storage without a separate analytics stack
- You want to query telemetry with SQL, not only a domain query language
- You are storing GenAI or agent telemetry ([OTel GenAI conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/)) alongside infrastructure signals
-- You need the same engine and semantics on resource-constrained devices
Learn more in [Why GreptimeDB](https://docs.greptime.com/user-guide/concepts/why-greptimedb).
-## Overview
-
-A quick overview of what GreptimeDB ingests, how it connects to other systems, and what its distributed engine lets you do.
-
-
-
-
-
-
-
## What's Supported
| | |
@@ -113,6 +83,22 @@ A quick overview of what GreptimeDB ingests, how it connects to other systems, a
Compute and storage are disaggregated: object storage holds the data, while memory and local-disk caches keep recent and frequently queried data close to compute.
+
+
+
+
+
+
+## Benchmarks
+
+* [Agent RCA Bench](https://rca-bench.greptime.com/): LLM agents doing root cause analysis
+ over GreptimeDB versus Prometheus + Loki + Tempo. 40% fewer wrong diagnoses, 48% fewer
+ input tokens, 45% lower cost
+ ([write-up](https://greptime.com/blogs/2026-09-08-agent-rca-bench-interface-semantic-layer))
+* [GreptimeDB tops JSONBench's billion-record cold run test](https://greptime.com/blogs/2025-03-18-jsonbench-greptimedb-performance)
+* [TSBS Benchmark](https://github.com/GreptimeTeam/greptimedb/tree/main/docs/benchmarks/tsbs)
+* [More benchmark reports](https://docs.greptime.com/user-guide/concepts/features-that-you-concern#how-is-greptimedbs-performance-compared-to-other-solutions)
+
## Compatibility and Migration
Compatibility is per protocol, and query-side coverage is narrower than ingestion.
@@ -123,11 +109,6 @@ Compatibility is per protocol, and query-side coverage is narrower than ingestio
| **Loki** | Push ingestion; dual-write through Grafana Alloy makes the [cutover gradual](https://greptime.com/blogs/2026-07-23-from-loki-to-greptimedb-dual-write-migration) | LogQL and the rest of the Loki query API |
| **Elasticsearch** | `_bulk` ingestion in the open-source core; [QueryDSL](https://docs.greptime.com/enterprise/elasticsearch-compatible/query/) partially, in Enterprise | Most other Elasticsearch APIs |
-**Benchmarks:**
-* [GreptimeDB tops JSONBench's billion-record cold run test](https://greptime.com/blogs/2025-03-18-jsonbench-greptimedb-performance)
-* [TSBS Benchmark](https://github.com/GreptimeTeam/greptimedb/tree/main/docs/benchmarks/tsbs)
-* [More benchmark reports](https://docs.greptime.com/user-guide/concepts/features-that-you-concern#how-is-greptimedbs-performance-compared-to-other-solutions)
-
## Limitations and Edition Boundary
Cluster deployment, object storage, the Flow engine, and every ingestion protocol listed above are in the Apache-2.0 build. Repartitioning, region migration, and index creation are manual operations there.
@@ -221,11 +202,7 @@ See the [Contribution Guidelines](CONTRIBUTING.md) for the full developer workfl
GreptimeDB is generally available, with stable APIs and regular releases. It runs in production at scale — [OceanBase Cloud](https://greptime.com/blogs/2025-07-22-user-case-obcloud-log-management-greptimedb) operates 80+ GreptimeDB clusters managing 300 TB of logs, cutting log storage cost by 60%+ after migrating from Grafana Loki. See more in [case studies](https://greptime.com/blogs/?category=Use%20Case).
-Read the [v1.0 highlights](https://greptime.com/blogs/2025-11-05-greptimedb-v1-highlights) and [2026 roadmap](https://greptime.com/blogs/2026-02-11-greptimedb-roadmap-2026), or browse the [version reference](https://docs.greptime.com/nightly/reference/about-greptimedb-version).
-
-If GreptimeDB is useful to you, please star the repo.
-
-
+Release lines and support windows are in the [version reference](https://docs.greptime.com/nightly/reference/about-greptimedb-version). For where the project is going, read the [v1.0 highlights](https://greptime.com/blogs/2025-11-05-greptimedb-v1-highlights) and the [2026 roadmap](https://greptime.com/blogs/2026-02-11-greptimedb-roadmap-2026).
## Community
@@ -239,6 +216,10 @@ We invite you to engage and contribute!
- [X (Twitter)](https://X.com/greptime)
- [YouTube](https://www.youtube.com/@greptime)
+If GreptimeDB is useful to you, please star the repo.
+
+
+
## License
GreptimeDB is an open-core project. Its core is licensed under the
@@ -262,6 +243,9 @@ security, and support layer for production deployments.
- Explore [Internal Concepts](https://docs.greptime.com/contributor-guide/overview.html) and [DeepWiki](https://deepwiki.com/GreptimeTeam/greptimedb).
- Pick up a [good first issue](https://github.com/GreptimeTeam/greptimedb/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and join the #contributors [Slack](https://greptime.com/slack) channel.
+[](https://github.com/GreptimeTeam/greptimedb/actions/workflows/integration.yml)
+[](https://codecov.io/gh/GreptimeTeam/greptimedb)
+
## Acknowledgement
Special thanks to all contributors! See [AUTHOR.md](AUTHOR.md).
diff --git a/config/config.md b/config/config.md
index a8d12c3557e..2d85ddd7176 100644
--- a/config/config.md
+++ b/config/config.md
@@ -249,7 +249,7 @@
| `pipeline.cache_ttl` | String | `10s` | Time to live of the local pipeline cache. Default is `10s`. |
| `event_recorder` | -- | -- | Configuration options for the event recorder. |
| `event_recorder.ttl` | String | `90d` | TTL for the events table that will be used to store the events. Default is `90d`. |
-| `event_recorder.event_types` | Array | -- | Event types to record. Current available event types: `create_database`, `alter_database`, `drop_database`, `create_flow`, `drop_flow`, `create_table`, `create_logical_tables`, `alter_table`, `alter_logical_tables`, `drop_table`, `undrop_table`, `purge_dropped_table`, `truncate_table`, `create_view`, `drop_view`, `admin_function`, `reconcile_table`, `reconcile_logical_tables`. When omitted, all current and future event types are recorded. Set to an empty array to disable event recording. |
+| `event_recorder.event_types` | Array | -- | Event types to record. Current available event types: `create_database`, `alter_database`, `drop_database`, `create_flow`, `drop_flow`, `create_table`, `create_logical_tables`, `alter_table`, `alter_logical_tables`, `drop_table`, `undrop_table`, `purge_dropped_table`, `truncate_table`, `create_view`, `drop_view`, `admin_function`, `reconcile_catalog`, `reconcile_database`, `reconcile_table`, `reconcile_logical_tables`. When omitted, all current and future event types are recorded. Set to an empty array to disable event recording. |
| `memory` | -- | -- | The memory options. |
| `memory.enable_heap_profiling` | Bool | `true` | Whether to enable heap profiling activation during startup. When enabled, heap profiling will be activated if the `MALLOC_CONF` environment variable is set to "prof:true,prof_active:false". The official image adds this env variable. Default is true. |
@@ -477,7 +477,7 @@
| `wal.create_topic_timeout` | String | `30s` | The timeout for creating a Kafka topic. **It's only used when the provider is `kafka`**. |
| `event_recorder` | -- | -- | Configuration options for the event recorder. |
| `event_recorder.ttl` | String | `90d` | TTL for the events table that will be used to store the events. Default is `90d`. |
-| `event_recorder.event_types` | Array | -- | Event types to record. Current available event types: `region_migration`, `create_database`, `alter_database`, `drop_database`, `create_flow`, `drop_flow`, `create_table`, `create_logical_tables`, `alter_table`, `alter_logical_tables`, `drop_table`, `undrop_table`, `purge_dropped_table`, `truncate_table`, `create_view`, `drop_view`, `repartition`, `repartition_group`, `wal_prune`, `batch_gc`, `reconcile_table`, `reconcile_logical_tables`. When omitted, all current and future event types are recorded. Set to an empty array to disable event recording. |
+| `event_recorder.event_types` | Array | -- | Event types to record. Current available event types: `region_migration`, `create_database`, `alter_database`, `drop_database`, `create_flow`, `drop_flow`, `create_table`, `create_logical_tables`, `alter_table`, `alter_logical_tables`, `drop_table`, `undrop_table`, `purge_dropped_table`, `truncate_table`, `create_view`, `drop_view`, `repartition`, `repartition_group`, `wal_prune`, `batch_gc`, `reconcile_catalog`, `reconcile_database`, `reconcile_table`, `reconcile_logical_tables`. When omitted, all current and future event types are recorded. Set to an empty array to disable event recording. |
| `stats_persistence` | -- | -- | Configuration options for the stats persistence. |
| `stats_persistence.ttl` | String | `0s` | TTL for the stats table that will be used to store the stats. Set to `0s` to disable stats persistence. Default is `0s`. If you want to enable stats persistence, set the TTL to a value greater than 0. It is recommended to set a small value, e.g., `3h`. |
| `stats_persistence.interval` | String | `10m` | The interval to persist the stats. Default is `10m`. The minimum value is `10m`, if the value is less than `10m`, it will be overridden to `10m`. |
diff --git a/config/metasrv.example.toml b/config/metasrv.example.toml
index 514993a4cae..b923c5a549c 100644
--- a/config/metasrv.example.toml
+++ b/config/metasrv.example.toml
@@ -328,8 +328,8 @@ ttl = "90d"
## `drop_flow`, `create_table`, `create_logical_tables`, `alter_table`,
## `alter_logical_tables`, `drop_table`, `undrop_table`, `purge_dropped_table`,
## `truncate_table`, `create_view`, `drop_view`, `repartition`,
-## `repartition_group`, `wal_prune`, `batch_gc`, `reconcile_table`,
-## `reconcile_logical_tables`.
+## `repartition_group`, `wal_prune`, `batch_gc`, `reconcile_catalog`,
+## `reconcile_database`, `reconcile_table`, `reconcile_logical_tables`.
## When omitted, all current and future event types are recorded.
## Set to an empty array to disable event recording.
#+ event_types = ["region_migration"]
diff --git a/config/standalone.example.toml b/config/standalone.example.toml
index b05cf0177d0..0bef0793247 100644
--- a/config/standalone.example.toml
+++ b/config/standalone.example.toml
@@ -973,8 +973,8 @@ ttl = "90d"
## `alter_database`, `drop_database`, `create_flow`, `drop_flow`,
## `create_table`, `create_logical_tables`, `alter_table`, `alter_logical_tables`,
## `drop_table`, `undrop_table`, `purge_dropped_table`, `truncate_table`,
-## `create_view`, `drop_view`, `admin_function`, `reconcile_table`,
-## `reconcile_logical_tables`.
+## `create_view`, `drop_view`, `admin_function`, `reconcile_catalog`,
+## `reconcile_database`, `reconcile_table`, `reconcile_logical_tables`.
## When omitted, all current and future event types are recorded.
## Set to an empty array to disable event recording.
#+ event_types = ["create_database"]
diff --git a/src/catalog/src/system_schema/information_schema/columns.rs b/src/catalog/src/system_schema/information_schema/columns.rs
index 4e68b093cd4..e92efd3bebf 100644
--- a/src/catalog/src/system_schema/information_schema/columns.rs
+++ b/src/catalog/src/system_schema/information_schema/columns.rs
@@ -31,7 +31,7 @@ use datatypes::scalars::ScalarVectorBuilder;
use datatypes::schema::{ColumnSchema, Schema, SchemaRef};
use datatypes::value::Value;
use datatypes::vectors::{
- ConstantVector, Int64Vector, Int64VectorBuilder, StringVector, StringVectorBuilder, VectorRef,
+ Int64Vector, Int64VectorBuilder, StringVector, StringVectorBuilder, VectorRef,
};
use futures::TryStreamExt;
use snafu::{OptionExt, ResultExt};
@@ -408,18 +408,9 @@ impl InformationSchemaColumnsBuilder {
fn finish(&mut self) -> Result {
let rows_num = self.collation_names.len();
- let privileges = Arc::new(ConstantVector::new(
- Arc::new(StringVector::from(vec![DEFAULT_PRIVILEGES])),
- rows_num,
- ));
- let empty_string = Arc::new(ConstantVector::new(
- Arc::new(StringVector::from(vec![EMPTY_STR])),
- rows_num,
- ));
- let srs_ids = Arc::new(ConstantVector::new(
- Arc::new(Int64Vector::from(vec![None])),
- rows_num,
- ));
+ let privileges = Arc::new(StringVector::from(vec![DEFAULT_PRIVILEGES; rows_num]));
+ let empty_string = Arc::new(StringVector::from(vec![EMPTY_STR; rows_num]));
+ let srs_ids = Arc::new(Int64Vector::from(vec![None; rows_num]));
let columns: Vec = vec![
Arc::new(self.catalog_names.finish()),
diff --git a/src/catalog/src/system_schema/information_schema/key_column_usage.rs b/src/catalog/src/system_schema/information_schema/key_column_usage.rs
index 99b0d7f6b73..4f8f116afaf 100644
--- a/src/catalog/src/system_schema/information_schema/key_column_usage.rs
+++ b/src/catalog/src/system_schema/information_schema/key_column_usage.rs
@@ -26,7 +26,7 @@ use datafusion::physical_plan::streaming::PartitionStream as DfPartitionStream;
use datatypes::prelude::{ConcreteDataType, MutableVector, ScalarVectorBuilder, VectorRef};
use datatypes::schema::{ColumnSchema, Schema, SchemaRef};
use datatypes::value::Value;
-use datatypes::vectors::{ConstantVector, StringVector, StringVectorBuilder, UInt32VectorBuilder};
+use datatypes::vectors::{StringVector, StringVectorBuilder, UInt32VectorBuilder};
use futures_util::TryStreamExt;
use snafu::{OptionExt, ResultExt};
use store_api::storage::{ScanRequest, TableId};
@@ -326,10 +326,7 @@ impl InformationSchemaKeyColumnUsageBuilder {
fn finish(&mut self) -> Result {
let rows_num = self.table_catalog.len();
- let null_string_vector = Arc::new(ConstantVector::new(
- Arc::new(StringVector::from(vec![None as Option<&str>])),
- rows_num,
- ));
+ let null_string_vector = Arc::new(StringVector::from(vec![None as Option<&str>; rows_num]));
let columns: Vec = vec![
Arc::new(self.constraint_catalog.finish()),
Arc::new(self.constraint_schema.finish()),
diff --git a/src/catalog/src/system_schema/information_schema/partitions.rs b/src/catalog/src/system_schema/information_schema/partitions.rs
index b9396fe5543..d2cce93150a 100644
--- a/src/catalog/src/system_schema/information_schema/partitions.rs
+++ b/src/catalog/src/system_schema/information_schema/partitions.rs
@@ -29,8 +29,8 @@ use datatypes::schema::{ColumnSchema, Schema, SchemaRef};
use datatypes::timestamp::TimestampSecond;
use datatypes::value::Value;
use datatypes::vectors::{
- ConstantVector, Int64Vector, Int64VectorBuilder, MutableVector, StringVector,
- StringVectorBuilder, TimestampSecondVector, TimestampSecondVectorBuilder, UInt64VectorBuilder,
+ Int64Vector, Int64VectorBuilder, MutableVector, StringVector, StringVectorBuilder,
+ TimestampSecondVector, TimestampSecondVectorBuilder, UInt64VectorBuilder,
};
use futures::{StreamExt, TryStreamExt};
use partition::manager::PartitionInfo;
@@ -361,22 +361,11 @@ impl InformationSchemaPartitionsBuilder {
fn finish(&mut self) -> Result {
let rows_num = self.catalog_names.len();
- let null_string_vector = Arc::new(ConstantVector::new(
- Arc::new(StringVector::from(vec![None as Option<&str>])),
- rows_num,
- ));
- let null_i64_vector = Arc::new(ConstantVector::new(
- Arc::new(Int64Vector::from(vec![None])),
- rows_num,
- ));
- let null_timestamp_second_vector = Arc::new(ConstantVector::new(
- Arc::new(TimestampSecondVector::from(vec![None])),
- rows_num,
- ));
- let partition_methods = Arc::new(ConstantVector::new(
- Arc::new(StringVector::from(vec![Some("RANGE")])),
- rows_num,
- ));
+ let null_string_vector = Arc::new(StringVector::from(vec![None as Option<&str>; rows_num]));
+ let null_i64_vector = Arc::new(Int64Vector::from(vec![None; rows_num]));
+ let null_timestamp_second_vector =
+ Arc::new(TimestampSecondVector::from(vec![None; rows_num]));
+ let partition_methods = Arc::new(StringVector::from(vec![Some("RANGE"); rows_num]));
let columns: Vec = vec![
Arc::new(self.catalog_names.finish()),
diff --git a/src/catalog/src/system_schema/information_schema/table_constraints.rs b/src/catalog/src/system_schema/information_schema/table_constraints.rs
index 8c975207f21..2208d303c2e 100644
--- a/src/catalog/src/system_schema/information_schema/table_constraints.rs
+++ b/src/catalog/src/system_schema/information_schema/table_constraints.rs
@@ -27,7 +27,7 @@ use datatypes::prelude::{ConcreteDataType, MutableVector};
use datatypes::scalars::ScalarVectorBuilder;
use datatypes::schema::{ColumnSchema, Schema, SchemaRef};
use datatypes::value::Value;
-use datatypes::vectors::{ConstantVector, StringVector, StringVectorBuilder, VectorRef};
+use datatypes::vectors::{StringVector, StringVectorBuilder, VectorRef};
use futures::TryStreamExt;
use snafu::{OptionExt, ResultExt};
use store_api::storage::{ScanRequest, TableId};
@@ -242,14 +242,8 @@ impl InformationSchemaTableConstraintsBuilder {
fn finish(&mut self) -> Result {
let rows_num = self.constraint_names.len();
- let constraint_catalogs = Arc::new(ConstantVector::new(
- Arc::new(StringVector::from(vec!["def"])),
- rows_num,
- ));
- let enforceds = Arc::new(ConstantVector::new(
- Arc::new(StringVector::from(vec!["YES"])),
- rows_num,
- ));
+ let constraint_catalogs = Arc::new(StringVector::from(vec!["def"; rows_num]));
+ let enforceds = Arc::new(StringVector::from(vec!["YES"; rows_num]));
let columns: Vec = vec![
constraint_catalogs,
diff --git a/src/client/src/database.rs b/src/client/src/database.rs
index 881730cfaaa..e97fff6db00 100644
--- a/src/client/src/database.rs
+++ b/src/client/src/database.rs
@@ -825,7 +825,17 @@ impl Database {
/// Ingest a stream of [RecordBatch]es that belong to a table, using Arrow Flight's "`DoPut`"
/// method. The return value is also a stream, produces [DoPutResponse]s.
pub async fn do_put(&self, stream: FlightDataStream) -> Result {
+ self.do_put_with_hints(stream, &[]).await
+ }
+
+ /// Ingest a stream of [RecordBatch]es using Arrow Flight's `DoPut` with request hints.
+ pub async fn do_put_with_hints(
+ &self,
+ stream: FlightDataStream,
+ hints: &[(&str, &str)],
+ ) -> Result {
let mut request = tonic::Request::new(stream);
+ Self::put_hints(request.metadata_mut(), hints)?;
if let Some(AuthHeader {
auth_scheme: Some(AuthScheme::Basic(Basic { username, password })),
diff --git a/src/cmd/Cargo.toml b/src/cmd/Cargo.toml
index 8cb8cff4300..722e48ae90a 100644
--- a/src/cmd/Cargo.toml
+++ b/src/cmd/Cargo.toml
@@ -119,12 +119,12 @@ tracing-appender.workspace = true
[target.'cfg(unix)'.dependencies]
daemonize = "0.5"
-pprof = { version = "0.14", features = [
+pprof = { version = "0.15", features = [
"flamegraph",
] }
[target.'cfg(not(windows))'.dependencies]
-tikv-jemallocator = "0.6"
+tikv-jemallocator = "0.7"
[dev-dependencies]
api.workspace = true
diff --git a/src/cmd/src/bin/query_perf_fixture/case.rs b/src/cmd/src/bin/query_perf_fixture/case.rs
index a76c49d43d0..0f5239a6b51 100644
--- a/src/cmd/src/bin/query_perf_fixture/case.rs
+++ b/src/cmd/src/bin/query_perf_fixture/case.rs
@@ -105,6 +105,10 @@ pub(super) struct PromRemoteWritePlan {
#[serde(default = "default_visibility_timeout_seconds")]
pub(super) visibility_timeout_seconds: u64,
#[serde(default)]
+ pub(super) base_setup_sql: Vec,
+ #[serde(default)]
+ pub(super) candidate_setup_sql: Vec,
+ #[serde(default)]
pub(super) prom_store: PromStoreConfig,
#[serde(default)]
pub(super) value: ValueConfig,
@@ -200,6 +204,7 @@ pub(super) enum ValuePattern {
QuantizedSignal,
SignalWithSporadicStalls,
MixedSignalRepeated,
+ BoundedMixed,
}
impl std::fmt::Display for ValuePattern {
@@ -436,6 +441,63 @@ pub(super) fn default_parallelism() -> u64 {
1
}
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn remote_write_setup_sql_defaults_to_empty() {
+ let case: CaseFile = toml::from_str(
+ r#"
+[scenario]
+kind = "prom_remote_write_then_query"
+
+[scenario.remote_write]
+metric = "metric"
+"#,
+ )
+ .unwrap();
+ let Scenario::PromRemoteWriteThenQuery(scenario) = case.scenario else {
+ panic!("expected prom_remote_write_then_query scenario");
+ };
+ assert!(scenario.remote_write.base_setup_sql.is_empty());
+ assert!(scenario.remote_write.candidate_setup_sql.is_empty());
+ }
+
+ #[test]
+ fn remote_write_setup_sql_roundtrips() {
+ let case: CaseFile = toml::from_str(
+ r#"
+[scenario]
+kind = "prom_remote_write_then_query"
+
+[scenario.remote_write]
+metric = "metric"
+base_setup_sql = ["CREATE TABLE base_table"]
+candidate_setup_sql = ["CREATE TABLE candidate_table", "ALTER TABLE candidate_table SET 'x'='y'"]
+"#,
+ )
+ .unwrap();
+
+ let roundtrip: CaseFile =
+ serde_json::from_str(&serde_json::to_string(&case).unwrap()).unwrap();
+ let Scenario::PromRemoteWriteThenQuery(scenario) = roundtrip.scenario else {
+ panic!("expected prom_remote_write_then_query scenario");
+ };
+ assert_eq!(
+ scenario.remote_write.base_setup_sql,
+ ["CREATE TABLE base_table"]
+ );
+ assert_eq!(
+ scenario.remote_write.candidate_setup_sql,
+ [
+ "CREATE TABLE candidate_table",
+ "ALTER TABLE candidate_table SET 'x'='y'"
+ ]
+ );
+ }
+}
+
impl Scenario {
pub(super) fn kind(&self) -> &'static str {
match self {
diff --git a/src/cmd/src/bin/query_perf_fixture/direct_sst.rs b/src/cmd/src/bin/query_perf_fixture/direct_sst.rs
index 746ecbd76e4..27a6b4109cd 100644
--- a/src/cmd/src/bin/query_perf_fixture/direct_sst.rs
+++ b/src/cmd/src/bin/query_perf_fixture/direct_sst.rs
@@ -469,6 +469,7 @@ pub(super) async fn run_direct_sst(args: DirectArgs) {
write_buffer_size: DEFAULT_WRITE_BUFFER_SIZE,
row_group_size: scenario.layout.row_group_size,
max_file_size: None,
+ float_field_encoding: Default::default(),
};
let infos = match format {
FormatType::Flat => writer.write_all_flat(source, Some(sequence), &opts).await,
diff --git a/src/cmd/src/bin/query_perf_fixture/prom_remote_write.rs b/src/cmd/src/bin/query_perf_fixture/prom_remote_write.rs
index 1f36b0c2d23..a59178f4a79 100644
--- a/src/cmd/src/bin/query_perf_fixture/prom_remote_write.rs
+++ b/src/cmd/src/bin/query_perf_fixture/prom_remote_write.rs
@@ -197,8 +197,35 @@ fn deterministic_prom_value(args: &PromRemoteWriteArgs, series_idx: u64, sample_
args.value_base + (series_idx % 97) as f64 + local as f64 * args.value_step
}
}
+ ValuePattern::BoundedMixed => bounded_mixed_value(args, series_idx, local),
}
}
+
+fn bounded_mixed_value(args: &PromRemoteWriteArgs, series_idx: u64, local: u64) -> f64 {
+ const SPANS: [i64; 4] = [10, 1_000, 100_000, 10_000_000];
+ let series = splitmix64(series_idx ^ args.value_seed);
+ let span = SPANS[(series >> 62) as usize] * (1 + ((series >> 32) % 10) as i64);
+ let baseline = (series % args.value_cardinality.max(1)) as f64 * span as f64 / 10.0;
+ let run_length = args.value_run_length.max(1);
+ let group = local / run_length;
+ let phase = local % run_length;
+ let anchor = bounded_mixed_anchor(series, group, span);
+ let next_anchor = bounded_mixed_anchor(series, group + 1, span);
+ let signal = baseline + anchor + (next_anchor - anchor) * phase as f64 / run_length as f64;
+ let value = (args.value_base + args.value_step * signal).round();
+ if series_idx % args.value_mixed_every.max(1) == args.value_mixed_every.max(1) - 1 {
+ let numerator = splitmix64(series ^ local) % 999 + 1;
+ value + numerator as f64 / 1_000.0
+ } else {
+ value
+ }
+}
+
+fn bounded_mixed_anchor(series: u64, group: u64, span: i64) -> f64 {
+ let limit = span - 1;
+ (splitmix64(series ^ group) % (2 * limit as u64 + 1)) as i64 as f64 - limit as f64
+}
+
fn splitmix64(mut value: u64) -> u64 {
value = value.wrapping_add(0x9e3779b97f4a7c15);
let mut mixed = value;
@@ -307,7 +334,7 @@ mod tests {
for chunk_idx in 0..10 {
let mut chunk = args(ValuePattern::SeededRandom);
chunk.samples_per_series = 1_440;
- chunk.value_sample_offset = chunk_idx * 1_440;
+ chunk.value_sample_offset = chunk_idx * 1_439;
chunk.value_total_samples_per_series = Some(14_400);
for sample_idx in 0..chunk.samples_per_series {
assert_eq!(
@@ -319,6 +346,118 @@ mod tests {
}
}
+ #[test]
+ fn bounded_mixed_has_bounded_95_5_per_series_values() {
+ let mut args = args(ValuePattern::BoundedMixed);
+ args.series_count = 1_000;
+ args.samples_per_series = 4_320;
+ args.value_cardinality = 1_000;
+ args.value_seed = 42;
+ args.value_run_length = 60;
+ args.value_mixed_every = 20;
+
+ let mut integral_series = 0;
+ let mut fractional_series = 0;
+ let mut spans = HashSet::new();
+ let mut ranges = HashSet::new();
+ for series_idx in 0..args.series_count {
+ let series = splitmix64(series_idx ^ args.value_seed);
+ let span = [10_i64, 1_000, 100_000, 10_000_000][(series >> 62) as usize]
+ * (1 + ((series >> 32) % 10) as i64);
+ let baseline = (series % args.value_cardinality.max(1)) as f64 * span as f64 / 10.0;
+ let values = (0..args.samples_per_series)
+ .map(|sample_idx| deterministic_prom_value(&args, series_idx, sample_idx))
+ .collect::>();
+
+ assert!(values.iter().all(|value| value.is_finite()));
+ assert!(
+ values
+ .iter()
+ .all(|value| *value >= baseline - span as f64 - 1.0
+ && *value <= baseline + span as f64 + 1.0)
+ );
+ assert!(
+ values.windows(2).any(|pair| pair[1] > pair[0]),
+ "series {series_idx} never rises"
+ );
+ assert!(
+ values.windows(2).any(|pair| pair[1] < pair[0]),
+ "series {series_idx} never falls"
+ );
+ ranges.insert((
+ values
+ .iter()
+ .copied()
+ .fold(f64::INFINITY, f64::min)
+ .to_bits(),
+ values
+ .iter()
+ .copied()
+ .fold(f64::NEG_INFINITY, f64::max)
+ .to_bits(),
+ ));
+ spans.insert(span);
+ if series_idx % args.value_mixed_every == args.value_mixed_every - 1 {
+ fractional_series += 1;
+ assert!(values.iter().all(|value| value.fract() != 0.0));
+ } else {
+ integral_series += 1;
+ assert!(values.iter().all(|value| value.fract() == 0.0));
+ }
+ }
+
+ assert_eq!((integral_series, fractional_series), (950, 50));
+ assert!(spans.len() > 4);
+ assert!(ranges.len() > 1);
+ }
+
+ #[test]
+ fn bounded_mixed_is_seeded_and_chunk_bit_identical() {
+ let mut monolithic = args(ValuePattern::BoundedMixed);
+ monolithic.samples_per_series = 14_400;
+ monolithic.value_total_samples_per_series = Some(14_400);
+ monolithic.value_cardinality = 1_000;
+ monolithic.value_seed = 42;
+ monolithic.value_run_length = 60;
+ monolithic.value_mixed_every = 20;
+
+ let original = (0..600)
+ .map(|sample_idx| deterministic_prom_value(&monolithic, 19, sample_idx))
+ .collect::>();
+ monolithic.value_seed = 43;
+ assert_ne!(
+ original,
+ (0..600)
+ .map(|sample_idx| deterministic_prom_value(&monolithic, 19, sample_idx))
+ .collect::>()
+ );
+
+ monolithic.value_seed = 42;
+ for series_idx in [0, 19] {
+ for chunk_idx in 0..10 {
+ let mut chunk = args(ValuePattern::BoundedMixed);
+ chunk.samples_per_series = 1_440;
+ chunk.value_sample_offset = chunk_idx * 1_439;
+ chunk.value_total_samples_per_series = Some(14_400);
+ chunk.value_cardinality = 1_000;
+ chunk.value_seed = 42;
+ chunk.value_run_length = 60;
+ chunk.value_mixed_every = 20;
+ for sample_idx in 0..chunk.samples_per_series {
+ assert_eq!(
+ deterministic_prom_value(&chunk, series_idx, sample_idx).to_bits(),
+ deterministic_prom_value(
+ &monolithic,
+ series_idx,
+ chunk.value_sample_offset + sample_idx
+ )
+ .to_bits()
+ );
+ }
+ }
+ }
+ }
+
#[test]
fn values_continue_across_series_boundaries() {
let mut args = args(ValuePattern::Unique);
diff --git a/src/cmd/src/bin/query_regression_runner/model.rs b/src/cmd/src/bin/query_regression_runner/model.rs
index 6831a5256a4..5fafdb5635b 100644
--- a/src/cmd/src/bin/query_regression_runner/model.rs
+++ b/src/cmd/src/bin/query_regression_runner/model.rs
@@ -116,6 +116,10 @@ pub(super) struct RemoteWrite {
pub(super) sample_chunk_size: Option,
pub(super) flush_every_sample_chunks: u64,
pub(super) visibility_timeout_seconds: u64,
+ #[serde(default)]
+ pub(super) base_setup_sql: Vec,
+ #[serde(default)]
+ pub(super) candidate_setup_sql: Vec,
pub(super) prom_store: PromStore,
pub(super) value: RemoteValue,
pub(super) storage: Option,
diff --git a/src/cmd/src/bin/query_regression_runner/remote/ingest.rs b/src/cmd/src/bin/query_regression_runner/remote/ingest.rs
index f563402afcd..271e27fac42 100644
--- a/src/cmd/src/bin/query_regression_runner/remote/ingest.rs
+++ b/src/cmd/src/bin/query_regression_runner/remote/ingest.rs
@@ -57,6 +57,7 @@ pub(super) async fn run_prepare_remote(args: PrepareRemoteArgs) -> Result<()> {
args.base_http_port,
&args.fixture_generator,
&remote,
+ &remote.base_setup_sql,
&client,
)
.await?;
@@ -65,6 +66,7 @@ pub(super) async fn run_prepare_remote(args: PrepareRemoteArgs) -> Result<()> {
args.candidate_http_port,
&args.fixture_generator,
&remote,
+ &remote.candidate_setup_sql,
&client,
)
.await?;
@@ -88,6 +90,7 @@ async fn prepare_remote_target(
port: u16,
generator: &Path,
remote: &RemoteWrite,
+ setup_sql: &[String],
client: &Client,
) -> Result {
let create_database = http_post_sql(
@@ -107,6 +110,7 @@ async fn prepare_remote_target(
)
.into());
}
+ let setup = run_setup_sql(client, port, &remote.database, name, setup_sql).await?;
let (remote_write, flushes) = ingest_remote_write(generator, port, remote, client).await?;
let expected_rows = remote
.series_count
@@ -124,6 +128,7 @@ async fn prepare_remote_target(
Ok(json!({
"name": name,
"create_database": create_database,
+ "setup_sql": setup,
"remote_write": remote_write,
"flushes": flushes,
"visibility": visibility,
@@ -131,6 +136,28 @@ async fn prepare_remote_target(
}))
}
+async fn run_setup_sql(
+ client: &Client,
+ port: u16,
+ database: &str,
+ target: &str,
+ setup_sql: &[String],
+) -> Result> {
+ let mut results = Vec::with_capacity(setup_sql.len());
+ for (index, statement) in setup_sql.iter().enumerate() {
+ let result = http_post_sql(client, port, statement, database).await;
+ if !result["ok"].as_bool().unwrap_or(false) {
+ return Err(format!(
+ "setup SQL statement {} failed for {target}: {result}",
+ index + 1
+ )
+ .into());
+ }
+ results.push(result);
+ }
+ Ok(results)
+}
+
#[derive(Clone, Debug, PartialEq, Eq)]
struct SampleChunk {
index: u64,
@@ -434,9 +461,110 @@ fn json_number(value: f64) -> String {
#[cfg(test)]
mod tests {
+ use tokio::io::{AsyncReadExt, AsyncWriteExt};
+ use tokio::net::{TcpListener, TcpStream};
+
use super::*;
use crate::query_regression_runner::model::{PromStore, RemoteValue};
+ async fn read_http_request(stream: &mut TcpStream) -> Vec {
+ let mut request = Vec::new();
+ let mut chunk = [0; 1024];
+ loop {
+ let read = stream.read(&mut chunk).await.unwrap();
+ assert_ne!(
+ read, 0,
+ "HTTP client closed request before sending its body"
+ );
+ request.extend_from_slice(&chunk[..read]);
+ let Some(header_end) = request.windows(4).position(|window| window == b"\r\n\r\n")
+ else {
+ continue;
+ };
+ let headers = std::str::from_utf8(&request[..header_end]).unwrap();
+ let content_length = headers
+ .lines()
+ .find_map(|line| line.strip_prefix("content-length: "))
+ .or_else(|| {
+ headers
+ .lines()
+ .find_map(|line| line.strip_prefix("Content-Length: "))
+ })
+ .unwrap()
+ .parse::()
+ .unwrap();
+ if request.len() >= header_end + 4 + content_length {
+ return request;
+ }
+ }
+ }
+
+ async fn spawn_sql_server(
+ responses: Vec,
+ ) -> (u16, tokio::task::JoinHandle>) {
+ let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let port = listener.local_addr().unwrap().port();
+ let server = tokio::spawn(async move {
+ let mut requests = Vec::with_capacity(responses.len());
+ for body in responses {
+ let (mut stream, _) = listener.accept().await.unwrap();
+ let request = read_http_request(&mut stream).await;
+ requests.push(String::from_utf8(request).unwrap());
+ let response = format!(
+ "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\
+ Connection: close\r\nContent-Length: {}\r\n\r\n{body}",
+ body.len()
+ );
+ stream.write_all(response.as_bytes()).await.unwrap();
+ }
+ requests
+ });
+ (port, server)
+ }
+
+ #[tokio::test]
+ async fn setup_sql_runs_in_order_with_configured_database() {
+ let (port, server) = spawn_sql_server(vec!["{\"code\":0}".to_string(); 2]).await;
+ let client = Client::new();
+ let setup = vec![
+ "CREATE TABLE physical (ts TIMESTAMP TIME INDEX) ENGINE=metric".to_string(),
+ "ALTER TABLE physical SET 'compaction.twcs.trigger_file_num'='100'".to_string(),
+ ];
+
+ let results = run_setup_sql(&client, port, "perf", "candidate", &setup)
+ .await
+ .unwrap();
+
+ assert_eq!(results.len(), 2);
+ let requests = server.await.unwrap();
+ assert_eq!(requests.len(), 2);
+ assert!(requests[0].contains("db=perf"));
+ assert!(requests[0].contains("sql=CREATE"));
+ assert!(requests[1].contains("db=perf"));
+ assert!(requests[1].contains("sql=ALTER"));
+ }
+
+ #[tokio::test]
+ async fn setup_sql_stops_at_the_failing_statement() {
+ let (port, server) =
+ spawn_sql_server(vec!["{\"code\":0}".to_string(), "{\"code\":1}".to_string()]).await;
+ let client = Client::new();
+ let setup = vec![
+ "CREATE TABLE physical (ts TIMESTAMP TIME INDEX) ENGINE=metric".to_string(),
+ "invalid SQL".to_string(),
+ "ALTER TABLE physical SET 'compaction.twcs.trigger_file_num'='100'".to_string(),
+ ];
+
+ let error = run_setup_sql(&client, port, "perf", "base", &setup)
+ .await
+ .unwrap_err();
+
+ assert!(error.to_string().contains("statement 2 failed for base"));
+ let requests = server.await.unwrap();
+ assert_eq!(requests.len(), 2);
+ assert!(requests[1].contains("sql=invalid+SQL"));
+ }
+
#[test]
fn schedules_remote_sample_chunks_and_flushes() {
let remote = RemoteWrite {
@@ -452,6 +580,8 @@ mod tests {
sample_chunk_size: Some(2),
flush_every_sample_chunks: 2,
visibility_timeout_seconds: 30,
+ base_setup_sql: Vec::new(),
+ candidate_setup_sql: Vec::new(),
prom_store: PromStore {
pending_rows_flush_interval: "1s".to_string(),
max_batch_rows: 1,
diff --git a/src/cmd/src/bin/query_regression_runner/remote/read_bench.rs b/src/cmd/src/bin/query_regression_runner/remote/read_bench.rs
index 6b20a31c181..b9a05f6a69a 100644
--- a/src/cmd/src/bin/query_regression_runner/remote/read_bench.rs
+++ b/src/cmd/src/bin/query_regression_runner/remote/read_bench.rs
@@ -306,13 +306,25 @@ fn run_bench_command(command: Vec, mut run: Value) -> Result {
}
fn parse_average_duration(stdout: &str) -> Option {
- Regex::new(r"(?i)Average duration[^0-9]*([0-9.]+)\s*ms")
- .ok()?
- .captures(stdout)?
- .get(1)?
- .as_str()
- .parse()
- .ok()
+ let captures = Regex::new(
+ r"(?i)Average:\s+(?:\x1b\[[0-9;]*m)*\d+(?:\x1b\[[0-9;]*m)*\s+rows(?:,\s+(?:\x1b\[[0-9;]*m)*\d+(?:\x1b\[[0-9;]*m)*\s+record batches)?\s+in\s+([0-9]+(?:\.[0-9]+)?)\s*(ns|µs|us|ms|s)\s+over\s+\d+\s+iterations|Average duration[^0-9]*([0-9]+(?:\.[0-9]+)?)\s*ms",
+ )
+ .ok()?
+ .captures(stdout)?;
+ let (value, unit) = match (captures.get(1), captures.get(2), captures.get(3)) {
+ (Some(value), Some(unit), _) => (value.as_str(), unit.as_str()),
+ (_, _, Some(value)) => (value.as_str(), "ms"),
+ _ => return None,
+ };
+ let value = value.parse::().ok()?;
+ let milliseconds = match unit.to_ascii_lowercase().as_str() {
+ "ns" => value / 1_000_000.0,
+ "µs" | "us" => value / 1_000.0,
+ "ms" => value,
+ "s" => value * 1_000.0,
+ _ => return None,
+ };
+ milliseconds.is_finite().then_some(milliseconds)
}
fn bench_median(runs: &[Value]) -> Option {
@@ -328,12 +340,51 @@ mod tests {
use super::*;
#[test]
- fn parses_average_duration_and_groups_scan_paths() {
- assert_eq!(
- parse_average_duration("work\nAverage duration: 12.5 ms\n"),
- Some(12.5)
- );
- assert_eq!(parse_average_duration("completed"), None);
+ fn parses_average_durations() {
+ let cases = [
+ (
+ "parquetbench nanoseconds",
+ "ℹ Average: 42 rows, 2 record batches in 500ns over 4 iterations",
+ Some(0.0005),
+ ),
+ (
+ "scanbench colored microseconds",
+ "\x1b[1;32mSummary\x1b[0m Average: \x1b[36m12\x1b[0m rows in 250µs over 2 iterations",
+ Some(0.25),
+ ),
+ (
+ "scanbench seconds",
+ "Summary Average: 12 rows in 2.5s over 2 iterations",
+ Some(2500.0),
+ ),
+ (
+ "legacy milliseconds",
+ "Average duration: 12.5 ms",
+ Some(12.5),
+ ),
+ ("absent", "Benchmark completed!", None),
+ (
+ "malformed",
+ "Summary Average: 12 rows in unknown over 2 iterations",
+ None,
+ ),
+ ];
+
+ for (name, stdout, expected) in cases {
+ let actual = parse_average_duration(stdout);
+ match (actual, expected) {
+ (Some(actual), Some(expected)) => assert!(
+ (actual - expected).abs() < f64::EPSILON,
+ "{name}: expected {expected}, got {actual}"
+ ),
+ (None, None) => {}
+ (actual, expected) => panic!("{name}: expected {expected:?}, got {actual:?}"),
+ }
+ }
+ }
+
+ #[test]
+ fn groups_scan_paths() {
let targets = vec![
BenchTarget {
relative_path: "data/a.parquet".to_string(),
diff --git a/src/common/datasource/src/object_store.rs b/src/common/datasource/src/object_store.rs
index 97bf779413e..37dd11aafdb 100644
--- a/src/common/datasource/src/object_store.rs
+++ b/src/common/datasource/src/object_store.rs
@@ -50,6 +50,22 @@ pub const AZBLOB_SCHEMA: &str = "AZBLOB";
pub struct BuiltBackend {
pub object_store: ObjectStore,
pub object_path: Option,
+ local_root: Option,
+}
+
+impl BuiltBackend {
+ /// Checks an explicit input using the same file-type filter as directory listing.
+ pub async fn is_file(&self, path: &str) -> object_store::Result {
+ // Stat first preserves errors for dangling or inaccessible symlink targets.
+ if self.object_store.stat(path).await?.mode() != object_store::EntryMode::FILE {
+ return Ok(false);
+ }
+ if let Some(root) = &self.local_root {
+ root.is_file(path).await
+ } else {
+ Ok(true)
+ }
+ }
}
/// Controls whether SQL paths may access the local filesystem.
@@ -379,6 +395,7 @@ async fn build_backend_inner(
return Ok(BuiltBackend {
object_store: build_fs_backend(&root)?,
object_path,
+ local_root: Some(root),
});
}
@@ -418,6 +435,7 @@ async fn build_backend_inner(
Ok(BuiltBackend {
object_store,
object_path,
+ local_root: None,
})
}
diff --git a/src/common/function/src/aggrs/vector/avg.rs b/src/common/function/src/aggrs/vector/avg.rs
index ddf1823d283..822a7c16efe 100644
--- a/src/common/function/src/aggrs/vector/avg.rs
+++ b/src/common/function/src/aggrs/vector/avg.rs
@@ -183,8 +183,6 @@ mod tests {
use std::sync::Arc;
use arrow::array::StringArray;
- use datatypes::scalars::ScalarVector;
- use datatypes::vectors::{ConstantVector, StringVector, Vector};
use super::*;
@@ -252,14 +250,12 @@ mod tests {
vec_avg.evaluate().unwrap()
);
- // test update with constant vector
+ // test update with repeated values
let mut vec_avg = VectorAvg::default();
- let v: Vec = vec![
- Arc::new(ConstantVector::new(
- Arc::new(StringVector::from_vec(vec!["[1.0,2.0,3.0]".to_string()])),
- 4,
- ))
- .to_arrow_array(),
+ let v = vec![
+ ScalarValue::Utf8(Some("[1.0,2.0,3.0]".to_string()))
+ .to_array_of_size(4)
+ .unwrap(),
];
vec_avg.update_batch(&v).unwrap();
assert_eq!(
diff --git a/src/common/function/src/aggrs/vector/product.rs b/src/common/function/src/aggrs/vector/product.rs
index d8f201966ab..11cfa4cb1a8 100644
--- a/src/common/function/src/aggrs/vector/product.rs
+++ b/src/common/function/src/aggrs/vector/product.rs
@@ -159,9 +159,6 @@ impl Accumulator for VectorProduct {
mod tests {
use std::sync::Arc;
- use datatypes::scalars::ScalarVector;
- use datatypes::vectors::{ConstantVector, StringVector, Vector};
-
use super::*;
#[test]
@@ -213,14 +210,12 @@ mod tests {
vec_product.update_batch(&v).unwrap();
assert_eq!(ScalarValue::Binary(None), vec_product.evaluate().unwrap());
- // test update with constant vector
+ // test update with repeated values
let mut vec_product = VectorProduct::default();
- let v: Vec = vec![
- Arc::new(ConstantVector::new(
- Arc::new(StringVector::from_vec(vec!["[1.0,2.0,3.0]".to_string()])),
- 4,
- ))
- .to_arrow_array(),
+ let v = vec![
+ ScalarValue::Utf8(Some("[1.0,2.0,3.0]".to_string()))
+ .to_array_of_size(4)
+ .unwrap(),
];
vec_product.update_batch(&v).unwrap();
diff --git a/src/common/function/src/aggrs/vector/sum.rs b/src/common/function/src/aggrs/vector/sum.rs
index 3f875a8412c..0ee8f0df7bf 100644
--- a/src/common/function/src/aggrs/vector/sum.rs
+++ b/src/common/function/src/aggrs/vector/sum.rs
@@ -171,8 +171,6 @@ mod tests {
use std::sync::Arc;
use arrow::array::StringArray;
- use datatypes::scalars::ScalarVector;
- use datatypes::vectors::{ConstantVector, StringVector, Vector};
use super::*;
@@ -225,14 +223,12 @@ mod tests {
vec_sum.update_batch(&v).unwrap();
assert_eq!(ScalarValue::Binary(None), vec_sum.evaluate().unwrap());
- // test update with constant vector
+ // test update with repeated values
let mut vec_sum = VectorSum::default();
- let v: Vec = vec![
- Arc::new(ConstantVector::new(
- Arc::new(StringVector::from_vec(vec!["[1.0,2.0,3.0]".to_string()])),
- 4,
- ))
- .to_arrow_array(),
+ let v = vec![
+ ScalarValue::Utf8(Some("[1.0,2.0,3.0]".to_string()))
+ .to_array_of_size(4)
+ .unwrap(),
];
vec_sum.update_batch(&v).unwrap();
assert_eq!(
diff --git a/src/common/function/src/scalars/expression.rs b/src/common/function/src/scalars/expression.rs
index 63ed40fc8f7..777c91e7c0c 100644
--- a/src/common/function/src/scalars/expression.rs
+++ b/src/common/function/src/scalars/expression.rs
@@ -12,13 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-mod binary;
mod ctx;
mod if_func;
mod is_null;
mod unary;
-pub use binary::scalar_binary_op;
pub use ctx::EvalContext;
pub use unary::scalar_unary_op;
diff --git a/src/common/function/src/scalars/expression/binary.rs b/src/common/function/src/scalars/expression/binary.rs
deleted file mode 100644
index 4d9f6268a89..00000000000
--- a/src/common/function/src/scalars/expression/binary.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright 2023 Greptime Team
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-use std::iter;
-
-use common_query::error::Result;
-use datatypes::prelude::*;
-use datatypes::vectors::{ConstantVector, Helper};
-
-use crate::scalars::expression::ctx::EvalContext;
-
-pub fn scalar_binary_op(
- l: &VectorRef,
- r: &VectorRef,
- f: F,
- ctx: &mut EvalContext,
-) -> Result<::VectorType>
-where
- F: Fn(Option>, Option>, &mut EvalContext) -> Option,
-{
- debug_assert!(
- l.len() == r.len(),
- "Size of vectors must match to apply binary expression"
- );
-
- let result = match (l.is_const(), r.is_const()) {
- (false, true) => {
- let left: &::VectorType = unsafe { Helper::static_cast(l) };
- let right: &ConstantVector = unsafe { Helper::static_cast(r) };
- let right: &::VectorType = unsafe { Helper::static_cast(right.inner()) };
- let b = right.get_data(0);
-
- let it = left.iter_data().map(|a| f(a, b.clone(), ctx));
- ::VectorType::from_owned_iterator(it)
- }
-
- (false, false) => {
- let left: &::VectorType = unsafe { Helper::static_cast(l) };
- let right: &::VectorType = unsafe { Helper::static_cast(r) };
-
- let it = left
- .iter_data()
- .zip(right.iter_data())
- .map(|(a, b)| f(a, b, ctx));
- ::VectorType::from_owned_iterator(it)
- }
-
- (true, false) => {
- let left: &ConstantVector = unsafe { Helper::static_cast(l) };
- let left: &::VectorType = unsafe { Helper::static_cast(left.inner()) };
- let a = left.get_data(0);
-
- let right: &::VectorType = unsafe { Helper::static_cast(r) };
- let it = right.iter_data().map(|b| f(a.clone(), b, ctx));
- ::VectorType::from_owned_iterator(it)
- }
-
- (true, true) => {
- let left: &ConstantVector = unsafe { Helper::static_cast(l) };
- let left: &::VectorType = unsafe { Helper::static_cast(left.inner()) };
- let a = left.get_data(0);
-
- let right: &ConstantVector = unsafe { Helper::static_cast(r) };
- let right: &::VectorType = unsafe { Helper::static_cast(right.inner()) };
- let b = right.get_data(0);
-
- let it = iter::repeat(a)
- .zip(iter::repeat(b))
- .map(|(a, b)| f(a, b, ctx))
- .take(left.len());
- ::VectorType::from_owned_iterator(it)
- }
- };
-
- if let Some(error) = ctx.error.take() {
- return Err(error);
- }
- Ok(result)
-}
diff --git a/src/common/function/src/scalars/test.rs b/src/common/function/src/scalars/test.rs
index 8768d546a32..e7f59a65925 100644
--- a/src/common/function/src/scalars/test.rs
+++ b/src/common/function/src/scalars/test.rs
@@ -14,13 +14,43 @@
use std::fmt;
+use common_query::error::Result;
use datafusion::logical_expr::ColumnarValue;
use datafusion_expr::{ScalarFunctionArgs, Signature, Volatility};
use datatypes::arrow::datatypes::DataType;
+use datatypes::prelude::{Scalar, ScalarVector, VectorRef};
use datatypes::vectors::{Helper, Vector};
use crate::function::{Function, extract_args};
-use crate::scalars::expression::{EvalContext, scalar_binary_op};
+use crate::scalars::expression::EvalContext;
+
+fn scalar_binary_op(
+ l: &VectorRef,
+ r: &VectorRef,
+ f: F,
+ ctx: &mut EvalContext,
+) -> Result<::VectorType>
+where
+ F: Fn(Option>, Option>, &mut EvalContext) -> Option,
+{
+ debug_assert!(
+ l.len() == r.len(),
+ "Size of vectors must match to apply binary expression"
+ );
+
+ let left: &::VectorType = unsafe { Helper::static_cast(l) };
+ let right: &::VectorType = unsafe { Helper::static_cast(r) };
+ let result = ::VectorType::from_owned_iterator(
+ left.iter_data()
+ .zip(right.iter_data())
+ .map(|(a, b)| f(a, b, ctx)),
+ );
+
+ if let Some(error) = ctx.error.take() {
+ return Err(error);
+ }
+ Ok(result)
+}
#[derive(Clone)]
pub(crate) struct TestAndFunction {
diff --git a/src/common/function/src/state.rs b/src/common/function/src/state.rs
index 2b9f3a517a5..f60b37dd36d 100644
--- a/src/common/function/src/state.rs
+++ b/src/common/function/src/state.rs
@@ -12,12 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+use common_base::Plugins;
+
use crate::handlers::{FlowServiceHandlerRef, ProcedureServiceHandlerRef, TableMutationHandlerRef};
/// Shared state for SQL functions.
/// The handlers in state may be `None` in cli command-line or test cases.
#[derive(Clone, Default)]
pub struct FunctionState {
+ /// Process plugins available to dynamically registered functions.
+ pub plugins: Plugins,
// The table mutation handler
pub table_mutation_handler: Option,
// The procedure service handler
@@ -212,6 +216,7 @@ impl FunctionState {
}
Self {
+ plugins: Plugins::default(),
table_mutation_handler: Some(Arc::new(MockTableMutationHandler)),
procedure_service_handler: Some(Arc::new(MockProcedureServiceHandler)),
flow_service_handler: Some(Arc::new(MockFlowServiceHandler)),
diff --git a/src/common/grpc-expr/src/alter.rs b/src/common/grpc-expr/src/alter.rs
index 85de11d5cdb..d5214ae5ddb 100644
--- a/src/common/grpc-expr/src/alter.rs
+++ b/src/common/grpc-expr/src/alter.rs
@@ -49,33 +49,14 @@ const LOCATION_TYPE_AFTER: i32 = LocationType::After as i32;
/// mixed batch — annotation alters skip region dispatch, so they cannot share
/// a statement with options that regions must see.
fn annotation_family_of_keys<'a>(
- mut keys: impl Iterator- ,
+ keys: impl Iterator
- ,
) -> Result
> {
- let Some(first) = keys.next() else {
- return Ok(None);
- };
- let family = AnnotationFamily::of_key(first);
- let mut count = 1usize;
- for key in keys {
- count += 1;
- let this = AnnotationFamily::of_key(key);
- if this != family {
- return error::InvalidTableOptionRequestSnafu {
- err_msg: family.or(this).unwrap().mixed_batch_error(),
- }
- .fail();
+ table::requests::validate_annotation_keys(keys).map_err(|err| {
+ error::InvalidTableOptionRequestSnafu {
+ err_msg: err.to_string(),
}
- }
- if let Some(family) = family
- && family.requires_single_key()
- && count > 1
- {
- return error::InvalidTableOptionRequestSnafu {
- err_msg: family.mixed_batch_error(),
- }
- .fail();
- }
- Ok(family)
+ .build()
+ })
}
/// Returns the annotation family when `kind` is a SET/UNSET whose keys all
@@ -583,6 +564,131 @@ mod tests {
assert_eq!("mem_usage".to_string(), drop_names.pop().unwrap());
}
+ #[test]
+ fn test_repartition_hints_together() {
+ let keys = [
+ REPARTITION_COLUMN_HINT_KEY,
+ table::requests::REPARTITION_PARTITION_NUM_HINT_KEY,
+ ];
+ let options = vec![
+ (keys[0].to_string(), "host".to_string()),
+ (keys[1].to_string(), "10".to_string()),
+ ];
+ let set = Kind::SetTableOptions(SetTableOptions {
+ table_options: options
+ .iter()
+ .map(|(key, value)| PbOption {
+ key: key.clone(),
+ value: value.clone(),
+ })
+ .collect(),
+ });
+ let unset = Kind::UnsetTableOptions(UnsetTableOptions {
+ keys: keys.map(str::to_string).to_vec(),
+ });
+ for kind in [set, unset] {
+ assert_eq!(
+ annotation_alter_family(&kind).unwrap(),
+ Some(AnnotationFamily::RepartitionHint)
+ );
+ let request = alter_expr_to_request(
+ 1,
+ AlterTableExpr {
+ kind: Some(kind),
+ ..Default::default()
+ },
+ None,
+ )
+ .unwrap();
+ match request.alter_kind {
+ AlterKind::SetAnnotations {
+ family,
+ options: actual,
+ } => {
+ assert_eq!(family, AnnotationFamily::RepartitionHint);
+ assert_eq!(actual, options);
+ }
+ AlterKind::UnsetAnnotations {
+ family,
+ keys: actual,
+ } => {
+ assert_eq!(family, AnnotationFamily::RepartitionHint);
+ assert_eq!(actual, keys);
+ }
+ other => panic!("unexpected alter kind: {other:?}"),
+ }
+ }
+ }
+
+ #[test]
+ fn test_repartition_partition_num_hint_expr() {
+ let key = table::requests::REPARTITION_PARTITION_NUM_HINT_KEY;
+ let set = Kind::SetTableOptions(SetTableOptions {
+ table_options: vec![PbOption {
+ key: key.to_string(),
+ value: "8".to_string(),
+ }],
+ });
+ let unset = Kind::UnsetTableOptions(UnsetTableOptions {
+ keys: vec![key.to_string()],
+ });
+ for kind in [set, unset] {
+ assert_eq!(
+ annotation_alter_family(&kind).unwrap(),
+ Some(AnnotationFamily::RepartitionHint)
+ );
+ let request = alter_expr_to_request(
+ 1,
+ AlterTableExpr {
+ kind: Some(kind),
+ ..Default::default()
+ },
+ None,
+ )
+ .unwrap();
+ match request.alter_kind {
+ AlterKind::SetAnnotations { family, options } => {
+ assert_eq!(family, AnnotationFamily::RepartitionHint);
+ assert_eq!(options, vec![(key.to_string(), "8".to_string())]);
+ }
+ AlterKind::UnsetAnnotations { family, keys } => {
+ assert_eq!(family, AnnotationFamily::RepartitionHint);
+ assert_eq!(keys, vec![key.to_string()]);
+ }
+ other => panic!("unexpected alter kind: {other:?}"),
+ }
+ }
+ for other in [key, table::requests::TTL_KEY] {
+ for kind in [
+ Kind::SetTableOptions(SetTableOptions {
+ table_options: [key, other]
+ .into_iter()
+ .map(|key| PbOption {
+ key: key.to_string(),
+ value: "8".to_string(),
+ })
+ .collect(),
+ }),
+ Kind::UnsetTableOptions(UnsetTableOptions {
+ keys: vec![key.to_string(), other.to_string()],
+ }),
+ ] {
+ assert!(annotation_alter_family(&kind).is_err());
+ assert!(
+ alter_expr_to_request(
+ 1,
+ AlterTableExpr {
+ kind: Some(kind),
+ ..Default::default()
+ },
+ None
+ )
+ .is_err()
+ );
+ }
+ }
+ }
+
#[test]
fn test_set_repartition_column_hint_expr() {
let expr = AlterTableExpr {
@@ -633,7 +739,7 @@ mod tests {
let err = alter_expr_to_request(1, expr, None).unwrap_err();
assert!(
err.to_string()
- .contains("repartition.column.hint must be altered separately")
+ .contains("repartition hints must be altered separately")
);
// Duplicate hint entries are not a meaningful batch either.
@@ -655,10 +761,7 @@ mod tests {
})),
};
let err = alter_expr_to_request(1, dup, None).unwrap_err();
- assert!(
- err.to_string()
- .contains("repartition.column.hint must be altered separately")
- );
+ assert!(err.to_string().contains("duplicate repartition hint keys"));
}
#[test]
diff --git a/src/common/macro/src/admin_fn.rs b/src/common/macro/src/admin_fn.rs
index c580389f3c6..053a2ad3f46 100644
--- a/src/common/macro/src/admin_fn.rs
+++ b/src/common/macro/src/admin_fn.rs
@@ -304,7 +304,7 @@ fn build_struct(
.and_then(|cv| match cv {
common_query::prelude::ColumnarValue::Vector(v) => Ok(v),
common_query::prelude::ColumnarValue::Scalar(s) => {
- datatypes::vectors::Helper::try_from_scalar_value(s, args.number_rows)
+ datatypes::vectors::Helper::try_from_scalar_value(s, args.number_rows, None)
.context(common_query::error::FromScalarValueSnafu)
}
})
diff --git a/src/common/macro/src/lib.rs b/src/common/macro/src/lib.rs
index 5a58c4d7fbe..a397292787b 100644
--- a/src/common/macro/src/lib.rs
+++ b/src/common/macro/src/lib.rs
@@ -75,6 +75,10 @@ pub fn as_aggr_func_creator(args: TokenStream, input: TokenStream) -> TokenStrea
/// - `name`: The name of the generated `ScalarUDF` struct.
/// - `ret`: The return type of the generated UDF function.
/// - `display_name`: The display name of the generated UDF function.
+/// - `evaluator`: Optional path to a specialized evaluator with the calling convention
+/// `fn(&[ColumnarValue], &str) -> Result`. When supplied,
+/// the generated UDF `calc` delegates directly to it; without it, the default expansion is
+/// unchanged.
#[proc_macro_attribute]
pub fn range_fn(args: TokenStream, input: TokenStream) -> TokenStream {
process_range_fn(args, input)
diff --git a/src/common/macro/src/range_fn.rs b/src/common/macro/src/range_fn.rs
index 5ee21ca4b04..7d282db0fac 100644
--- a/src/common/macro/src/range_fn.rs
+++ b/src/common/macro/src/range_fn.rs
@@ -16,7 +16,7 @@ use proc_macro::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
use syn::{
- Attribute, Ident, ItemFn, Signature, Type, TypeReference, Visibility, parse_macro_input,
+ Attribute, Ident, ItemFn, Path, Signature, Type, TypeReference, Visibility, parse_macro_input,
};
use crate::utils::extract_input_types;
@@ -34,6 +34,7 @@ pub(crate) fn process_range_fn(args: TokenStream, input: TokenStream) -> TokenSt
let mut name: Option = None;
let mut display_name: Option = None;
let mut ret: Option = None;
+ let mut evaluator: Option = None;
let parser = syn::meta::parser(|meta| {
if meta.path.is_ident("name") {
@@ -45,6 +46,9 @@ pub(crate) fn process_range_fn(args: TokenStream, input: TokenStream) -> TokenSt
} else if meta.path.is_ident("ret") {
ret = Some(meta.value()?.parse()?);
Ok(())
+ } else if meta.path.is_ident("evaluator") {
+ evaluator = Some(meta.value()?.parse()?);
+ Ok(())
} else {
Err(meta.error("unsupported property"))
}
@@ -103,10 +107,19 @@ pub(crate) fn process_range_fn(args: TokenStream, input: TokenStream) -> TokenSt
arg_types,
fn_name.clone(),
ret.expect("ret required"),
+ evaluator.clone(),
);
- // preserve this fn, but remove its `pub` modifier
- let input_fn_code: TokenStream = quote! {
- #sig { #block }
+ // Preserve this fn, but remove its `pub` modifier. Specialized evaluators
+ // do not call it in production, while tests keep it as the slice oracle.
+ let input_fn_code: TokenStream = if evaluator.is_some() {
+ quote! {
+ #[cfg(test)]
+ #sig { #block }
+ }
+ } else {
+ quote! {
+ #sig { #block }
+ }
}
.into();
@@ -161,7 +174,19 @@ fn build_calc_fn(
param_types: Vec,
fn_name: Ident,
ret_type: Ident,
+ evaluator: Option,
) -> TokenStream {
+ if let Some(evaluator) = evaluator {
+ return quote! {
+ impl #name {
+ fn calc(input: &[ColumnarValue]) -> Result {
+ #evaluator(input, Self::name())
+ }
+ }
+ }
+ .into();
+ }
+
let param_names = param_types
.iter()
.enumerate()
diff --git a/src/common/mem-prof/Cargo.toml b/src/common/mem-prof/Cargo.toml
index ee4753ee885..10b67cf7fa7 100644
--- a/src/common/mem-prof/Cargo.toml
+++ b/src/common/mem-prof/Cargo.toml
@@ -16,7 +16,7 @@ tempfile = "3.4"
tokio.workspace = true
[target.'cfg(not(windows))'.dependencies]
-tikv-jemalloc-ctl = { version = "0.6", features = ["use_std", "stats"] }
+tikv-jemalloc-ctl = { version = "0.7", features = ["use_std", "stats"] }
jemalloc-pprof-utils = { version = "0.8", package = "pprof_util", features = [
"flamegraph",
"symbolize",
@@ -25,4 +25,4 @@ jemalloc-pprof-mappings = { version = "0.7", package = "mappings" } # for get th
[target.'cfg(not(windows))'.dependencies.tikv-jemalloc-sys]
features = ["stats", "profiling", "unprefixed_malloc_on_supported_platforms"]
-version = "0.6"
+version = "0.7"
diff --git a/src/common/meta/src/ddl/tests/alter_table.rs b/src/common/meta/src/ddl/tests/alter_table.rs
index 30910eb0ba5..f0ceaf592e1 100644
--- a/src/common/meta/src/ddl/tests/alter_table.rs
+++ b/src/common/meta/src/ddl/tests/alter_table.rs
@@ -1123,58 +1123,85 @@ async fn test_on_submit_alter_request_with_exist_poison() {
}
#[tokio::test]
-async fn test_semantic_annotation_alter_is_metadata_only() {
- let (tx, mut rx) = mpsc::channel(8);
- let node_manager = Arc::new(MockDatanodeManager::new(DatanodeWatcher::new(tx)));
- let ddl_context = new_ddl_context(node_manager);
- let table_id = 1024;
- let table_name = "foo";
- let task = test_create_table_task(table_name, table_id);
- ddl_context
- .table_metadata_manager
- .create_table_metadata(
- task.table_info.clone(),
- prepare_table_route(table_id),
- HashMap::new(),
- )
- .await
- .unwrap();
+async fn test_annotation_alter_is_metadata_only() {
+ for (key, value) in [
+ ("greptime.semantic.signal_type", "metric"),
+ (table::requests::REPARTITION_PARTITION_NUM_HINT_KEY, "8"),
+ ] {
+ let (tx, mut rx) = mpsc::channel(8);
+ let node_manager = Arc::new(MockDatanodeManager::new(DatanodeWatcher::new(tx)));
+ let ddl_context = new_ddl_context(node_manager);
+ let table_id = 1024;
+ let table_name = "foo";
+ let task = test_create_table_task(table_name, table_id);
+ ddl_context
+ .table_metadata_manager
+ .create_table_metadata(
+ task.table_info.clone(),
+ prepare_table_route(table_id),
+ HashMap::new(),
+ )
+ .await
+ .unwrap();
- let alter_table_task = AlterTableTask {
- alter_table: AlterTableExpr {
- catalog_name: DEFAULT_CATALOG_NAME.to_string(),
- schema_name: DEFAULT_SCHEMA_NAME.to_string(),
- table_name: table_name.to_string(),
- kind: Some(Kind::SetTableOptions(SetTableOptions {
- table_options: vec![api::v1::Option {
- key: "greptime.semantic.signal_type".to_string(),
- value: "metric".to_string(),
- }],
- })),
- },
- };
- let mut procedure =
- AlterTableProcedure::new(table_id, alter_table_task, ddl_context.clone()).unwrap();
- execute_procedure_until_done(&mut procedure).await;
+ let alter_table_task = AlterTableTask {
+ alter_table: AlterTableExpr {
+ catalog_name: DEFAULT_CATALOG_NAME.to_string(),
+ schema_name: DEFAULT_SCHEMA_NAME.to_string(),
+ table_name: table_name.to_string(),
+ kind: Some(Kind::SetTableOptions(SetTableOptions {
+ table_options: vec![api::v1::Option {
+ key: key.to_string(),
+ value: value.to_string(),
+ }],
+ })),
+ },
+ };
+ let mut procedure =
+ AlterTableProcedure::new(table_id, alter_table_task, ddl_context.clone()).unwrap();
+ execute_procedure_until_done(&mut procedure).await;
- // Metadata-only: no region request reaches any datanode.
- rx.try_recv().unwrap_err();
+ // Metadata-only: no region request reaches any datanode.
+ rx.try_recv().unwrap_err();
- let table_info = ddl_context
- .table_metadata_manager
- .table_info_manager()
- .get(table_id)
- .await
- .unwrap()
- .unwrap()
- .into_inner()
- .table_info;
- assert_eq!(
- table_info
- .meta
- .options
- .extra_options
- .get("greptime.semantic.signal_type"),
- Some(&"metric".to_string())
- );
+ let table_info = ddl_context
+ .table_metadata_manager
+ .table_info_manager()
+ .get(table_id)
+ .await
+ .unwrap()
+ .unwrap()
+ .into_inner()
+ .table_info;
+ assert_eq!(
+ table_info.meta.options.extra_options.get(key),
+ Some(&value.to_string())
+ );
+ for _ in 0..2 {
+ let task = AlterTableTask {
+ alter_table: AlterTableExpr {
+ catalog_name: DEFAULT_CATALOG_NAME.to_string(),
+ schema_name: DEFAULT_SCHEMA_NAME.to_string(),
+ table_name: table_name.to_string(),
+ kind: Some(Kind::UnsetTableOptions(api::v1::UnsetTableOptions {
+ keys: vec![key.to_string()],
+ })),
+ },
+ };
+ let mut procedure =
+ AlterTableProcedure::new(table_id, task, ddl_context.clone()).unwrap();
+ execute_procedure_until_done(&mut procedure).await;
+ rx.try_recv().unwrap_err();
+ let info = ddl_context
+ .table_metadata_manager
+ .table_info_manager()
+ .get(table_id)
+ .await
+ .unwrap()
+ .unwrap()
+ .into_inner()
+ .table_info;
+ assert!(!info.meta.options.extra_options.contains_key(key));
+ }
+ }
}
diff --git a/src/common/meta/src/ddl_manager.rs b/src/common/meta/src/ddl_manager.rs
index d88f08918e9..30911b7b272 100644
--- a/src/common/meta/src/ddl_manager.rs
+++ b/src/common/meta/src/ddl_manager.rs
@@ -2036,20 +2036,25 @@ mod tests {
// The repartition hint drives physical repartitioning; on a logical
// route it stays rejected by the guard.
- let err = ddl_manager
- .submit_ddl_task(
- ExecutorContext {
- query_context: Some(QueryContext::default()),
- ..Default::default()
- },
- SubmitDdlTaskRequest::new(DdlTask::new_alter_table(set_options_expr(
- "logical",
- &[("repartition.column.hint", "host")],
- ))),
- )
- .await
- .unwrap_err();
- let msg = common_error::ext::ErrorExt::output_msg(&err);
- assert!(msg.contains("non-physical TableRouteValue"), "{msg}");
+ for (key, value) in [
+ (table::requests::REPARTITION_COLUMN_HINT_KEY, "host"),
+ (table::requests::REPARTITION_PARTITION_NUM_HINT_KEY, "8"),
+ ] {
+ let err = ddl_manager
+ .submit_ddl_task(
+ ExecutorContext {
+ query_context: Some(QueryContext::default()),
+ ..Default::default()
+ },
+ SubmitDdlTaskRequest::new(DdlTask::new_alter_table(set_options_expr(
+ "logical",
+ &[(key, value)],
+ ))),
+ )
+ .await
+ .unwrap_err();
+ let msg = common_error::ext::ErrorExt::output_msg(&err);
+ assert!(msg.contains("non-physical TableRouteValue"), "{msg}");
+ }
}
}
diff --git a/src/common/meta/src/error.rs b/src/common/meta/src/error.rs
index 3bd1475cb40..3f4868ffba0 100644
--- a/src/common/meta/src/error.rs
+++ b/src/common/meta/src/error.rs
@@ -269,6 +269,20 @@ pub enum Error {
error: prost::DecodeError,
},
+ #[snafu(display("Failed to decode packed file references"))]
+ DecodePackedFileRefs {
+ #[snafu(implicit)]
+ location: Location,
+ #[snafu(source)]
+ error: base64::DecodeError,
+ },
+
+ #[snafu(display("Invalid packed file references framing"))]
+ InvalidPackedFileRefs {
+ #[snafu(implicit)]
+ location: Location,
+ },
+
#[snafu(display("Failed to encode object into json"))]
EncodeJson {
#[snafu(implicit)]
@@ -1212,6 +1226,8 @@ impl ErrorExt for Error {
| PayloadNotExist { .. }
| ConvertRawKey { .. }
| DecodeProto { .. }
+ | DecodePackedFileRefs { .. }
+ | InvalidPackedFileRefs { .. }
| BuildTableMeta { .. }
| TableRouteNotFound { .. }
| TableRepartNotFound { .. }
diff --git a/src/common/meta/src/heartbeat/handler.rs b/src/common/meta/src/heartbeat/handler.rs
index ecc735083b5..fb5f16653a9 100644
--- a/src/common/meta/src/heartbeat/handler.rs
+++ b/src/common/meta/src/heartbeat/handler.rs
@@ -94,7 +94,21 @@ impl HeartbeatResponseHandlerExecutor for HandlerGroupExecutor {
Ok(HandleControl::Done) => break,
Ok(HandleControl::Continue) => {}
Err(e) => {
- error!(e;"Error while handling: {:?}", ctx.response);
+ let mailbox_message_id = ctx
+ .response
+ .mailbox_message
+ .as_ref()
+ .map(|message| message.id);
+ let json_payload_len =
+ ctx.response.mailbox_message.as_ref().and_then(|message| {
+ message.payload.as_ref().map(|payload| match payload {
+ api::v1::meta::mailbox_message::Payload::Json(json) => json.len(),
+ })
+ });
+ error!(
+ %e;
+ "Error while handling heartbeat response: mailbox_message_id={mailbox_message_id:?}, json_payload_len={json_payload_len:?}"
+ );
break;
}
}
@@ -102,3 +116,86 @@ impl HeartbeatResponseHandlerExecutor for HandlerGroupExecutor {
Ok(())
}
}
+
+#[cfg(test)]
+mod error_log_tests {
+ use std::fmt::Debug;
+ use std::sync::{Arc, Mutex};
+
+ use api::v1::meta::mailbox_message::Payload;
+ use api::v1::meta::{HeartbeatResponse, MailboxMessage};
+ use common_telemetry::tracing::field::{Field, Visit};
+ use common_telemetry::tracing::{Event, Subscriber};
+ use common_telemetry::tracing_subscriber::layer::{Context, SubscriberExt};
+ use common_telemetry::{tracing, tracing_subscriber};
+
+ use super::parse_mailbox_message::ParseMailboxMessageHandler;
+ use super::*;
+ use crate::heartbeat::mailbox::HeartbeatMailbox;
+
+ #[derive(Clone, Default)]
+ struct LogCapture(Arc>>);
+
+ impl tracing_subscriber::Layer for LogCapture
+ where
+ S: Subscriber,
+ {
+ fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
+ let mut visitor = FieldVisitor::default();
+ event.record(&mut visitor);
+ self.0.lock().unwrap().push(visitor.fields.join(", "));
+ }
+ }
+
+ #[derive(Default)]
+ struct FieldVisitor {
+ fields: Vec,
+ }
+
+ impl Visit for FieldVisitor {
+ fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
+ self.fields.push(format!("{}={value:?}", field.name()));
+ }
+
+ fn record_str(&mut self, field: &Field, value: &str) {
+ self.fields.push(format!("{}={value:?}", field.name()));
+ }
+ }
+
+ #[tokio::test(flavor = "current_thread")]
+ async fn test_handler_error_log_excludes_mailbox_payload() {
+ let payload_sentinel = "MALFORMED_PACKED_PAYLOAD_MUST_NOT_BE_LOGGED".repeat(256);
+ let payload = format!(
+ r#"{{"PackedGcRegions":{{"regions":[],"packed_file_refs_manifest":"{}"}}"#,
+ payload_sentinel
+ );
+ let payload_len = payload.len();
+ let capture = LogCapture::default();
+ let subscriber = tracing_subscriber::registry().with(capture.clone());
+ let _guard = tracing::subscriber::set_default(subscriber);
+ let (mailbox_tx, _) = tokio::sync::mpsc::channel(1);
+ let ctx = HeartbeatResponseHandlerContext::new(
+ Arc::new(HeartbeatMailbox::new(mailbox_tx)),
+ HeartbeatResponse {
+ mailbox_message: Some(MailboxMessage {
+ id: 42,
+ subject: "unsafe subject".to_string(),
+ to: "unsafe recipient".to_string(),
+ from: "unsafe sender".to_string(),
+ payload: Some(Payload::Json(payload)),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ );
+ let executor = HandlerGroupExecutor::new(vec![Arc::new(ParseMailboxMessageHandler)]);
+
+ executor.handle(ctx).await.unwrap();
+
+ let logs = capture.0.lock().unwrap().join("\n");
+ assert!(logs.contains("Error while handling heartbeat response"));
+ assert!(logs.contains("mailbox_message_id=Some(42)"));
+ assert!(logs.contains(&format!("json_payload_len=Some({payload_len})")));
+ assert!(!logs.contains(&payload_sentinel));
+ }
+}
diff --git a/src/common/meta/src/instruction.rs b/src/common/meta/src/instruction.rs
index ba4f2e32ab7..60ebe14e37c 100644
--- a/src/common/meta/src/instruction.rs
+++ b/src/common/meta/src/instruction.rs
@@ -16,16 +16,19 @@ use std::collections::{HashMap, HashSet};
use std::fmt::{Display, Formatter};
use std::time::Duration;
+use base64::Engine;
use common_error::ext::{ErrorExt, RetryHint};
use common_error::status_code::StatusCode;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use snafu::ResultExt as _;
use store_api::region_engine::SyncRegionFromRequest;
use store_api::region_request::{RegionFlushReason, RegionRequirements};
-use store_api::storage::{FileRefsManifest, GcReport, RegionId, RegionNumber};
+use store_api::storage::{FileId, FileRef, FileRefsManifest, GcReport, RegionId, RegionNumber};
use strum::Display;
use table::metadata::TableId;
use table::table_name::TableName;
+use crate::error::{DecodePackedFileRefsSnafu, InvalidPackedFileRefsSnafu};
use crate::flow_name::FlowName;
use crate::key::schema_name::SchemaName;
use crate::key::{FlowId, FlowPartitionId};
@@ -579,6 +582,24 @@ impl Display for GetFileRefs {
}
}
+/// Instruction to get file references using the packed manifest format.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct GetPackedFileRefs {
+ /// List of region IDs to get file references from active FileHandles (in-memory).
+ pub query_regions: Vec,
+ /// Mapping from the src region IDs (whose file references to look for) to
+ /// the dst region IDs (where to read the manifests).
+ /// Key: The source region IDs (where files originally came from).
+ /// Value: The set of destination region IDs (whose manifests need to be read).
+ pub related_regions: HashMap>,
+}
+
+impl Display for GetPackedFileRefs {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(f, "GetPackedFileRefs(region_ids={:?})", self.query_regions)
+ }
+}
+
/// Instruction to trigger garbage collection for a region.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GcRegions {
@@ -590,6 +611,147 @@ pub struct GcRegions {
pub full_file_listing: bool,
}
+/// Instruction to trigger garbage collection with a packed file-reference manifest.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct PackedGcRegions {
+ /// The region ID to perform GC on.
+ pub regions: Vec,
+ /// Packed manifest used by the packed-only GC protocol.
+ pub packed_file_refs_manifest: PackedFileRefsManifest,
+ /// Whether to perform a full file listing to find orphan files.
+ pub full_file_listing: bool,
+}
+
+impl Display for PackedGcRegions {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ let file_refs_count = self.packed_file_refs_manifest.file_refs.len();
+ write!(
+ f,
+ "PackedGcRegions(regions={:?}, file_refs_count={}, full_file_listing={})",
+ self.regions, file_refs_count, self.full_file_listing
+ )
+ }
+}
+
+/// A packed, JSON-compatible encoding of a file reference manifest.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
+pub struct PackedFileRefsManifest {
+ /// Required packed file references for each region.
+ pub file_refs: HashMap,
+ #[serde(default)]
+ pub manifest_version: HashMap,
+ #[serde(default)]
+ pub cross_region_refs: HashMap>,
+}
+
+/// Packed file references for one region, encoded with STANDARD Base64.
+///
+/// `files` is required and contains 16-byte UUID records for references whose
+/// `index_version` is `None`. `indexed` is required and contains 24-byte records
+/// for references whose `index_version` is `Some`: a 16-byte UUID followed by
+/// an 8-byte big-endian `u64` version. Decoding fails when either field is not
+/// valid STANDARD Base64 or its decoded bytes do not align to the corresponding
+/// record width.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
+pub struct PackedRegionFileRefs {
+ /// Required STANDARD Base64-encoded 16-byte UUID records.
+ pub files: String,
+ /// Required STANDARD Base64-encoded 16-byte UUID and 8-byte version records.
+ pub indexed: String,
+}
+
+impl PackedFileRefsManifest {
+ /// Packs a file reference manifest using required [`PackedRegionFileRefs`]
+ /// STANDARD Base64 fields.
+ ///
+ /// References whose `index_version` is `None` are encoded as 16-byte UUID
+ /// `files` records; references whose `index_version` is `Some` are encoded
+ /// as 24-byte `indexed` records: a 16-byte UUID followed by an 8-byte
+ /// big-endian `u64` version.
+ pub fn from_manifest(manifest: &FileRefsManifest) -> Self {
+ Self {
+ file_refs: manifest
+ .file_refs
+ .iter()
+ .map(|(region, refs)| (*region, PackedRegionFileRefs::from_refs(refs)))
+ .collect(),
+ manifest_version: manifest.manifest_version.clone(),
+ cross_region_refs: manifest.cross_region_refs.clone(),
+ }
+ }
+
+ /// Decodes this packed representation into a file reference manifest.
+ ///
+ /// Returns a typed error for invalid STANDARD Base64 or malformed record
+ /// framing without retaining encoded payloads.
+ pub fn into_manifest(self) -> crate::error::Result {
+ let mut file_refs = HashMap::new();
+ for (region, encoded) in self.file_refs {
+ file_refs.insert(region, encoded.into_refs(region)?);
+ }
+ Ok(FileRefsManifest {
+ file_refs,
+ manifest_version: self.manifest_version,
+ cross_region_refs: self.cross_region_refs,
+ })
+ }
+}
+
+impl PackedRegionFileRefs {
+ /// Packs file references as the documented STANDARD Base64 record streams.
+ pub fn from_refs(refs: &HashSet) -> Self {
+ let engine = base64::engine::general_purpose::STANDARD;
+ let mut files = Vec::new();
+ let mut indexed = Vec::new();
+ for file_ref in refs {
+ match file_ref.index_version {
+ None => files.extend_from_slice(file_ref.file_id.as_bytes()),
+ Some(version) => {
+ indexed.extend_from_slice(file_ref.file_id.as_bytes());
+ indexed.extend_from_slice(&version.to_be_bytes());
+ }
+ }
+ }
+ Self {
+ files: engine.encode(files),
+ indexed: engine.encode(indexed),
+ }
+ }
+
+ /// Decodes the documented STANDARD Base64 record streams for `region`.
+ fn into_refs(self, region: RegionId) -> crate::error::Result> {
+ let engine = base64::engine::general_purpose::STANDARD;
+ let files = engine
+ .decode(self.files)
+ .context(DecodePackedFileRefsSnafu)?;
+ let indexed = engine
+ .decode(self.indexed)
+ .context(DecodePackedFileRefsSnafu)?;
+ if files.len() % 16 != 0 || indexed.len() % 24 != 0 {
+ return InvalidPackedFileRefsSnafu.fail();
+ }
+
+ let mut refs = HashSet::new();
+ for bytes in files.chunks_exact(16) {
+ let mut id = [0; 16];
+ id.copy_from_slice(bytes);
+ refs.insert(FileRef::new(region, FileId::from_bytes(id), None));
+ }
+ for bytes in indexed.chunks_exact(24) {
+ let mut id = [0; 16];
+ id.copy_from_slice(&bytes[..16]);
+ let mut version = [0; 8];
+ version.copy_from_slice(&bytes[16..]);
+ refs.insert(FileRef::new(
+ region,
+ FileId::from_bytes(id),
+ Some(u64::from_be_bytes(version)),
+ ));
+ }
+ Ok(refs)
+ }
+}
+
impl Display for GcRegions {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
@@ -625,6 +787,26 @@ impl Display for GetFileRefsReply {
}
}
+/// Reply for GetPackedFileRefs instruction.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct GetPackedFileRefsReply {
+ pub packed_file_refs_manifest: PackedFileRefsManifest,
+ pub success: bool,
+ pub error: Option,
+}
+
+impl Display for GetPackedFileRefsReply {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "GetPackedFileRefsReply(success={}, file_refs_count={}, error={:?})",
+ self.success,
+ self.packed_file_refs_manifest.file_refs.len(),
+ self.error
+ )
+ }
+}
+
/// Reply for GC instruction.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GcRegionsReply {
@@ -827,8 +1009,12 @@ pub enum Instruction {
FlushRegions(FlushRegions),
/// Gets file references for regions.
GetFileRefs(GetFileRefs),
+ /// Gets file references using the packed manifest format.
+ GetPackedFileRefs(GetPackedFileRefs),
/// Triggers garbage collection for a region.
GcRegions(GcRegions),
+ /// Triggers garbage collection with a packed file-reference manifest.
+ PackedGcRegions(PackedGcRegions),
/// Temporary suspend serving reads or writes
Suspend,
/// Makes regions enter staging state.
@@ -1111,6 +1297,7 @@ pub enum InstructionReply {
DowngradeRegions(DowngradeRegionsReply),
FlushRegions(FlushRegionReply),
GetFileRefs(GetFileRefsReply),
+ GetPackedFileRefs(GetPackedFileRefsReply),
GcRegions(GcRegionsReply),
EnterStagingRegions(EnterStagingRegionsReply),
SyncRegions(SyncRegionsReply),
@@ -1131,6 +1318,9 @@ impl Display for InstructionReply {
}
Self::FlushRegions(reply) => write!(f, "InstructionReply::FlushRegions({})", reply),
Self::GetFileRefs(reply) => write!(f, "InstructionReply::GetFileRefs({})", reply),
+ Self::GetPackedFileRefs(reply) => {
+ write!(f, "InstructionReply::GetPackedFileRefs({})", reply)
+ }
Self::GcRegions(reply) => write!(f, "InstructionReply::GcRegion({})", reply),
Self::EnterStagingRegions(reply) => {
write!(
@@ -1221,6 +1411,7 @@ impl InstructionReply {
#[cfg(test)]
mod tests {
use std::collections::HashSet;
+ use std::error::Error as _;
use common_error::mock::MockError;
use common_wal::options::WalOptions;
@@ -1771,6 +1962,161 @@ mod tests {
}
}
+ #[test]
+ fn test_legacy_gc_file_refs_compatibility() {
+ #[derive(Debug, Deserialize)]
+ struct LegacyGetFileRefs {
+ query_regions: Vec,
+ related_regions: HashMap>,
+ }
+
+ #[derive(Debug, Deserialize)]
+ struct LegacyGetFileRefsReply {
+ file_refs_manifest: FileRefsManifest,
+ success: bool,
+ error: Option,
+ }
+
+ #[derive(Debug, Deserialize)]
+ enum LegacyInstruction {
+ GetFileRefs(LegacyGetFileRefs),
+ }
+
+ let get_file_refs = Instruction::GetFileRefs(GetFileRefs {
+ query_regions: vec![RegionId::new(7, 3)],
+ related_regions: HashMap::new(),
+ });
+ let get_file_refs_json = serde_json::to_string(&get_file_refs).unwrap();
+ let legacy_get_file_refs: LegacyInstruction =
+ serde_json::from_str(&get_file_refs_json).unwrap();
+ let LegacyInstruction::GetFileRefs(legacy_get_file_refs) = legacy_get_file_refs;
+ assert_eq!(legacy_get_file_refs.query_regions.len(), 1);
+ assert!(legacy_get_file_refs.related_regions.is_empty());
+
+ let reply = GetFileRefsReply {
+ file_refs_manifest: FileRefsManifest::default(),
+ success: true,
+ error: None,
+ };
+ let reply_json = serde_json::to_string(&reply).unwrap();
+ let legacy_reply: LegacyGetFileRefsReply = serde_json::from_str(&reply_json).unwrap();
+ let current_reply: GetFileRefsReply = serde_json::from_str(&reply_json).unwrap();
+ assert!(legacy_reply.success);
+ assert!(legacy_reply.error.is_none());
+ assert!(legacy_reply.file_refs_manifest.file_refs.is_empty());
+ assert_eq!(current_reply, reply);
+
+ let packed = Instruction::PackedGcRegions(PackedGcRegions {
+ regions: vec![],
+ packed_file_refs_manifest: PackedFileRefsManifest::default(),
+ full_file_listing: false,
+ });
+ let packed_json = serde_json::to_string(&packed).unwrap();
+ assert!(serde_json::from_str::(&packed_json).is_err());
+ }
+
+ #[test]
+ fn test_packed_gc_regions_round_trip_is_distinct() {
+ let instruction = Instruction::PackedGcRegions(PackedGcRegions {
+ regions: vec![RegionId::new(7, 3)],
+ packed_file_refs_manifest: PackedFileRefsManifest::default(),
+ full_file_listing: true,
+ });
+ let serialized = serde_json::to_string(&instruction).unwrap();
+ assert!(serialized.contains("PackedGcRegions"));
+ assert_eq!(
+ serde_json::from_str::(&serialized).unwrap(),
+ instruction
+ );
+ }
+
+ #[test]
+ fn test_packed_file_refs_manifest_round_trip_and_validation() {
+ let region = RegionId::new(7, 3);
+ let file_id = FileId::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
+ let mut manifest = FileRefsManifest::default();
+ manifest.file_refs.insert(
+ region,
+ HashSet::from([
+ FileRef::new(region, file_id, None),
+ FileRef::new(region, file_id, Some(0)),
+ FileRef::new(region, file_id, Some(u64::MAX)),
+ ]),
+ );
+ manifest.manifest_version.insert(region, 42);
+ manifest
+ .cross_region_refs
+ .insert(region, HashSet::from([RegionId::new(7, 4)]));
+ let packed = PackedFileRefsManifest::from_manifest(&manifest);
+ assert_eq!(packed.clone().into_manifest().unwrap(), manifest);
+ let mut missing_files = serde_json::to_value(&packed).unwrap();
+ missing_files
+ .get_mut("file_refs")
+ .and_then(serde_json::Value::as_object_mut)
+ .and_then(|file_refs| file_refs.values_mut().next())
+ .and_then(serde_json::Value::as_object_mut)
+ .unwrap()
+ .remove("files");
+ assert!(serde_json::from_value::(missing_files).is_err());
+
+ let mut missing_indexed = serde_json::to_value(&packed).unwrap();
+ missing_indexed
+ .get_mut("file_refs")
+ .and_then(serde_json::Value::as_object_mut)
+ .and_then(|file_refs| file_refs.values_mut().next())
+ .and_then(serde_json::Value::as_object_mut)
+ .unwrap()
+ .remove("indexed");
+ assert!(serde_json::from_value::(missing_indexed).is_err());
+
+ let mut malformed = packed.clone();
+ malformed.file_refs.get_mut(®ion).unwrap().indexed = "!".to_string();
+ let err = malformed.into_manifest().unwrap_err();
+ assert!(matches!(
+ &err,
+ crate::error::Error::DecodePackedFileRefs { .. }
+ ));
+ assert!(
+ err.source()
+ .is_some_and(|source| source.is::())
+ );
+ assert_eq!(err.status_code(), StatusCode::Unexpected);
+ assert_eq!(err.retry_hint(), RetryHint::NonRetryable);
+
+ let mut malformed = packed;
+ malformed.file_refs.get_mut(®ion).unwrap().indexed =
+ base64::engine::general_purpose::STANDARD.encode([0; 1]);
+ let err = malformed.into_manifest().unwrap_err();
+ assert!(matches!(
+ &err,
+ crate::error::Error::InvalidPackedFileRefs { .. }
+ ));
+ assert!(err.source().is_none());
+ assert_eq!(err.status_code(), StatusCode::Unexpected);
+ assert_eq!(err.retry_hint(), RetryHint::NonRetryable);
+ }
+
+ #[test]
+ fn test_packed_file_refs_is_smaller_than_legacy() {
+ let region = RegionId::new(7, 3);
+ let mut manifest = FileRefsManifest::default();
+ manifest.file_refs.insert(
+ region,
+ (0..500)
+ .map(|i| {
+ let id =
+ FileId::parse_str(&format!("00000000-0000-0000-0000-{i:012}")).unwrap();
+ FileRef::new(region, id, None)
+ })
+ .collect(),
+ );
+ let legacy = serde_json::to_vec(&manifest).unwrap().len();
+ let packed = serde_json::to_vec(&PackedFileRefsManifest::from_manifest(&manifest))
+ .unwrap()
+ .len();
+ assert!(packed * 2 < legacy, "packed={packed}, legacy={legacy}");
+ }
+
#[test]
fn test_serialize_get_file_refs_instruction_reply() {
let mut manifest = FileRefsManifest::default();
diff --git a/src/common/meta/src/reconciliation/event.rs b/src/common/meta/src/reconciliation/event.rs
index cff4a0db19d..06629bc48c9 100644
--- a/src/common/meta/src/reconciliation/event.rs
+++ b/src/common/meta/src/reconciliation/event.rs
@@ -28,6 +28,10 @@ use store_api::storage::TableId;
use crate::reconciliation::ResolveStrategy;
+/// Stable event type stored for catalog reconciliation procedures.
+pub(crate) const RECONCILE_CATALOG_EVENT_TYPE: &str = "reconcile_catalog";
+/// Stable event type stored for database reconciliation procedures.
+pub(crate) const RECONCILE_DATABASE_EVENT_TYPE: &str = "reconcile_database";
/// Stable event type stored for logical table reconciliation procedures.
pub(crate) const RECONCILE_LOGICAL_TABLES_EVENT_TYPE: &str = "reconcile_logical_tables";
/// Stable event type stored for physical table reconciliation procedures.
@@ -45,6 +49,23 @@ pub(crate) struct ReconciliationLocator {
}
impl ReconciliationLocator {
+ /// Creates a locator for a catalog.
+ pub(crate) fn catalog(catalog_name: &str) -> Self {
+ Self {
+ catalog_name: Some(catalog_name.to_string()),
+ ..Default::default()
+ }
+ }
+
+ /// Creates a locator for a database.
+ pub(crate) fn database(catalog_name: &str, schema_name: &str) -> Self {
+ Self {
+ catalog_name: Some(catalog_name.to_string()),
+ schema_name: Some(schema_name.to_string()),
+ ..Default::default()
+ }
+ }
+
/// Creates a locator for a physical table with its fully qualified name and ID.
pub(crate) fn physical_table(
catalog_name: &str,
@@ -101,6 +122,229 @@ impl ReconciliationLocator {
}
}
+#[derive(Debug, Serialize)]
+#[serde(untagged)]
+enum ReconcileCatalogPayload {
+ Submitted(CatalogSubmittedPayload),
+ Result(CatalogResultPayload),
+}
+
+#[derive(Debug, Serialize)]
+struct CatalogSubmittedPayload {
+ version: u8,
+ resolve_strategy: &'static str,
+ fail_fast: bool,
+ parallelism: usize,
+}
+
+#[derive(Debug, Serialize)]
+struct CatalogResultPayload {
+ version: u8,
+ complete: bool,
+ processed_database_count: usize,
+ succeeded_database_count: usize,
+ failed_database_count: usize,
+}
+
+/// Event representation for catalog reconciliation.
+#[derive(Debug)]
+pub(crate) struct ReconcileCatalogEvent {
+ locator: ReconciliationLocator,
+ payload: Option,
+}
+
+impl ReconcileCatalogEvent {
+ /// Builds the bounded intent event emitted when catalog reconciliation is submitted.
+ pub(crate) fn submitted(
+ locator: ReconciliationLocator,
+ resolve_strategy: ResolveStrategy,
+ fail_fast: bool,
+ parallelism: usize,
+ ) -> Self {
+ Self {
+ locator,
+ payload: Some(ReconcileCatalogPayload::Submitted(
+ CatalogSubmittedPayload {
+ version: PAYLOAD_VERSION,
+ resolve_strategy: resolve_strategy_name(resolve_strategy),
+ fail_fast,
+ parallelism,
+ },
+ )),
+ }
+ }
+
+ /// Builds a terminal event from the existing volatile reconciliation metrics.
+ ///
+ /// Metrics are best-effort observations from the current process and reset on recovery.
+ pub(crate) fn result(
+ locator: ReconciliationLocator,
+ complete: bool,
+ succeeded_database_count: usize,
+ failed_database_count: usize,
+ ) -> Self {
+ Self {
+ locator,
+ payload: Some(ReconcileCatalogPayload::Result(CatalogResultPayload {
+ version: PAYLOAD_VERSION,
+ complete,
+ processed_database_count: succeeded_database_count + failed_database_count,
+ succeeded_database_count,
+ failed_database_count,
+ })),
+ }
+ }
+
+ /// Builds a catalog lifecycle event whose reconciliation payload is null.
+ pub(crate) fn lifecycle(locator: ReconciliationLocator) -> Self {
+ Self {
+ locator,
+ payload: None,
+ }
+ }
+}
+
+impl Event for ReconcileCatalogEvent {
+ fn event_type(&self) -> &str {
+ RECONCILE_CATALOG_EVENT_TYPE
+ }
+
+ fn json_payload(&self) -> Result {
+ match &self.payload {
+ Some(payload) => serde_json::to_value(payload).context(SerializeEventSnafu),
+ None => Ok(serde_json::Value::Null),
+ }
+ }
+
+ fn extra_schema(&self) -> Vec {
+ ReconciliationLocator::schema()
+ }
+
+ fn extra_rows(&self) -> Result> {
+ Ok(vec![self.locator.row()])
+ }
+
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+#[derive(Debug, Serialize)]
+#[serde(untagged)]
+enum ReconcileDatabasePayload {
+ Submitted(DatabaseSubmittedPayload),
+ Result(DatabaseResultPayload),
+}
+
+#[derive(Debug, Serialize)]
+struct DatabaseSubmittedPayload {
+ version: u8,
+ resolve_strategy: &'static str,
+ fail_fast: bool,
+ parallelism: usize,
+ is_subprocedure: bool,
+}
+
+#[derive(Debug, Serialize)]
+struct DatabaseResultPayload {
+ version: u8,
+ complete: bool,
+ processed_table_count: usize,
+ succeeded_table_count: usize,
+ failed_table_count: usize,
+ succeeded_subprocedure_count: usize,
+ failed_subprocedure_count: usize,
+}
+
+/// Event representation for database reconciliation.
+#[derive(Debug)]
+pub(crate) struct ReconcileDatabaseEvent {
+ locator: ReconciliationLocator,
+ payload: Option,
+}
+
+impl ReconcileDatabaseEvent {
+ /// Builds the bounded intent event emitted when database reconciliation is submitted.
+ pub(crate) fn submitted(
+ locator: ReconciliationLocator,
+ resolve_strategy: ResolveStrategy,
+ fail_fast: bool,
+ parallelism: usize,
+ is_subprocedure: bool,
+ ) -> Self {
+ Self {
+ locator,
+ payload: Some(ReconcileDatabasePayload::Submitted(
+ DatabaseSubmittedPayload {
+ version: PAYLOAD_VERSION,
+ resolve_strategy: resolve_strategy_name(resolve_strategy),
+ fail_fast,
+ parallelism,
+ is_subprocedure,
+ },
+ )),
+ }
+ }
+
+ /// Builds a terminal event from the existing volatile reconciliation metrics.
+ ///
+ /// Metrics are best-effort observations from the current process and reset on recovery.
+ pub(crate) fn result(
+ locator: ReconciliationLocator,
+ complete: bool,
+ succeeded_table_count: usize,
+ failed_table_count: usize,
+ succeeded_subprocedure_count: usize,
+ failed_subprocedure_count: usize,
+ ) -> Self {
+ Self {
+ locator,
+ payload: Some(ReconcileDatabasePayload::Result(DatabaseResultPayload {
+ version: PAYLOAD_VERSION,
+ complete,
+ processed_table_count: succeeded_table_count + failed_table_count,
+ succeeded_table_count,
+ failed_table_count,
+ succeeded_subprocedure_count,
+ failed_subprocedure_count,
+ })),
+ }
+ }
+
+ /// Builds a database lifecycle event whose reconciliation payload is null.
+ pub(crate) fn lifecycle(locator: ReconciliationLocator) -> Self {
+ Self {
+ locator,
+ payload: None,
+ }
+ }
+}
+
+impl Event for ReconcileDatabaseEvent {
+ fn event_type(&self) -> &str {
+ RECONCILE_DATABASE_EVENT_TYPE
+ }
+
+ fn json_payload(&self) -> Result {
+ match &self.payload {
+ Some(payload) => serde_json::to_value(payload).context(SerializeEventSnafu),
+ None => Ok(serde_json::Value::Null),
+ }
+ }
+
+ fn extra_schema(&self) -> Vec {
+ ReconciliationLocator::schema()
+ }
+
+ fn extra_rows(&self) -> Result> {
+ Ok(vec![self.locator.row()])
+ }
+
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
#[derive(Debug, Serialize)]
#[serde(untagged)]
enum ReconcileTablePayload {
@@ -355,12 +599,14 @@ mod tests {
#[test]
fn reconciliation_events_use_the_shared_locator_contract() {
+ let catalog = ReconcileCatalogEvent::lifecycle(ReconciliationLocator::catalog("greptime"));
+ assert_eq!(catalog.event_type(), RECONCILE_CATALOG_EVENT_TYPE);
let table = ReconcileTableEvent::table_lifecycle(ReconciliationLocator::physical_table(
"greptime", "public", "metrics", 42,
));
assert_eq!(table.event_type(), RECONCILE_TABLE_EVENT_TYPE);
assert_eq!(
- table
+ catalog
.extra_schema()
.into_iter()
.map(|column| {
@@ -375,22 +621,22 @@ mod tests {
(
"catalog_name".to_string(),
ColumnDataType::String,
- SemanticType::Field
+ SemanticType::Field,
),
(
"schema_name".to_string(),
ColumnDataType::String,
- SemanticType::Field
+ SemanticType::Field,
),
(
"table_name".to_string(),
ColumnDataType::String,
- SemanticType::Field
+ SemanticType::Field,
),
(
"table_id".to_string(),
ColumnDataType::Uint32,
- SemanticType::Field
+ SemanticType::Field,
),
(
"physical_table_id".to_string(),
@@ -399,6 +645,40 @@ mod tests {
),
]
);
+ assert_eq!(
+ catalog.extra_rows().unwrap(),
+ vec![Row {
+ values: vec![
+ ValueData::StringValue("greptime".to_string()).into(),
+ Value::default(),
+ Value::default(),
+ Value::default(),
+ Value::default(),
+ ],
+ }]
+ );
+ assert_eq!(catalog.json_payload().unwrap(), serde_json::Value::Null);
+
+ let database = ReconcileDatabaseEvent::lifecycle(ReconciliationLocator::database(
+ "greptime", "public",
+ ));
+ assert_eq!(database.event_type(), RECONCILE_DATABASE_EVENT_TYPE);
+ assert_eq!(database.extra_schema(), catalog.extra_schema());
+ assert_eq!(
+ database.extra_rows().unwrap(),
+ vec![Row {
+ values: vec![
+ ValueData::StringValue("greptime".to_string()).into(),
+ ValueData::StringValue("public".to_string()).into(),
+ Value::default(),
+ Value::default(),
+ Value::default(),
+ ],
+ }]
+ );
+ assert_eq!(database.json_payload().unwrap(), serde_json::Value::Null);
+
+ assert_eq!(table.extra_schema(), catalog.extra_schema());
assert_eq!(
table.extra_rows().unwrap(),
vec![Row {
@@ -458,6 +738,22 @@ mod tests {
(ResolveStrategy::UseMetasrv, "use_metasrv"),
(ResolveStrategy::AbortOnConflict, "abort_on_conflict"),
] {
+ let catalog = ReconcileCatalogEvent::submitted(
+ ReconciliationLocator::catalog("greptime"),
+ strategy,
+ true,
+ 16,
+ );
+ assert_eq!(
+ catalog.json_payload().unwrap(),
+ json!({
+ "version": 1,
+ "resolve_strategy": expected,
+ "fail_fast": true,
+ "parallelism": 16,
+ })
+ );
+
let table = ReconcileTableEvent::table_submitted(
ReconciliationLocator::physical_table("greptime", "public", "metrics", 42),
strategy,
@@ -473,6 +769,24 @@ mod tests {
);
}
+ let database = ReconcileDatabaseEvent::submitted(
+ ReconciliationLocator::database("greptime", "public"),
+ ResolveStrategy::UseMetasrv,
+ false,
+ 64,
+ true,
+ );
+ assert_eq!(
+ database.json_payload().unwrap(),
+ json!({
+ "version": 1,
+ "resolve_strategy": "use_metasrv",
+ "fail_fast": false,
+ "parallelism": 64,
+ "is_subprocedure": true,
+ })
+ );
+
let logical_tables = ReconcileLogicalTablesEvent::submitted(
vec![
ReconciliationLocator::logical_table("greptime", "public", "cpu", 43, 42),
@@ -492,6 +806,40 @@ mod tests {
#[test]
fn terminal_payloads_distinguish_complete_and_partial_results() {
+ let catalog =
+ ReconcileCatalogEvent::result(ReconciliationLocator::catalog("greptime"), true, 3, 1);
+ assert_eq!(
+ catalog.json_payload().unwrap(),
+ json!({
+ "version": 1,
+ "complete": true,
+ "processed_database_count": 4,
+ "succeeded_database_count": 3,
+ "failed_database_count": 1,
+ })
+ );
+
+ let database = ReconcileDatabaseEvent::result(
+ ReconciliationLocator::database("greptime", "public"),
+ false,
+ 5,
+ 2,
+ 4,
+ 1,
+ );
+ assert_eq!(
+ database.json_payload().unwrap(),
+ json!({
+ "version": 1,
+ "complete": false,
+ "processed_table_count": 7,
+ "succeeded_table_count": 5,
+ "failed_table_count": 2,
+ "succeeded_subprocedure_count": 4,
+ "failed_subprocedure_count": 1,
+ })
+ );
+
let table = ReconcileTableEvent::table_result(
ReconciliationLocator::physical_table("greptime", "public", "metrics", 42),
false,
diff --git a/src/common/meta/src/reconciliation/reconcile_catalog.rs b/src/common/meta/src/reconciliation/reconcile_catalog.rs
index d3c2e526dd3..e28e81d323d 100644
--- a/src/common/meta/src/reconciliation/reconcile_catalog.rs
+++ b/src/common/meta/src/reconciliation/reconcile_catalog.rs
@@ -18,8 +18,8 @@ use std::time::Instant;
use common_procedure::error::FromJsonSnafu;
use common_procedure::{
- Context as ProcedureContext, Error as ProcedureError, LockKey, Procedure,
- Result as ProcedureResult, Status,
+ Context as ProcedureContext, Error as ProcedureError, EventContext, EventTrigger, LockKey,
+ Procedure, Result as ProcedureResult, Status,
};
use futures::stream::BoxStream;
use serde::{Deserialize, Serialize};
@@ -31,6 +31,9 @@ use crate::key::TableMetadataManagerRef;
use crate::lock_key::CatalogLock;
use crate::metrics;
use crate::node_manager::NodeManagerRef;
+use crate::reconciliation::event::{
+ RECONCILE_CATALOG_EVENT_TYPE, ReconcileCatalogEvent, ReconciliationLocator,
+};
use crate::reconciliation::reconcile_catalog::start::ReconcileCatalogStart;
use crate::reconciliation::reconcile_table::resolve_column_metadata::ResolveStrategy;
use crate::reconciliation::utils::{
@@ -216,6 +219,43 @@ impl Procedure for ReconcileCatalogProcedure {
LockKey::new(vec![CatalogLock::Write(catalog).into()])
}
+
+ fn event(&self, ctx: &EventContext<'_>) -> Option> {
+ if !ctx.event_type_filter.allows(RECONCILE_CATALOG_EVENT_TYPE) {
+ return None;
+ }
+
+ let persistent_ctx = &self.context.persistent_ctx;
+ let locator = ReconciliationLocator::catalog(&persistent_ctx.catalog);
+ let event = match ctx.trigger {
+ EventTrigger::Submitted => ReconcileCatalogEvent::submitted(
+ locator,
+ persistent_ctx.resolve_strategy,
+ persistent_ctx.fast_fail,
+ persistent_ctx.parallelism,
+ ),
+ EventTrigger::Succeeded => self.result_event(locator, true),
+ EventTrigger::Failed | EventTrigger::Poisoned => self.result_event(locator, false),
+ _ => ReconcileCatalogEvent::lifecycle(locator),
+ };
+ Some(Box::new(event))
+ }
+}
+
+impl ReconcileCatalogProcedure {
+ fn result_event(
+ &self,
+ locator: ReconciliationLocator,
+ complete: bool,
+ ) -> ReconcileCatalogEvent {
+ let metrics = &self.context.volatile_ctx.metrics;
+ ReconcileCatalogEvent::result(
+ locator,
+ complete,
+ metrics.succeeded_databases,
+ metrics.failed_databases,
+ )
+ }
}
#[async_trait::async_trait]
@@ -235,3 +275,217 @@ pub(crate) trait State: Sync + Send + Debug {
fn as_any(&self) -> &dyn Any;
}
+
+#[cfg(test)]
+mod tests {
+ use std::sync::Arc;
+
+ use common_event_recorder::{EventTypeFilter, EventTypeFilterRef};
+ use common_procedure::{
+ ChildSubmissionOutcome, EventContext, EventTrigger, Procedure, ProcedureId, ProcedureState,
+ RetryPhase,
+ };
+ use serde_json::{Value, json};
+
+ use super::*;
+ use crate::reconciliation::event::RECONCILE_DATABASE_EVENT_TYPE;
+ use crate::test_util::{MockDatanodeManager, new_ddl_context};
+
+ struct CatalogEventHarness {
+ procedure_id: ProcedureId,
+ lifecycle_state: ProcedureState,
+ event_type_filter: EventTypeFilterRef,
+ }
+
+ impl CatalogEventHarness {
+ fn all() -> Self {
+ Self {
+ procedure_id: ProcedureId::random(),
+ lifecycle_state: ProcedureState::Running,
+ event_type_filter: Arc::new(EventTypeFilter::All),
+ }
+ }
+
+ fn selected(event_types: impl IntoIterator- ) -> Self {
+ Self {
+ event_type_filter: Arc::new(EventTypeFilter::Only(
+ event_types.into_iter().map(str::to_string).collect(),
+ )),
+ ..Self::all()
+ }
+ }
+
+ fn event(
+ &self,
+ procedure: &dyn Procedure,
+ trigger: EventTrigger,
+ ) -> Option
> {
+ procedure.event(&EventContext {
+ procedure_id: self.procedure_id,
+ lifecycle_state: &self.lifecycle_state,
+ trigger,
+ event_type_filter: self.event_type_filter.clone(),
+ event_context: None,
+ })
+ }
+ }
+
+ #[test]
+ fn catalog_submitted_event_reports_intent() {
+ let submitted = CatalogEventHarness::all()
+ .event(&test_procedure(), EventTrigger::Submitted)
+ .unwrap();
+ assert_eq!(submitted.event_type(), RECONCILE_CATALOG_EVENT_TYPE);
+ assert_eq!(
+ submitted.json_payload().unwrap(),
+ json!({
+ "version": 1,
+ "resolve_strategy": "use_latest",
+ "fail_fast": false,
+ "parallelism": 8,
+ })
+ );
+ }
+
+ #[test]
+ fn catalog_non_terminal_lifecycle_events_have_null_payloads() {
+ let events = CatalogEventHarness::all();
+ let mut procedure = test_procedure();
+ procedure.context.volatile_ctx.metrics = populated_metrics();
+
+ for trigger in [
+ EventTrigger::Recovered,
+ EventTrigger::ChildSubmitted {
+ procedure_id: ProcedureId::random(),
+ outcome: ChildSubmissionOutcome::Accepted,
+ },
+ EventTrigger::Retrying {
+ phase: RetryPhase::Execute,
+ attempt: 1,
+ },
+ EventTrigger::RollingBack,
+ ] {
+ assert_eq!(
+ events
+ .event(&procedure, trigger)
+ .unwrap()
+ .json_payload()
+ .unwrap(),
+ Value::Null
+ );
+ }
+ }
+
+ #[test]
+ fn catalog_terminal_events_report_existing_metrics() {
+ let events = CatalogEventHarness::all();
+ let mut procedure = test_procedure();
+ procedure.context.volatile_ctx.metrics = populated_metrics();
+
+ for (trigger, complete) in [
+ (EventTrigger::Succeeded, true),
+ (EventTrigger::Failed, false),
+ (EventTrigger::Poisoned, false),
+ ] {
+ assert_eq!(
+ events
+ .event(&procedure, trigger)
+ .unwrap()
+ .json_payload()
+ .unwrap(),
+ json!({
+ "version": 1,
+ "complete": complete,
+ "processed_database_count": 4,
+ "succeeded_database_count": 3,
+ "failed_database_count": 1,
+ })
+ );
+ }
+ }
+
+ #[test]
+ fn catalog_event_filtering_uses_the_catalog_event_type() {
+ let procedure = test_procedure();
+ assert!(
+ CatalogEventHarness::selected([RECONCILE_CATALOG_EVENT_TYPE])
+ .event(&procedure, EventTrigger::Submitted)
+ .is_some()
+ );
+ assert!(
+ CatalogEventHarness::selected([RECONCILE_DATABASE_EVENT_TYPE])
+ .event(&procedure, EventTrigger::Submitted)
+ .is_none()
+ );
+ assert!(
+ CatalogEventHarness::selected([])
+ .event(&procedure, EventTrigger::Submitted)
+ .is_none()
+ );
+ }
+
+ #[test]
+ fn catalog_recovery_preserves_locator_and_resets_metrics() {
+ let events = CatalogEventHarness::all();
+ let mut procedure = test_procedure();
+ procedure.context.volatile_ctx.metrics = populated_metrics();
+ let original_dump = procedure.dump().unwrap();
+ procedure.context.volatile_ctx.metrics = ReconcileCatalogMetrics::default();
+ assert_eq!(procedure.dump().unwrap(), original_dump);
+
+ let loaded = ReconcileCatalogProcedure::from_json(test_context(), &original_dump).unwrap();
+ assert_eq!(loaded.dump().unwrap(), original_dump);
+ assert_eq!(
+ events
+ .event(&loaded, EventTrigger::Recovered)
+ .unwrap()
+ .extra_rows()
+ .unwrap(),
+ events
+ .event(&procedure, EventTrigger::Submitted)
+ .unwrap()
+ .extra_rows()
+ .unwrap(),
+ );
+ assert_eq!(
+ events
+ .event(&loaded, EventTrigger::Succeeded)
+ .unwrap()
+ .json_payload()
+ .unwrap(),
+ json!({
+ "version": 1,
+ "complete": true,
+ "processed_database_count": 0,
+ "succeeded_database_count": 0,
+ "failed_database_count": 0,
+ })
+ );
+ }
+
+ fn populated_metrics() -> ReconcileCatalogMetrics {
+ ReconcileCatalogMetrics {
+ succeeded_databases: 3,
+ failed_databases: 1,
+ }
+ }
+
+ fn test_procedure() -> ReconcileCatalogProcedure {
+ ReconcileCatalogProcedure::new(
+ test_context(),
+ "greptime".to_string(),
+ false,
+ ResolveStrategy::UseLatest,
+ 8,
+ )
+ }
+
+ fn test_context() -> Context {
+ let ddl_context = new_ddl_context(Arc::new(MockDatanodeManager::new(())));
+ Context {
+ node_manager: ddl_context.node_manager,
+ table_metadata_manager: ddl_context.table_metadata_manager,
+ cache_invalidator: ddl_context.cache_invalidator,
+ }
+ }
+}
diff --git a/src/common/meta/src/reconciliation/reconcile_database.rs b/src/common/meta/src/reconciliation/reconcile_database.rs
index 7d4262877c1..f5e2fe9f9a0 100644
--- a/src/common/meta/src/reconciliation/reconcile_database.rs
+++ b/src/common/meta/src/reconciliation/reconcile_database.rs
@@ -25,8 +25,8 @@ use std::time::Instant;
use async_trait::async_trait;
use common_procedure::error::{FromJsonSnafu, ToJsonSnafu};
use common_procedure::{
- Context as ProcedureContext, Error as ProcedureError, LockKey, Procedure,
- Result as ProcedureResult, Status,
+ Context as ProcedureContext, Error as ProcedureError, EventContext, EventTrigger, LockKey,
+ Procedure, Result as ProcedureResult, Status,
};
use futures::stream::BoxStream;
use serde::{Deserialize, Serialize};
@@ -41,6 +41,9 @@ use crate::key::table_name::TableNameValue;
use crate::lock_key::{CatalogLock, SchemaLock};
use crate::metrics;
use crate::node_manager::NodeManagerRef;
+use crate::reconciliation::event::{
+ RECONCILE_DATABASE_EVENT_TYPE, ReconcileDatabaseEvent, ReconciliationLocator,
+};
use crate::reconciliation::reconcile_database::start::ReconcileDatabaseStart;
use crate::reconciliation::reconcile_table::resolve_column_metadata::ResolveStrategy;
use crate::reconciliation::utils::{
@@ -264,6 +267,47 @@ impl Procedure for ReconcileDatabaseProcedure {
SchemaLock::write(catalog, schema).into(),
])
}
+
+ fn event(&self, ctx: &EventContext<'_>) -> Option> {
+ if !ctx.event_type_filter.allows(RECONCILE_DATABASE_EVENT_TYPE) {
+ return None;
+ }
+
+ let persistent_ctx = &self.context.persistent_ctx;
+ let locator =
+ ReconciliationLocator::database(&persistent_ctx.catalog, &persistent_ctx.schema);
+ let event = match ctx.trigger {
+ EventTrigger::Submitted => ReconcileDatabaseEvent::submitted(
+ locator,
+ persistent_ctx.resolve_strategy,
+ persistent_ctx.fail_fast,
+ persistent_ctx.parallelism,
+ persistent_ctx.is_subprocedure,
+ ),
+ EventTrigger::Succeeded => self.result_event(locator, true),
+ EventTrigger::Failed | EventTrigger::Poisoned => self.result_event(locator, false),
+ _ => ReconcileDatabaseEvent::lifecycle(locator),
+ };
+ Some(Box::new(event))
+ }
+}
+
+impl ReconcileDatabaseProcedure {
+ fn result_event(
+ &self,
+ locator: ReconciliationLocator,
+ complete: bool,
+ ) -> ReconcileDatabaseEvent {
+ let metrics = self.context.metrics();
+ ReconcileDatabaseEvent::result(
+ locator,
+ complete,
+ metrics.succeeded_tables,
+ metrics.failed_tables,
+ metrics.succeeded_procedures,
+ metrics.failed_procedures,
+ )
+ }
}
#[async_trait::async_trait]
@@ -283,3 +327,229 @@ pub(crate) trait State: Sync + Send + Debug {
fn as_any(&self) -> &dyn Any;
}
+
+#[cfg(test)]
+mod tests {
+ use std::sync::Arc;
+
+ use common_event_recorder::{EventTypeFilter, EventTypeFilterRef};
+ use common_procedure::{
+ ChildSubmissionOutcome, EventContext, EventTrigger, Procedure, ProcedureId, ProcedureState,
+ RetryPhase,
+ };
+ use serde_json::{Value, json};
+
+ use super::*;
+ use crate::reconciliation::event::RECONCILE_CATALOG_EVENT_TYPE;
+ use crate::test_util::{MockDatanodeManager, new_ddl_context};
+
+ struct DatabaseEventHarness {
+ procedure_id: ProcedureId,
+ lifecycle_state: ProcedureState,
+ event_type_filter: EventTypeFilterRef,
+ }
+
+ impl DatabaseEventHarness {
+ fn all() -> Self {
+ Self {
+ procedure_id: ProcedureId::random(),
+ lifecycle_state: ProcedureState::Running,
+ event_type_filter: Arc::new(EventTypeFilter::All),
+ }
+ }
+
+ fn selected(event_types: impl IntoIterator- ) -> Self {
+ Self {
+ event_type_filter: Arc::new(EventTypeFilter::Only(
+ event_types.into_iter().map(str::to_string).collect(),
+ )),
+ ..Self::all()
+ }
+ }
+
+ fn event(
+ &self,
+ procedure: &dyn Procedure,
+ trigger: EventTrigger,
+ ) -> Option
> {
+ procedure.event(&EventContext {
+ procedure_id: self.procedure_id,
+ lifecycle_state: &self.lifecycle_state,
+ trigger,
+ event_type_filter: self.event_type_filter.clone(),
+ event_context: None,
+ })
+ }
+ }
+
+ #[test]
+ fn database_submitted_events_cover_root_and_child_intent() {
+ let events = DatabaseEventHarness::all();
+ let root = test_procedure(false);
+ let child = test_procedure(true);
+ for (procedure, is_subprocedure) in [(&root, false), (&child, true)] {
+ let submitted = events.event(procedure, EventTrigger::Submitted).unwrap();
+ assert_eq!(submitted.event_type(), RECONCILE_DATABASE_EVENT_TYPE);
+ assert_eq!(
+ submitted.json_payload().unwrap(),
+ json!({
+ "version": 1,
+ "resolve_strategy": "use_metasrv",
+ "fail_fast": false,
+ "parallelism": 8,
+ "is_subprocedure": is_subprocedure,
+ })
+ );
+ }
+ }
+
+ #[test]
+ fn database_non_terminal_lifecycle_events_have_null_payloads() {
+ let events = DatabaseEventHarness::all();
+ let mut procedure = test_procedure(true);
+ procedure.context.volatile_ctx.metrics = populated_metrics();
+
+ for trigger in [
+ EventTrigger::Recovered,
+ EventTrigger::ChildSubmitted {
+ procedure_id: ProcedureId::random(),
+ outcome: ChildSubmissionOutcome::Accepted,
+ },
+ EventTrigger::Retrying {
+ phase: RetryPhase::Execute,
+ attempt: 2,
+ },
+ EventTrigger::RollingBack,
+ ] {
+ assert_eq!(
+ events
+ .event(&procedure, trigger)
+ .unwrap()
+ .json_payload()
+ .unwrap(),
+ Value::Null
+ );
+ }
+ }
+
+ #[test]
+ fn database_terminal_events_report_existing_metrics() {
+ let events = DatabaseEventHarness::all();
+ let mut procedure = test_procedure(true);
+ procedure.context.volatile_ctx.metrics = populated_metrics();
+
+ for (trigger, complete) in [
+ (EventTrigger::Succeeded, true),
+ (EventTrigger::Failed, false),
+ (EventTrigger::Poisoned, false),
+ ] {
+ assert_eq!(
+ events
+ .event(&procedure, trigger)
+ .unwrap()
+ .json_payload()
+ .unwrap(),
+ json!({
+ "version": 1,
+ "complete": complete,
+ "processed_table_count": 8,
+ "succeeded_table_count": 5,
+ "failed_table_count": 3,
+ "succeeded_subprocedure_count": 3,
+ "failed_subprocedure_count": 2,
+ })
+ );
+ }
+ }
+
+ #[test]
+ fn database_event_filtering_uses_the_database_event_type() {
+ let procedure = test_procedure(false);
+ assert!(
+ DatabaseEventHarness::selected([RECONCILE_DATABASE_EVENT_TYPE])
+ .event(&procedure, EventTrigger::Submitted)
+ .is_some()
+ );
+ assert!(
+ DatabaseEventHarness::selected([RECONCILE_CATALOG_EVENT_TYPE])
+ .event(&procedure, EventTrigger::Submitted)
+ .is_none()
+ );
+ assert!(
+ DatabaseEventHarness::selected([])
+ .event(&procedure, EventTrigger::Submitted)
+ .is_none()
+ );
+ }
+
+ #[test]
+ fn database_recovery_preserves_locator_and_resets_metrics() {
+ let events = DatabaseEventHarness::all();
+ let mut procedure = test_procedure(false);
+ procedure.context.volatile_ctx.metrics = populated_metrics();
+ let original_dump = procedure.dump().unwrap();
+ procedure.context.volatile_ctx.metrics = ReconcileDatabaseMetrics::default();
+ assert_eq!(procedure.dump().unwrap(), original_dump);
+
+ let loaded = ReconcileDatabaseProcedure::from_json(test_context(), &original_dump).unwrap();
+ assert_eq!(loaded.dump().unwrap(), original_dump);
+ assert_eq!(
+ events
+ .event(&loaded, EventTrigger::Recovered)
+ .unwrap()
+ .extra_rows()
+ .unwrap(),
+ events
+ .event(&procedure, EventTrigger::Submitted)
+ .unwrap()
+ .extra_rows()
+ .unwrap(),
+ );
+ assert_eq!(
+ events
+ .event(&loaded, EventTrigger::Succeeded)
+ .unwrap()
+ .json_payload()
+ .unwrap(),
+ json!({
+ "version": 1,
+ "complete": true,
+ "processed_table_count": 0,
+ "succeeded_table_count": 0,
+ "failed_table_count": 0,
+ "succeeded_subprocedure_count": 0,
+ "failed_subprocedure_count": 0,
+ })
+ );
+ }
+
+ fn populated_metrics() -> ReconcileDatabaseMetrics {
+ ReconcileDatabaseMetrics {
+ succeeded_tables: 5,
+ failed_tables: 3,
+ succeeded_procedures: 3,
+ failed_procedures: 2,
+ }
+ }
+
+ fn test_procedure(is_subprocedure: bool) -> ReconcileDatabaseProcedure {
+ ReconcileDatabaseProcedure::new(
+ test_context(),
+ "greptime".to_string(),
+ "public".to_string(),
+ false,
+ 8,
+ ResolveStrategy::UseMetasrv,
+ is_subprocedure,
+ )
+ }
+
+ fn test_context() -> Context {
+ let ddl_context = new_ddl_context(Arc::new(MockDatanodeManager::new(())));
+ Context {
+ node_manager: ddl_context.node_manager,
+ table_metadata_manager: ddl_context.table_metadata_manager,
+ cache_invalidator: ddl_context.cache_invalidator,
+ }
+ }
+}
diff --git a/src/common/pprof/Cargo.toml b/src/common/pprof/Cargo.toml
index 6d96b26d9fd..e3a5bce78ea 100644
--- a/src/common/pprof/Cargo.toml
+++ b/src/common/pprof/Cargo.toml
@@ -11,11 +11,17 @@ snafu.workspace = true
tokio.workspace = true
[target.'cfg(unix)'.dependencies]
-pprof = { version = "0.14", features = [
+pprof = { version = "0.15", features = [
"flamegraph",
"prost-codec",
"protobuf",
] }
+# framehop-unwinder is only available on x86_64/aarch64 Linux/macOS.
+# Enabling it on other targets (e.g. riscv64) would disable the
+# backtrace-rs fallback without providing a TraceImpl, breaking the build.
+[target.'cfg(all(unix, any(target_arch = "x86_64", target_arch = "aarch64"), any(target_os = "linux", target_os = "macos")))'.dependencies]
+pprof = { version = "0.15", features = ["framehop-unwinder"] }
+
[lints]
workspace = true
diff --git a/src/common/telemetry/src/logging/file_retention.rs b/src/common/telemetry/src/logging/file_retention.rs
index 49cc976db36..6ea9846be1a 100644
--- a/src/common/telemetry/src/logging/file_retention.rs
+++ b/src/common/telemetry/src/logging/file_retention.rs
@@ -453,7 +453,9 @@ pub(crate) fn build_file_appender(
#[cfg(test)]
mod tests {
- use std::fs::{self, File};
+ #[cfg(unix)]
+ use std::fs;
+ use std::fs::File;
use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::symlink;
@@ -464,8 +466,11 @@ mod tests {
use tempfile::TempDir;
use super::FileIndex;
+ #[cfg(unix)]
use crate::logging::LoggingOptions;
- use crate::logging::file_retention::{DirectoryRetention, LogFileKind, build_file_appender};
+ #[cfg(unix)]
+ use crate::logging::file_retention::build_file_appender;
+ use crate::logging::file_retention::{DirectoryRetention, LogFileKind};
fn write_file(path: &Path, contents: &[u8]) {
let mut file = File::create(path).unwrap();
diff --git a/src/datanode/src/heartbeat.rs b/src/datanode/src/heartbeat.rs
index 0e0c51624e3..f8d92212728 100644
--- a/src/datanode/src/heartbeat.rs
+++ b/src/datanode/src/heartbeat.rs
@@ -18,6 +18,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use api::v1::meta::heartbeat_request::NodeWorkloads;
+use api::v1::meta::mailbox_message::Payload;
use api::v1::meta::{DatanodeWorkloads, HeartbeatRequest, NodeInfo, Peer, RegionRole, RegionStat};
use common_base::Plugins;
use common_meta::cache_invalidator::CacheInvalidatorRef;
@@ -146,7 +147,15 @@ impl HeartbeatTask {
None
}) {
if let Some(msg) = res.mailbox_message.as_ref() {
- info!("Received mailbox message: {msg:?}, meta_client id: {client_id:?}");
+ let payload_len = msg
+ .payload
+ .as_ref()
+ .map(|Payload::Json(payload)| payload.len())
+ .unwrap_or_default();
+ info!(
+ message_id = msg.id,
+ payload_len, client_id, "Received mailbox message"
+ );
}
if let Some(lease) = res.region_lease.as_ref() {
metrics::LAST_RECEIVED_HEARTBEAT_ELAPSED
@@ -193,7 +202,8 @@ impl HeartbeatTask {
ctx: HeartbeatResponseHandlerContext,
handler_executor: HeartbeatResponseHandlerExecutorRef,
) -> Result<()> {
- trace!("Heartbeat response: {:?}", ctx.response);
+ let mailbox_message_id = ctx.response.mailbox_message.as_ref().map(|msg| msg.id);
+ trace!(?mailbox_message_id, "Handling heartbeat response");
handler_executor
.handle(ctx)
.await
@@ -379,7 +389,8 @@ impl HeartbeatTask {
.set(last_sent.elapsed().as_millis() as i64);
// Resets the timer.
last_sent = Instant::now();
- debug!("Sending heartbeat request: {:?}", req);
+ let mailbox_message_id = req.mailbox_message.as_ref().map(|msg| msg.id);
+ debug!(?mailbox_message_id, "Sending heartbeat request");
if let Err(e) = tx.send(req).await {
error!(e; "Failed to send heartbeat to metasrv");
match Self::create_streams(
diff --git a/src/datanode/src/heartbeat/handler.rs b/src/datanode/src/heartbeat/handler.rs
index 71797592c3b..2d18d56004b 100644
--- a/src/datanode/src/heartbeat/handler.rs
+++ b/src/datanode/src/heartbeat/handler.rs
@@ -41,9 +41,9 @@ use crate::heartbeat::handler::apply_staging_manifest::ApplyStagingManifestsHand
use crate::heartbeat::handler::close_region::CloseRegionsHandler;
use crate::heartbeat::handler::downgrade_region::DowngradeRegionsHandler;
use crate::heartbeat::handler::enter_staging::EnterStagingRegionsHandler;
-use crate::heartbeat::handler::file_ref::GetFileRefsHandler;
+use crate::heartbeat::handler::file_ref::{GetFileRefsHandler, GetPackedFileRefsHandler};
use crate::heartbeat::handler::flush_region::FlushRegionsHandler;
-use crate::heartbeat::handler::gc_worker::GcRegionsHandler;
+use crate::heartbeat::handler::gc_worker::{GcRegionsHandler, PackedGcRegionsHandler};
use crate::heartbeat::handler::open_region::OpenRegionsHandler;
use crate::heartbeat::handler::remap_manifest::RemapManifestHandler;
use crate::heartbeat::handler::sync_region::SyncRegionHandler;
@@ -135,7 +135,11 @@ impl RegionHeartbeatResponseHandler {
.into(),
))),
Instruction::GetFileRefs(_) => Ok(Some(Box::new(GetFileRefsHandler.into()))),
+ Instruction::GetPackedFileRefs(_) => {
+ Ok(Some(Box::new(GetPackedFileRefsHandler.into())))
+ }
Instruction::GcRegions(_) => Ok(Some(Box::new(GcRegionsHandler.into()))),
+ Instruction::PackedGcRegions(_) => Ok(Some(Box::new(PackedGcRegionsHandler.into()))),
Instruction::InvalidateCaches(_) => InvalidHeartbeatResponseSnafu.fail(),
Instruction::Suspend => Ok(None),
Instruction::EnterStagingRegions(_) => {
@@ -159,7 +163,9 @@ pub enum InstructionHandlers {
DowngradeRegions(DowngradeRegionsHandler),
UpgradeRegions(UpgradeRegionsHandler),
GetFileRefs(GetFileRefsHandler),
+ GetPackedFileRefs(GetPackedFileRefsHandler),
GcRegions(GcRegionsHandler),
+ PackedGcRegions(PackedGcRegionsHandler),
EnterStagingRegions(EnterStagingRegionsHandler),
SyncRegions(SyncRegionHandler),
RemapManifest(RemapManifestHandler),
@@ -192,7 +198,9 @@ impl_from_handler!(
DowngradeRegionsHandler => DowngradeRegions,
UpgradeRegionsHandler => UpgradeRegions,
GetFileRefsHandler => GetFileRefs,
+ GetPackedFileRefsHandler => GetPackedFileRefs,
GcRegionsHandler => GcRegions,
+ PackedGcRegionsHandler => PackedGcRegions,
EnterStagingRegionsHandler => EnterStagingRegions,
SyncRegionHandler => SyncRegions,
RemapManifestHandler => RemapManifest,
@@ -240,7 +248,9 @@ dispatch_instr!(
DowngradeRegions => DowngradeRegions,
UpgradeRegions => UpgradeRegions,
GetFileRefs => GetFileRefs,
+ GetPackedFileRefs => GetPackedFileRefs,
GcRegions => GcRegions,
+ PackedGcRegions => PackedGcRegions,
EnterStagingRegions => EnterStagingRegions,
SyncRegions => SyncRegions,
RemapManifest => RemapManifest,
diff --git a/src/datanode/src/heartbeat/handler/file_ref.rs b/src/datanode/src/heartbeat/handler/file_ref.rs
index ef9cbd4158e..554f4119b05 100644
--- a/src/datanode/src/heartbeat/handler/file_ref.rs
+++ b/src/datanode/src/heartbeat/handler/file_ref.rs
@@ -13,7 +13,10 @@
// limitations under the License.
use common_error::ext::ErrorExt;
-use common_meta::instruction::{GetFileRefs, GetFileRefsReply, InstructionError, InstructionReply};
+use common_meta::instruction::{
+ GetFileRefs, GetFileRefsReply, GetPackedFileRefs, GetPackedFileRefsReply, InstructionError,
+ InstructionReply, PackedFileRefsManifest,
+};
use store_api::storage::FileRefsManifest;
use crate::heartbeat::handler::{HandlerContext, InstructionHandler};
@@ -65,3 +68,54 @@ impl InstructionHandler for GetFileRefsHandler {
}
}
}
+
+pub struct GetPackedFileRefsHandler;
+
+#[async_trait::async_trait]
+impl InstructionHandler for GetPackedFileRefsHandler {
+ type Instruction = GetPackedFileRefs;
+
+ async fn handle(
+ &self,
+ ctx: &HandlerContext,
+ get_file_refs: Self::Instruction,
+ ) -> Option {
+ let region_server = &ctx.region_server;
+ let Some(mito_engine) = region_server.mito_engine() else {
+ return Some(InstructionReply::GetPackedFileRefs(
+ GetPackedFileRefsReply {
+ packed_file_refs_manifest: PackedFileRefsManifest::default(),
+ success: false,
+ error: Some(InstructionError::legacy_internal_retryable(
+ "MitoEngine not found",
+ )),
+ },
+ ));
+ };
+ match mito_engine
+ .get_snapshot_of_file_refs(get_file_refs.query_regions, get_file_refs.related_regions)
+ .await
+ {
+ Ok(all_file_refs) => Some(InstructionReply::GetPackedFileRefs(
+ GetPackedFileRefsReply {
+ packed_file_refs_manifest: PackedFileRefsManifest::from_manifest(
+ &all_file_refs,
+ ),
+ success: true,
+ error: None,
+ },
+ )),
+ Err(e) => Some(InstructionReply::GetPackedFileRefs(
+ GetPackedFileRefsReply {
+ packed_file_refs_manifest: PackedFileRefsManifest::default(),
+ success: false,
+ error: Some(InstructionError::new(
+ e.status_code(),
+ format!("Failed to get file refs: {}", e.output_msg()),
+ e.retry_hint(),
+ )),
+ },
+ )),
+ }
+ }
+}
diff --git a/src/datanode/src/heartbeat/handler/gc_worker.rs b/src/datanode/src/heartbeat/handler/gc_worker.rs
index b32a1c29ad4..e63d21b6e5b 100644
--- a/src/datanode/src/heartbeat/handler/gc_worker.rs
+++ b/src/datanode/src/heartbeat/handler/gc_worker.rs
@@ -15,7 +15,7 @@
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
-use common_meta::instruction::{GcRegions, GcRegionsReply, InstructionReply};
+use common_meta::instruction::{GcRegions, GcRegionsReply, InstructionReply, PackedGcRegions};
use common_meta::key::table_info::TableInfoManager;
use common_meta::key::table_route::TableRouteManager;
use common_telemetry::{debug, warn};
@@ -34,6 +34,7 @@ use crate::error::{GcMitoEngineSnafu, GetMetadataSnafu, Result, UnexpectedSnafu}
use crate::heartbeat::handler::{HandlerContext, InstructionHandler};
pub struct GcRegionsHandler;
+pub struct PackedGcRegionsHandler;
#[async_trait::async_trait]
impl InstructionHandler for GcRegionsHandler {
@@ -67,7 +68,7 @@ impl InstructionHandler for GcRegionsHandler {
.push(rid);
}
- let file_refs_manifest = gc_regions.file_refs_manifest.clone();
+ let file_refs_manifest = gc_regions.file_refs_manifest;
let full_file_listing = gc_regions.full_file_listing;
let ctx_clone = ctx.clone();
@@ -138,6 +139,36 @@ impl InstructionHandler for GcRegionsHandler {
}
}
+#[async_trait::async_trait]
+impl InstructionHandler for PackedGcRegionsHandler {
+ type Instruction = PackedGcRegions;
+
+ async fn handle(
+ &self,
+ ctx: &HandlerContext,
+ instruction: Self::Instruction,
+ ) -> Option {
+ let manifest = match instruction.packed_file_refs_manifest.into_manifest() {
+ Ok(manifest) => manifest,
+ Err(err) => {
+ return Some(InstructionReply::GcRegions(GcRegionsReply {
+ result: Err(common_meta::instruction::InstructionError::from_error(&err)),
+ }));
+ }
+ };
+ GcRegionsHandler
+ .handle(
+ ctx,
+ GcRegions {
+ regions: instruction.regions,
+ file_refs_manifest: manifest,
+ full_file_listing: instruction.full_file_listing,
+ },
+ )
+ .await
+ }
+}
+
impl GcRegionsHandler {
/// Create a GC worker for the given table and region IDs.
async fn create_gc_worker(
diff --git a/src/datanode/src/region_server.rs b/src/datanode/src/region_server.rs
index 79d0dd19152..10bcf32b13d 100644
--- a/src/datanode/src/region_server.rs
+++ b/src/datanode/src/region_server.rs
@@ -2018,6 +2018,7 @@ mod tests {
assert!(RegionServerInner::is_ingest_request(&RegionRequest::Put(
RegionPutRequest {
+ skip_wal: false,
rows: rows(),
hint: None,
partition_expr_version: None,
diff --git a/src/datatypes/src/schema/constraint.rs b/src/datatypes/src/schema/constraint.rs
index 60025be68e4..d946a92bec5 100644
--- a/src/datatypes/src/schema/constraint.rs
+++ b/src/datatypes/src/schema/constraint.rs
@@ -23,7 +23,7 @@ use crate::error::{self, Result};
use crate::types::cast;
use crate::value::Value;
use crate::vectors::operations::VectorOp;
-use crate::vectors::{TimestampMillisecondVector, VectorRef};
+use crate::vectors::{Helper, TimestampMillisecondVector, VectorRef};
pub const CURRENT_TIMESTAMP: &str = "current_timestamp";
pub const CURRENT_TIMESTAMP_FN: &str = "current_timestamp()";
@@ -151,15 +151,20 @@ impl ColumnDefaultConstraint {
ColumnDefaultConstraint::Value(v) => {
ensure!(is_nullable || !v.is_null(), error::NullDefaultSnafu);
- // TODO(yingwen):
- // 1. For null value, we could use NullVector once it supports custom logical type.
- // 2. For non null value, we could use ConstantVector, but it would cause all codes
- // attempt to downcast the vector fail if they don't check whether the vector is const
- // first.
- let mut mutable_vector = data_type.create_mutable_vector(1);
- mutable_vector.try_push_value_ref(&v.as_value_ref())?;
- let base_vector = mutable_vector.to_vector();
- Ok(base_vector.replicate(&[num_rows]))
+ if let Ok(vector) = v.try_to_scalar_value(data_type).and_then(|scalar| {
+ Helper::try_from_scalar_value(scalar, num_rows, Some(data_type))
+ }) {
+ return Ok(vector);
+ }
+
+ // Some extension values, such as JSON nested in a struct, cannot safely
+ // round-trip through ScalarValue. Preserve their logical type with the
+ // type-specific vector builder instead.
+ let mut mutable_vector = data_type.create_mutable_vector(num_rows);
+ for _ in 0..num_rows {
+ mutable_vector.try_push_value_ref(&v.as_value_ref())?;
+ }
+ Ok(mutable_vector.to_vector())
}
}
}
@@ -391,6 +396,82 @@ mod tests {
assert_eq!(Value::Int32(10), v);
}
+ #[test]
+ fn test_struct_default_null_fields_and_json() {
+ use crate::types::{StructField, StructType};
+ use crate::value::StructValue;
+
+ let inner_type = StructType::from([StructField::new(
+ "x",
+ ConcreteDataType::int32_datatype(),
+ true,
+ )]);
+ let inner = Value::Struct(StructValue::new(vec![Value::Null], inner_type));
+ let json = crate::json::JsonSettings::default()
+ .encode(serde_json::json!({"answer": 42}))
+ .unwrap();
+ let nested_type = StructType::from([StructField::new("nested", inner.data_type(), true)]);
+ let json_type = StructType::from([StructField::new("json", json.data_type(), true)]);
+ let values = [
+ inner.clone(),
+ Value::Struct(StructValue::new(vec![inner], nested_type)),
+ Value::Struct(StructValue::new(vec![], StructType::default())),
+ Value::Struct(StructValue::new(vec![json], json_type)),
+ ];
+ for value in values {
+ let data_type = value.data_type();
+ // JSON children are read back as their underlying struct values.
+ let expected = serde_json::Value::try_from(value.clone()).unwrap();
+ for num_rows in [1, 3] {
+ let vector = ColumnDefaultConstraint::Value(value.clone())
+ .create_default_vector(&data_type, false, num_rows)
+ .unwrap();
+ assert_eq!(data_type, vector.data_type());
+ assert_eq!(
+ data_type.as_arrow_type(),
+ *vector.to_arrow_array().data_type()
+ );
+ assert_eq!(num_rows, vector.len());
+ assert_eq!(0, vector.null_count());
+ for row in 0..num_rows {
+ assert_eq!(
+ expected,
+ serde_json::Value::try_from(vector.get(row)).unwrap()
+ );
+ }
+ }
+ }
+ }
+
+ #[test]
+ fn test_string_default_preserves_batch_schema() {
+ use arrow::datatypes::{Field, Schema};
+ use arrow::record_batch::RecordBatch;
+
+ for data_type in [
+ ConcreteDataType::large_string_datatype(),
+ ConcreteDataType::utf8_view_datatype(),
+ ] {
+ let schema = Arc::new(Schema::new(vec![Field::new(
+ "tag",
+ data_type.as_arrow_type(),
+ true,
+ )]));
+ for value in [Value::from("greptime"), Value::Null] {
+ let vector = ColumnDefaultConstraint::Value(value.clone())
+ .create_default_vector(&data_type, true, 3)
+ .unwrap();
+ let batch =
+ RecordBatch::try_new(schema.clone(), vec![vector.to_arrow_array()]).unwrap();
+ assert_eq!(3, batch.num_rows());
+ assert_eq!(data_type, vector.data_type());
+ for row in 0..batch.num_rows() {
+ assert_eq!(value, vector.get(row));
+ }
+ }
+ }
+ }
+
#[test]
fn test_create_default_vector_by_func() {
let constraint = ColumnDefaultConstraint::Function(CURRENT_TIMESTAMP.to_string());
diff --git a/src/datatypes/src/types/cast.rs b/src/datatypes/src/types/cast.rs
index d7a27a45e10..2f92713c26f 100644
--- a/src/datatypes/src/types/cast.rs
+++ b/src/datatypes/src/types/cast.rs
@@ -26,7 +26,7 @@ pub fn cast(src_value: Value, dest_type: &ConcreteDataType) -> Result {
}
let src_type = src_value.data_type();
let scalar_value = src_value.try_to_scalar_value(&src_type)?;
- let new_value = Helper::try_from_scalar_value(scalar_value, 1)?
+ let new_value = Helper::try_from_scalar_value(scalar_value, 1, None)?
.cast(dest_type)?
.get(0);
Ok(new_value)
diff --git a/src/datatypes/src/value.rs b/src/datatypes/src/value.rs
index 9ad9e1d46ef..35a6acabd33 100644
--- a/src/datatypes/src/value.rs
+++ b/src/datatypes/src/value.rs
@@ -1085,11 +1085,21 @@ impl StructValue {
}
fn try_to_scalar_value(&self, output_type: &StructType) -> Result {
+ let output_fields = output_type.fields();
+ ensure!(
+ self.items.len() == output_fields.len(),
+ InconsistentStructFieldsAndItemsSnafu {
+ field_len: output_fields.len(),
+ item_len: self.items.len()
+ }
+ );
let arrays = self
.items
.iter()
- .map(|value| {
- let scalar_value = value.try_to_scalar_value(&value.data_type())?;
+ .zip(output_fields.iter())
+ .map(|(value, field)| {
+ // Null values need the declared field type to produce a typed null array.
+ let scalar_value = value.try_to_scalar_value(field.data_type())?;
scalar_value
.to_array()
.context(ConvertScalarToArrowArraySnafu)
@@ -1097,7 +1107,13 @@ impl StructValue {
.collect::>>>()?;
let fields = output_type.as_arrow_fields();
- let struct_array = StructArray::new(fields, arrays, None);
+ let struct_array =
+ StructArray::try_new_with_length(fields, arrays, None, 1).map_err(|error| {
+ error::ToScalarValueSnafu {
+ reason: error.to_string(),
+ }
+ .build()
+ })?;
Ok(ScalarValue::Struct(Arc::new(struct_array)))
}
}
@@ -3082,6 +3098,66 @@ pub(crate) mod tests {
}
}
+ #[test]
+ fn test_struct_scalar_null_fields() {
+ let struct_type = StructType::from([
+ StructField::new("x", ConcreteDataType::int32_datatype(), true),
+ StructField::new("name", ConcreteDataType::string_datatype(), true),
+ ]);
+ let value = StructValue::new(vec![Value::Null, Value::from("hello")], struct_type.clone());
+ let ScalarValue::Struct(array) = value.try_to_scalar_value(&struct_type).unwrap() else {
+ panic!("Expected struct scalar");
+ };
+ assert_eq!(1, array.len());
+ assert_eq!(0, array.null_count());
+ assert_eq!(&struct_type.as_arrow_fields(), array.fields());
+ assert_eq!(
+ ScalarValue::Int32(None),
+ ScalarValue::try_from_array(array.column(0), 0).unwrap()
+ );
+
+ let nested_type = StructType::from([StructField::new(
+ "nested",
+ ConcreteDataType::struct_datatype(struct_type),
+ true,
+ )]);
+ for child in [Value::Struct(value), Value::Null] {
+ let nested = StructValue::new(vec![child.clone()], nested_type.clone());
+ let ScalarValue::Struct(array) = nested.try_to_scalar_value(&nested_type).unwrap()
+ else {
+ panic!("Expected struct scalar");
+ };
+ let vector = crate::vectors::Helper::try_into_vector(array.column(0).clone()).unwrap();
+ assert_eq!(child, vector.get(0));
+ }
+
+ let empty_type = StructType::default();
+ let empty = StructValue::new(vec![], empty_type.clone());
+ let ScalarValue::Struct(array) = empty.try_to_scalar_value(&empty_type).unwrap() else {
+ panic!("Expected struct scalar");
+ };
+ assert_eq!(1, array.len());
+ assert_eq!(0, array.num_columns());
+ assert_eq!(0, array.null_count());
+ }
+
+ #[test]
+ fn test_struct_scalar_invalid_fields() {
+ let struct_type = StructType::from([StructField::new(
+ "x",
+ ConcreteDataType::int32_datatype(),
+ false,
+ )]);
+ for child in [Value::Null, Value::from("wrong type")] {
+ let value = StructValue::new(vec![child], struct_type.clone());
+ assert!(value.try_to_scalar_value(&struct_type).is_err());
+ }
+ let value = StructValue::new(vec![Value::Int32(1)], struct_type.clone());
+ assert!(value.try_to_scalar_value(&StructType::default()).is_err());
+ let empty = StructValue::new(vec![], StructType::default());
+ assert!(empty.try_to_scalar_value(&struct_type).is_err());
+ }
+
#[test]
fn test_timestamp_to_scalar_value() {
assert_eq!(
diff --git a/src/datatypes/src/vectors.rs b/src/datatypes/src/vectors.rs
index 593bec833da..38db5940df9 100644
--- a/src/datatypes/src/vectors.rs
+++ b/src/datatypes/src/vectors.rs
@@ -27,7 +27,6 @@ use crate::vectors::operations::VectorOp;
mod binary;
mod boolean;
-mod constant;
mod date;
mod decimal;
mod dictionary;
@@ -48,7 +47,6 @@ mod validity;
pub use binary::{BinaryVector, BinaryVectorBuilder};
pub use boolean::{BooleanVector, BooleanVectorBuilder};
-pub use constant::ConstantVector;
pub use date::{DateVector, DateVectorBuilder};
pub use decimal::{Decimal128Vector, Decimal128VectorBuilder};
pub(crate) use dictionary::StringDictionaryVectorBuilder;
@@ -126,11 +124,6 @@ pub trait Vector: Send + Sync + Serializable + Debug + VectorOp {
/// This is `O(1)`.
fn null_count(&self) -> usize;
- /// Returns true when it's a ConstantColumn
- fn is_const(&self) -> bool {
- false
- }
-
/// Returns whether row is null.
fn is_null(&self, row: usize) -> bool;
diff --git a/src/datatypes/src/vectors/binary.rs b/src/datatypes/src/vectors/binary.rs
index 6d9954fb80f..77344b46512 100644
--- a/src/datatypes/src/vectors/binary.rs
+++ b/src/datatypes/src/vectors/binary.rs
@@ -479,7 +479,6 @@ mod tests {
assert_eq!(2, v.len());
assert_eq!("BinaryVector", v.vector_type_name());
- assert!(!v.is_const());
assert!(v.validity().is_all_valid());
assert!(!v.only_null());
assert_eq!(128, v.memory_size());
diff --git a/src/datatypes/src/vectors/boolean.rs b/src/datatypes/src/vectors/boolean.rs
index 4ea7a4591b9..48625da516b 100644
--- a/src/datatypes/src/vectors/boolean.rs
+++ b/src/datatypes/src/vectors/boolean.rs
@@ -40,10 +40,6 @@ impl BooleanVector {
pub fn as_boolean_array(&self) -> &BooleanArray {
&self.array
}
-
- pub(crate) fn false_count(&self) -> usize {
- self.array.false_count()
- }
}
impl From> for BooleanVector {
@@ -251,7 +247,6 @@ mod tests {
let v = BooleanVector::from(bools.clone());
assert_eq!(9, v.len());
assert_eq!("BooleanVector", v.vector_type_name());
- assert!(!v.is_const());
assert!(v.validity().is_all_valid());
assert!(!v.only_null());
assert_eq!(2, v.memory_size());
diff --git a/src/datatypes/src/vectors/constant.rs b/src/datatypes/src/vectors/constant.rs
deleted file mode 100644
index 3a0adc7b62a..00000000000
--- a/src/datatypes/src/vectors/constant.rs
+++ /dev/null
@@ -1,252 +0,0 @@
-// Copyright 2023 Greptime Team
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-use std::any::Any;
-use std::fmt;
-use std::sync::Arc;
-
-use arrow::array::{Array, ArrayRef, UInt32Array};
-use snafu::{ResultExt, ensure};
-
-use crate::data_type::ConcreteDataType;
-use crate::error::{self, Result, SerializeSnafu};
-use crate::serialize::Serializable;
-use crate::value::{Value, ValueRef};
-use crate::vectors::{BooleanVector, Helper, UInt32Vector, Validity, Vector, VectorRef};
-
-#[derive(Clone)]
-pub struct ConstantVector {
- length: usize,
- vector: VectorRef,
-}
-
-impl ConstantVector {
- /// Create a new [ConstantVector].
- ///
- /// # Panics
- /// Panics if `vector.len() != 1`.
- pub fn new(vector: VectorRef, length: usize) -> Self {
- assert_eq!(1, vector.len());
-
- // Avoid const recursion.
- if vector.is_const() {
- let vec: &ConstantVector = unsafe { Helper::static_cast(&vector) };
- return Self::new(vec.inner().clone(), length);
- }
- Self { vector, length }
- }
-
- pub fn inner(&self) -> &VectorRef {
- &self.vector
- }
-
- /// Returns the constant value.
- pub fn get_constant_ref(&self) -> ValueRef<'_> {
- self.vector.get_ref(0)
- }
-
- pub(crate) fn replicate_vector(&self, offsets: &[usize]) -> VectorRef {
- assert_eq!(offsets.len(), self.len());
-
- if offsets.is_empty() {
- return self.slice(0, 0);
- }
-
- Arc::new(ConstantVector::new(
- self.vector.clone(),
- *offsets.last().unwrap(),
- ))
- }
-
- pub(crate) fn filter_vector(&self, filter: &BooleanVector) -> Result {
- let length = self.len() - filter.false_count();
- if length == self.len() {
- return Ok(Arc::new(self.clone()));
- }
- Ok(Arc::new(ConstantVector::new(self.inner().clone(), length)))
- }
-
- pub(crate) fn cast_vector(&self, to_type: &ConcreteDataType) -> Result {
- Ok(Arc::new(ConstantVector::new(
- self.inner().cast(to_type)?,
- self.length,
- )))
- }
-
- pub(crate) fn take_vector(&self, indices: &UInt32Vector) -> Result {
- if indices.is_empty() {
- return Ok(self.slice(0, 0));
- }
- ensure!(
- indices.null_count() == 0,
- error::UnsupportedOperationSnafu {
- op: "taking a null index",
- vector_type: self.vector_type_name(),
- }
- );
-
- let len = self.len();
- let arr = indices.to_arrow_array();
- let indices_arr = arr.as_any().downcast_ref::().unwrap();
- if !arrow::compute::min_boolean(
- &arrow::compute::kernels::cmp::lt(indices_arr, &UInt32Array::new_scalar(len as u32))
- .unwrap(),
- )
- .unwrap()
- {
- panic!(
- "Array index out of bounds, cannot take index out of the length of the array: {len}"
- );
- }
-
- Ok(Arc::new(ConstantVector::new(
- self.inner().clone(),
- indices.len(),
- )))
- }
-}
-
-impl Vector for ConstantVector {
- fn data_type(&self) -> ConcreteDataType {
- self.vector.data_type()
- }
-
- fn vector_type_name(&self) -> String {
- "ConstantVector".to_string()
- }
-
- fn as_any(&self) -> &dyn Any {
- self
- }
-
- fn len(&self) -> usize {
- self.length
- }
-
- fn to_arrow_array(&self) -> ArrayRef {
- let v = self.vector.replicate(&[self.length]);
- v.to_arrow_array()
- }
-
- fn to_boxed_arrow_array(&self) -> Box {
- let v = self.vector.replicate(&[self.length]);
- v.to_boxed_arrow_array()
- }
-
- fn is_const(&self) -> bool {
- true
- }
-
- fn validity(&self) -> Validity {
- if self.vector.is_null(0) {
- Validity::all_null(self.length)
- } else {
- Validity::all_valid(self.length)
- }
- }
-
- fn memory_size(&self) -> usize {
- self.vector.memory_size()
- }
-
- fn is_null(&self, _row: usize) -> bool {
- self.vector.is_null(0)
- }
-
- fn only_null(&self) -> bool {
- self.vector.is_null(0)
- }
-
- fn slice(&self, _offset: usize, length: usize) -> VectorRef {
- Arc::new(Self {
- vector: self.vector.clone(),
- length,
- })
- }
-
- fn get(&self, _index: usize) -> Value {
- self.vector.get(0)
- }
-
- fn get_ref(&self, _index: usize) -> ValueRef<'_> {
- self.vector.get_ref(0)
- }
-
- fn null_count(&self) -> usize {
- if self.only_null() { self.len() } else { 0 }
- }
-}
-
-impl fmt::Debug for ConstantVector {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "ConstantVector([{:?}; {}])", self.get(0), self.len())
- }
-}
-
-impl Serializable for ConstantVector {
- fn serialize_to_json(&self) -> Result> {
- std::iter::repeat_n(self.get(0), self.len())
- .map(serde_json::Value::try_from)
- .collect::>()
- .context(SerializeSnafu)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use arrow::datatypes::DataType as ArrowDataType;
-
- use super::*;
- use crate::vectors::Int32Vector;
-
- #[test]
- fn test_constant_vector_misc() {
- let a = Int32Vector::from_slice(vec![1]);
- let c = ConstantVector::new(Arc::new(a), 10);
-
- assert_eq!("ConstantVector", c.vector_type_name());
- assert!(c.is_const());
- assert_eq!(10, c.len());
- assert!(c.validity().is_all_valid());
- assert!(!c.only_null());
- assert_eq!(4, c.memory_size());
-
- for i in 0..10 {
- assert!(!c.is_null(i));
- assert_eq!(Value::Int32(1), c.get(i));
- }
-
- let arrow_arr = c.to_arrow_array();
- assert_eq!(10, arrow_arr.len());
- assert_eq!(&ArrowDataType::Int32, arrow_arr.data_type());
- }
-
- #[test]
- fn test_debug_null_array() {
- let a = Int32Vector::from_slice(vec![1]);
- let c = ConstantVector::new(Arc::new(a), 10);
-
- let s = format!("{c:?}");
- assert_eq!(s, "ConstantVector([Int32(1); 10])");
- }
-
- #[test]
- fn test_serialize_json() {
- let a = Int32Vector::from_slice(vec![1]);
- let c = ConstantVector::new(Arc::new(a), 10);
-
- let s = serde_json::to_string(&c.serialize_to_json().unwrap()).unwrap();
- assert_eq!(s, "[1,1,1,1,1,1,1,1,1,1]");
- }
-}
diff --git a/src/datatypes/src/vectors/decimal.rs b/src/datatypes/src/vectors/decimal.rs
index 46fa73297a5..34d066f0725 100644
--- a/src/datatypes/src/vectors/decimal.rs
+++ b/src/datatypes/src/vectors/decimal.rs
@@ -385,43 +385,6 @@ impl Decimal128VectorBuilder {
vectors::impl_try_from_arrow_array_for_vector!(Decimal128Array, Decimal128Vector);
-pub(crate) fn replicate_decimal128(
- vector: &Decimal128Vector,
- offsets: &[usize],
-) -> Decimal128Vector {
- assert_eq!(offsets.len(), vector.len());
-
- if offsets.is_empty() {
- return vector.get_slice(0, 0);
- }
-
- // Safety: safe to unwrap because we the vector ensures precision and scale are valid.
- let mut builder = Decimal128VectorBuilder::with_capacity(*offsets.last().unwrap())
- .with_precision_and_scale(vector.precision(), vector.scale())
- .unwrap();
-
- let mut previous_offset = 0;
-
- for (offset, value) in offsets.iter().zip(vector.array.iter()) {
- let repeat_times = *offset - previous_offset;
- match value {
- Some(data) => {
- unsafe {
- // Safety: std::iter::Repeat and std::iter::Take implement TrustedLen.
- builder
- .mutable_array
- .append_trusted_len_iter(std::iter::repeat_n(data, repeat_times));
- }
- }
- None => {
- builder.mutable_array.append_nulls(repeat_times);
- }
- }
- previous_offset = *offset;
- }
- builder.finish()
-}
-
#[cfg(test)]
pub mod tests {
use arrow_array::Decimal128Array;
diff --git a/src/datatypes/src/vectors/dictionary.rs b/src/datatypes/src/vectors/dictionary.rs
index d41daf4638f..5f117b0ae88 100644
--- a/src/datatypes/src/vectors/dictionary.rs
+++ b/src/datatypes/src/vectors/dictionary.rs
@@ -17,8 +17,7 @@ use std::fmt;
use std::sync::Arc;
use arrow::array::{
- Array, ArrayBuilder, ArrayRef, DictionaryArray, PrimitiveArray, PrimitiveBuilder,
- StringDictionaryBuilder,
+ Array, ArrayBuilder, ArrayRef, DictionaryArray, PrimitiveArray, StringDictionaryBuilder,
};
use arrow::datatypes::{ArrowDictionaryKeyType, ArrowNativeType, UInt32Type};
use serde_json::Value as JsonValue;
@@ -320,36 +319,6 @@ impl<'a, K: ArrowDictionaryKeyType> Iterator for DictionaryIter<'a, K> {
}
impl VectorOp for DictionaryVector {
- fn replicate(&self, offsets: &[usize]) -> VectorRef {
- let keys = self.array.keys();
- let mut replicated_keys = PrimitiveBuilder::new();
-
- let mut previous_offset = 0;
- let mut key_iter = keys.iter().chain(std::iter::repeat(None));
- for &offset in offsets {
- let key = key_iter.next().unwrap();
-
- // repeat this key (offset - previous_offset) times
- let repeat_count = offset - previous_offset;
- for _ in 0..repeat_count {
- replicated_keys.append_option(key);
- }
-
- previous_offset = offset;
- }
-
- let new_keys = replicated_keys.finish();
- let new_array = DictionaryArray::try_new(new_keys, self.values().clone())
- .expect("Failed to create replicated dictionary array");
-
- Arc::new(Self {
- array: new_array,
- key_type: self.key_type.clone(),
- item_type: self.item_type.clone(),
- item_vector: self.item_vector.clone(),
- })
- }
-
fn filter(&self, filter: &vectors::BooleanVector) -> Result {
let key_array: ArrayRef = Arc::new(self.array.keys().clone());
let key_vector = Helper::try_into_vector(&key_array)?;
@@ -485,21 +454,6 @@ mod tests {
assert_eq!(sliced.get(2), Value::Null);
}
- #[test]
- fn test_replicate() {
- let dict_vec = create_test_dictionary();
-
- // Replicate with offsets [0, 2, 5] - should get values at these indices
- let offsets = vec![0, 2, 5];
- let replicated = dict_vec.replicate(&offsets);
- assert_eq!(replicated.len(), 5);
- assert_eq!(replicated.get(0), Value::String("b".to_string().into()));
- assert_eq!(replicated.get(1), Value::String("b".to_string().into()));
- assert_eq!(replicated.get(2), Value::String("c".to_string().into()));
- assert_eq!(replicated.get(3), Value::String("c".to_string().into()));
- assert_eq!(replicated.get(4), Value::String("c".to_string().into()));
- }
-
#[test]
fn test_filter() {
let dict_vec = create_test_dictionary();
diff --git a/src/datatypes/src/vectors/eq.rs b/src/datatypes/src/vectors/eq.rs
index e8e942d3ff4..937e4587c35 100644
--- a/src/datatypes/src/vectors/eq.rs
+++ b/src/datatypes/src/vectors/eq.rs
@@ -18,7 +18,6 @@ use common_time::interval::IntervalUnit;
use crate::data_type::DataType;
use crate::types::{DurationType, TimeType, TimestampType};
-use crate::vectors::constant::ConstantVector;
use crate::vectors::struct_vector::StructVector;
use crate::vectors::{
BinaryVector, BooleanVector, DateVector, Decimal128Vector, DurationMicrosecondVector,
@@ -58,23 +57,6 @@ fn equal(lhs: &dyn Vector, rhs: &dyn Vector) -> bool {
return false;
}
- if lhs.is_const() || rhs.is_const() {
- // Length has been checked before, so we only need to compare inner
- // vector here.
- return equal(
- &**lhs
- .as_any()
- .downcast_ref::()
- .unwrap()
- .inner(),
- &**rhs
- .as_any()
- .downcast_ref::()
- .unwrap()
- .inner(),
- );
- }
-
use crate::data_type::ConcreteDataType::*;
let lhs_type = lhs.data_type();
@@ -191,10 +173,6 @@ mod tests {
Some(b"world".to_vec()),
])));
assert_vector_ref_eq(Arc::new(BooleanVector::from(vec![true, false])));
- assert_vector_ref_eq(Arc::new(ConstantVector::new(
- Arc::new(BooleanVector::from(vec![true])),
- 5,
- )));
assert_vector_ref_eq(Arc::new(BooleanVector::from(vec![true, false])));
assert_vector_ref_eq(Arc::new(DateVector::from(vec![Some(100), Some(120)])));
assert_vector_ref_eq(Arc::new(TimestampSecondVector::from_values([100, 120])));
@@ -259,22 +237,6 @@ mod tests {
])));
}
- // Regression: second arm must downcast `rhs` (was `lhs`), or same-length ConstantVectors
- // with different inners compare equal.
- #[test]
- fn test_constant_vector_eq_compares_both_inners() {
- assert_vector_ref_ne(
- Arc::new(ConstantVector::new(
- Arc::new(BooleanVector::from(vec![true])),
- 5,
- )),
- Arc::new(ConstantVector::new(
- Arc::new(BooleanVector::from(vec![false])),
- 5,
- )),
- );
- }
-
#[test]
fn test_vector_ne() {
assert_vector_ref_ne(
@@ -289,36 +251,6 @@ mod tests {
Arc::new(Int32Vector::from_slice([1, 2, 3, 4])),
Arc::new(BooleanVector::from(vec![true, true])),
);
- assert_vector_ref_ne(
- Arc::new(ConstantVector::new(
- Arc::new(BooleanVector::from(vec![true])),
- 5,
- )),
- Arc::new(ConstantVector::new(
- Arc::new(BooleanVector::from(vec![true])),
- 4,
- )),
- );
- assert_vector_ref_ne(
- Arc::new(ConstantVector::new(
- Arc::new(BooleanVector::from(vec![true])),
- 5,
- )),
- Arc::new(ConstantVector::new(
- Arc::new(BooleanVector::from(vec![false])),
- 4,
- )),
- );
- assert_vector_ref_ne(
- Arc::new(ConstantVector::new(
- Arc::new(BooleanVector::from(vec![true])),
- 5,
- )),
- Arc::new(ConstantVector::new(
- Arc::new(Int32Vector::from_slice(vec![1])),
- 4,
- )),
- );
assert_vector_ref_ne(Arc::new(NullVector::new(5)), Arc::new(NullVector::new(8)));
assert_vector_ref_ne(
diff --git a/src/datatypes/src/vectors/helper.rs b/src/datatypes/src/vectors/helper.rs
index 685b07419f9..3d9e71853cf 100644
--- a/src/datatypes/src/vectors/helper.rs
+++ b/src/datatypes/src/vectors/helper.rs
@@ -30,21 +30,21 @@ use datafusion_common::ScalarValue;
use snafu::{OptionExt, ResultExt};
use crate::data_type::ConcreteDataType;
-use crate::error::{self, ConvertArrowArrayToScalarsSnafu, Result};
+use crate::error::{self, ConvertScalarToArrowArraySnafu, Result};
use crate::prelude::DataType;
-use crate::scalars::{Scalar, ScalarVectorBuilder};
+use crate::scalars::Scalar;
use crate::types::StructType;
-use crate::value::{ListValue, ListValueRef, Value};
+use crate::value::Value;
use crate::vectors::struct_vector::StructVector;
use crate::vectors::{
- BinaryVector, BooleanVector, ConstantVector, DateVector, Decimal128Vector, DictionaryVector,
+ BinaryVector, BooleanVector, DateVector, Decimal128Vector, DictionaryVector,
DurationMicrosecondVector, DurationMillisecondVector, DurationNanosecondVector,
DurationSecondVector, Float32Vector, Float64Vector, Int8Vector, Int16Vector, Int32Vector,
Int64Vector, IntervalDayTimeVector, IntervalMonthDayNanoVector, IntervalYearMonthVector,
- ListVector, ListVectorBuilder, MutableVector, NullVector, StringVector, TimeMicrosecondVector,
- TimeMillisecondVector, TimeNanosecondVector, TimeSecondVector, TimestampMicrosecondVector,
- TimestampMillisecondVector, TimestampNanosecondVector, TimestampSecondVector, UInt8Vector,
- UInt16Vector, UInt32Vector, UInt64Vector, Vector, VectorRef,
+ ListVector, NullVector, StringVector, TimeMicrosecondVector, TimeMillisecondVector,
+ TimeNanosecondVector, TimeSecondVector, TimestampMicrosecondVector, TimestampMillisecondVector,
+ TimestampNanosecondVector, TimestampSecondVector, UInt8Vector, UInt16Vector, UInt32Vector,
+ UInt64Vector, Vector, VectorRef,
};
/// Helper functions for `Vector`.
@@ -102,135 +102,50 @@ impl Helper {
})
}
- /// Try to cast an arrow scalar value into vector
- pub fn try_from_scalar_value(value: ScalarValue, length: usize) -> Result {
- let vector = match value {
- ScalarValue::Null => ConstantVector::new(Arc::new(NullVector::new(1)), length),
- ScalarValue::Boolean(v) => {
- ConstantVector::new(Arc::new(BooleanVector::from(vec![v])), length)
+ /// Materializes an Arrow scalar as a vector of the given length.
+ ///
+ /// With a type hint, casts the array to the requested representation and returns
+ /// an error if the vector cannot preserve that concrete type. Without a hint,
+ /// normalizes the scalar to a supported GreptimeDB representation.
+ pub fn try_from_scalar_value(
+ value: ScalarValue,
+ length: usize,
+ type_hint: Option<&ConcreteDataType>,
+ ) -> Result {
+ if let Some(data_type) = type_hint {
+ let mut array = value
+ .to_array_of_size(length)
+ .context(ConvertScalarToArrowArraySnafu)?;
+ let arrow_type = data_type.as_arrow_type();
+ if array.data_type() != &arrow_type {
+ array = compute::cast(&array, &arrow_type).context(error::ArrowComputeSnafu)?;
}
- ScalarValue::Float16(v) => ConstantVector::new(
- Arc::new(Float32Vector::from(vec![v.map(f32::from)])),
- length,
- ),
- ScalarValue::Float32(v) => {
- ConstantVector::new(Arc::new(Float32Vector::from(vec![v])), length)
+ let vector = Self::try_into_vector(array)?;
+ if &vector.data_type() != data_type {
+ return error::CastTypeSnafu {
+ msg: format!(
+ "Scalar materialization produced {:?}, expected {data_type:?}",
+ vector.data_type()
+ ),
+ }
+ .fail();
}
- ScalarValue::Float64(v) => {
- ConstantVector::new(Arc::new(Float64Vector::from(vec![v])), length)
+ return Ok(vector);
+ }
+
+ let value = match value {
+ // GreptimeDB doesn't support Float16 vectors.
+ ScalarValue::Float16(v) => ScalarValue::Float32(v.map(f32::from)),
+ ScalarValue::LargeUtf8(v) => ScalarValue::Utf8(v),
+ ScalarValue::LargeBinary(v) | ScalarValue::FixedSizeBinary(_, v) => {
+ ScalarValue::Binary(v)
}
- ScalarValue::Int8(v) => {
- ConstantVector::new(Arc::new(Int8Vector::from(vec![v])), length)
- }
- ScalarValue::Int16(v) => {
- ConstantVector::new(Arc::new(Int16Vector::from(vec![v])), length)
- }
- ScalarValue::Int32(v) => {
- ConstantVector::new(Arc::new(Int32Vector::from(vec![v])), length)
- }
- ScalarValue::Int64(v) => {
- ConstantVector::new(Arc::new(Int64Vector::from(vec![v])), length)
- }
- ScalarValue::UInt8(v) => {
- ConstantVector::new(Arc::new(UInt8Vector::from(vec![v])), length)
- }
- ScalarValue::UInt16(v) => {
- ConstantVector::new(Arc::new(UInt16Vector::from(vec![v])), length)
- }
- ScalarValue::UInt32(v) => {
- ConstantVector::new(Arc::new(UInt32Vector::from(vec![v])), length)
- }
- ScalarValue::UInt64(v) => {
- ConstantVector::new(Arc::new(UInt64Vector::from(vec![v])), length)
- }
- ScalarValue::Utf8(v) | ScalarValue::LargeUtf8(v) => {
- ConstantVector::new(Arc::new(StringVector::from(vec![v])), length)
- }
- ScalarValue::Binary(v)
- | ScalarValue::LargeBinary(v)
- | ScalarValue::FixedSizeBinary(_, v) => {
- ConstantVector::new(Arc::new(BinaryVector::from(vec![v])), length)
- }
- ScalarValue::List(array) => {
- let item_type = Arc::new(ConcreteDataType::try_from(&array.value_type())?);
- let mut builder = ListVectorBuilder::with_type_capacity(item_type.clone(), 1);
- let scalar_values = ScalarValue::convert_array_to_scalar_vec(array.as_ref())
- .context(ConvertArrowArrayToScalarsSnafu)?;
- let values = scalar_values
- .into_iter()
- .flat_map(|v| v.unwrap_or_else(|| vec![ScalarValue::Null]))
- .map(ScalarValue::try_into)
- .collect::>>()?;
- builder.push(Some(ListValueRef::Ref {
- val: &ListValue::new(values, item_type),
- }));
- let list_vector = builder.to_vector();
- ConstantVector::new(list_vector, length)
- }
- ScalarValue::Date32(v) => {
- ConstantVector::new(Arc::new(DateVector::from(vec![v])), length)
- }
- ScalarValue::TimestampSecond(v, _) => {
- // Timezone is unimplemented now.
- ConstantVector::new(Arc::new(TimestampSecondVector::from(vec![v])), length)
- }
- ScalarValue::TimestampMillisecond(v, _) => {
- // Timezone is unimplemented now.
- ConstantVector::new(Arc::new(TimestampMillisecondVector::from(vec![v])), length)
- }
- ScalarValue::TimestampMicrosecond(v, _) => {
- // Timezone is unimplemented now.
- ConstantVector::new(Arc::new(TimestampMicrosecondVector::from(vec![v])), length)
- }
- ScalarValue::TimestampNanosecond(v, _) => {
- // Timezone is unimplemented now.
- ConstantVector::new(Arc::new(TimestampNanosecondVector::from(vec![v])), length)
- }
- ScalarValue::Time32Second(v) => {
- ConstantVector::new(Arc::new(TimeSecondVector::from(vec![v])), length)
- }
- ScalarValue::Time32Millisecond(v) => {
- ConstantVector::new(Arc::new(TimeMillisecondVector::from(vec![v])), length)
- }
- ScalarValue::Time64Microsecond(v) => {
- ConstantVector::new(Arc::new(TimeMicrosecondVector::from(vec![v])), length)
- }
- ScalarValue::Time64Nanosecond(v) => {
- ConstantVector::new(Arc::new(TimeNanosecondVector::from(vec![v])), length)
- }
- ScalarValue::IntervalYearMonth(v) => {
- ConstantVector::new(Arc::new(IntervalYearMonthVector::from(vec![v])), length)
- }
- ScalarValue::IntervalDayTime(v) => {
- ConstantVector::new(Arc::new(IntervalDayTimeVector::from(vec![v])), length)
- }
- ScalarValue::IntervalMonthDayNano(v) => {
- ConstantVector::new(Arc::new(IntervalMonthDayNanoVector::from(vec![v])), length)
- }
- ScalarValue::DurationSecond(v) => {
- ConstantVector::new(Arc::new(DurationSecondVector::from(vec![v])), length)
- }
- ScalarValue::DurationMillisecond(v) => {
- ConstantVector::new(Arc::new(DurationMillisecondVector::from(vec![v])), length)
- }
- ScalarValue::DurationMicrosecond(v) => {
- ConstantVector::new(Arc::new(DurationMicrosecondVector::from(vec![v])), length)
- }
- ScalarValue::DurationNanosecond(v) => {
- ConstantVector::new(Arc::new(DurationNanosecondVector::from(vec![v])), length)
- }
- ScalarValue::Decimal128(v, p, s) => {
- let vector = Decimal128Vector::from(vec![v]).with_precision_and_scale(p, s)?;
- ConstantVector::new(Arc::new(vector), length)
- }
- ScalarValue::Struct(v) => {
- let struct_type = StructType::from(v.fields());
- ConstantVector::new(
- Arc::new(StructVector::try_new(struct_type, (*v).clone())?),
- length,
- )
- }
- ScalarValue::Decimal32(_, _, _)
+ // Timezones are not supported by GreptimeDB vectors.
+ ScalarValue::TimestampSecond(v, _) => ScalarValue::TimestampSecond(v, None),
+ ScalarValue::TimestampMillisecond(v, _) => ScalarValue::TimestampMillisecond(v, None),
+ ScalarValue::TimestampMicrosecond(v, _) => ScalarValue::TimestampMicrosecond(v, None),
+ ScalarValue::TimestampNanosecond(v, _) => ScalarValue::TimestampNanosecond(v, None),
+ value @ (ScalarValue::Decimal32(_, _, _)
| ScalarValue::Decimal64(_, _, _)
| ScalarValue::Decimal256(_, _, _)
| ScalarValue::FixedSizeList(_)
@@ -241,15 +156,19 @@ impl Helper {
| ScalarValue::BinaryView(_)
| ScalarValue::Map(_)
| ScalarValue::Date64(_)
- | ScalarValue::RunEndEncoded(_, _, _) => {
+ | ScalarValue::RunEndEncoded(_, _, _)) => {
return error::ConversionSnafu {
from: format!("Unsupported scalar value: {value}"),
}
.fail();
}
+ value => value,
};
- Ok(Arc::new(vector))
+ let array = value
+ .to_array_of_size(length)
+ .context(ConvertScalarToArrowArraySnafu)?;
+ Self::try_into_vector(array)
}
/// Try to cast an arrow array into vector
@@ -457,12 +376,15 @@ mod tests {
};
use arrow::buffer::Buffer;
use arrow::datatypes::{Int32Type, IntervalMonthDayNano};
- use arrow_array::{BinaryArray, DictionaryArray, FixedSizeBinaryArray, LargeStringArray};
- use arrow_schema::DataType;
+ use arrow_array::{
+ BinaryArray, DictionaryArray, FixedSizeBinaryArray, LargeStringArray, StructArray,
+ };
+ use arrow_schema::{DataType, Field, Fields};
use common_decimal::Decimal128;
use common_time::time::Time;
use common_time::timestamp::TimeUnit;
use common_time::{Date, Duration};
+ use datafusion_common::scalar::ScalarStructBuilder;
use super::*;
use crate::value::Value;
@@ -496,7 +418,7 @@ mod tests {
#[test]
fn test_try_from_scalar_date_value() {
- let vector = Helper::try_from_scalar_value(ScalarValue::Date32(Some(42)), 3).unwrap();
+ let vector = Helper::try_from_scalar_value(ScalarValue::Date32(Some(42)), 3, None).unwrap();
assert_eq!(ConcreteDataType::date_datatype(), vector.data_type());
assert_eq!(3, vector.len());
for i in 0..vector.len() {
@@ -507,7 +429,7 @@ mod tests {
#[test]
fn test_try_from_scalar_duration_value() {
let vector =
- Helper::try_from_scalar_value(ScalarValue::DurationSecond(Some(42)), 3).unwrap();
+ Helper::try_from_scalar_value(ScalarValue::DurationSecond(Some(42)), 3, None).unwrap();
assert_eq!(
ConcreteDataType::duration_second_datatype(),
vector.data_type()
@@ -524,7 +446,8 @@ mod tests {
#[test]
fn test_try_from_scalar_decimal128_value() {
let vector =
- Helper::try_from_scalar_value(ScalarValue::Decimal128(Some(42), 3, 1), 3).unwrap();
+ Helper::try_from_scalar_value(ScalarValue::Decimal128(Some(42), 3, 1), 3, None)
+ .unwrap();
assert_eq!(
ConcreteDataType::decimal128_datatype(3, 1),
vector.data_type()
@@ -542,7 +465,7 @@ mod tests {
&ArrowDataType::Int32,
true,
));
- let vector = Helper::try_from_scalar_value(value, 3).unwrap();
+ let vector = Helper::try_from_scalar_value(value, 3, None).unwrap();
assert_eq!(
ConcreteDataType::list_datatype(Arc::new(ConcreteDataType::int32_datatype())),
vector.data_type()
@@ -555,6 +478,101 @@ mod tests {
}
}
+ #[test]
+ fn test_try_from_scalar_value_materializes_values() {
+ let vector = Helper::try_from_scalar_value(ScalarValue::Int32(Some(42)), 4, None).unwrap();
+ assert_eq!(ConcreteDataType::int32_datatype(), vector.data_type());
+ assert_eq!(4, vector.len());
+ assert_eq!(0, vector.null_count());
+ for i in 0..vector.len() {
+ assert_eq!(Value::Int32(42), vector.get(i));
+ }
+
+ let empty = Helper::try_from_scalar_value(ScalarValue::Int32(Some(42)), 0, None).unwrap();
+ assert_eq!(ConcreteDataType::int32_datatype(), empty.data_type());
+ assert!(empty.is_empty());
+
+ let nulls = Helper::try_from_scalar_value(ScalarValue::Int32(None), 3, None).unwrap();
+ assert_eq!(3, nulls.len());
+ assert_eq!(3, nulls.null_count());
+ for i in 0..nulls.len() {
+ assert_eq!(Value::Null, nulls.get(i));
+ }
+ }
+
+ #[test]
+ fn test_try_from_scalar_struct_value() {
+ let fields = Fields::from(vec![
+ Field::new("id", ArrowDataType::Int32, false),
+ Field::new("name", ArrowDataType::Utf8, true),
+ ]);
+ let value = ScalarValue::Struct(Arc::new(StructArray::new(
+ fields.clone(),
+ vec![
+ ScalarValue::Int32(Some(7)).to_array().unwrap(),
+ ScalarValue::Utf8(Some("greptime".to_string()))
+ .to_array()
+ .unwrap(),
+ ],
+ None,
+ )));
+
+ let vector = Helper::try_from_scalar_value(value, 3, None).unwrap();
+ assert_eq!(
+ ConcreteDataType::struct_datatype(StructType::from(&fields)),
+ vector.data_type()
+ );
+ assert_eq!(3, vector.len());
+ for i in 0..vector.len() {
+ let Value::Struct(value) = vector.get(i) else {
+ panic!("expected struct value");
+ };
+ assert_eq!(
+ &[Value::Int32(7), Value::String("greptime".into())],
+ value.items()
+ );
+ }
+
+ let null = ScalarStructBuilder::new_null(fields);
+ let vector = Helper::try_from_scalar_value(null, 2, None).unwrap();
+ assert_eq!(2, vector.len());
+ assert_eq!(2, vector.null_count());
+ assert_eq!(Value::Null, vector.get(0));
+ assert_eq!(Value::Null, vector.get(1));
+ }
+
+ #[test]
+ fn test_try_from_scalar_value_normalizes_arrow_types() {
+ let string = Helper::try_from_scalar_value(
+ ScalarValue::LargeUtf8(Some("greptime".to_string())),
+ 2,
+ None,
+ )
+ .unwrap();
+ assert_eq!(ConcreteDataType::string_datatype(), string.data_type());
+ assert_eq!(&ArrowDataType::Utf8, string.to_arrow_array().data_type());
+
+ let binary = Helper::try_from_scalar_value(
+ ScalarValue::FixedSizeBinary(2, Some(vec![1, 2])),
+ 2,
+ None,
+ )
+ .unwrap();
+ assert_eq!(ConcreteDataType::binary_datatype(), binary.data_type());
+ assert_eq!(&ArrowDataType::Binary, binary.to_arrow_array().data_type());
+
+ let timestamp = Helper::try_from_scalar_value(
+ ScalarValue::TimestampMillisecond(Some(42), Some("UTC".into())),
+ 2,
+ None,
+ )
+ .unwrap();
+ assert_eq!(
+ &ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
+ timestamp.to_arrow_array().data_type()
+ );
+ }
+
#[test]
fn test_like_utf8() {
fn assert_vector(expected: Vec<&str>, actual: &VectorRef) {
@@ -745,7 +763,8 @@ mod tests {
#[test]
fn test_try_from_scalar_time_value() {
- let vector = Helper::try_from_scalar_value(ScalarValue::Time32Second(Some(42)), 3).unwrap();
+ let vector =
+ Helper::try_from_scalar_value(ScalarValue::Time32Second(Some(42)), 3, None).unwrap();
assert_eq!(ConcreteDataType::time_second_datatype(), vector.data_type());
assert_eq!(3, vector.len());
for i in 0..vector.len() {
@@ -758,6 +777,7 @@ mod tests {
let vector = Helper::try_from_scalar_value(
ScalarValue::IntervalMonthDayNano(Some(IntervalMonthDayNano::new(1, 1, 2000))),
3,
+ None,
)
.unwrap();
diff --git a/src/datatypes/src/vectors/null.rs b/src/datatypes/src/vectors/null.rs
index e049eb205c6..2510824ce0d 100644
--- a/src/datatypes/src/vectors/null.rs
+++ b/src/datatypes/src/vectors/null.rs
@@ -260,16 +260,6 @@ impl ScalarVectorBuilder for NullVectorBuilder {
}
}
-pub(crate) fn replicate_null(vector: &NullVector, offsets: &[usize]) -> VectorRef {
- assert_eq!(offsets.len(), vector.len());
-
- if offsets.is_empty() {
- return vector.slice(0, 0);
- }
-
- Arc::new(NullVector::new(*offsets.last().unwrap()))
-}
-
#[cfg(test)]
mod tests {
use serde_json;
@@ -290,7 +280,6 @@ mod tests {
assert_eq!(vector2.null_count(), 16);
assert_eq!("NullVector", v.vector_type_name());
- assert!(!v.is_const());
assert!(v.validity().is_all_null());
assert!(v.only_null());
diff --git a/src/datatypes/src/vectors/operations.rs b/src/datatypes/src/vectors/operations.rs
index 1dc9e21a0d2..f75ff060f3e 100644
--- a/src/datatypes/src/vectors/operations.rs
+++ b/src/datatypes/src/vectors/operations.rs
@@ -14,14 +14,12 @@
mod cast;
mod filter;
-mod replicate;
mod take;
use std::sync::Arc;
use crate::error::{self, Result};
use crate::types::LogicalPrimitiveType;
-use crate::vectors::constant::ConstantVector;
use crate::vectors::{
BinaryVector, BooleanVector, ConcreteDataType, Decimal128Vector, ListVector, NullVector,
PrimitiveVector, StringVector, UInt32Vector, Vector, VectorRef,
@@ -29,14 +27,6 @@ use crate::vectors::{
/// Vector compute operations.
pub trait VectorOp {
- /// Copies each element according `offsets` parameter.
- /// - `i-th` element should be copied `offsets[i] - offsets[i - 1]` times
- /// - `0-th` element would be copied `offsets[0]` times
- ///
- /// # Panics
- /// Panics if `offsets.len() != self.len()`.
- fn replicate(&self, offsets: &[usize]) -> VectorRef;
-
/// Filters the vector, returns elements matching the `filter` (i.e. where the values are true).
///
/// Note that the nulls of `filter` are interpreted as `false` will lead to these elements being masked out.
@@ -57,10 +47,6 @@ pub trait VectorOp {
macro_rules! impl_scalar_vector_op {
($($VectorType: ident),+) => {$(
impl VectorOp for $VectorType {
- fn replicate(&self, offsets: &[usize]) -> VectorRef {
- replicate::replicate_scalar(self, offsets)
- }
-
fn filter(&self, filter: &BooleanVector) -> Result {
filter::filter_non_constant!(self, $VectorType, filter)
}
@@ -92,10 +78,6 @@ macro_rules! impl_scalar_vector_op {
impl_scalar_vector_op!(BinaryVector, BooleanVector, StringVector);
impl VectorOp for ListVector {
- fn replicate(&self, offsets: &[usize]) -> VectorRef {
- replicate::replicate_list(self, offsets)
- }
-
fn filter(&self, filter: &BooleanVector) -> Result {
filter::filter_non_constant!(self, ListVector, filter)
}
@@ -110,10 +92,6 @@ impl VectorOp for ListVector {
}
impl VectorOp for Decimal128Vector {
- fn replicate(&self, offsets: &[usize]) -> VectorRef {
- std::sync::Arc::new(replicate::replicate_decimal128(self, offsets))
- }
-
fn filter(&self, filter: &BooleanVector) -> Result {
filter::filter_non_constant!(self, Decimal128Vector, filter)
}
@@ -128,10 +106,6 @@ impl VectorOp for Decimal128Vector {
}
impl VectorOp for PrimitiveVector {
- fn replicate(&self, offsets: &[usize]) -> VectorRef {
- std::sync::Arc::new(replicate::replicate_primitive(self, offsets))
- }
-
fn filter(&self, filter: &BooleanVector) -> Result {
filter::filter_non_constant!(self, PrimitiveVector, filter)
}
@@ -146,10 +120,6 @@ impl VectorOp for PrimitiveVector {
}
impl VectorOp for NullVector {
- fn replicate(&self, offsets: &[usize]) -> VectorRef {
- replicate::replicate_null(self, offsets)
- }
-
fn filter(&self, filter: &BooleanVector) -> Result {
filter::filter_non_constant!(self, NullVector, filter)
}
@@ -166,21 +136,3 @@ impl VectorOp for NullVector {
take::take_indices!(self, NullVector, indices)
}
}
-
-impl VectorOp for ConstantVector {
- fn replicate(&self, offsets: &[usize]) -> VectorRef {
- self.replicate_vector(offsets)
- }
-
- fn filter(&self, filter: &BooleanVector) -> Result {
- self.filter_vector(filter)
- }
-
- fn cast(&self, to_type: &ConcreteDataType) -> Result {
- self.cast_vector(to_type)
- }
-
- fn take(&self, indices: &UInt32Vector) -> Result {
- self.take_vector(indices)
- }
-}
diff --git a/src/datatypes/src/vectors/operations/filter.rs b/src/datatypes/src/vectors/operations/filter.rs
index 7dde6c1f946..b27bbb5de48 100644
--- a/src/datatypes/src/vectors/operations/filter.rs
+++ b/src/datatypes/src/vectors/operations/filter.rs
@@ -39,7 +39,6 @@ mod tests {
TimestampMicrosecond, TimestampMillisecond, TimestampNanosecond, TimestampSecond,
};
use crate::types::WrapperType;
- use crate::vectors::constant::ConstantVector;
use crate::vectors::{
BooleanVector, Int32Vector, NullVector, StringVector, VectorOp, VectorRef,
};
@@ -67,26 +66,6 @@ mod tests {
check_filter_primitive(&[5, 7], &[5, 6, 7], &[true, false, true]);
}
- fn check_filter_constant(expect_length: usize, input_length: usize, filter: &[bool]) {
- let v = ConstantVector::new(Arc::new(Int32Vector::from_slice([123])), input_length);
- let filter = BooleanVector::from_slice(filter);
- let out = v.filter(&filter).unwrap();
-
- assert!(out.is_const());
- assert_eq!(expect_length, out.len());
- }
-
- #[test]
- fn test_filter_constant() {
- check_filter_constant(0, 0, &[]);
- check_filter_constant(1, 1, &[true]);
- check_filter_constant(0, 1, &[false]);
- check_filter_constant(1, 2, &[false, true]);
- check_filter_constant(2, 2, &[true, true]);
- check_filter_constant(1, 4, &[false, false, false, true]);
- check_filter_constant(2, 4, &[false, true, false, true]);
- }
-
#[test]
fn test_filter_scalar() {
let v = StringVector::from_slice(&["0", "1", "2", "3"]);
diff --git a/src/datatypes/src/vectors/operations/replicate.rs b/src/datatypes/src/vectors/operations/replicate.rs
deleted file mode 100644
index dee0b3855b0..00000000000
--- a/src/datatypes/src/vectors/operations/replicate.rs
+++ /dev/null
@@ -1,232 +0,0 @@
-// Copyright 2023 Greptime Team
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-use crate::prelude::*;
-pub(crate) use crate::vectors::decimal::replicate_decimal128;
-pub(crate) use crate::vectors::null::replicate_null;
-pub(crate) use crate::vectors::primitive::replicate_primitive;
-use crate::vectors::{ListVector, ListVectorBuilder};
-
-pub(crate) fn replicate_scalar(c: &C, offsets: &[usize]) -> VectorRef {
- assert_eq!(offsets.len(), c.len());
-
- if offsets.is_empty() {
- return c.slice(0, 0);
- }
- let mut builder = <::Builder>::with_capacity(c.len());
-
- let mut previous_offset = 0;
- for (i, offset) in offsets.iter().enumerate() {
- let data = c.get_data(i);
- for _ in previous_offset..*offset {
- builder.push(data.clone());
- }
- previous_offset = *offset;
- }
- builder.to_vector()
-}
-
-pub(crate) fn replicate_list(c: &ListVector, offsets: &[usize]) -> VectorRef {
- assert_eq!(offsets.len(), c.len());
-
- if offsets.is_empty() {
- return c.slice(0, 0);
- }
- let mut builder =
- ListVectorBuilder::with_type_capacity(c.item_type(), *offsets.last().unwrap());
-
- let mut previous_offset = 0;
- for (i, offset) in offsets.iter().enumerate() {
- let data = c.get_data(i);
- for _ in previous_offset..*offset {
- builder.push(data.clone());
- }
- previous_offset = *offset;
- }
- builder.to_vector()
-}
-
-#[cfg(test)]
-mod tests {
- use std::sync::Arc;
-
- use common_time::timestamp::TimeUnit;
- use common_time::{Date, Timestamp};
- use paste::paste;
-
- use super::*;
- use crate::value::{ListValue, ListValueRef};
- use crate::vectors::constant::ConstantVector;
- use crate::vectors::{
- Decimal128Vector, Int32Vector, ListVectorBuilder, NullVector, StringVector, VectorOp,
- };
-
- #[test]
- fn test_replicate_primitive() {
- let v = Int32Vector::from_iterator(0..5);
- let offsets = [0, 1, 2, 3, 4];
-
- let v = v.replicate(&offsets);
- assert_eq!(4, v.len());
-
- for i in 0..4 {
- assert_eq!(Value::Int32(i as i32 + 1), v.get(i));
- }
- }
-
- #[test]
- fn test_replicate_nullable_primitive() {
- let v = Int32Vector::from(vec![None, Some(1), None, Some(2)]);
- let offsets = [2, 4, 6, 8];
- let v = v.replicate(&offsets);
- assert_eq!(8, v.len());
-
- let expect: VectorRef = Arc::new(Int32Vector::from(vec![
- None,
- None,
- Some(1),
- Some(1),
- None,
- None,
- Some(2),
- Some(2),
- ]));
- assert_eq!(expect, v);
- }
-
- #[test]
- fn test_replicate_scalar() {
- let v = StringVector::from_slice(&["0", "1", "2", "3"]);
- let offsets = [1, 3, 5, 6];
-
- let v = v.replicate(&offsets);
- assert_eq!(6, v.len());
-
- let expect: VectorRef = Arc::new(StringVector::from_slice(&["0", "1", "1", "2", "2", "3"]));
- assert_eq!(expect, v);
- }
-
- #[test]
- fn test_replicate_list() {
- let item_type = Arc::new(ConcreteDataType::int32_datatype());
- let first = ListValue::new(vec![Value::Int32(1), Value::Int32(2)], item_type.clone());
- let second = ListValue::new(vec![Value::Int32(3)], item_type.clone());
- let mut builder = ListVectorBuilder::with_type_capacity(item_type, 2);
- builder.push(Some(ListValueRef::Ref { val: &first }));
- builder.push(Some(ListValueRef::Ref { val: &second }));
- let v = builder.finish();
-
- let v = v.replicate(&[1, 3]);
- let mut expect_builder =
- ListVectorBuilder::with_type_capacity(Arc::new(ConcreteDataType::int32_datatype()), 3);
- expect_builder.push(Some(ListValueRef::Ref { val: &first }));
- expect_builder.push(Some(ListValueRef::Ref { val: &second }));
- expect_builder.push(Some(ListValueRef::Ref { val: &second }));
- let expect = expect_builder.to_vector();
- assert_eq!(expect, v);
- }
-
- #[test]
- fn test_replicate_constant() {
- let v = Arc::new(StringVector::from_slice(&["hello"]));
- let cv = ConstantVector::new(v.clone(), 2);
- let offsets = [1, 4];
-
- let cv = cv.replicate(&offsets);
- assert_eq!(4, cv.len());
-
- let expect: VectorRef = Arc::new(ConstantVector::new(v, 4));
- assert_eq!(expect, cv);
- }
-
- #[test]
- fn test_replicate_null() {
- let v = NullVector::new(0);
- let offsets = [];
- let v = v.replicate(&offsets);
- assert!(v.is_empty());
-
- let v = NullVector::new(3);
- let offsets = [1, 3, 5];
-
- let v = v.replicate(&offsets);
- assert_eq!(5, v.len());
- }
-
- macro_rules! impl_replicate_date_like_test {
- ($VectorType: ident, $ValueType: ident, $method: ident) => {{
- use $crate::vectors::$VectorType;
-
- let v = $VectorType::from_iterator((0..5).map($ValueType::$method));
- let offsets = [0, 1, 2, 3, 4];
-
- let v = v.replicate(&offsets);
- assert_eq!(4, v.len());
-
- for i in 0..4 {
- assert_eq!(
- Value::$ValueType($ValueType::$method((i as i32 + 1).into())),
- v.get(i)
- );
- }
- }};
- }
-
- macro_rules! impl_replicate_timestamp_test {
- ($unit: ident) => {{
- paste!{
- use $crate::vectors::[];
- use $crate::timestamp::[];
- let v = []::from_iterator((0..5).map([]::from));
- let offsets = [0, 1, 2, 3, 4];
- let v = v.replicate(&offsets);
- assert_eq!(4, v.len());
- for i in 0..4 {
- assert_eq!(
- Value::Timestamp(Timestamp::new(i as i64 + 1, TimeUnit::$unit)),
- v.get(i)
- );
- }
- }
- }};
- }
-
- #[test]
- fn test_replicate_date_like() {
- impl_replicate_date_like_test!(DateVector, Date, new);
- impl_replicate_timestamp_test!(Second);
- impl_replicate_timestamp_test!(Millisecond);
- impl_replicate_timestamp_test!(Microsecond);
- impl_replicate_timestamp_test!(Nanosecond);
- }
-
- #[test]
- fn test_replicate_decimal() {
- let data = vec![100];
- // create a decimal vector
- let v = Decimal128Vector::from_values(data.clone())
- .with_precision_and_scale(10, 2)
- .unwrap();
- let offsets = [5];
- let v = v.replicate(&offsets);
- assert_eq!(5, v.len());
-
- let expect: VectorRef = Arc::new(
- Decimal128Vector::from_values(vec![100; 5])
- .with_precision_and_scale(10, 2)
- .unwrap(),
- );
- assert_eq!(expect, v);
- }
-}
diff --git a/src/datatypes/src/vectors/operations/take.rs b/src/datatypes/src/vectors/operations/take.rs
index 41947d3ef51..167f650c8ed 100644
--- a/src/datatypes/src/vectors/operations/take.rs
+++ b/src/datatypes/src/vectors/operations/take.rs
@@ -43,8 +43,7 @@ mod tests {
use crate::types::{LogicalPrimitiveType, WrapperType};
use crate::vectors::operations::VectorOp;
use crate::vectors::{
- BooleanVector, ConstantVector, Int32Vector, NullVector, PrimitiveVector, StringVector,
- UInt32Vector,
+ BooleanVector, Int32Vector, NullVector, PrimitiveVector, StringVector, UInt32Vector,
};
fn check_take_primitive(
@@ -119,29 +118,6 @@ mod tests {
take_time_like_test!(TimestampNanosecondVector, TimestampNanosecond, from_native);
}
- fn check_take_constant(expect_length: usize, input_length: usize, indices: &[u32]) {
- let v = ConstantVector::new(Arc::new(Int32Vector::from_slice([111])), input_length);
- let indices = UInt32Vector::from_slice(indices);
- let out = v.take(&indices).unwrap();
-
- assert!(out.is_const());
- assert_eq!(expect_length, out.len());
- }
-
- #[test]
- fn test_take_constant() {
- check_take_constant(2, 5, &[3, 4]);
- check_take_constant(3, 10, &[1, 2, 3]);
- check_take_constant(4, 10, &[1, 5, 3, 6]);
- check_take_constant(5, 10, &[1, 9, 8, 7, 3]);
- }
-
- #[test]
- #[should_panic]
- fn test_take_constant_out_of_index() {
- check_take_constant(2, 5, &[3, 5]);
- }
-
#[test]
#[should_panic]
fn test_take_out_of_index() {
diff --git a/src/datatypes/src/vectors/primitive.rs b/src/datatypes/src/vectors/primitive.rs
index dc85f94c4ff..92c8394e710 100644
--- a/src/datatypes/src/vectors/primitive.rs
+++ b/src/datatypes/src/vectors/primitive.rs
@@ -366,40 +366,6 @@ where
}
}
-pub(crate) fn replicate_primitive(
- vector: &PrimitiveVector,
- offsets: &[usize],
-) -> PrimitiveVector {
- assert_eq!(offsets.len(), vector.len());
-
- if offsets.is_empty() {
- return vector.get_slice(0, 0);
- }
-
- let mut builder = PrimitiveVectorBuilder::::with_capacity(*offsets.last().unwrap());
-
- let mut previous_offset = 0;
-
- for (offset, value) in offsets.iter().zip(vector.array.iter()) {
- let repeat_times = *offset - previous_offset;
- match value {
- Some(data) => {
- unsafe {
- // Safety: std::iter::Repeat and std::iter::Take implement TrustedLen.
- builder
- .mutable_array
- .append_trusted_len_iter(std::iter::repeat_n(data, repeat_times));
- }
- }
- None => {
- builder.mutable_array.append_nulls(repeat_times);
- }
- }
- previous_offset = *offset;
- }
- builder.finish()
-}
-
#[cfg(test)]
mod tests {
use std::vec;
@@ -433,7 +399,6 @@ mod tests {
fn check_vec(v: Int32Vector) {
assert_eq!(4, v.len());
assert_eq!("Int32Vector", v.vector_type_name());
- assert!(!v.is_const());
assert!(v.validity().is_all_valid());
assert!(!v.only_null());
diff --git a/src/datatypes/src/vectors/string.rs b/src/datatypes/src/vectors/string.rs
index 53324280c3f..0476ecc6440 100644
--- a/src/datatypes/src/vectors/string.rs
+++ b/src/datatypes/src/vectors/string.rs
@@ -604,7 +604,6 @@ mod tests {
let v = StringVector::from(strs.clone());
assert_eq!(3, v.len());
assert_eq!("StringVector", v.vector_type_name());
- assert!(!v.is_const());
assert!(v.validity().is_all_valid());
assert!(!v.only_null());
assert_eq!(1040, v.memory_size());
diff --git a/src/datatypes/src/vectors/struct_vector.rs b/src/datatypes/src/vectors/struct_vector.rs
index e6284351b4f..4798c8fddcd 100644
--- a/src/datatypes/src/vectors/struct_vector.rs
+++ b/src/datatypes/src/vectors/struct_vector.rs
@@ -15,7 +15,7 @@
use std::any::Any;
use std::sync::Arc;
-use arrow::array::{MutableArrayData, NullBufferBuilder};
+use arrow::array::NullBufferBuilder;
use arrow::compute::TakeOptions;
use arrow::datatypes::DataType as ArrowDataType;
use arrow_array::{Array, ArrayRef, StructArray};
@@ -143,31 +143,6 @@ impl Vector for StructVector {
}
impl VectorOp for StructVector {
- fn replicate(&self, offsets: &[usize]) -> VectorRef {
- assert_eq!(offsets.len(), self.len());
- assert!(offsets.is_sorted(), "offsets must be non-decreasing");
-
- let Some(&output_len) = offsets.last() else {
- return self.slice(0, 0);
- };
-
- let source = self.array.to_data();
- let mut output = MutableArrayData::new(vec![&source], false, output_len);
- let mut previous_offset = 0;
-
- for (index, &offset) in offsets.iter().enumerate() {
- for _ in previous_offset..offset {
- output.extend(0, index, index + 1);
- }
- previous_offset = offset;
- }
-
- Arc::new(StructVector {
- array: StructArray::from(output.freeze()),
- fields: self.fields.clone(),
- })
- }
-
fn cast(&self, _to_type: &ConcreteDataType) -> Result {
UnsupportedOperationSnafu {
op: "cast",
@@ -466,10 +441,6 @@ impl ScalarVectorBuilder for StructVectorBuilder {
#[cfg(test)]
mod tests {
- use arrow::array::{DictionaryArray, Int8Array, StringArray};
- use arrow::buffer::NullBuffer;
- use arrow::datatypes::Int8Type;
-
use super::*;
use crate::json::JsonSettings;
use crate::schema::{ColumnDefaultConstraint, ColumnSchema};
@@ -555,7 +526,7 @@ mod tests {
}
#[test]
- fn test_replicate_preserves_json2_identity() {
+ fn test_default_vector_preserves_json2_identity() {
let json = JsonSettings::default()
.encode(serde_json::json!({"answer": 42}))
.unwrap();
@@ -576,39 +547,6 @@ mod tests {
assert_eq!(replicated.len(), 2);
}
- #[test]
- fn test_replicate_preserves_dictionary_and_nulls() {
- let fields = StructType::new(Arc::new(vec![StructField::new(
- "label",
- ConcreteDataType::dictionary_datatype(
- ConcreteDataType::int8_datatype(),
- ConcreteDataType::string_datatype(),
- ),
- true,
- )]));
- let dictionary = DictionaryArray::::new(
- Int8Array::from(vec![Some(0), Some(1)]),
- Arc::new(StringArray::from(vec!["a", "b"])),
- );
- let array = StructArray::new(
- fields.as_arrow_fields(),
- vec![Arc::new(dictionary)],
- Some(NullBuffer::from(vec![true, false])),
- );
- let vector = StructVector::try_new(fields.clone(), array).unwrap();
-
- let replicated = vector.replicate(&[2, 3]);
-
- assert_eq!(
- replicated.data_type(),
- ConcreteDataType::struct_datatype(fields)
- );
- assert_eq!(replicated.len(), 3);
- assert_eq!(replicated.null_count(), 1);
- assert_eq!(replicated.get(0), replicated.get(1));
- assert!(replicated.is_null(2));
- }
-
#[test]
fn test_deep_nested_struct_list() {
// level 1: struct
diff --git a/src/flow/src/batching_mode/frontend_client.rs b/src/flow/src/batching_mode/frontend_client.rs
index 10ce3b6820d..e932933ff84 100644
--- a/src/flow/src/batching_mode/frontend_client.rs
+++ b/src/flow/src/batching_mode/frontend_client.rs
@@ -183,13 +183,21 @@ impl DatabaseWithPeer {
/// Try sending a "SELECT 1" to the database
async fn try_select_one(&self) -> Result<(), Error> {
// notice here use `sql` for `SELECT 1` return 1 row
- let _ = self
+ let output = self
.database
.sql("SELECT 1")
.await
.with_context(|_| InvalidRequestSnafu {
context: format!("Failed to handle `SELECT 1` request at {:?}", self.peer),
})?;
+
+ if let OutputData::Stream(stream) = output.data {
+ common_recordbatch::util::collect(stream)
+ .await
+ .map_err(BoxedError::new)
+ .context(ExternalSnafu)?;
+ }
+
Ok(())
}
}
@@ -698,9 +706,11 @@ mod tests {
use api::v1::query_request::Query;
use arrow_flight::flight_service_server::FlightServiceServer;
use arrow_flight::{FlightData, Ticket};
+ use common_grpc::flight::FlightEncoder;
use common_query::{Output, OutputData};
use common_recordbatch::adapter::RecordBatchMetrics;
use common_recordbatch::{OrderOption, RecordBatch, RecordBatchStream};
+ use datatypes::arrow::datatypes::Schema as ArrowSchema;
use datatypes::prelude::{ConcreteDataType, VectorRef};
use datatypes::schema::{ColumnSchema, Schema};
use datatypes::vectors::Int32Vector;
@@ -775,6 +785,14 @@ mod tests {
#[derive(Debug)]
struct SlowFlight;
+ struct DelayedEofFlight {
+ schema_sent: Mutex>>,
+ release: Arc,
+ }
+
+ #[derive(Debug)]
+ struct LateStreamErrorFlight;
+
struct WaitForConcurrentFlight {
barrier: Arc,
}
@@ -870,6 +888,45 @@ mod tests {
}
}
+ #[async_trait::async_trait]
+ impl FlightCraft for DelayedEofFlight {
+ async fn do_get(
+ &self,
+ _request: TonicRequest,
+ ) -> std::result::Result>, Status> {
+ let schema = FlightEncoder::default().encode_schema(&ArrowSchema::empty());
+ let schema_sent = self.schema_sent.lock().unwrap().take();
+ let schema_stream = futures::stream::once(async move {
+ if let Some(schema_sent) = schema_sent {
+ let _ = schema_sent.send(());
+ }
+ Ok(schema)
+ });
+ let release = self.release.clone();
+ let delayed_eof = futures::stream::unfold(release, |release| async move {
+ release.notified().await;
+ None::<(std::result::Result, _)>
+ });
+
+ Ok(TonicResponse::new(Box::pin(
+ schema_stream.chain(delayed_eof),
+ )))
+ }
+ }
+
+ #[async_trait::async_trait]
+ impl FlightCraft for LateStreamErrorFlight {
+ async fn do_get(
+ &self,
+ _request: TonicRequest,
+ ) -> std::result::Result>, Status> {
+ let schema = FlightEncoder::default().encode_schema(&ArrowSchema::empty());
+ let stream =
+ futures::stream::iter([Ok(schema), Err(Status::unavailable("late stream error"))]);
+ Ok(TonicResponse::new(Box::pin(stream)))
+ }
+ }
+
#[async_trait::async_trait]
impl FlightCraft for WaitForConcurrentFlight {
async fn do_get(
@@ -1056,6 +1113,71 @@ mod tests {
assert!(format!("{err:?}").contains("Invalid value for flow.return_region_seq"));
}
+ #[tokio::test]
+ async fn test_try_select_one_waits_for_stream_eof() {
+ let (schema_sent, schema_sent_rx) = tokio::sync::oneshot::channel();
+ let release = Arc::new(tokio::sync::Notify::new());
+ let (addr, server) = start_flight_server(DelayedEofFlight {
+ schema_sent: Mutex::new(Some(schema_sent)),
+ release: release.clone(),
+ })
+ .await;
+ let database = Database::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ Client::with_urls([addr.as_str()]),
+ );
+ let db = DatabaseWithPeer::new(
+ database,
+ Peer {
+ id: 1,
+ addr: addr.clone(),
+ },
+ );
+ let mut probe = tokio::spawn(async move { db.try_select_one().await });
+
+ timeout(Duration::from_secs(1), schema_sent_rx)
+ .await
+ .expect("server should send the schema")
+ .expect("schema signal should be sent");
+ assert!(
+ timeout(Duration::from_millis(100), &mut probe)
+ .await
+ .is_err(),
+ "SELECT 1 must wait for the delayed stream tail and EOF"
+ );
+
+ release.notify_one();
+ timeout(Duration::from_secs(1), &mut probe)
+ .await
+ .expect("SELECT 1 should complete after EOF")
+ .expect("probe task should not panic")
+ .expect("SELECT 1 should succeed after EOF");
+ server.abort();
+ }
+
+ #[tokio::test]
+ async fn test_try_select_one_propagates_late_stream_error() {
+ let (addr, server) = start_flight_server(LateStreamErrorFlight).await;
+ let database = Database::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ Client::with_urls([addr.as_str()]),
+ );
+ let db = DatabaseWithPeer::new(
+ database,
+ Peer {
+ id: 1,
+ addr: addr.clone(),
+ },
+ );
+
+ let err = db.try_select_one().await.unwrap_err();
+ server.abort();
+
+ assert!(format!("{err:?}").contains("late stream error"));
+ }
+
#[tokio::test]
async fn test_check_all_frontends_without_auth_fails_fast_on_unauthenticated_frontend() {
let (addr, server) = start_flight_server(RejectUnauthenticatedFlight).await;
diff --git a/src/flow/src/expr/scalar.rs b/src/flow/src/expr/scalar.rs
index 18649bff123..af16381e4cf 100644
--- a/src/flow/src/expr/scalar.rs
+++ b/src/flow/src/expr/scalar.rs
@@ -216,6 +216,7 @@ impl ScalarExpr {
msg: "Failed to convert literal to scalar value",
})?,
batch.row_count(),
+ None,
)
.context(DataTypeSnafu {
msg: "Failed to convert scalar value to vector ref when parsing literal",
diff --git a/src/flow/src/server.rs b/src/flow/src/server.rs
index 9bda516f24c..1b26fbbf15e 100644
--- a/src/flow/src/server.rs
+++ b/src/flow/src/server.rs
@@ -124,6 +124,7 @@ impl flow_server::Flow for FlowService {
api::v1::region::InsertRequest {
region_id: insert.region_id,
rows: insert.rows,
+ skip_wal: false,
partition_expr_version: insert.partition_expr_version,
}
})
diff --git a/src/frontend/src/instance.rs b/src/frontend/src/instance.rs
index dd89a1f3f95..f834722cca5 100644
--- a/src/frontend/src/instance.rs
+++ b/src/frontend/src/instance.rs
@@ -1329,6 +1329,7 @@ impl PrometheusHandler for Instance {
self.check_query_target_permission(targets, &query_ctx)
.await?;
+ let requires_output_ordering = query.requires_output_ordering();
let (query, stmt) = query.into_parts();
let QueryStatement::Promql(eval_stmt, _) = &stmt else {
@@ -1342,6 +1343,12 @@ impl PrometheusHandler for Instance {
.map_err(BoxedError::new)
.context(ExecuteQuerySnafu)?;
+ let plan = if requires_output_ordering {
+ plan
+ } else {
+ promql::remove_output_sort(plan)
+ };
+
interceptor.pre_execute(&query, &eval_stmt.expr, Some(&plan), query_ctx.clone())?;
// Take the EvalStmt from the original QueryStatement and use it to create the CatalogQueryStatement.
diff --git a/src/frontend/src/instance/grpc.rs b/src/frontend/src/instance/grpc.rs
index 7d4f0ee7959..2d89b5489df 100644
--- a/src/frontend/src/instance/grpc.rs
+++ b/src/frontend/src/instance/grpc.rs
@@ -453,6 +453,7 @@ impl Instance {
request.flight_data,
request.record_batch,
request.schema_bytes,
+ ctx.skip_wal(),
)
.await
.context(TableOperationSnafu)?;
diff --git a/src/frontend/src/instance/promql.rs b/src/frontend/src/instance/promql.rs
index 0f6121441f6..3df70224703 100644
--- a/src/frontend/src/instance/promql.rs
+++ b/src/frontend/src/instance/promql.rs
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+use std::sync::Arc;
use std::time::SystemTime;
use auth::PermissionTableTarget;
@@ -21,6 +22,7 @@ use common_catalog::consts::INFORMATION_SCHEMA_NAME;
use common_catalog::format_full_table_name;
use common_recordbatch::util;
use common_telemetry::tracing;
+use datafusion_expr::LogicalPlan;
use promql_parser::label::{Matcher, Matchers};
use query::promql;
use query::promql::planner::PromPlanner;
@@ -35,6 +37,21 @@ use crate::error::{
};
use crate::instance::Instance;
+/// Strips the output sort a PromQL plan ends with, keeping the plan schema intact.
+///
+/// Only the sort the caller would observe is removed: recursion stops at any other
+/// node, so ordering consumed by windows, limits or PromQL extension nodes stays.
+pub(super) fn remove_output_sort(plan: LogicalPlan) -> LogicalPlan {
+ match plan {
+ LogicalPlan::Sort(sort) if sort.fetch.is_none() => Arc::unwrap_or_clone(sort.input),
+ LogicalPlan::Projection(mut projection) => {
+ projection.input = Arc::new(remove_output_sort(Arc::unwrap_or_clone(projection.input)));
+ LogicalPlan::Projection(projection)
+ }
+ plan => plan,
+ }
+}
+
impl Instance {
/// Handles metric names query request, returns the names.
#[tracing::instrument(skip_all)]
@@ -180,3 +197,79 @@ impl Instance {
Ok(results)
}
}
+
+#[cfg(test)]
+mod tests {
+ use datafusion::arrow::array::Int64Array;
+ use datafusion::prelude::{SessionConfig, SessionContext};
+ use datafusion_expr::{LogicalPlanBuilder, Sort, col, lit};
+
+ use super::*;
+
+ #[tokio::test]
+ async fn remove_output_sort_keeps_schema_and_row_selection() {
+ let input =
+ LogicalPlanBuilder::values(vec![vec![lit(3_i64)], vec![lit(1_i64)], vec![lit(2_i64)]])
+ .unwrap()
+ .build()
+ .unwrap();
+ let sort = Sort {
+ expr: vec![col("column1").sort(true, false)],
+ input: Arc::new(input),
+ fetch: None,
+ };
+ let projected = LogicalPlanBuilder::from(LogicalPlan::Sort(sort.clone()))
+ .project(vec![col("column1").alias("sample")])
+ .unwrap()
+ .project(vec![col("sample")])
+ .unwrap()
+ .build()
+ .unwrap();
+ // The root sort is removed, but the sort feeding the limit selects the rows.
+ let limited = LogicalPlanBuilder::from(LogicalPlan::Sort(sort.clone()))
+ .limit(0, Some(2))
+ .unwrap()
+ .sort(vec![col("column1").sort(false, false)])
+ .unwrap()
+ .build()
+ .unwrap();
+ let fetched = LogicalPlan::Sort(Sort {
+ fetch: Some(2),
+ ..sort.clone()
+ });
+
+ let context =
+ SessionContext::new_with_config(SessionConfig::new().with_target_partitions(1));
+ for (name, plan, expected) in [
+ ("root", LogicalPlan::Sort(sort), vec![3, 1, 2]),
+ ("projection", projected, vec![3, 1, 2]),
+ ("sort below limit", limited, vec![1, 2]),
+ ("fetch", fetched, vec![1, 2]),
+ ] {
+ let schema = plan.schema().clone();
+ let plan = remove_output_sort(plan);
+ assert_eq!(plan.schema(), &schema, "{name}");
+ let output = context
+ .execute_logical_plan(plan)
+ .await
+ .unwrap()
+ .collect()
+ .await
+ .unwrap();
+ let values = output
+ .iter()
+ .flat_map(|batch| {
+ batch
+ .column(0)
+ .as_any()
+ .downcast_ref::()
+ .unwrap()
+ .values()
+ .iter()
+ .copied()
+ })
+ .collect::>();
+ assert_eq!(values, expected, "{name}");
+ }
+ }
+}
diff --git a/src/meta-srv/src/error.rs b/src/meta-srv/src/error.rs
index 53872d4817f..70997ebe7d1 100644
--- a/src/meta-srv/src/error.rs
+++ b/src/meta-srv/src/error.rs
@@ -368,9 +368,9 @@ pub enum Error {
location: Location,
},
- #[snafu(display("Failed to deserialize from json: {}", input))]
+ #[snafu(display("Failed to deserialize from json payload of length {}", input_len))]
DeserializeFromJson {
- input: String,
+ input_len: usize,
#[snafu(source)]
error: serde_json::error::Error,
#[snafu(implicit)]
diff --git a/src/meta-srv/src/event/gc.rs b/src/meta-srv/src/event/gc.rs
index 7f679f1785b..94d671a176b 100644
--- a/src/meta-srv/src/event/gc.rs
+++ b/src/meta-srv/src/event/gc.rs
@@ -218,6 +218,7 @@ mod tests {
use common_event_recorder::testing::assert_event_contract;
use common_event_recorder::{EventTypeFilter, PersistentEventContext, TriggerReason};
use common_meta::key::TableMetadataManager;
+ use common_meta::key::runtime_switch::RuntimeSwitchManager;
use common_meta::kv_backend::memory::MemoryKvBackend;
use common_meta::sequence::SequenceBuilder;
use common_procedure::{
@@ -588,11 +589,13 @@ mod tests {
fn batch_gc_procedure() -> BatchGcProcedure {
let kv_backend = Arc::new(MemoryKvBackend::new());
let table_metadata_manager = Arc::new(TableMetadataManager::new(kv_backend.clone()));
+ let runtime_switch_manager = Arc::new(RuntimeSwitchManager::new(kv_backend.clone()));
let mailbox_sequence = SequenceBuilder::new("test_batch_gc_event", kv_backend).build();
let mailbox = MailboxContext::new(mailbox_sequence);
BatchGcProcedure::new(
mailbox.mailbox().clone(),
table_metadata_manager,
+ runtime_switch_manager,
"localhost".to_string(),
vec![RegionId::new(1024, 1)],
true,
diff --git a/src/meta-srv/src/gc/ctx.rs b/src/meta-srv/src/gc/ctx.rs
index 4b46a941e25..857f5e87cdc 100644
--- a/src/meta-srv/src/gc/ctx.rs
+++ b/src/meta-srv/src/gc/ctx.rs
@@ -27,6 +27,7 @@ use common_meta::ddl_manager::DdlManagerRef;
#[cfg(feature = "enterprise")]
use common_meta::key::DroppedTableName;
use common_meta::key::TableMetadataManagerRef;
+use common_meta::key::runtime_switch::RuntimeSwitchManagerRef;
use common_meta::key::table_repart::TableRepartValue;
use common_meta::key::table_route::PhysicalTableRouteValue;
#[cfg(feature = "enterprise")]
@@ -155,6 +156,8 @@ pub(crate) struct DefaultGcSchedulerCtx {
pub(crate) table_metadata_manager: TableMetadataManagerRef,
/// Procedure manager.
pub(crate) procedure_manager: ProcedureManagerRef,
+ /// Runtime switch manager used by recovered and newly submitted GC procedures.
+ pub(crate) runtime_switch_manager: RuntimeSwitchManagerRef,
/// DDL manager used to submit the existing purge procedure.
#[cfg(feature = "enterprise")]
pub(crate) ddl_manager: DdlManagerRef,
@@ -176,6 +179,7 @@ impl DefaultGcSchedulerCtx {
pub fn try_new(
table_metadata_manager: TableMetadataManagerRef,
procedure_manager: ProcedureManagerRef,
+ runtime_switch_manager: RuntimeSwitchManagerRef,
#[cfg(feature = "enterprise")] ddl_manager: DdlManagerRef,
meta_peer_client: MetaPeerClientRef,
mailbox: MailboxRef,
@@ -184,6 +188,7 @@ impl DefaultGcSchedulerCtx {
Ok(Self {
table_metadata_manager,
procedure_manager,
+ runtime_switch_manager,
#[cfg(feature = "enterprise")]
ddl_manager,
#[cfg(feature = "enterprise")]
@@ -320,6 +325,7 @@ impl DefaultGcSchedulerCtx {
let procedure = BatchGcProcedure::new(
self.mailbox.clone(),
self.table_metadata_manager.clone(),
+ self.runtime_switch_manager.clone(),
self.server_addr.clone(),
region_ids.to_vec(),
full_file_listing,
diff --git a/src/meta-srv/src/gc/procedure.rs b/src/meta-srv/src/gc/procedure.rs
index f9f597577ae..db22f3ab00d 100644
--- a/src/meta-srv/src/gc/procedure.rs
+++ b/src/meta-srv/src/gc/procedure.rs
@@ -18,8 +18,14 @@ use std::sync::Arc;
use std::time::Duration;
use api::v1::meta::MailboxMessage;
-use common_meta::instruction::{self, GcRegions, GetFileRefs, GetFileRefsReply, InstructionReply};
+use common_error::ext::PlainError;
+use common_error::status_code::StatusCode;
+use common_meta::instruction::{
+ self, GetPackedFileRefs, GetPackedFileRefsReply, InstructionReply, PackedFileRefsManifest,
+ PackedGcRegions, PackedRegionFileRefs,
+};
use common_meta::key::TableMetadataManagerRef;
+use common_meta::key::runtime_switch::RuntimeSwitchManagerRef;
use common_meta::key::table_repart::TableRepartValue;
use common_meta::key::table_route::PhysicalTableRouteValue;
use common_meta::lock_key::{RegionLock, TableLock};
@@ -34,6 +40,7 @@ use common_telemetry::tracing::Instrument as _;
use common_telemetry::tracing_context::TracingContext;
use common_telemetry::{debug, error, info, warn};
use futures::future::join_all;
+use futures::stream::{FuturesUnordered, StreamExt};
use itertools::Itertools as _;
use serde::{Deserialize, Serialize};
use snafu::ResultExt as _;
@@ -49,14 +56,14 @@ use crate::metrics::{METRIC_META_GC_DATANODE_CALLS_TOTAL, METRIC_META_GC_FAILED_
use crate::procedure::utils::{instruction_error_result, instruction_to_error};
use crate::service::mailbox::{Channel, MailboxReceiver, MailboxRef};
-async fn send_get_file_refs_inner(
+async fn send_get_packed_file_refs_inner(
mailbox: &MailboxRef,
server_addr: &str,
peer: &Peer,
- instruction: GetFileRefs,
+ instruction: GetPackedFileRefs,
timeout: Duration,
) -> Result {
- let instruction = instruction::Instruction::GetFileRefs(instruction);
+ let instruction = instruction::Instruction::GetPackedFileRefs(instruction);
let tracing_ctx = TracingContext::from_current_span();
let msg = MailboxMessage::json_message(
&format!("Get file references: {}", instruction),
@@ -75,25 +82,25 @@ async fn send_get_file_refs_inner(
.await
}
-async fn recv_get_file_refs_reply(
+async fn recv_get_packed_file_refs_reply(
peer: &Peer,
mailbox_rx: MailboxReceiver,
-) -> Result {
+) -> Result {
let reply = match mailbox_rx.await {
Ok(reply_msg) => HeartbeatMailbox::json_reply(&reply_msg)?,
Err(e) => {
error!(
- e; "Failed to receive reply from datanode {} for GetFileRefs instruction",
+ e; "Failed to receive reply from datanode {} for GetPackedFileRefs instruction",
peer,
);
return Err(e);
}
};
- let InstructionReply::GetFileRefs(reply) = reply else {
+ let InstructionReply::GetPackedFileRefs(reply) = reply else {
return error::UnexpectedInstructionReplySnafu {
- mailbox_message: format!("{:?}", reply),
- reason: "Unexpected reply of the GetFileRefs instruction",
+ mailbox_message: "unexpected instruction reply for GetPackedFileRefs".to_string(),
+ reason: "Unexpected reply of the GetPackedFileRefs instruction",
}
.fail();
};
@@ -104,12 +111,11 @@ async fn recv_get_file_refs_reply(
async fn send_gc_regions_inner(
mailbox: &MailboxRef,
peer: &Peer,
- gc_regions: &GcRegions,
+ instruction: instruction::Instruction,
server_addr: &str,
timeout: Duration,
description: &str,
) -> Result {
- let instruction = instruction::Instruction::GcRegions(gc_regions.clone());
let tracing_ctx = TracingContext::from_current_span();
let msg = MailboxMessage::json_message(
&format!("{}: {}", description, instruction),
@@ -128,9 +134,43 @@ async fn send_gc_regions_inner(
.await
}
+fn scoped_gc_instruction(
+ regions: Vec,
+ full_manifest: &FileRefsManifest,
+ full_file_listing: bool,
+) -> instruction::Instruction {
+ let packed_file_refs_manifest = PackedFileRefsManifest {
+ file_refs: regions
+ .iter()
+ .filter_map(|region| {
+ full_manifest
+ .file_refs
+ .get(region)
+ .map(|refs| (*region, PackedRegionFileRefs::from_refs(refs)))
+ })
+ .collect(),
+ manifest_version: regions
+ .iter()
+ .filter_map(|region| {
+ full_manifest
+ .manifest_version
+ .get(region)
+ .map(|version| (*region, *version))
+ })
+ .collect(),
+ cross_region_refs: HashMap::new(),
+ };
+
+ instruction::Instruction::PackedGcRegions(PackedGcRegions {
+ regions,
+ packed_file_refs_manifest,
+ full_file_listing,
+ })
+}
+
async fn recv_gc_regions_reply(
peer: &Peer,
- gc_regions: &GcRegions,
+ regions: &[RegionId],
description: &str,
mailbox_rx: MailboxReceiver,
) -> Result {
@@ -147,7 +187,7 @@ async fn recv_gc_regions_reply(
let InstructionReply::GcRegions(reply) = reply else {
return error::UnexpectedInstructionReplySnafu {
- mailbox_message: format!("{:?}", reply),
+ mailbox_message: "unexpected instruction reply for GcRegions".to_string(),
reason: "Unexpected reply of the GcRegions instruction",
}
.fail();
@@ -158,14 +198,16 @@ async fn recv_gc_regions_reply(
Ok(report) => Ok(report),
Err(e) => {
error!(
- e; "Datanode {} reported error during GC for regions {:?}",
- peer, gc_regions
+ e; "Datanode {} reported error during GC for {} regions",
+ peer, regions.len()
);
instruction_error_result(
&e,
format!(
- "Datanode {} reported error during GC for regions {:?}: {}",
- peer, gc_regions, e
+ "Datanode {} reported error during GC for {} regions: {}",
+ peer,
+ regions.len(),
+ e
),
)
}
@@ -177,6 +219,7 @@ async fn recv_gc_regions_reply(
pub struct BatchGcProcedure {
mailbox: MailboxRef,
table_metadata_manager: TableMetadataManagerRef,
+ runtime_switch_manager: RuntimeSwitchManagerRef,
data: BatchGcData,
}
@@ -217,9 +260,11 @@ pub enum State {
impl BatchGcProcedure {
pub const TYPE_NAME: &'static str = "metasrv-procedure::BatchGcProcedure";
+ #[allow(clippy::too_many_arguments)]
pub fn new(
mailbox: MailboxRef,
table_metadata_manager: TableMetadataManagerRef,
+ runtime_switch_manager: RuntimeSwitchManagerRef,
server_addr: String,
regions: Vec,
full_file_listing: bool,
@@ -229,6 +274,7 @@ impl BatchGcProcedure {
Self {
mailbox,
table_metadata_manager,
+ runtime_switch_manager,
data: BatchGcData {
state: State::Start,
server_addr,
@@ -251,6 +297,7 @@ impl BatchGcProcedure {
pub fn new_update_repartition_for_test(
mailbox: MailboxRef,
table_metadata_manager: TableMetadataManagerRef,
+ runtime_switch_manager: RuntimeSwitchManagerRef,
server_addr: String,
regions: Vec,
file_refs: FileRefsManifest,
@@ -259,6 +306,7 @@ impl BatchGcProcedure {
Self {
mailbox,
table_metadata_manager,
+ runtime_switch_manager,
data: BatchGcData {
state: State::UpdateRepartition,
server_addr,
@@ -286,6 +334,22 @@ impl BatchGcProcedure {
})
}
+ async fn check_maintenance_mode(&self) -> ProcedureResult<()> {
+ let enabled = self
+ .runtime_switch_manager
+ .maintenance_mode()
+ .await
+ .context(error::RuntimeSwitchManagerSnafu)
+ .map_err(ProcedureError::retry_later)?;
+ if enabled {
+ return Err(ProcedureError::external(PlainError::new(
+ "maintenance mode is enabled".to_string(),
+ StatusCode::IllegalState,
+ )));
+ }
+ Ok(())
+ }
+
fn merge_gc_report(&mut self, report: GcReport) {
let accumulated = self.data.gc_report.get_or_insert_default();
// Deleted objects are cumulative, while these sets describe the latest outcome
@@ -644,10 +708,10 @@ impl BatchGcProcedure {
}
}
- // Send GetFileRefs instructions to each datanode
+ // Send packed GetFileRefs instructions to each datanode
let mut all_file_refs: HashMap> = HashMap::new();
let mut all_manifest_versions = HashMap::new();
- let mut all_cross_region_refs = HashMap::new();
+ let mut all_cross_region_refs: HashMap> = HashMap::new();
let mut peers = HashSet::new();
peers.extend(datanode2query_regions.keys().cloned());
@@ -655,33 +719,34 @@ impl BatchGcProcedure {
let mailbox = &self.mailbox;
let server_addr = &self.data.server_addr;
- let mut tasks = Vec::new();
-
+ // Each future owns both send and receive, so a completed reply is merged and
+ // dropped immediately rather than retained in a second join_all buffer.
+ let mut tasks = FuturesUnordered::new();
for peer in peers {
let regions = datanode2query_regions.remove(&peer).unwrap_or_default();
let related_regions_for_peer =
datanode2related_regions.remove(&peer).unwrap_or_default();
-
if regions.is_empty() && related_regions_for_peer.is_empty() {
continue;
}
-
tasks.push(async move {
- let instruction = GetFileRefs {
- query_regions: regions.clone(),
- related_regions: related_regions_for_peer.clone(),
+ let instruction = GetPackedFileRefs {
+ query_regions: regions,
+ related_regions: related_regions_for_peer,
};
-
- let reply =
- send_get_file_refs_inner(mailbox, server_addr, &peer, instruction, timeout)
- .await;
-
- (peer, regions, related_regions_for_peer, reply)
+ let rx = send_get_packed_file_refs_inner(
+ mailbox,
+ server_addr,
+ &peer,
+ instruction,
+ timeout,
+ )
+ .await?;
+ let reply = recv_get_packed_file_refs_reply(&peer, rx).await?;
+ Ok::<_, crate::error::Error>((peer, reply))
});
}
- let mut recv_tasks = Vec::new();
- // store error to make sure metrics doesn't ignore other peers
let mut first_error = None;
let mut record_get_file_refs_error = |e| {
METRIC_META_GC_DATANODE_CALLS_TOTAL
@@ -691,80 +756,56 @@ impl BatchGcProcedure {
first_error = Some(e);
}
};
- for (peer, regions, related_regions_for_peer, reply) in join_all(tasks).await {
- match reply {
- Ok(mailbox_rx) => {
- recv_tasks.push(async move {
- let reply = recv_get_file_refs_reply(&peer, mailbox_rx).await;
- (peer, regions, related_regions_for_peer, reply)
- });
- }
- Err(e) => record_get_file_refs_error(e),
- }
- }
-
- let replies = join_all(recv_tasks).await;
-
- for (peer, regions, related_regions_for_peer, reply) in replies {
- let reply = match reply {
+ while let Some(result) = tasks.next().await {
+ let (peer, reply) = match result {
Ok(reply) => reply,
Err(e) => {
record_get_file_refs_error(e);
continue;
}
};
- debug!(
- "Got file references from datanode: {:?}, query_regions: {:?}, related_regions: {:?}, reply: {:?}",
- peer, regions, related_regions_for_peer, reply
- );
-
if !reply.success {
- METRIC_META_GC_DATANODE_CALLS_TOTAL
- .with_label_values(&["get_file_refs", "error"])
- .inc();
let err = if let Some(error) = &reply.error {
instruction_to_error(
error,
- format!(
- "Failed to get file references from datanode {}: {:?}",
- peer, error
- ),
+ format!("Failed to get file references from datanode {peer}"),
)
} else {
error::UnexpectedSnafu {
- violated: format!(
- "Failed to get file references from datanode {}: {:?}",
- peer, reply.error
- ),
+ violated:
+ "Datanode returned an unsuccessful GetPackedFileRefs reply without an error"
+ .to_string(),
}
.build()
};
record_get_file_refs_error(err);
continue;
}
+ let manifest = match reply.packed_file_refs_manifest.into_manifest() {
+ Ok(manifest) => manifest,
+ Err(err) => {
+ record_get_file_refs_error(error::Error::Other {
+ source: common_error::ext::BoxedError::new(err),
+ location: snafu::Location::new(file!(), line!(), 0),
+ });
+ continue;
+ }
+ };
METRIC_META_GC_DATANODE_CALLS_TOTAL
.with_label_values(&["get_file_refs", "success"])
.inc();
-
- // Merge the file references from this datanode
- for (region_id, file_refs) in reply.file_refs_manifest.file_refs {
- all_file_refs
- .entry(region_id)
- .or_default()
- .extend(file_refs);
+ for (region_id, refs) in manifest.file_refs {
+ all_file_refs.entry(region_id).or_default().extend(refs);
}
-
- // region manifest version should be the smallest one among all peers, so outdated region can be detected
- for (region_id, version) in reply.file_refs_manifest.manifest_version {
+ for (region_id, version) in manifest.manifest_version {
let entry = all_manifest_versions.entry(region_id).or_insert(version);
*entry = (*entry).min(version);
}
-
- for (region_id, related_region_ids) in reply.file_refs_manifest.cross_region_refs {
- let entry = all_cross_region_refs
+ for (region_id, related) in manifest.cross_region_refs {
+ all_cross_region_refs
.entry(region_id)
- .or_insert_with(HashSet::new);
- entry.extend(related_region_ids);
+ .or_default()
+ .extend(related);
}
}
@@ -813,28 +854,23 @@ impl BatchGcProcedure {
let tasks = datanode2regions
.into_iter()
.map(|(peer, regions_for_peer)| {
- let gc_regions = GcRegions {
- regions: regions_for_peer.clone(),
- // file_refs_manifest could be somewhere large. But still intentionally clone per datanode here:
- // this path is admin-triggered or scheduler-triggered, peer count is expected to be bounded, and
- // and abnormal manifest growth should be addressed at the source
- file_refs_manifest: file_refs.clone(),
- full_file_listing,
- };
- let region_count = gc_regions.regions.len() as u64;
+ let region_count = regions_for_peer.len() as u64;
+ let regions = regions_for_peer.clone();
+ let instruction =
+ scoped_gc_instruction(regions_for_peer, file_refs, full_file_listing);
async move {
let report = send_gc_regions_inner(
mailbox,
&peer,
- &gc_regions,
+ instruction,
server_addr,
timeout,
"Batch GC",
)
.await;
- (peer, gc_regions, region_count, report)
+ (peer, regions, region_count, report)
}
});
@@ -851,12 +887,12 @@ impl BatchGcProcedure {
first_error = Some(e);
}
};
- for (peer, gc_regions, region_count, report) in join_all(tasks).await {
+ for (peer, regions, region_count, report) in join_all(tasks).await {
match report {
Ok(mailbox_rx) => {
recv_tasks.push(async move {
let report =
- recv_gc_regions_reply(&peer, &gc_regions, "Batch GC", mailbox_rx).await;
+ recv_gc_regions_reply(&peer, ®ions, "Batch GC", mailbox_rx).await;
(peer, region_count, report)
});
}
@@ -887,12 +923,12 @@ impl BatchGcProcedure {
if need_retry.is_empty() {
info!(
- "GC report from datanode {}: successfully deleted files for regions {:?}",
+ "GC report from datanode {}: successfully deleted files for region IDs {:?}",
peer, success
);
} else {
warn!(
- "GC report from datanode {}: successfully deleted files for regions {:?}, need retry for regions {:?}",
+ "GC report from datanode {}: successfully deleted files for region IDs {:?}, need retry for region IDs {:?}",
peer, success, need_retry
);
}
@@ -921,6 +957,8 @@ impl Procedure for BatchGcProcedure {
}
async fn execute(&mut self, ctx: &ProcedureContext) -> ProcedureResult {
+ self.check_maintenance_mode().await?;
+
match self.data.state {
State::Start => {
let _regions_span = common_telemetry::tracing::debug_span!(
@@ -1122,11 +1160,15 @@ mod tests {
use api::v1::meta::MailboxMessage;
use api::v1::meta::mailbox_message::Payload;
- use common_meta::instruction::{GcRegionsReply, InstructionReply};
+ use common_meta::instruction::{GcRegionsReply, Instruction, InstructionReply};
use common_meta::key::TableMetadataManager;
+ use common_meta::key::runtime_switch::RuntimeSwitchManager;
use common_meta::kv_backend::memory::MemoryKvBackend;
+ use common_meta::kv_backend::test_util::MockKvBackendBuilder;
use common_meta::peer::Peer;
use common_meta::sequence::SequenceBuilder;
+ use common_procedure::Context as ProcedureContext;
+ use common_procedure_test::MockContextProvider;
use common_time::util::current_time_millis;
use store_api::storage::FileId;
use tokio::sync::mpsc;
@@ -1135,6 +1177,38 @@ mod tests {
use crate::procedure::test_util::{MailboxContext, send_mock_reply};
use crate::service::mailbox::Channel;
+ #[test]
+ fn test_scoped_gc_instruction_selects_and_scopes_manifest() {
+ let region = RegionId::new(7, 3);
+ let other = RegionId::new(7, 4);
+ let mut manifest = FileRefsManifest::default();
+ manifest.file_refs.insert(region, HashSet::new());
+ manifest.file_refs.insert(other, HashSet::new());
+ manifest.manifest_version.insert(region, 42);
+
+ let packed = scoped_gc_instruction(vec![region], &manifest, true);
+ let Instruction::PackedGcRegions(packed) = packed else {
+ panic!("expected packed instruction");
+ };
+ assert_eq!(packed.regions, vec![region]);
+ assert_eq!(
+ packed.packed_file_refs_manifest.manifest_version[®ion],
+ 42
+ );
+ assert!(
+ !packed
+ .packed_file_refs_manifest
+ .file_refs
+ .contains_key(&other)
+ );
+ assert!(
+ packed
+ .packed_file_refs_manifest
+ .cross_region_refs
+ .is_empty()
+ );
+ }
+
#[test]
fn test_done_with_gc_report_keeps_report() {
let region_id = RegionId::new(1024, 1);
@@ -1215,6 +1289,7 @@ mod tests {
let kv_backend = Arc::new(MemoryKvBackend::new());
let table_metadata_manager = Arc::new(TableMetadataManager::new(kv_backend.clone()));
+ let runtime_switch_manager = Arc::new(RuntimeSwitchManager::new(kv_backend.clone()));
let mailbox_sequence =
SequenceBuilder::new("test_batch_gc_partial_report", kv_backend).build();
let mut mailbox = MailboxContext::new(mailbox_sequence);
@@ -1230,6 +1305,7 @@ mod tests {
let mut procedure = BatchGcProcedure::new(
mailbox.mailbox().clone(),
table_metadata_manager,
+ runtime_switch_manager,
"localhost".to_string(),
vec![first_region, second_region],
true,
@@ -1265,11 +1341,15 @@ mod tests {
fn batch_gc_procedure() -> BatchGcProcedure {
let kv_backend = Arc::new(MemoryKvBackend::new());
let table_metadata_manager = Arc::new(TableMetadataManager::new(kv_backend.clone()));
- let mailbox_sequence = SequenceBuilder::new("test_batch_gc_procedure", kv_backend).build();
+ let runtime_switch_manager = Arc::new(RuntimeSwitchManager::new(kv_backend));
+ let mailbox_sequence =
+ SequenceBuilder::new("test_batch_gc_procedure", Arc::new(MemoryKvBackend::new()))
+ .build();
let mailbox = MailboxContext::new(mailbox_sequence);
BatchGcProcedure::new(
mailbox.mailbox().clone(),
table_metadata_manager,
+ runtime_switch_manager,
"localhost".to_string(),
vec![RegionId::new(1024, 1)],
true,
@@ -1277,4 +1357,113 @@ mod tests {
HashMap::new(),
)
}
+
+ #[tokio::test]
+ async fn test_maintenance_mode_gates_every_gc_state() {
+ let states = [
+ State::Start,
+ State::Acquiring,
+ State::Gcing,
+ State::UpdateRepartition,
+ ];
+
+ for state in states {
+ let kv_backend = Arc::new(MemoryKvBackend::new());
+ let table_metadata_manager = Arc::new(TableMetadataManager::new(kv_backend.clone()));
+ let runtime_switch_manager = Arc::new(RuntimeSwitchManager::new(kv_backend.clone()));
+ runtime_switch_manager.set_maintenance_mode().await.unwrap();
+ let mailbox_sequence =
+ SequenceBuilder::new("test_batch_gc_maintenance_gate", kv_backend).build();
+ let mut mailbox = MailboxContext::new(mailbox_sequence);
+ let (tx, mut rx) = mpsc::channel(1);
+ mailbox
+ .insert_heartbeat_response_receiver(Channel::Datanode(1), tx)
+ .await;
+ let mut procedure = BatchGcProcedure::new(
+ mailbox.mailbox().clone(),
+ table_metadata_manager,
+ runtime_switch_manager,
+ "localhost".to_string(),
+ vec![RegionId::new(1024, 1)],
+ true,
+ Duration::from_secs(10),
+ HashMap::new(),
+ );
+ procedure.data.state = state.clone();
+ let dump_before = procedure.dump().unwrap();
+
+ let ctx = ProcedureContext {
+ procedure_id: common_procedure::ProcedureId::random(),
+ provider: Arc::new(MockContextProvider::default()),
+ event_context: None,
+ };
+ let err = procedure.execute(&ctx).await.unwrap_err();
+
+ assert!(!err.is_retry_later());
+ assert_eq!(
+ common_error::ext::ErrorExt::retry_hint(&err),
+ common_error::ext::RetryHint::NonRetryable
+ );
+ assert_eq!(procedure.data.state, state);
+ assert_eq!(procedure.dump().unwrap(), dump_before);
+ if matches!(state, State::Acquiring | State::Gcing) {
+ assert!(rx.try_recv().is_err());
+ }
+ }
+ }
+
+ #[tokio::test]
+ async fn test_maintenance_mode_read_error_retries_without_advancing_state() {
+ let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
+ let failing_once_calls = calls.clone();
+ let kv_backend = Arc::new(
+ MockKvBackendBuilder::default()
+ .range_fn(Arc::new(move |_| {
+ if failing_once_calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst) == 0 {
+ common_meta::error::UnexpectedSnafu {
+ err_msg: "maintenance read failed",
+ }
+ .fail()
+ } else {
+ Ok(common_meta::rpc::store::RangeResponse {
+ kvs: vec![],
+ more: false,
+ })
+ }
+ }))
+ .build()
+ .unwrap(),
+ );
+ let table_metadata_manager = Arc::new(TableMetadataManager::new(kv_backend.clone()));
+ let runtime_switch_manager = Arc::new(RuntimeSwitchManager::new(kv_backend.clone()));
+ let mailbox_sequence =
+ SequenceBuilder::new("test_batch_gc_maintenance_read_error", kv_backend).build();
+ let mailbox = MailboxContext::new(mailbox_sequence);
+ let mut procedure = BatchGcProcedure::new(
+ mailbox.mailbox().clone(),
+ table_metadata_manager,
+ runtime_switch_manager,
+ "localhost".to_string(),
+ vec![RegionId::new(1024, 1)],
+ true,
+ Duration::from_secs(10),
+ HashMap::new(),
+ );
+ let ctx = ProcedureContext {
+ procedure_id: common_procedure::ProcedureId::random(),
+ provider: Arc::new(MockContextProvider::default()),
+ event_context: None,
+ };
+ let dump_before = procedure.dump().unwrap();
+
+ assert!(procedure.execute(&ctx).await.unwrap_err().is_retry_later());
+ assert_eq!(procedure.data.state, State::Start);
+ assert_eq!(procedure.dump().unwrap(), dump_before);
+
+ assert!(matches!(
+ procedure.execute(&ctx).await.unwrap(),
+ Status::Executing { .. }
+ ));
+ assert_eq!(procedure.data.state, State::Acquiring);
+ }
}
diff --git a/src/meta-srv/src/handler.rs b/src/meta-srv/src/handler.rs
index 50b53f4970a..320f26fae7c 100644
--- a/src/meta-srv/src/handler.rs
+++ b/src/meta-srv/src/handler.rs
@@ -428,9 +428,11 @@ impl HeartbeatMailbox {
.as_ref()
.with_context(|| UnexpectedInstructionReplySnafu {
mailbox_message: msg.to_string(),
- reason: format!("empty payload, msg: {msg:?}"),
+ reason: "empty JSON payload".to_string(),
})?;
- serde_json::from_str(payload).context(DeserializeFromJsonSnafu { input: payload })
+ serde_json::from_str(payload).context(DeserializeFromJsonSnafu {
+ input_len: payload.len(),
+ })
}
/// Parses the [Instruction] from [MailboxMessage].
@@ -443,9 +445,11 @@ impl HeartbeatMailbox {
.as_ref()
.with_context(|| UnexpectedInstructionReplySnafu {
mailbox_message: msg.to_string(),
- reason: format!("empty payload, msg: {msg:?}"),
+ reason: "empty JSON payload".to_string(),
})?;
- serde_json::from_str(payload).context(DeserializeFromJsonSnafu { input: payload })
+ serde_json::from_str(payload).context(DeserializeFromJsonSnafu {
+ input_len: payload.len(),
+ })
}
pub fn create(pushers: Pushers, sequence: Sequence) -> MailboxRef {
@@ -526,7 +530,12 @@ impl Mailbox for HeartbeatMailbox {
msg.id = message_id;
let pusher_id = ch.pusher_id();
- debug!("Sending mailbox message {msg:?} to {pusher_id}");
+ let payload_len = msg
+ .payload
+ .as_ref()
+ .map(|Payload::Json(payload)| payload.len())
+ .unwrap_or_default();
+ debug!(message_id, payload_len, %pusher_id, "Sending mailbox message");
let (tx, rx) = oneshot::channel();
let _ = self.senders.insert(message_id, tx);
@@ -548,7 +557,12 @@ impl Mailbox for HeartbeatMailbox {
msg.id = message_id;
let pusher_id = ch.pusher_id();
- debug!("Sending mailbox message {msg:?} to {pusher_id}");
+ let payload_len = msg
+ .payload
+ .as_ref()
+ .map(|Payload::Json(payload)| payload.len())
+ .unwrap_or_default();
+ debug!(message_id, payload_len, %pusher_id, "Sending one-way mailbox message");
self.pushers.push(pusher_id, msg).await?;
@@ -560,7 +574,18 @@ impl Mailbox for HeartbeatMailbox {
}
async fn on_recv(&self, id: MessageId, maybe_msg: Result) -> Result<()> {
- debug!("Received mailbox message {maybe_msg:?}");
+ let payload_len = maybe_msg
+ .as_ref()
+ .ok()
+ .and_then(|msg| msg.payload.as_ref())
+ .map(|Payload::Json(payload)| payload.len())
+ .unwrap_or_default();
+ debug!(
+ message_id = id,
+ payload_len,
+ success = maybe_msg.is_ok(),
+ "Received mailbox message"
+ );
let _ = self.timeouts.remove(&id);
@@ -568,7 +593,15 @@ impl Mailbox for HeartbeatMailbox {
tx.send(maybe_msg)
.map_err(|_| error::MailboxClosedSnafu { id }.build())?;
} else if let Ok(finally_msg) = maybe_msg {
- warn!("The response arrived too late: {finally_msg:?}");
+ let payload_len = finally_msg
+ .payload
+ .as_ref()
+ .map(|Payload::Json(payload)| payload.len())
+ .unwrap_or_default();
+ warn!(
+ message_id = id,
+ payload_len, "The mailbox response arrived too late"
+ );
}
Ok(())
diff --git a/src/meta-srv/src/key.rs b/src/meta-srv/src/key.rs
index 803ee9f0c6e..abdc92ff6d4 100644
--- a/src/meta-srv/src/key.rs
+++ b/src/meta-srv/src/key.rs
@@ -87,7 +87,9 @@ impl FromStr for LeaseValue {
type Err = error::Error;
fn from_str(value: &str) -> crate::Result {
- serde_json::from_str(value).context(error::DeserializeFromJsonSnafu { input: value })
+ serde_json::from_str(value).context(error::DeserializeFromJsonSnafu {
+ input_len: value.len(),
+ })
}
}
diff --git a/src/meta-srv/src/metasrv/builder.rs b/src/meta-srv/src/metasrv/builder.rs
index 75b72f9a114..b40f06e3cfe 100644
--- a/src/meta-srv/src/metasrv/builder.rs
+++ b/src/meta-srv/src/metasrv/builder.rs
@@ -530,6 +530,7 @@ impl MetasrvBuilder {
let gc_scheduler_ctx = DefaultGcSchedulerCtx::try_new(
table_metadata_manager.clone(),
procedure_manager.clone(),
+ runtime_switch_manager.clone(),
#[cfg(feature = "enterprise")]
ddl_manager.clone(),
meta_peer_client.clone(),
diff --git a/src/meta-srv/src/service/heartbeat.rs b/src/meta-srv/src/service/heartbeat.rs
index 066d1560477..5cdc5b57894 100644
--- a/src/meta-srv/src/service/heartbeat.rs
+++ b/src/meta-srv/src/service/heartbeat.rs
@@ -206,7 +206,13 @@ where
/// Handles the incoming heartbeat request, and returns whether to continue the session.
async fn handle_request(&mut self, req: HeartbeatRequest, is_handshake: bool) -> bool {
- debug!("Receiving heartbeat request: {:?}", req);
+ let mailbox_message_id = req.mailbox_message.as_ref().map(|msg| msg.id);
+ debug!(
+ sender_id = %self.sender_id,
+ ?mailbox_message_id,
+ is_handshake,
+ "Receiving heartbeat request"
+ );
let sender_id = self.sender_id.to_string();
METRIC_META_HEARTBEAT_RECV
@@ -224,7 +230,12 @@ where
let is_not_leader = res.as_ref().is_ok_and(|r| r.is_not_leader());
- debug!("Sending heartbeat response: {:?}", res);
+ debug!(
+ sender_id = %self.sender_id,
+ success = res.is_ok(),
+ is_not_leader,
+ "Sending heartbeat response"
+ );
if self.tx.send(res).await.is_err() {
info!(
diff --git a/src/metric-engine/src/engine.rs b/src/metric-engine/src/engine.rs
index 6d5dace9c43..18d6ff6f197 100644
--- a/src/metric-engine/src/engine.rs
+++ b/src/metric-engine/src/engine.rs
@@ -636,6 +636,7 @@ mod test {
.handle_request(
logical_region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema: row_schema_with_tags(&["job"]),
rows: build_rows(1, 5),
diff --git a/src/metric-engine/src/engine/bulk_insert.rs b/src/metric-engine/src/engine/bulk_insert.rs
index d700d858acf..cb43a71406d 100644
--- a/src/metric-engine/src/engine/bulk_insert.rs
+++ b/src/metric-engine/src/engine/bulk_insert.rs
@@ -124,6 +124,7 @@ impl MetricEngineInner {
let aligned_schema_version = Some(self.physical_schema_version(data_region_id).await?);
let request = RegionBulkInsertsRequest {
+ skip_wal: request.skip_wal,
region_id: data_region_id,
payload: modified_batch,
raw_data: ArrowIpc {
@@ -241,18 +242,28 @@ mod tests {
use common_error::ext::ErrorExt;
use common_query::prelude::{greptime_timestamp, greptime_value};
use common_recordbatch::RecordBatches;
+ use datafusion::parquet::basic::Encoding;
use datatypes::arrow::array::{Float64Array, StringArray, TimestampMillisecondArray};
use datatypes::arrow::datatypes::{DataType, Field, Schema as ArrowSchema, TimeUnit};
use datatypes::arrow::record_batch::RecordBatch;
use mito2::config::MitoConfig;
- use store_api::metric_engine_consts::PRIMARY_KEY_ENCODING;
+ use mito2::sst::parquet::metadata::MetadataLoader;
+ use mito2::sst::parquet::reader::MetadataCacheMetrics;
+ use store_api::metric_engine_consts::{
+ METRIC_ENGINE_NAME, PHYSICAL_TABLE_METADATA_KEY, PRIMARY_KEY_ENCODING,
+ };
+ use store_api::mito_engine_options::EXPERIMENTAL_SST_FLOAT_FIELD_ENCODING;
use store_api::path_utils::table_dir;
use store_api::region_engine::RegionEngine;
- use store_api::region_request::{RegionBulkInsertsRequest, RegionPutRequest, RegionRequest};
+ use store_api::region_request::{
+ PathType, RegionBulkInsertsRequest, RegionCloseRequest, RegionFlushRequest,
+ RegionOpenRequest, RegionPutRequest, RegionRequest,
+ };
use store_api::storage::{RegionId, ScanRequest};
use super::record_batch_to_ipc;
use crate::batch_modifier::{TagColumnInfo, modify_batch_sparse};
+ use crate::engine::MetricEngine;
use crate::error::Error;
use crate::test_util::{self, TestEnv};
@@ -287,9 +298,14 @@ mod tests {
.unwrap()
}
- fn build_bulk_request(logical_region_id: RegionId, batch: RecordBatch) -> RegionRequest {
+ fn build_bulk_request(
+ logical_region_id: RegionId,
+ batch: RecordBatch,
+ skip_wal: bool,
+ ) -> RegionRequest {
let (schema, data_header, payload) = record_batch_to_ipc(&batch).unwrap();
RegionRequest::BulkInserts(RegionBulkInsertsRequest {
+ skip_wal,
region_id: logical_region_id,
payload: batch,
raw_data: ArrowIpc {
@@ -332,6 +348,7 @@ mod tests {
let batch = build_logical_batch(0, 0);
let request = RegionRequest::BulkInserts(RegionBulkInsertsRequest {
+ skip_wal: false,
region_id: logical_region_id,
payload: batch,
raw_data: ArrowIpc::default(),
@@ -348,6 +365,11 @@ mod tests {
#[tokio::test]
async fn test_bulk_insert_physical_region_passthrough() {
+ check_bulk_insert_physical_region_passthrough(false).await;
+ check_bulk_insert_physical_region_passthrough(true).await;
+ }
+
+ async fn check_bulk_insert_physical_region_passthrough(skip_wal: bool) {
// Use flat format so that BulkMemtable is used (supports write_bulk).
let mito_config = MitoConfig {
default_flat_format: true,
@@ -355,12 +377,13 @@ mod tests {
};
let env = TestEnv::with_mito_config("", mito_config, Default::default()).await;
env.init_metric_region().await;
+ env.metric().inner.flush_task.stop().await.unwrap();
let physical_region_id = env.default_physical_region_id();
let logical_region_id = env.default_logical_region_id();
// First, do a normal logical bulk insert so we can compare results.
let logical_batch = build_logical_batch(0, 3);
- let logical_request = build_bulk_request(logical_region_id, logical_batch.clone());
+ let logical_request = build_bulk_request(logical_region_id, logical_batch, skip_wal);
let response = env
.metric()
.handle_request(logical_region_id, logical_request)
@@ -385,7 +408,7 @@ mod tests {
&non_tag_indices,
)
.unwrap();
- let request = build_bulk_request(physical_region_id, physical_batch);
+ let request = build_bulk_request(physical_region_id, physical_batch, skip_wal);
let response = env
.metric()
.handle_request(physical_region_id, request)
@@ -401,6 +424,48 @@ mod tests {
.unwrap();
let batches = RecordBatches::try_collect(stream).await.unwrap();
assert_eq!(batches.iter().map(|b| b.num_rows()).sum::(), 6);
+
+ // Closing without a flush must recover only WAL-backed data. Recreate
+ // the wrapper too, so its metadata cache cannot hide missing metadata.
+ let stat = env.mito().region_statistic(physical_region_id).unwrap();
+ assert_eq!(stat.sst_num, 0);
+ env.metric()
+ .handle_request(
+ physical_region_id,
+ RegionRequest::Close(RegionCloseRequest {
+ flush_on_close: false,
+ }),
+ )
+ .await
+ .unwrap();
+ let reopened = MetricEngine::try_new(env.mito(), Default::default()).unwrap();
+ reopened.inner.flush_task.stop().await.unwrap();
+ reopened
+ .handle_request(
+ physical_region_id,
+ RegionRequest::Open(RegionOpenRequest {
+ engine: METRIC_ENGINE_NAME.to_string(),
+ table_dir: TestEnv::default_table_dir(),
+ path_type: PathType::Bare,
+ options: [(PHYSICAL_TABLE_METADATA_KEY.to_string(), String::new())]
+ .into_iter()
+ .collect(),
+ skip_wal_replay: false,
+ checkpoint: None,
+ requirements: Default::default(),
+ }),
+ )
+ .await
+ .unwrap();
+ let stream = reopened
+ .scan_to_stream(logical_region_id, ScanRequest::default())
+ .await
+ .unwrap();
+ let batches = RecordBatches::try_collect(stream).await.unwrap();
+ assert_eq!(
+ batches.iter().map(|b| b.num_rows()).sum::(),
+ if skip_wal { 0 } else { 6 },
+ );
}
#[tokio::test]
@@ -415,7 +480,7 @@ mod tests {
let physical_region_id = env.default_physical_region_id();
let batch = build_logical_batch(0, 0);
- let request = build_bulk_request(physical_region_id, batch);
+ let request = build_bulk_request(physical_region_id, batch, false);
let response = env
.metric()
.handle_request(physical_region_id, request)
@@ -449,7 +514,7 @@ mod tests {
)
.unwrap();
- let request = build_bulk_request(logical_region_id, batch);
+ let request = build_bulk_request(logical_region_id, batch, false);
let err = env
.metric()
.handle_request(logical_region_id, request)
@@ -499,7 +564,7 @@ mod tests {
)
.unwrap();
- let request = build_bulk_request(logical_region_id, batch);
+ let request = build_bulk_request(logical_region_id, batch, false);
let response = env
.metric()
.handle_request(logical_region_id, request)
@@ -522,7 +587,7 @@ mod tests {
env.init_metric_region().await;
let logical_region_id = env.default_logical_region_id();
- let request = build_bulk_request(logical_region_id, build_logical_batch(0, 3));
+ let request = build_bulk_request(logical_region_id, build_logical_batch(0, 3), false);
let response = env
.metric()
.handle_request(logical_region_id, request)
@@ -530,7 +595,7 @@ mod tests {
.unwrap();
assert_eq!(response.affected_rows, 3);
- let request = build_bulk_request(logical_region_id, build_logical_batch(3, 5));
+ let request = build_bulk_request(logical_region_id, build_logical_batch(3, 5), false);
let response = env
.metric()
.handle_request(logical_region_id, request)
@@ -550,16 +615,68 @@ mod tests {
#[tokio::test]
async fn test_bulk_insert_sparse_encoding() {
let env = TestEnv::new().await;
- env.init_metric_region().await;
+ let physical_region_id = env.default_physical_region_id();
+ env.create_physical_region(
+ physical_region_id,
+ &TestEnv::default_table_dir(),
+ vec![(
+ EXPERIMENTAL_SST_FLOAT_FIELD_ENCODING.to_string(),
+ "byte_stream_split".to_string(),
+ )],
+ )
+ .await;
let logical_region_id = env.default_logical_region_id();
+ env.create_logical_region(physical_region_id, logical_region_id)
+ .await;
- let request = build_bulk_request(logical_region_id, build_logical_batch(0, 4));
+ let rows = 4;
+ let request = build_bulk_request(logical_region_id, build_logical_batch(0, rows), false);
let response = env
.metric()
.handle_request(logical_region_id, request)
.await
.unwrap();
- assert_eq!(response.affected_rows, 4);
+ assert_eq!(response.affected_rows, rows);
+
+ let data_region_id = crate::utils::to_data_region_id(physical_region_id);
+ env.mito()
+ .handle_request(
+ data_region_id,
+ RegionRequest::Flush(RegionFlushRequest::default()),
+ )
+ .await
+ .unwrap();
+
+ let region = env.mito().find_region(data_region_id).unwrap();
+ let entry = region
+ .manifest_sst_entries()
+ .await
+ .into_iter()
+ .find(|entry| entry.visible && entry.file_path.ends_with(".parquet"))
+ .unwrap();
+ let mut cache_metrics = MetadataCacheMetrics::default();
+ let footer = MetadataLoader::new(
+ region.access_layer().object_store().clone(),
+ &entry.file_path,
+ entry.file_size,
+ )
+ .load(&mut cache_metrics)
+ .await
+ .unwrap();
+ let field_column_index = footer
+ .file_metadata()
+ .schema_descr()
+ .columns()
+ .iter()
+ .position(|column| column.name() == greptime_value())
+ .unwrap();
+ assert!(!footer.row_groups().is_empty());
+ assert!(footer.row_groups().iter().all(|row_group| {
+ row_group
+ .column(field_column_index)
+ .encodings()
+ .any(|encoding| encoding == Encoding::BYTE_STREAM_SPLIT)
+ }));
let stream = env
.metric()
@@ -567,7 +684,7 @@ mod tests {
.await
.unwrap();
let batches = RecordBatches::try_collect(stream).await.unwrap();
- assert_eq!(batches.iter().map(|b| b.num_rows()).sum::(), 4);
+ assert_eq!(batches.iter().map(|b| b.num_rows()).sum::(), rows);
}
#[tokio::test]
@@ -575,7 +692,7 @@ mod tests {
let env = TestEnv::new().await;
let logical_region_id = init_dense_metric_region(&env).await;
- let request = build_bulk_request(logical_region_id, build_logical_batch(0, 2));
+ let request = build_bulk_request(logical_region_id, build_logical_batch(0, 2), false);
let err = env
.metric()
.handle_request(logical_region_id, request)
@@ -599,6 +716,7 @@ mod tests {
.handle_request(
logical_region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: api::v1::Rows { schema, rows },
hint: None,
partition_expr_version: None,
@@ -616,7 +734,7 @@ mod tests {
let env_bulk = TestEnv::new().await;
env_bulk.init_metric_region().await;
- let request = build_bulk_request(logical_region_id, build_logical_batch(0, 5));
+ let request = build_bulk_request(logical_region_id, build_logical_batch(0, 5), false);
env_bulk
.metric()
.handle_request(logical_region_id, request)
diff --git a/src/metric-engine/src/engine/flush.rs b/src/metric-engine/src/engine/flush.rs
index 21e642de0b2..426aa918d04 100644
--- a/src/metric-engine/src/engine/flush.rs
+++ b/src/metric-engine/src/engine/flush.rs
@@ -90,6 +90,7 @@ mod tests {
let schema = row_schema_with_tags(&["job"]);
let rows = build_rows(1, 10);
let request = RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: Rows { schema, rows },
hint: None,
partition_expr_version: None,
diff --git a/src/metric-engine/src/engine/put.rs b/src/metric-engine/src/engine/put.rs
index 96ed2c3e60a..8858251d082 100644
--- a/src/metric-engine/src/engine/put.rs
+++ b/src/metric-engine/src/engine/put.rs
@@ -142,6 +142,8 @@ impl MetricEngineInner {
let data_region_id = to_data_region_id(physical_region_id);
let primary_key_encoding = self.get_primary_key_encoding(data_region_id)?;
+ // TODO(weny): Consolidate validation and merging to avoid redundant request traversals,
+ // while ensuring the entire batch is validated before writing.
// Validate all requests
self.validate_batch_requests(physical_region_id, &mut requests)
.await?;
@@ -176,6 +178,18 @@ impl MetricEngineInner {
physical_region_id: RegionId,
requests: &mut [(RegionId, RegionPutRequest)],
) -> Result<()> {
+ let skip_wal = requests
+ .first()
+ .is_some_and(|(_, request)| request.skip_wal);
+ ensure!(
+ requests
+ .iter()
+ .all(|(_, request)| request.skip_wal == skip_wal),
+ InvalidRequestSnafu {
+ region_id: physical_region_id,
+ reason: "inconsistent WAL policy in batch"
+ }
+ );
for (logical_region_id, request) in requests {
self.verify_rows(
*logical_region_id,
@@ -194,6 +208,9 @@ impl MetricEngineInner {
physical_region_id: RegionId,
requests: Vec<(RegionId, RegionPutRequest)>,
) -> Result<(RegionPutRequest, AffectedRows)> {
+ let skip_wal = requests
+ .first()
+ .is_some_and(|(_, request)| request.skip_wal);
let total_rows: usize = requests.iter().map(|(_, req)| req.rows.rows.len()).sum();
let mut modified_requests = Vec::with_capacity(requests.len());
let mut total_affected_rows: AffectedRows = 0;
@@ -233,6 +250,7 @@ impl MetricEngineInner {
}
let merged_request = RegionPutRequest {
+ skip_wal,
rows: Rows {
schema,
rows: merged_rows,
@@ -256,6 +274,9 @@ impl MetricEngineInner {
data_region_id: RegionId,
requests: Vec<(RegionId, RegionPutRequest)>,
) -> Result<(RegionPutRequest, AffectedRows)> {
+ let skip_wal = requests
+ .first()
+ .is_some_and(|(_, request)| request.skip_wal);
// Build union schema from all requests
let merged_schema =
Self::build_union_schema(requests.iter().map(|(_, req)| req.rows.schema.as_slice()));
@@ -291,6 +312,7 @@ impl MetricEngineInner {
};
let merged_request = RegionPutRequest {
+ skip_wal,
rows: final_rows,
hint: None,
partition_expr_version: merged_version,
@@ -769,25 +791,272 @@ mod tests {
use common_function::utils::partition_expr_version;
use common_query::prelude::{greptime_native_histogram, greptime_timestamp, greptime_value};
use common_recordbatch::RecordBatches;
+ use datatypes::arrow::array::{Float64Array, TimestampMillisecondArray};
use datatypes::prelude::ConcreteDataType;
use datatypes::schema::{ColumnDefaultConstraint, ColumnSchema};
use datatypes::value::Value as PartitionValue;
use partition::expr::col;
use store_api::metadata::ColumnMetadata;
use store_api::metric_engine_consts::{
- DATA_SCHEMA_TABLE_ID_COLUMN_NAME, DATA_SCHEMA_TSID_COLUMN_NAME, PRIMARY_KEY_ENCODING,
+ DATA_SCHEMA_TABLE_ID_COLUMN_NAME, DATA_SCHEMA_TSID_COLUMN_NAME, METRIC_ENGINE_NAME,
+ PHYSICAL_TABLE_METADATA_KEY, PRIMARY_KEY_ENCODING,
};
use store_api::path_utils::table_dir;
use store_api::region_engine::RegionEngine;
use store_api::region_request::{
- EnterStagingRequest, RegionRequest, StagingPartitionDirective,
+ EnterStagingRequest, PathType, RegionCloseRequest, RegionOpenRequest, RegionRequest,
+ StagingPartitionDirective,
};
use store_api::storage::ScanRequest;
use store_api::storage::consts::PRIMARY_KEY_COLUMN_NAME;
use super::*;
+ use crate::engine::MetricEngine;
use crate::test_util::{self, TestEnv};
+ async fn scan_timestamp_values(engine: &MetricEngine, region_id: RegionId) -> Vec<(i64, f64)> {
+ let stream = engine
+ .scan_to_stream(region_id, ScanRequest::default())
+ .await
+ .unwrap();
+ let batches = RecordBatches::try_collect(stream).await.unwrap();
+ let mut rows = Vec::new();
+ for batch in batches.iter() {
+ let batch = batch.df_record_batch();
+ let timestamp_index = batch.schema().index_of(greptime_timestamp()).unwrap();
+ let value_index = batch.schema().index_of(greptime_value()).unwrap();
+ let timestamps = batch
+ .column(timestamp_index)
+ .as_any()
+ .downcast_ref::()
+ .unwrap();
+ let values = batch
+ .column(value_index)
+ .as_any()
+ .downcast_ref::()
+ .unwrap();
+ rows.extend(
+ timestamps
+ .values()
+ .iter()
+ .copied()
+ .zip(values.values().iter().copied()),
+ );
+ }
+ rows.sort_unstable_by_key(|(timestamp, _)| *timestamp);
+ rows
+ }
+
+ #[tokio::test]
+ async fn test_batch_partition_versions() {
+ check_batch_partition_versions("sparse").await;
+ check_batch_partition_versions("dense").await;
+ }
+
+ async fn check_batch_partition_versions(encoding: &str) {
+ let env = TestEnv::new().await;
+ let physical_region_id = env.default_physical_region_id();
+ let logical_region_id = env.default_logical_region_id();
+ env.create_physical_region(
+ physical_region_id,
+ &TestEnv::default_table_dir(),
+ vec![(PRIMARY_KEY_ENCODING.to_string(), encoding.to_string())],
+ )
+ .await;
+ create_logical_region_with_tags(&env, physical_region_id, logical_region_id, &["job"])
+ .await;
+ let build_requests = |versions: [Option; 3]| {
+ versions
+ .into_iter()
+ .map(|partition_expr_version| {
+ (
+ logical_region_id,
+ RegionPutRequest {
+ skip_wal: false,
+ rows: Rows {
+ schema: test_util::row_schema_with_tags(&["job"]),
+ rows: test_util::build_rows(1, 1),
+ },
+ hint: None,
+ partition_expr_version,
+ },
+ )
+ })
+ .collect::>()
+ };
+ // Conflicting explicit versions must fail before any data is written.
+ let err = env
+ .metric()
+ .inner
+ .put_regions_batch_single_physical(
+ physical_region_id,
+ build_requests([None, Some(10), Some(11)]),
+ )
+ .await
+ .unwrap_err();
+ assert!(
+ err.to_string()
+ .contains("inconsistent partition expr version")
+ );
+ assert!(
+ scan_timestamp_values(&env.metric(), logical_region_id)
+ .await
+ .is_empty()
+ );
+
+ for (versions, expected) in [
+ ([None, None, None], None),
+ ([None, Some(7), None], Some(7)),
+ ([Some(7), None, Some(7)], Some(7)),
+ ] {
+ let mut requests = build_requests(versions);
+ let engine = env.metric();
+ engine
+ .inner
+ .validate_batch_requests(physical_region_id, &mut requests)
+ .await
+ .unwrap();
+ let (merged, _) = match encoding {
+ "sparse" => engine
+ .inner
+ .merge_sparse_batch(physical_region_id, requests),
+ "dense" => engine
+ .inner
+ .merge_dense_batch(to_data_region_id(physical_region_id), requests),
+ _ => unreachable!(),
+ }
+ .unwrap();
+ assert_eq!(merged.partition_expr_version, expected);
+ }
+ }
+
+ #[tokio::test]
+ async fn test_put_skip_wal_batch_recovery() {
+ check_put_skip_wal_batch_recovery("sparse", false).await;
+ check_put_skip_wal_batch_recovery("sparse", true).await;
+ check_put_skip_wal_batch_recovery("dense", false).await;
+ check_put_skip_wal_batch_recovery("dense", true).await;
+ }
+
+ async fn check_put_skip_wal_batch_recovery(encoding: &str, skip_wal: bool) {
+ let env = TestEnv::new().await;
+ let engine = env.metric();
+ engine.inner.flush_task.stop().await.unwrap();
+ let physical_region_id = env.default_physical_region_id();
+ let logical_region_id = env.default_logical_region_id();
+ env.create_physical_region(
+ physical_region_id,
+ &TestEnv::default_table_dir(),
+ vec![(PRIMARY_KEY_ENCODING.to_string(), encoding.to_string())],
+ )
+ .await;
+ create_logical_region_with_tags(&env, physical_region_id, logical_region_id, &["job"])
+ .await;
+ let metadata_before = engine.get_metadata(logical_region_id).await.unwrap();
+
+ let requests = [skip_wal; 3]
+ .into_iter()
+ .enumerate()
+ .map(|(index, skip_wal)| {
+ let timestamp = index as i64 + 1;
+ let value = timestamp as f64 * 10.0;
+ // Every request updates the same key at timestamp zero and
+ // also inserts a distinct key to verify merge order.
+ let rows = [0, timestamp]
+ .into_iter()
+ .map(|timestamp| Row {
+ values: vec![
+ Value {
+ value_data: Some(ValueData::TimestampMillisecondValue(timestamp)),
+ },
+ Value {
+ value_data: Some(ValueData::F64Value(value)),
+ },
+ Value {
+ value_data: Some(ValueData::StringValue("tag_0".to_string())),
+ },
+ ],
+ })
+ .collect();
+ (
+ logical_region_id,
+ RegionPutRequest {
+ rows: Rows {
+ schema: test_util::row_schema_with_tags(&["job"]),
+ rows,
+ },
+ hint: None,
+ partition_expr_version: None,
+ skip_wal,
+ },
+ )
+ });
+ let affected_rows = engine.inner.put_regions_batch(requests).await.unwrap();
+ assert_eq!(affected_rows, 6);
+ assert_eq!(
+ scan_timestamp_values(&engine, logical_region_id).await,
+ vec![(0, 30.0), (1, 10.0), (2, 20.0), (3, 30.0)]
+ );
+
+ // Neither data nor metadata has an SST to hide missing WAL.
+ for region_id in [
+ to_data_region_id(physical_region_id),
+ crate::utils::to_metadata_region_id(physical_region_id),
+ ] {
+ let stat = env.mito().region_statistic(region_id).unwrap();
+ assert!(stat.memtable_size > 0);
+ assert_eq!(stat.sst_num, 0);
+ }
+ engine
+ .handle_request(
+ physical_region_id,
+ RegionRequest::Close(RegionCloseRequest {
+ flush_on_close: false,
+ }),
+ )
+ .await
+ .unwrap();
+
+ // Recreate the wrapper as well, discarding its metadata cache.
+ let reopened = MetricEngine::try_new(env.mito(), Default::default()).unwrap();
+ reopened.inner.flush_task.stop().await.unwrap();
+ reopened
+ .handle_request(
+ physical_region_id,
+ RegionRequest::Open(RegionOpenRequest {
+ engine: METRIC_ENGINE_NAME.to_string(),
+ table_dir: TestEnv::default_table_dir(),
+ path_type: PathType::Bare,
+ options: [
+ (PHYSICAL_TABLE_METADATA_KEY.to_string(), String::new()),
+ (PRIMARY_KEY_ENCODING.to_string(), encoding.to_string()),
+ ]
+ .into_iter()
+ .collect(),
+ skip_wal_replay: false,
+ checkpoint: None,
+ requirements: Default::default(),
+ }),
+ )
+ .await
+ .unwrap();
+ let recovered_metadata = reopened.get_metadata(logical_region_id).await.unwrap();
+ assert_eq!(
+ metadata_before.column_metadatas,
+ recovered_metadata.column_metadatas
+ );
+ let expected = if skip_wal {
+ vec![]
+ } else {
+ vec![(0, 30.0), (1, 10.0), (2, 20.0), (3, 30.0)]
+ };
+ assert_eq!(
+ scan_timestamp_values(&reopened, logical_region_id).await,
+ expected,
+ "encoding={encoding}, skip_wal={skip_wal}"
+ );
+ }
+
fn assert_merged_schema(rows: &Rows, expect_sparse: bool) {
let column_names: HashSet = rows
.schema
@@ -874,6 +1143,45 @@ mod tests {
.unwrap()
}
+ fn check_batch_merge_wal_policy(
+ env: &TestEnv,
+ physical_region_id: RegionId,
+ mut requests: Vec<(RegionId, RegionPutRequest)>,
+ expect_sparse: bool,
+ skip_wal: bool,
+ ) {
+ for (_, request) in &mut requests {
+ request.skip_wal = skip_wal;
+ }
+ let (merged_request, affected_rows) = if expect_sparse {
+ let (merged_request, affected_rows) = env
+ .metric()
+ .inner
+ .merge_sparse_batch(physical_region_id, requests)
+ .unwrap();
+ let hint = merged_request
+ .hint
+ .as_ref()
+ .expect("missing sparse write hint");
+ assert_eq!(
+ hint.primary_key_encoding,
+ PrimaryKeyEncodingProto::Sparse as i32
+ );
+ (merged_request, affected_rows)
+ } else {
+ let (merged_request, affected_rows) = env
+ .metric()
+ .inner
+ .merge_dense_batch(to_data_region_id(physical_region_id), requests)
+ .unwrap();
+ assert!(merged_request.hint.is_none());
+ (merged_request, affected_rows)
+ };
+ assert_merged_schema(&merged_request.rows, expect_sparse);
+ assert_eq!(merged_request.skip_wal, skip_wal);
+ assert_eq!(affected_rows, 5);
+ }
+
async fn run_batch_write_with_schema_variants(
env: &TestEnv,
physical_region_id: RegionId,
@@ -921,6 +1229,7 @@ mod tests {
(
logical_region_1,
RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema: schema_1.clone(),
rows: rows_1,
@@ -932,6 +1241,7 @@ mod tests {
(
logical_region_2,
RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema: schema_2.clone(),
rows: rows_2,
@@ -943,32 +1253,41 @@ mod tests {
]
};
- let merged_request = if expect_sparse {
- let (merged_request, _) = env
- .metric()
- .inner
- .merge_sparse_batch(physical_region_id, build_requests())
- .unwrap();
- let hint = merged_request
- .hint
- .as_ref()
- .expect("missing sparse write hint");
- assert_eq!(
- hint.primary_key_encoding,
- PrimaryKeyEncodingProto::Sparse as i32
- );
- merged_request
- } else {
- let (merged_request, _) = env
- .metric()
- .inner
- .merge_dense_batch(data_region_id, build_requests())
- .unwrap();
- assert!(merged_request.hint.is_none());
- merged_request
- };
+ check_batch_merge_wal_policy(
+ env,
+ physical_region_id,
+ build_requests(),
+ expect_sparse,
+ false,
+ );
+ check_batch_merge_wal_policy(
+ env,
+ physical_region_id,
+ build_requests(),
+ expect_sparse,
+ true,
+ );
- assert_merged_schema(&merged_request.rows, expect_sparse);
+ for policies in [[false, true], [true, false]] {
+ let mut mixed_requests = build_requests();
+ for ((_, request), skip_wal) in mixed_requests.iter_mut().zip(policies) {
+ request.skip_wal = skip_wal;
+ }
+ let err = env
+ .metric()
+ .inner
+ .put_regions_batch(mixed_requests.into_iter())
+ .await
+ .unwrap_err();
+ assert!(err.to_string().contains("inconsistent WAL policy in batch"));
+ for logical_region_id in [logical_region_1, logical_region_2] {
+ assert!(
+ scan_timestamp_values(&env.metric(), logical_region_id)
+ .await
+ .is_empty()
+ );
+ }
+ }
let affected_rows = env
.metric()
@@ -1095,6 +1414,7 @@ mod tests {
let schema = test_util::row_schema_with_tags(&["job"]);
let rows = test_util::build_rows(1, 5);
let request = RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: Rows { schema, rows },
hint: None,
partition_expr_version: None,
@@ -1170,6 +1490,7 @@ mod tests {
let schema = test_util::row_schema_with_tags(columns);
let rows = test_util::build_rows(3, 100);
let request = RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: Rows { schema, rows },
hint: None,
partition_expr_version: None,
@@ -1193,6 +1514,7 @@ mod tests {
let schema = test_util::row_schema_with_tags(&["abc"]);
let rows = test_util::build_rows(1, 100);
let request = RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: Rows { schema, rows },
hint: None,
partition_expr_version: None,
@@ -1214,6 +1536,7 @@ mod tests {
let schema = test_util::row_schema_with_tags(&["def"]);
let rows = test_util::build_rows(1, 100);
let request = RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: Rows { schema, rows },
hint: None,
partition_expr_version: None,
@@ -1274,6 +1597,7 @@ mod tests {
(
logical_region_1,
RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema: schema.clone(),
rows: rows1,
@@ -1285,6 +1609,7 @@ mod tests {
(
logical_region_2,
RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema: schema.clone(),
rows: rows2,
@@ -1296,6 +1621,7 @@ mod tests {
(
logical_region_3,
RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema: schema.clone(),
rows: rows3,
@@ -1347,6 +1673,7 @@ mod tests {
(
logical_region_1,
RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema: schema.clone(),
rows: test_util::build_rows(1, 3),
@@ -1358,6 +1685,7 @@ mod tests {
(
nonexistent_region,
RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema: schema.clone(),
rows: test_util::build_rows(1, 2),
@@ -1369,6 +1697,7 @@ mod tests {
(
logical_region_2,
RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema: schema.clone(),
rows: test_util::build_rows(1, 5),
@@ -1407,6 +1736,7 @@ mod tests {
let requests = vec![(
physical_region_id,
RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema,
rows: test_util::build_rows(1, 1),
@@ -1441,6 +1771,7 @@ mod tests {
(
logical_region_id,
RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema: schema.clone(),
rows: test_util::build_rows(1, 1),
@@ -1452,6 +1783,7 @@ mod tests {
(
physical_region_id,
RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema,
rows: test_util::build_rows(1, 1),
@@ -1487,6 +1819,7 @@ mod tests {
let requests = vec![(
logical_region_id,
RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema,
rows: test_util::build_rows(1, 5),
@@ -1565,6 +1898,7 @@ mod tests {
.handle_request(
logical_region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows,
hint: None,
partition_expr_version: Some(1),
@@ -1607,6 +1941,7 @@ mod tests {
.handle_request(
logical_region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: rows.clone(),
hint: None,
partition_expr_version: Some(expected_version.wrapping_add(1)),
@@ -1621,6 +1956,7 @@ mod tests {
.handle_request(
logical_region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: rows.clone(),
hint: None,
partition_expr_version: None,
@@ -1635,6 +1971,7 @@ mod tests {
.handle_request(
logical_region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows,
hint: None,
partition_expr_version: Some(expected_version),
@@ -1703,6 +2040,7 @@ mod tests {
.handle_request(
logical_region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: Rows { schema, rows },
hint: None,
partition_expr_version: None,
@@ -1759,6 +2097,7 @@ mod tests {
.handle_request(
logical_region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: Rows { schema, rows },
hint: None,
partition_expr_version: None,
@@ -1813,6 +2152,7 @@ mod tests {
.handle_request(
logical_region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: Rows { schema, rows },
hint: None,
partition_expr_version: None,
diff --git a/src/metric-engine/src/engine/read.rs b/src/metric-engine/src/engine/read.rs
index d1afce5e22d..8d8d32fcc6c 100644
--- a/src/metric-engine/src/engine/read.rs
+++ b/src/metric-engine/src/engine/read.rs
@@ -500,6 +500,7 @@ mod test {
let schema = test_util::row_schema_with_tags(&["job"]);
let put = |rows| {
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema: schema.clone(),
rows: test_util::build_rows(1, rows),
diff --git a/src/metric-engine/src/metadata_region.rs b/src/metric-engine/src/metadata_region.rs
index ec67ac70101..18fa51f3597 100644
--- a/src/metric-engine/src/metadata_region.rs
+++ b/src/metric-engine/src/metadata_region.rs
@@ -585,6 +585,8 @@ impl MetadataRegion {
};
RegionPutRequest {
+ // Metadata must remain recoverable regardless of the user write policy.
+ skip_wal: false,
rows,
hint: None,
partition_expr_version: None,
@@ -819,6 +821,37 @@ mod test {
use crate::test_util::TestEnv;
use crate::utils::to_metadata_region_id;
+ #[test]
+ fn test_metadata_put_always_writes_wal() {
+ // Both metadata put paths use this constructor rather than forwarding
+ // a user insert request, so its WAL policy must always be independent.
+ for entries in [
+ vec![],
+ vec![("region", "")],
+ vec![("region", ""), ("column", "metadata")],
+ ] {
+ let request = MetadataRegion::build_put_request_from_iter(
+ entries
+ .iter()
+ .map(|(key, value)| (key.to_string(), value.to_string())),
+ );
+ assert!(!request.skip_wal);
+ assert!(request.hint.is_none());
+ assert!(request.partition_expr_version.is_none());
+ let expected_rows = entries
+ .into_iter()
+ .map(|(key, value)| {
+ row(vec![
+ ValueData::TimestampMillisecondValue(0),
+ ValueData::StringValue(key.to_string()),
+ ValueData::StringValue(value.to_string()),
+ ])
+ })
+ .collect::>();
+ assert_eq!(request.rows.rows, expected_rows);
+ }
+ }
+
#[test]
fn test_concat_table_key() {
let region_id = RegionId::new(1234, 7844);
diff --git a/src/mito2/AGENTS.md b/src/mito2/AGENTS.md
index b22ee758bb9..faa581331b4 100644
--- a/src/mito2/AGENTS.md
+++ b/src/mito2/AGENTS.md
@@ -26,7 +26,7 @@ snapshot isolation). It implements the `RegionEngine` trait from `store-api`.
| `compaction` | `src/mito2/src/compaction/` | Compaction scheduler (`scheduler.rs` + `scheduler/`), TWCS picker, strict-window manual picker, compactor, memory control |
| `access_layer` | `src/mito2/src/access_layer.rs` | SST read/write over the object store |
| `sst` | `src/mito2/src/sst/` | Parquet format, file metadata, index layout |
-| `series_index` | `src/mito2/src/series_index/` | Series index writer/searcher, catalogs, immutable snapshots, file lifecycle, and background maintenance |
+| `series_index` | `src/mito2/src/series_index/` | Series index writer/searcher, bucket planning and SST builders, catalogs, immutable snapshots, file lifecycle, and background maintenance |
| `read` | `src/mito2/src/read/` | `ScanRegion`, merge, dedup, projection, streaming |
| `manifest` | `src/mito2/src/manifest/` | `RegionManifestManager`, manifest actions/edits |
| `cache` | `src/mito2/src/cache.rs` | Write/file/page caches |
@@ -62,6 +62,8 @@ filtered `RecordBatch` stream.
- **Manifest format** (`manifest/action.rs`): affects crash recovery and
follower replay. Keep it backward compatible.
- **SST/Parquet layout** (`sst/`): readers must stay compatible with existing files.
+- **Series-index coverage** (`series_index/catalog.rs`): `SeriesIndexEntry` stores
+ compaction-window width and SST summaries keyed by aligned start in both catalogs and Parquet footers.
- **Request types** (`request.rs`): usually tied to proto definitions consumed by `datanode`.
- **WAL/memtable encoding** (`wal/`, `memtable/`): breaks replay if changed incompatibly.
diff --git a/src/mito2/src/access_layer.rs b/src/mito2/src/access_layer.rs
index 85808842741..14c629a01b3 100644
--- a/src/mito2/src/access_layer.rs
+++ b/src/mito2/src/access_layer.rs
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, Instant};
@@ -655,14 +656,17 @@ impl TempFileCleaner {
}
pub(crate) async fn new_fs_cache_store(root: &str) -> Result {
- let atomic_write_dir = join_dir(root, ATOMIC_WRITE_DIR);
- clean_dir(&atomic_write_dir).await?;
+ // Preserve native filesystem prefixes such as Windows UNC shares.
+ let atomic_write_dir = Path::new(root).join(ATOMIC_WRITE_DIR);
+ clean_dir(&atomic_write_dir.to_string_lossy()).await?;
// Compatible code. Remove this after a major release.
let old_atomic_temp_dir = join_dir(root, OLD_ATOMIC_WRITE_DIR);
clean_dir(&old_atomic_temp_dir).await?;
- let builder = Fs::default().root(root).atomic_write_dir(&atomic_write_dir);
+ let builder = Fs::default()
+ .root(root)
+ .atomic_write_dir(&atomic_write_dir.to_string_lossy());
let store = ObjectStore::new(builder).context(OpenDalSnafu)?;
Ok(with_instrument_layers(store, false))
@@ -759,3 +763,37 @@ impl FilePathProvider for RegionFilePathFactory {
location::sst_file_path(&self.table_dir, file_id, self.path_type)
}
}
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use super::*;
+
+ #[tokio::test]
+ async fn test_new_fs_cache_store() {
+ let root = common_test_util::temp_dir::create_temp_dir("fs-cache-store");
+ let dirs = [
+ root.path().join(ATOMIC_WRITE_DIR),
+ PathBuf::from(join_dir(
+ root.path().to_str().unwrap(),
+ OLD_ATOMIC_WRITE_DIR,
+ )),
+ ];
+ for dir in &dirs {
+ tokio::fs::create_dir_all(dir).await.unwrap();
+ tokio::fs::write(dir.join("stale"), b"stale").await.unwrap();
+ }
+ let store = new_fs_cache_store(root.path().to_str().unwrap())
+ .await
+ .unwrap();
+ for dir in &dirs {
+ assert!(!dir.join("stale").exists());
+ }
+ store.write("index", "contents").await.unwrap();
+ assert_eq!(
+ tokio::fs::read(root.path().join("index")).await.unwrap(),
+ b"contents"
+ );
+ }
+}
diff --git a/src/mito2/src/compaction/compactor.rs b/src/mito2/src/compaction/compactor.rs
index 8ad1cdf0b22..fb2172b4875 100644
--- a/src/mito2/src/compaction/compactor.rs
+++ b/src/mito2/src/compaction/compactor.rs
@@ -655,6 +655,7 @@ where
write_buffer_size: compaction_region.engine_config.sst_write_buffer_size,
max_file_size: picker_output.max_file_size,
row_group_size: compaction_region.region_options.row_group_size(),
+ float_field_encoding: compaction_region.region_options.float_field_encoding,
};
let merger = self.merger.clone();
let compaction_region = compaction_region.clone();
diff --git a/src/mito2/src/compaction/json2.rs b/src/mito2/src/compaction/json2.rs
index 71b571c586d..8eec0e11520 100644
--- a/src/mito2/src/compaction/json2.rs
+++ b/src/mito2/src/compaction/json2.rs
@@ -52,6 +52,8 @@ pub(crate) type Json2RewritePlans = HashMap;
#[derive(Clone)]
struct Json2LeafPathStats {
rows: u64,
+ /// Number of input schemas that explicitly represent this leaf.
+ sources: usize,
data_type: JsonNativeType,
is_type_conflicted: bool,
}
@@ -59,8 +61,8 @@ struct Json2LeafPathStats {
/// Builds the JSON2 rewrite plans for a compaction.
///
/// Type hints from current region metadata are always retained. Existing explicit dynamic paths
-/// from all input SST schemas are ranked once to produce a fixed layout; paths found only in a v2
-/// remainder are deliberately not promoted. [`rewrite_json2_batch`] decodes inputs and rewrites
+/// shared by every input SST schema are ranked once to produce a fixed layout; paths that may
+/// reside in a v2 remainder are deliberately not promoted. [`rewrite_json2_batch`] decodes inputs and rewrites
/// them according to these plans. Non-JSON2 columns are omitted from the returned map.
///
/// Returns an error when a JSON2 column has invalid or missing extension metadata, or when its
@@ -144,6 +146,14 @@ pub(crate) fn collect_json2_rewrite_plans(
collect_json2_path_stats(field, *rows, &hint_paths, &mut stats)?;
}
+ // A leaf absent from an input's explicit schema may have arbitrary values
+ // (including scalar ancestors) in its remainder. Only common explicit leaves
+ // are safe to promote without inspecting rows. Keep unsafe paths opaque for
+ // the entire output SST so existing readers never miss remainder values when
+ // projecting an explicit leaf.
+ for stat in stats.values_mut() {
+ stat.is_type_conflicted |= stat.sources != schemas.len();
+ }
let mut hints = settings.type_hints().to_vec();
hints.extend(select_dynamic_hints(settings, &hint_paths, &stats));
let target_layout = JsonSettings::try_new(hints, Some(0)).context(DataTypeMismatchSnafu)?;
@@ -187,12 +197,14 @@ fn collect_json2_path_stats<'a>(
path,
Json2LeafPathStats {
rows,
+ sources: 1,
data_type,
is_type_conflicted: false,
},
);
continue;
};
+ stat.sources += 1;
if stat.data_type != data_type {
stat.is_type_conflicted = true;
} else {
@@ -368,6 +380,7 @@ mod tests {
)?;
let stat = |rows, data_type, is_type_conflicted| Json2LeafPathStats {
rows,
+ sources: 1,
data_type,
is_type_conflicted,
};
diff --git a/src/mito2/src/compaction/window.rs b/src/mito2/src/compaction/window.rs
index a9f3f021b84..b9053466444 100644
--- a/src/mito2/src/compaction/window.rs
+++ b/src/mito2/src/compaction/window.rs
@@ -337,6 +337,7 @@ mod tests {
primary_key_encoding: None,
write_buffer_size: None,
preserve_row_sequence: false,
+ float_field_encoding: Default::default(),
},
compaction_time_window: None,
}
diff --git a/src/mito2/src/config.rs b/src/mito2/src/config.rs
index 6ec790c52b0..667f156fb22 100644
--- a/src/mito2/src/config.rs
+++ b/src/mito2/src/config.rs
@@ -89,11 +89,9 @@ pub struct MitoConfig {
// Background job configs:
/// Max number of running background index build jobs (default: 1/8 of cpu cores).
pub max_background_index_builds: usize,
- // TODO: Document both series-index settings in the example configs and regenerate configuration
- // docs before exposing the feature.
- /// Under development; do not enable. Root directory for loading series indexes, currently stored
- /// on the local filesystem. Empty disables the feature. Relative paths resolve under `data_home`.
- pub experimental_series_index_root: String,
+ /// Under development; do not enable. Whether to enable series indexes (default false).
+ /// Indexes are stored on the local filesystem under `{data_home}/series_index`.
+ pub experimental_enable_series_index: bool,
/// Interval between series-index maintenance runs (default 5 min). Zero uses the default.
#[serde(with = "humantime_serde")]
pub experimental_series_index_maintenance_interval: Duration,
@@ -214,7 +212,7 @@ impl Default for MitoConfig {
experimental_manifest_keep_removed_file_ttl: Duration::from_secs(60 * 60),
compress_manifest: false,
max_background_index_builds: divide_num_cpus(8),
- experimental_series_index_root: String::new(),
+ experimental_enable_series_index: false,
experimental_series_index_maintenance_interval:
DEFAULT_SERIES_INDEX_MAINTENANCE_INTERVAL,
max_background_flushes: divide_num_cpus(2),
@@ -312,15 +310,6 @@ impl MitoConfig {
DEFAULT_SERIES_INDEX_MAINTENANCE_INTERVAL;
}
- if !self.experimental_series_index_root.trim().is_empty()
- && Path::new(&self.experimental_series_index_root).is_relative()
- {
- self.experimental_series_index_root = Path::new(data_home)
- .join(&self.experimental_series_index_root)
- .display()
- .to_string();
- }
-
if self.global_write_buffer_reject_size <= self.global_write_buffer_size {
self.global_write_buffer_reject_size = self.global_write_buffer_size * 2;
warn!(
@@ -432,36 +421,6 @@ mod tests {
assert_eq!(ReadableSize::mb(128), config.prefilter_result_cache_size);
}
- #[test]
- fn test_series_index_config() {
- assert!(
- MitoConfig::default()
- .experimental_series_index_root
- .is_empty()
- );
- let mut config: MitoConfig = toml::from_str(
- "experimental_series_index_root = 'indexes'
- experimental_series_index_maintenance_interval = '30s'",
- )
- .unwrap();
- config.sanitize("/data").unwrap();
- assert_eq!(config.experimental_series_index_root, "/data/indexes");
- assert_eq!(
- config.experimental_series_index_maintenance_interval,
- Duration::from_secs(30)
- );
- let restored: MitoConfig = toml::from_str(&toml::to_string(&config).unwrap()).unwrap();
- assert_eq!(config, restored);
-
- let mut config: MitoConfig =
- toml::from_str("experimental_series_index_maintenance_interval = '0s'").unwrap();
- config.sanitize("/data").unwrap();
- assert_eq!(
- config.experimental_series_index_maintenance_interval,
- MitoConfig::default().experimental_series_index_maintenance_interval
- );
- }
-
#[test]
fn test_experimental_series_scan_v2_config() {
assert!(MitoConfig::default().experimental_series_scan_v2);
diff --git a/src/mito2/src/engine.rs b/src/mito2/src/engine.rs
index 1c674abadbf..348ec4c1222 100644
--- a/src/mito2/src/engine.rs
+++ b/src/mito2/src/engine.rs
@@ -225,6 +225,7 @@ impl<'a, S: LogStore> MitoEngineBuilder<'a, S> {
// so the engine (and thus the GC worker) can fire `on_region_gc`.
let region_hook = self.plugins.get::();
let workers = WorkerGroup::start(
+ self.data_home,
config.clone(),
self.log_store.clone(),
self.object_store_manager,
@@ -1620,6 +1621,7 @@ impl MitoEngine {
Ok(MitoEngine {
inner: Arc::new(EngineInner {
workers: WorkerGroup::start_for_test(
+ data_home,
config.clone(),
log_store,
object_store_manager,
diff --git a/src/mito2/src/engine/alter_test.rs b/src/mito2/src/engine/alter_test.rs
index 736f32cc8ae..fd74c4b498a 100644
--- a/src/mito2/src/engine/alter_test.rs
+++ b/src/mito2/src/engine/alter_test.rs
@@ -3003,6 +3003,7 @@ fn build_schema_v0_bulk_request(region_id: RegionId) -> RegionBulkInsertsRequest
let raw_data = encode_arrow_ipc(&payload);
RegionBulkInsertsRequest {
+ skip_wal: false,
region_id,
payload,
raw_data,
@@ -3411,6 +3412,7 @@ async fn test_alter_time_index_widen_sparse_compaction() {
};
let put_sparse = |rows| {
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows,
hint: Some(WriteHint {
primary_key_encoding: api::v1::PrimaryKeyEncoding::Sparse.into(),
diff --git a/src/mito2/src/engine/apply_staging_manifest_test.rs b/src/mito2/src/engine/apply_staging_manifest_test.rs
index 5e7f77257a3..e6ac2bc24b4 100644
--- a/src/mito2/src/engine/apply_staging_manifest_test.rs
+++ b/src/mito2/src/engine/apply_staging_manifest_test.rs
@@ -952,6 +952,7 @@ async fn test_apply_staging_manifest_preserves_unflushed_memtable_with_format(fl
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: unflushed_rows,
hint: None,
partition_expr_version: Some(expected_version),
diff --git a/src/mito2/src/engine/basic_test.rs b/src/mito2/src/engine/basic_test.rs
index edaa97ff585..a9ee4054158 100644
--- a/src/mito2/src/engine/basic_test.rs
+++ b/src/mito2/src/engine/basic_test.rs
@@ -29,6 +29,7 @@ use datatypes::prelude::ConcreteDataType;
use datatypes::schema::ColumnSchema;
use futures::TryStreamExt;
use itertools::Itertools;
+use parquet::basic::{Encoding, Type as PhysicalType};
use rstest::rstest;
use rstest_reuse::{self, apply};
use store_api::metadata::ColumnMetadata;
@@ -732,6 +733,7 @@ async fn test_absent_and_invalid_columns_with_format(flat_format: bool) {
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows,
hint: None,
partition_expr_version: None,
@@ -825,7 +827,9 @@ async fn test_engine_with_write_cache_with_format(flat_format: bool) {
let engine = env.create_engine(mito_config).await;
let region_id = RegionId::new(1, 1);
- let request = CreateRequestBuilder::new().build();
+ let request = CreateRequestBuilder::new()
+ .insert_option("experimental_sst_float_field_encoding", "byte_stream_split")
+ .build();
let column_schemas = rows_schema(&request);
engine
@@ -841,6 +845,38 @@ async fn test_engine_with_write_cache_with_format(flat_format: bool) {
flush_region(&engine, region_id, None).await;
+ let region = engine.get_region(region_id).unwrap();
+ let file = region
+ .version()
+ .ssts
+ .levels()
+ .iter()
+ .flat_map(|level| level.files.values())
+ .next()
+ .expect("write-cache flushed SST")
+ .clone();
+ let reader = region
+ .access_layer
+ .read_sst(file)
+ .build()
+ .await
+ .unwrap()
+ .expect("write-cache flushed SST reader");
+ assert!(
+ reader
+ .parquet_metadata()
+ .row_groups()
+ .iter()
+ .flat_map(|row_group| row_group.columns())
+ .any(|column| {
+ column.column_path().string() == "field_0"
+ && column.column_type() == PhysicalType::DOUBLE
+ && column
+ .encodings()
+ .any(|encoding| encoding == Encoding::BYTE_STREAM_SPLIT)
+ })
+ );
+
let request = ScanRequest::default();
let scanner = engine.scanner(region_id, request).await.unwrap();
@@ -1320,3 +1356,388 @@ async fn test_all_index_metas_list_all_types_with_format(flat_format: bool, expe
assert_eq!(expect_format, debug_format);
}
+
+#[tokio::test]
+async fn test_request_skip_wal_recovery() {
+ check_request_skip_wal_recovery(false, false).await;
+ check_request_skip_wal_recovery(false, true).await;
+ check_request_skip_wal_recovery(true, false).await;
+ check_request_skip_wal_recovery(true, true).await;
+}
+
+async fn check_request_skip_wal_recovery(flat_format: bool, skip_wal: bool) {
+ let mut env = TestEnv::new().await;
+ let engine = env
+ .create_engine(MitoConfig {
+ default_flat_format: flat_format,
+ ..Default::default()
+ })
+ .await;
+ let region_id = RegionId::new(1, 1);
+ let request = CreateRequestBuilder::new().build();
+ let table_dir = request.table_dir.clone();
+ let schema = rows_schema(&request);
+ engine
+ .handle_request(region_id, RegionRequest::Create(request))
+ .await
+ .unwrap();
+ let affected = engine
+ .handle_request(
+ region_id,
+ RegionRequest::Put(RegionPutRequest {
+ rows: Rows {
+ schema,
+ rows: build_rows_for_key("a", 0, 4, 0),
+ },
+ hint: None,
+ partition_expr_version: None,
+ skip_wal,
+ }),
+ )
+ .await
+ .unwrap();
+ assert_eq!(affected.affected_rows, 4);
+ let current = engine
+ .get_region(region_id)
+ .unwrap()
+ .version_control
+ .current();
+ assert_eq!(current.committed_sequence, 4);
+ assert_eq!(current.last_entry_id, u64::from(!skip_wal));
+ let stream = engine
+ .scan_to_stream(region_id, ScanRequest::default())
+ .await
+ .unwrap();
+ let before = RecordBatches::try_collect(stream).await.unwrap();
+ assert_eq!(before.iter().map(|b| b.num_rows()).sum::(), 4);
+
+ reopen_region(&engine, region_id, table_dir, false, HashMap::new()).await;
+ let stream = engine
+ .scan_to_stream(region_id, ScanRequest::default())
+ .await
+ .unwrap();
+ let after = RecordBatches::try_collect(stream).await.unwrap();
+ assert_eq!(
+ after.iter().map(|b| b.num_rows()).sum::(),
+ if skip_wal { 0 } else { 4 }
+ );
+}
+
+#[tokio::test]
+async fn test_request_skip_wal_flush_watermarks() {
+ check_request_skip_wal_flush_watermarks(false).await;
+ check_request_skip_wal_flush_watermarks(true).await;
+}
+
+async fn check_request_skip_wal_flush_watermarks(flat_format: bool) {
+ let mut env = TestEnv::new().await;
+ let engine = env
+ .create_engine(MitoConfig {
+ default_flat_format: flat_format,
+ ..Default::default()
+ })
+ .await;
+ let region_id = RegionId::new(1, 1);
+ let request = CreateRequestBuilder::new().build();
+ let schema = rows_schema(&request);
+ engine
+ .handle_request(region_id, RegionRequest::Create(request))
+ .await
+ .unwrap();
+
+ // Establish a nonzero flushed baseline, skip one write, then resume WAL.
+ for (round, skip_wal) in [false, true, false].into_iter().enumerate() {
+ let region = engine.get_region(region_id).unwrap();
+ let before = region.version_control.current();
+ let flushed_entry_id = engine
+ .region_statistic(region_id)
+ .unwrap()
+ .manifest
+ .data_flushed_entry_id();
+ let affected = engine
+ .handle_request(
+ region_id,
+ RegionRequest::Put(RegionPutRequest {
+ rows: Rows {
+ schema: schema.clone(),
+ rows: build_rows_for_key("a", 0, 4, 0),
+ },
+ hint: None,
+ partition_expr_version: None,
+ skip_wal,
+ }),
+ )
+ .await
+ .unwrap();
+ assert_eq!(affected.affected_rows, 4);
+ let after_write = region.version_control.current();
+ let expected_entry_id = before.last_entry_id + u64::from(!skip_wal);
+ assert_eq!(after_write.last_entry_id, expected_entry_id);
+ assert_eq!(
+ after_write.committed_sequence,
+ before.committed_sequence + 4
+ );
+ assert_eq!(after_write.version.flushed_entry_id, flushed_entry_id);
+ assert_eq!(
+ engine
+ .region_statistic(region_id)
+ .unwrap()
+ .manifest
+ .data_flushed_entry_id(),
+ flushed_entry_id
+ );
+ assert_eq!(
+ after_write.version.flushed_sequence,
+ before.version.flushed_sequence
+ );
+
+ flush_region(&engine, region_id, None).await;
+ let after_flush = region.version_control.current();
+ assert_eq!(after_flush.last_entry_id, expected_entry_id);
+ assert_eq!(after_flush.version.flushed_sequence, (round as u64 + 1) * 4);
+ assert_eq!(
+ engine
+ .region_statistic(region_id)
+ .unwrap()
+ .manifest
+ .data_flushed_entry_id(),
+ expected_entry_id
+ );
+ if skip_wal {
+ assert_eq!(expected_entry_id, flushed_entry_id);
+ } else {
+ assert!(expected_entry_id > flushed_entry_id);
+ }
+ }
+}
+
+/// Returns `(committed_sequence, last_entry_id)` from one region version snapshot.
+/// Returns `None` if the region is not open.
+fn region_write_watermarks(engine: &MitoEngine, region_id: RegionId) -> Option<(u64, u64)> {
+ let region = engine.find_region(region_id)?;
+ let current = region.version_control.current();
+ Some((current.committed_sequence, current.last_entry_id))
+}
+
+#[tokio::test]
+async fn test_request_skip_wal_mixed_batch_recovery() {
+ check_request_skip_wal_mixed_batch_recovery(false, false, false).await;
+ check_request_skip_wal_mixed_batch_recovery(false, false, true).await;
+ check_request_skip_wal_mixed_batch_recovery(false, true, false).await;
+ check_request_skip_wal_mixed_batch_recovery(false, true, true).await;
+ check_request_skip_wal_mixed_batch_recovery(true, false, false).await;
+ check_request_skip_wal_mixed_batch_recovery(true, false, true).await;
+ check_request_skip_wal_mixed_batch_recovery(true, true, false).await;
+ check_request_skip_wal_mixed_batch_recovery(true, true, true).await;
+}
+
+async fn check_request_skip_wal_mixed_batch_recovery(
+ flat_format: bool,
+ skip_wal: bool,
+ flush_before_reopen: bool,
+) {
+ use crate::region_write_ctx::RegionWriteCtx;
+ use crate::request::OptionOutputTx;
+ use crate::test_util::LogStoreImpl;
+ use crate::wal::Wal;
+
+ let mut env = TestEnv::new().await;
+ let engine = env
+ .create_engine(MitoConfig {
+ default_flat_format: flat_format,
+ ..Default::default()
+ })
+ .await;
+ let region_id = RegionId::new(1, 1);
+ let request = CreateRequestBuilder::new().build();
+ let table_dir = request.table_dir.clone();
+ let schema = rows_schema(&request);
+ engine
+ .handle_request(region_id, RegionRequest::Create(request))
+ .await
+ .unwrap();
+ let region = engine.get_region(region_id).unwrap();
+ let LogStoreImpl::RaftEngine(store) = env.get_log_store().unwrap() else {
+ panic!("expected the default local WAL");
+ };
+ let wal = Wal::new(store);
+ // Assemble one real worker write context deterministically, instead
+ // of relying on concurrently submitted requests landing in one batch.
+ let mut ctx = RegionWriteCtx::new(
+ region_id,
+ ®ion.version_control,
+ region.provider.clone(),
+ None,
+ );
+ let mut receivers = Vec::with_capacity(4);
+ for (index, (skip, timestamp)) in [(false, 0), (skip_wal, 0), (false, 0), (skip_wal, 1)]
+ .into_iter()
+ .enumerate()
+ {
+ let (tx, rx) = tokio::sync::oneshot::channel();
+ receivers.push(rx);
+ ctx.push_mutation(
+ api::v1::OpType::Put as i32,
+ Some(Rows {
+ schema: schema.clone(),
+ // Timestamp zero ends with a WAL-backed update; timestamp one
+ // ends with a skipped update. Grouping mutations must not
+ // change either winner chosen by the original sequences.
+ rows: build_rows_for_key("a", timestamp, timestamp + 1, index * 10)
+ .into_iter()
+ .chain(build_rows_for_key(
+ "a",
+ index + 1,
+ index + 2,
+ index * 10 + 1,
+ ))
+ .collect(),
+ }),
+ None,
+ OptionOutputTx::from(tx),
+ None,
+ skip,
+ );
+ }
+ let mut writer = wal.writer();
+ ctx.add_wal_entry(&mut writer).unwrap();
+ let response = writer.write_to_wal().await.unwrap();
+ assert_eq!(response.last_entry_ids.get(®ion_id), Some(&1));
+ ctx.write_memtable().await;
+ ctx.publish_sequence_and_entry_id();
+ drop(ctx);
+ for rx in receivers {
+ assert_eq!(rx.await.unwrap().unwrap(), 2);
+ }
+ assert_eq!(region_write_watermarks(&engine, region_id), Some((8, 1)));
+ assert_eq!(
+ request_skip_wal_values(&engine, region_id).await,
+ vec![
+ (0, 20.0),
+ (1000, 30.0),
+ (2000, 11.0),
+ (3000, 21.0),
+ (4000, 31.0)
+ ]
+ );
+
+ // Inspect persisted WAL mutations, not just an encoder or counter.
+ let mut reader = wal.wal_entry_reader(®ion.provider, region_id, None);
+ let entries = reader
+ .read(®ion.provider, 1)
+ .unwrap()
+ .try_collect::>()
+ .await
+ .unwrap();
+ assert_eq!(entries.len(), 1);
+ assert_eq!(entries[0].0, 1);
+ assert_eq!(
+ entries[0]
+ .1
+ .mutations
+ .iter()
+ .map(|m| m.sequence)
+ .collect::>(),
+ if skip_wal {
+ vec![1, 5]
+ } else {
+ vec![1, 3, 5, 7]
+ }
+ );
+ assert!(entries[0].1.bulk_entries.is_empty());
+ drop(region);
+ if flush_before_reopen {
+ flush_region(&engine, region_id, None).await;
+ let current = engine
+ .get_region(region_id)
+ .unwrap()
+ .version_control
+ .current();
+ assert_eq!(
+ (
+ current.version.flushed_sequence,
+ current.version.flushed_entry_id
+ ),
+ (8, 1)
+ );
+ }
+
+ // A no-flush close discards memtables. Only WAL-backed rows recover
+ // unless an explicit flush has already persisted all requests.
+ reopen_region(&engine, region_id, table_dir, true, HashMap::new()).await;
+ let loses_skipped_rows = skip_wal && !flush_before_reopen;
+ let mut expected_values = if loses_skipped_rows {
+ vec![(0, 20.0), (1000, 1.0), (3000, 21.0)]
+ } else {
+ vec![
+ (0, 20.0),
+ (1000, 30.0),
+ (2000, 11.0),
+ (3000, 21.0),
+ (4000, 31.0),
+ ]
+ };
+ assert_eq!(
+ request_skip_wal_values(&engine, region_id).await,
+ expected_values
+ );
+ let recovered_sequence = if loses_skipped_rows { 6 } else { 8 };
+ assert_eq!(
+ region_write_watermarks(&engine, region_id),
+ Some((recovered_sequence, 1))
+ );
+
+ // A subsequent default request still writes WAL, even after a
+ // trailing skipped request or a flush with sequence/entry-id gaps.
+ put_rows(
+ &engine,
+ region_id,
+ Rows {
+ schema,
+ rows: build_rows_for_key("a", 8, 9, 8),
+ },
+ )
+ .await;
+ assert_eq!(
+ region_write_watermarks(&engine, region_id),
+ Some((recovered_sequence + 1, 2))
+ );
+ expected_values.push((8000, 8.0));
+ assert_eq!(
+ request_skip_wal_values(&engine, region_id).await,
+ expected_values
+ );
+}
+
+async fn request_skip_wal_values(engine: &MitoEngine, region_id: RegionId) -> Vec<(i64, f64)> {
+ use datatypes::arrow::array::{Float64Array, TimestampMillisecondArray};
+
+ let stream = engine
+ .scan_to_stream(region_id, ScanRequest::default())
+ .await
+ .unwrap();
+ let batches = RecordBatches::try_collect(stream).await.unwrap();
+ let mut values = batches
+ .iter()
+ .flat_map(|batch| {
+ let batch = batch.df_record_batch();
+ let timestamps = batch
+ .column(2)
+ .as_any()
+ .downcast_ref::()
+ .unwrap();
+ let values = batch
+ .column(1)
+ .as_any()
+ .downcast_ref::()
+ .unwrap();
+ timestamps
+ .values()
+ .iter()
+ .copied()
+ .zip(values.values().iter().copied())
+ })
+ .collect::>();
+ values.sort_unstable_by_key(|(timestamp, _)| *timestamp);
+ values
+}
diff --git a/src/mito2/src/engine/compaction_test.rs b/src/mito2/src/engine/compaction_test.rs
index c67afbf8c6f..e427f3f34f5 100644
--- a/src/mito2/src/engine/compaction_test.rs
+++ b/src/mito2/src/engine/compaction_test.rs
@@ -26,6 +26,7 @@ use common_recordbatch::{RecordBatches, SendableRecordBatchStream};
use common_time::Timestamp;
use datatypes::arrow::array::AsArray;
use datatypes::arrow::datatypes::TimestampMillisecondType;
+use parquet::basic::{Encoding, Type as PhysicalType};
use store_api::region_engine::{RegionEngine, RegionRole};
use store_api::region_request::AlterKind::SetRegionOptions;
use store_api::region_request::{
@@ -954,6 +955,7 @@ async fn test_compaction_region_with_format(flat_format: bool) {
let request = CreateRequestBuilder::new()
.insert_option("compaction.type", "twcs")
+ .insert_option("experimental_sst_float_field_encoding", "byte_stream_split")
.build();
let column_schemas = request
@@ -974,6 +976,35 @@ async fn test_compaction_region_with_format(flat_format: bool) {
compact(&engine, region_id).await;
+ let region = engine.get_region(region_id).unwrap();
+ let file = region.version().ssts.levels()[1]
+ .files
+ .values()
+ .next()
+ .expect("compaction output SST")
+ .clone();
+ let reader = region
+ .access_layer
+ .read_sst(file)
+ .build()
+ .await
+ .unwrap()
+ .expect("compaction output SST reader");
+ assert!(
+ reader
+ .parquet_metadata()
+ .row_groups()
+ .iter()
+ .flat_map(|row_group| row_group.columns())
+ .any(|column| {
+ column.column_path().string() == "field_0"
+ && column.column_type() == PhysicalType::DOUBLE
+ && column
+ .encodings()
+ .any(|encoding| encoding == Encoding::BYTE_STREAM_SPLIT)
+ })
+ );
+
let scanner = engine
.scanner(region_id, ScanRequest::default())
.await
diff --git a/src/mito2/src/engine/edit_region_test.rs b/src/mito2/src/engine/edit_region_test.rs
index 992060d75d7..39e688c5c4d 100644
--- a/src/mito2/src/engine/edit_region_test.rs
+++ b/src/mito2/src/engine/edit_region_test.rs
@@ -286,6 +286,7 @@ async fn test_write_during_region_editing_is_queued() {
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows,
hint: None,
partition_expr_version: None,
@@ -400,6 +401,7 @@ async fn test_stalled_write_fails_fast_if_region_closed_during_editing() {
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows,
hint: None,
partition_expr_version: None,
@@ -698,6 +700,7 @@ fn build_bulk_insert_request(
let (schema, record_batch) = encode_to_flight_data(payload.clone());
RegionBulkInsertsRequest {
+ skip_wal: false,
region_id,
payload,
raw_data: ArrowIpc {
diff --git a/src/mito2/src/engine/flush_test.rs b/src/mito2/src/engine/flush_test.rs
index b71ce9f9fc0..2b1e6f0cc2d 100644
--- a/src/mito2/src/engine/flush_test.rs
+++ b/src/mito2/src/engine/flush_test.rs
@@ -27,6 +27,7 @@ use common_base::readable_size::ReadableSize;
use common_recordbatch::RecordBatches;
use common_time::util::current_time_millis;
use common_wal::options::{KafkaWalOptions, WAL_OPTIONS_KEY, WalOptions};
+use parquet::basic::{Encoding, Type as PhysicalType};
use rstest::rstest;
use rstest_reuse::{self, apply};
use store_api::ManifestVersion;
@@ -279,7 +280,9 @@ async fn test_manual_flush_with_format(flat_format: bool) {
)
.await;
- let request = CreateRequestBuilder::new().build();
+ let request = CreateRequestBuilder::new()
+ .insert_option("experimental_sst_float_field_encoding", "byte_stream_split")
+ .build();
let column_schemas = rows_schema(&request);
engine
@@ -295,6 +298,38 @@ async fn test_manual_flush_with_format(flat_format: bool) {
flush_region(&engine, region_id, None).await;
+ let region = engine.get_region(region_id).unwrap();
+ let file = region
+ .version()
+ .ssts
+ .levels()
+ .iter()
+ .flat_map(|level| level.files.values())
+ .next()
+ .expect("flushed SST")
+ .clone();
+ let reader = region
+ .access_layer
+ .read_sst(file)
+ .build()
+ .await
+ .unwrap()
+ .expect("flushed SST reader");
+ assert!(
+ reader
+ .parquet_metadata()
+ .row_groups()
+ .iter()
+ .flat_map(|row_group| row_group.columns())
+ .any(|column| {
+ column.column_path().string() == "field_0"
+ && column.column_type() == PhysicalType::DOUBLE
+ && column
+ .encodings()
+ .any(|encoding| encoding == Encoding::BYTE_STREAM_SPLIT)
+ })
+ );
+
let request = ScanRequest::default();
let scanner = engine.scanner(region_id, request).await.unwrap();
assert_eq!(0, scanner.num_memtables());
@@ -740,6 +775,7 @@ async fn test_region_write_buffer_does_not_stall_follower_write() {
engine.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema,
rows: build_rows_for_key("follower", 2, 4, 0),
@@ -935,6 +971,7 @@ async fn test_region_write_buffer_rejects_only_full_region_queue() {
.handle_request(
hot_region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema: stalled_schema,
rows: build_rows_for_key("hot", 2, 1026, 2),
diff --git a/src/mito2/src/engine/open_test.rs b/src/mito2/src/engine/open_test.rs
index 55cd92e6eda..c29c9aa3915 100644
--- a/src/mito2/src/engine/open_test.rs
+++ b/src/mito2/src/engine/open_test.rs
@@ -478,6 +478,7 @@ async fn test_engine_open_readonly_with_format(flat_format: bool) {
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: rows.clone(),
hint: None,
partition_expr_version: None,
diff --git a/src/mito2/src/engine/scan_test.rs b/src/mito2/src/engine/scan_test.rs
index 69c08c17bb1..d63dcc77bd1 100644
--- a/src/mito2/src/engine/scan_test.rs
+++ b/src/mito2/src/engine/scan_test.rs
@@ -394,6 +394,167 @@ async fn test_json2_v1_region_reopen_and_compaction() -> WhateverResult<()> {
Ok(())
}
+#[tokio::test]
+async fn test_json2_mixed_subject_compaction_preserves_values() -> WhateverResult<()> {
+ let request = CreateRequestBuilder::new()
+ .field_datatype(ConcreteDataType::json2(JsonNativeType::Object(
+ JsonObjectType::new(),
+ )))
+ .insert_option("append_mode", "true")
+ .insert_option("memtable.type", "bulk")
+ .insert_option("sst_format", "flat")
+ .build();
+ let table_dir = request.table_dir.clone();
+ let schema = test_util::rows_schema(&request);
+ let mut env = TestEnv::new().await;
+ let engine = env
+ .create_engine(MitoConfig {
+ min_compaction_interval: std::time::Duration::from_secs(3600),
+ ..Default::default()
+ })
+ .await;
+ let region_id = RegionId::new(1027, 0);
+ engine
+ .handle_request(region_id, RegionRequest::Create(request))
+ .await?;
+ // #9133: the second SST stores a heterogeneous subject in its remainder.
+ let values = [
+ json!({"subject": {"cid": "first", "uri": "at://first"}}),
+ json!({"subject": "did:plc:x"}),
+ json!({"subject": {"cid": "last", "uri": "at://last"}}),
+ json!({"subject": {"cid": 42}}),
+ ];
+ for (offset, batch) in [(0, &values[..1]), (1, &values[1..])] {
+ test_util::put_rows(
+ &engine,
+ region_id,
+ Rows {
+ schema: schema.clone(),
+ rows: batch
+ .iter()
+ .enumerate()
+ .map(|(i, value)| {
+ row(vec![
+ ValueData::StringValue("tag".into()),
+ ValueData::JsonValue(encode_json_value(JsonValue::from(value.clone()))),
+ ValueData::TimestampMillisecondValue((offset + i) as i64 * 1000),
+ ])
+ })
+ .collect(),
+ },
+ )
+ .await;
+ test_util::flush_region(&engine, region_id, None).await;
+ }
+ let old_ids = engine
+ .scanner(region_id, ScanRequest::default())
+ .await?
+ .file_ids();
+ assert_eq!(2, old_ids.len());
+ for _ in 0..2 {
+ engine
+ .handle_request(
+ region_id,
+ RegionRequest::Compact(RegionCompactRequest {
+ options: compact_request::Options::StrictWindow(StrictWindow {
+ window_seconds: 86400,
+ }),
+ ..Default::default()
+ }),
+ )
+ .await?;
+ let scanner = engine
+ .scanner(
+ region_id,
+ ScanRequest {
+ projection: Some(vec![1]),
+ ..Default::default()
+ },
+ )
+ .await?;
+ assert_eq!(
+ 1,
+ scanner.num_files(),
+ "a swallowed merge failure must not pass"
+ );
+ assert!(scanner.file_ids().iter().all(|id| !old_ids.contains(id)));
+ let batches = RecordBatches::try_collect(scanner.scan().await?).await?;
+ let mut actual = Vec::new();
+ for batch in batches.iter() {
+ let array = batch.column_by_name("field_0").unwrap().clone();
+ for i in 0..array.len() {
+ actual.push(JsonArray::from(&array).try_get_value(i)?);
+ }
+ }
+ assert_eq!(values.as_slice(), actual.as_slice());
+ reopen_region(&engine, region_id, table_dir.clone(), true, HashMap::new()).await;
+ let scanner = engine
+ .scanner(
+ region_id,
+ ScanRequest {
+ projection: Some(vec![1]),
+ json_type_hint: HashMap::from([(
+ "field_0".into(),
+ JsonNativeType::Object(JsonObjectType::from([(
+ "subject".into(),
+ JsonNativeType::String,
+ )])),
+ )]),
+ ..Default::default()
+ },
+ )
+ .await?;
+ let batches = RecordBatches::try_collect(scanner.scan().await?).await?;
+ let mut subjects = Vec::new();
+ for batch in batches.iter() {
+ let array = batch.column_by_name("field_0").unwrap().clone();
+ for i in 0..array.len() {
+ subjects.push(JsonArray::from(&array).try_get_value(i)?);
+ }
+ }
+ let expected = values.iter().map(|value| {
+ let subject = &value["subject"];
+ json!({"subject": subject.as_str().map(str::to_string).unwrap_or_else(|| subject.to_string())})
+ }).collect::>();
+ assert_eq!(
+ expected, subjects,
+ "projection must read values spilled to remainder"
+ );
+ let scanner = engine
+ .scanner(
+ region_id,
+ ScanRequest {
+ projection: Some(vec![1]),
+ json_type_hint: HashMap::from([(
+ "field_0".into(),
+ JsonNativeType::Object(JsonObjectType::from([(
+ "subject".into(),
+ JsonNativeType::Object(JsonObjectType::from([(
+ "cid".into(),
+ JsonNativeType::String,
+ )])),
+ )])),
+ )]),
+ ..Default::default()
+ },
+ )
+ .await?;
+ let batches = RecordBatches::try_collect(scanner.scan().await?).await?;
+ let mut cids = Vec::new();
+ for batch in batches.iter() {
+ let array = batch.column_by_name("field_0").unwrap().clone();
+ for i in 0..array.len() {
+ cids.push(JsonArray::from(&array).try_get_value(i)?["subject"]["cid"].clone());
+ }
+ }
+ assert_eq!(
+ vec![json!("first"), json!(null), json!("last"), json!("42")],
+ cids
+ );
+ }
+ Ok(())
+}
+
#[tokio::test]
async fn test_flush_aligns_different_json2_layouts() -> WhateverResult<()> {
let mut request = CreateRequestBuilder::new()
@@ -1129,6 +1290,7 @@ async fn test_two_phase_series_scan() {
};
let put = |rows| {
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows,
hint: Some(WriteHint {
primary_key_encoding: api::v1::PrimaryKeyEncoding::Sparse.into(),
@@ -2389,6 +2551,7 @@ fn build_bulk_insert_request(
let (schema, record_batch) = encode_to_flight_data(payload.clone());
RegionBulkInsertsRequest {
+ skip_wal: false,
region_id,
payload,
raw_data: ArrowIpc {
@@ -2401,6 +2564,139 @@ fn build_bulk_insert_request(
}
}
+#[tokio::test]
+async fn test_bulk_skip_wal_recovery() {
+ check_bulk_skip_wal_recovery(false, false).await;
+ check_bulk_skip_wal_recovery(false, true).await;
+ check_bulk_skip_wal_recovery(true, false).await;
+ check_bulk_skip_wal_recovery(true, true).await;
+}
+
+async fn check_bulk_skip_wal_recovery(skip_wal: bool, flush: bool) {
+ let mut env = TestEnv::new().await;
+ let engine = env.create_engine(MitoConfig::default()).await;
+ let region_id = RegionId::new(1, 1);
+ let request = CreateRequestBuilder::new()
+ .insert_option("memtable.type", "bulk")
+ .build();
+ let table_dir = request.table_dir.clone();
+ engine
+ .handle_request(region_id, RegionRequest::Create(request))
+ .await
+ .unwrap();
+ let mut request = build_bulk_insert_request(region_id, 0, 4);
+ request.skip_wal = skip_wal;
+ let affected = engine
+ .handle_request(region_id, RegionRequest::BulkInserts(request))
+ .await
+ .unwrap();
+ assert_eq!(affected.affected_rows, 4);
+ let region = engine.get_region(region_id).unwrap();
+ let current = region.version_control.current();
+ assert_eq!(current.committed_sequence, 4);
+ assert_eq!(current.last_entry_id, u64::from(!skip_wal));
+ assert_eq!(current.version.flushed_entry_id, 0);
+ let stream = engine
+ .scan_to_stream(region_id, ScanRequest::default())
+ .await
+ .unwrap();
+ assert_eq!(
+ RecordBatches::try_collect(stream)
+ .await
+ .unwrap()
+ .iter()
+ .map(|b| b.num_rows())
+ .sum::(),
+ 4
+ );
+ if flush {
+ test_util::flush_region(&engine, region_id, None).await;
+ let current = region.version_control.current();
+ assert_eq!(current.last_entry_id, u64::from(!skip_wal));
+ assert_eq!(current.version.flushed_sequence, 4);
+ assert_eq!(
+ engine
+ .region_statistic(region_id)
+ .unwrap()
+ .manifest
+ .data_flushed_entry_id(),
+ u64::from(!skip_wal)
+ );
+ }
+ reopen_region(&engine, region_id, table_dir, false, HashMap::new()).await;
+ let stream = engine
+ .scan_to_stream(region_id, ScanRequest::default())
+ .await
+ .unwrap();
+ assert_eq!(
+ RecordBatches::try_collect(stream)
+ .await
+ .unwrap()
+ .iter()
+ .map(|b| b.num_rows())
+ .sum::(),
+ if skip_wal && !flush { 0 } else { 4 }
+ );
+}
+
+#[tokio::test]
+async fn test_bulk_skip_wal_flush_watermarks() {
+ let mut env = TestEnv::new().await;
+ let engine = env.create_engine(MitoConfig::default()).await;
+ let region_id = RegionId::new(1, 1);
+ engine
+ .handle_request(
+ region_id,
+ RegionRequest::Create(
+ CreateRequestBuilder::new()
+ .insert_option("memtable.type", "bulk")
+ .build(),
+ ),
+ )
+ .await
+ .unwrap();
+ // Establish a persisted baseline, skip one bulk write, then resume WAL.
+ for (round, skip_wal) in [false, true, false].into_iter().enumerate() {
+ let region = engine.get_region(region_id).unwrap();
+ let before = region.version_control.current();
+ let mut request = build_bulk_insert_request(region_id, round * 4, (round + 1) * 4);
+ request.skip_wal = skip_wal;
+ let affected = engine
+ .handle_request(region_id, RegionRequest::BulkInserts(request))
+ .await
+ .unwrap();
+ assert_eq!(affected.affected_rows, 4);
+ let expected_entry_id = before.last_entry_id + u64::from(!skip_wal);
+ let after = region.version_control.current();
+ assert_eq!(after.last_entry_id, expected_entry_id);
+ assert_eq!(after.committed_sequence, before.committed_sequence + 4);
+ assert_eq!(
+ after.version.flushed_sequence,
+ before.version.flushed_sequence
+ );
+ assert_eq!(
+ engine
+ .region_statistic(region_id)
+ .unwrap()
+ .manifest
+ .data_flushed_entry_id(),
+ before.version.flushed_entry_id
+ );
+ test_util::flush_region(&engine, region_id, None).await;
+ let after = region.version_control.current();
+ assert_eq!(after.last_entry_id, expected_entry_id);
+ assert_eq!(after.version.flushed_sequence, (round as u64 + 1) * 4);
+ assert_eq!(
+ engine
+ .region_statistic(region_id)
+ .unwrap()
+ .manifest
+ .data_flushed_entry_id(),
+ expected_entry_id
+ );
+ }
+}
+
/// Bulk parts fold per-part sequences (commit-unit granularity): an exact range
/// keeps or drops each whole part. The same set must hold before and after flush.
#[tokio::test]
@@ -3234,6 +3530,7 @@ async fn build_sparse_exact_metric_engine(
};
let put = |rows| {
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows,
hint: Some(WriteHint {
primary_key_encoding: api::v1::PrimaryKeyEncoding::Sparse.into(),
@@ -3509,6 +3806,7 @@ async fn test_two_phase_sparse_metric_exact_sequence_cache_isolation() {
panic!("sparse metric scan must use SeriesScan");
};
assert_eq!("two_phase", scanner.mode());
+ assert_eq!(1, scanner.properties().num_partitions());
let batches = RecordBatches::try_collect(scanner.build_stream().await.unwrap())
.await
.unwrap();
@@ -3520,25 +3818,27 @@ async fn test_two_phase_sparse_metric_exact_sequence_cache_isolation() {
let (cold, _) = scan(restrictive.clone()).await;
// Cache insertion is performed by the range stream's async concat task.
- // Wait for a completed cache read rather than assuming a fixed number of
- // scheduler yields. The aggregate metric includes candidate-stage hits, so
- // output-isolation assertions below remain the data-stage acceptance check.
- let (warm, warm_verbose) = tokio::time::timeout(Duration::from_secs(5), async {
+ // Wait for the SeriesReader's data-stage entry, rather than treating a
+ // candidate-stage cache hit as evidence that the reader replayed its data.
+ let warm = tokio::time::timeout(Duration::from_secs(5), async {
loop {
let (rows, verbose) = scan(restrictive.clone()).await;
- if verbose.contains("\"range_cache_hit\"") {
- return (rows, verbose);
+ // The single output partition is 0; the candidate distributor uses
+ // synthetic partition 1. Only a partition-0 hit proves data replay.
+ let data_metrics = verbose
+ .split("\"partition\":0, \"metrics\":")
+ .nth(1)
+ .and_then(|metrics| metrics.split("\"partition\":").next())
+ .expect("missing two-phase data-reader metrics");
+ if data_metrics.contains("\"range_cache_hit\":") {
+ return rows;
}
tokio::task::yield_now().await;
}
})
.await
- .expect("range cache did not complete within timeout");
+ .expect("series-data range cache did not complete and replay within timeout");
assert_eq!(cold, warm);
- assert!(
- warm_verbose.contains("\"range_cache_hit\":"),
- "warm two-phase scan did not report a range-cache hit: {warm_verbose}"
- );
assert_eq!(
vec![
diff --git a/src/mito2/src/engine/set_role_state_test.rs b/src/mito2/src/engine/set_role_state_test.rs
index 1d0f3c188b8..53aa3511459 100644
--- a/src/mito2/src/engine/set_role_state_test.rs
+++ b/src/mito2/src/engine/set_role_state_test.rs
@@ -114,6 +114,7 @@ async fn test_set_role_state_gracefully_with_format(flat_format: bool) {
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: rows.clone(),
hint: None,
partition_expr_version: None,
@@ -208,6 +209,7 @@ async fn test_write_downgrading_region_with_format(flat_format: bool) {
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: rows.clone(),
hint: None,
partition_expr_version: None,
diff --git a/src/mito2/src/engine/skip_wal_test.rs b/src/mito2/src/engine/skip_wal_test.rs
index a564411473c..88304777b2d 100644
--- a/src/mito2/src/engine/skip_wal_test.rs
+++ b/src/mito2/src/engine/skip_wal_test.rs
@@ -522,6 +522,7 @@ async fn test_close_region_skip_wal_rejects_writes_queued_after_close() {
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema: rows_schema(&request),
rows: build_rows(3, 4),
@@ -553,6 +554,7 @@ async fn test_close_region_skip_wal_rejects_writes_queued_after_close() {
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema: rows_schema(&request_cloned),
rows: build_rows(4, 5),
@@ -579,6 +581,7 @@ async fn test_close_region_skip_wal_rejects_writes_queued_after_close() {
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: Rows {
schema: rows_schema(&request_cloned),
rows: build_rows(5, 6),
diff --git a/src/mito2/src/engine/staging_test.rs b/src/mito2/src/engine/staging_test.rs
index 12263976aa2..bd823708509 100644
--- a/src/mito2/src/engine/staging_test.rs
+++ b/src/mito2/src/engine/staging_test.rs
@@ -285,6 +285,7 @@ async fn test_staging_reject_all_writes_rejects_put() {
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows,
hint: None,
partition_expr_version: None,
@@ -355,6 +356,7 @@ async fn test_staging_write_partition_expr_version_with_format(flat_format: bool
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: bad_rows,
hint: None,
partition_expr_version: Some(origin_version),
@@ -376,6 +378,7 @@ async fn test_staging_write_partition_expr_version_with_format(flat_format: bool
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: compat_rows,
hint: None,
partition_expr_version: None,
@@ -393,6 +396,7 @@ async fn test_staging_write_partition_expr_version_with_format(flat_format: bool
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: ok_rows,
hint: None,
partition_expr_version: Some(expected_version),
@@ -440,6 +444,7 @@ async fn test_staging_write_partition_expr_version_with_format(flat_format: bool
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: exit_rows,
hint: None,
partition_expr_version: Some(origin_version),
@@ -457,6 +462,7 @@ async fn test_staging_write_partition_expr_version_with_format(flat_format: bool
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: compat_rows,
hint: None,
partition_expr_version: None,
@@ -483,6 +489,7 @@ async fn test_staging_write_partition_expr_version_with_format(flat_format: bool
.handle_request(
region_id,
RegionRequest::Put(RegionPutRequest {
+ skip_wal: false,
rows: commit_rows,
hint: None,
partition_expr_version: Some(expected_version),
diff --git a/src/mito2/src/flush.rs b/src/mito2/src/flush.rs
index b7c8513afc7..7b431c1b890 100644
--- a/src/mito2/src/flush.rs
+++ b/src/mito2/src/flush.rs
@@ -451,6 +451,7 @@ impl RegionFlushTask {
let mut write_opts = WriteOptions {
write_buffer_size: self.engine_config.sst_write_buffer_size,
+ float_field_encoding: version.options.float_field_encoding,
..Default::default()
};
if let Some(row_group_size) = self.row_group_size {
@@ -1728,6 +1729,7 @@ mod tests {
(
SenderBulkRequest {
+ skip_wal: false,
sender: OptionOutputTx::from(sender),
region_id,
request: converter.convert().unwrap(),
diff --git a/src/mito2/src/memtable.rs b/src/mito2/src/memtable.rs
index f6481fbcdce..bc085c3bd4e 100644
--- a/src/mito2/src/memtable.rs
+++ b/src/mito2/src/memtable.rs
@@ -491,6 +491,7 @@ impl MemtableBuilderProvider {
BulkMemtableBuilder::new(self.write_buffer_manager.clone(), !dedup, merge_mode)
.with_config(config.clone())
.with_row_group_size(options.row_group_size())
+ .with_float_field_encoding(options.float_field_encoding)
.with_compact_dispatcher(self.compact_dispatcher.clone()),
),
Some(MemtableOptions::TimeSeries) => Arc::new(TimeSeriesMemtableBuilder::new(
@@ -515,6 +516,7 @@ impl MemtableBuilderProvider {
)
.with_config(self.default_bulk_memtable_config.clone())
.with_row_group_size(options.row_group_size())
+ .with_float_field_encoding(options.float_field_encoding)
.with_compact_dispatcher(self.compact_dispatcher.clone());
if let Some(MemtableOptions::Bulk(config)) = &options.memtable {
diff --git a/src/mito2/src/memtable/bulk.rs b/src/mito2/src/memtable/bulk.rs
index 8e1346b9921..b30349cc386 100644
--- a/src/mito2/src/memtable/bulk.rs
+++ b/src/mito2/src/memtable/bulk.rs
@@ -40,6 +40,7 @@ use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use serde_with::{DisplayFromStr, serde_as};
use store_api::metadata::RegionMetadataRef;
+use store_api::mito_engine_options::FloatFieldEncoding;
use store_api::storage::{ColumnId, FileId, RegionId, SequenceRange};
use tokio::sync::Semaphore;
@@ -435,6 +436,7 @@ pub struct BulkMemtable {
merge_mode: MergeMode,
/// Max number of rows in a parquet row group for encoded parts.
row_group_size: usize,
+ float_field_encoding: FloatFieldEncoding,
}
impl std::fmt::Debug for BulkMemtable {
@@ -665,27 +667,7 @@ impl Memtable for BulkMemtable {
}
fn fork(&self, id: MemtableId, metadata: &RegionMetadataRef) -> MemtableRef {
- Arc::new(Self {
- id,
- config: self.config.clone(),
- parts: Arc::new(RwLock::new(BulkParts::default())),
- metadata: metadata.clone(),
- alloc_tracker: AllocTracker::new(self.alloc_tracker.write_buffer_manager()),
- max_timestamp: AtomicI64::new(i64::MIN),
- min_timestamp: AtomicI64::new(i64::MAX),
- max_sequence: AtomicU64::new(0),
- num_rows: AtomicUsize::new(0),
- compactor: Arc::new(Mutex::new(MemtableCompactor::new(
- metadata.region_id,
- id,
- self.config.clone(),
- self.row_group_size,
- ))),
- compact_dispatcher: self.compact_dispatcher.clone(),
- append_mode: self.append_mode,
- merge_mode: self.merge_mode,
- row_group_size: self.row_group_size,
- })
+ self.fork_inner(id, metadata)
}
fn compact(&self, for_flush: bool) -> Result<()> {
@@ -714,6 +696,32 @@ impl Memtable for BulkMemtable {
}
impl BulkMemtable {
+ fn fork_inner(&self, id: MemtableId, metadata: &RegionMetadataRef) -> Arc {
+ Arc::new(Self {
+ id,
+ config: self.config.clone(),
+ parts: Arc::new(RwLock::new(BulkParts::default())),
+ metadata: metadata.clone(),
+ alloc_tracker: AllocTracker::new(self.alloc_tracker.write_buffer_manager()),
+ max_timestamp: AtomicI64::new(i64::MIN),
+ min_timestamp: AtomicI64::new(i64::MAX),
+ max_sequence: AtomicU64::new(0),
+ num_rows: AtomicUsize::new(0),
+ compactor: Arc::new(Mutex::new(MemtableCompactor::new(
+ metadata.region_id,
+ id,
+ self.config.clone(),
+ self.row_group_size,
+ self.float_field_encoding,
+ ))),
+ compact_dispatcher: self.compact_dispatcher.clone(),
+ append_mode: self.append_mode,
+ merge_mode: self.merge_mode,
+ row_group_size: self.row_group_size,
+ float_field_encoding: self.float_field_encoding,
+ })
+ }
+
/// Creates a new BulkMemtable with the default row group size.
pub fn new(
id: MemtableId,
@@ -747,6 +755,32 @@ impl BulkMemtable {
append_mode: bool,
merge_mode: MergeMode,
row_group_size: usize,
+ ) -> Self {
+ Self::new_with_row_group_size_and_encoding(
+ id,
+ config,
+ metadata,
+ write_buffer_manager,
+ compact_dispatcher,
+ append_mode,
+ merge_mode,
+ row_group_size,
+ FloatFieldEncoding::default(),
+ )
+ }
+
+ /// Creates a new BulkMemtable with the given row group size and float encoding.
+ #[allow(clippy::too_many_arguments)]
+ fn new_with_row_group_size_and_encoding(
+ id: MemtableId,
+ config: BulkMemtableConfig,
+ metadata: RegionMetadataRef,
+ write_buffer_manager: Option,
+ compact_dispatcher: Option>,
+ append_mode: bool,
+ merge_mode: MergeMode,
+ row_group_size: usize,
+ float_field_encoding: FloatFieldEncoding,
) -> Self {
let config = config.sanitize();
let region_id = metadata.region_id;
@@ -765,11 +799,13 @@ impl BulkMemtable {
id,
config,
row_group_size,
+ float_field_encoding,
))),
compact_dispatcher,
append_mode,
merge_mode,
row_group_size,
+ float_field_encoding,
}
}
@@ -1179,6 +1215,7 @@ struct MemtableCompactor {
config: BulkMemtableConfig,
/// Max number of rows in a parquet row group for encoded parts.
row_group_size: usize,
+ float_field_encoding: FloatFieldEncoding,
}
impl MemtableCompactor {
@@ -1188,12 +1225,14 @@ impl MemtableCompactor {
memtable_id: MemtableId,
config: BulkMemtableConfig,
row_group_size: usize,
+ float_field_encoding: FloatFieldEncoding,
) -> Self {
Self {
region_id,
memtable_id,
config,
row_group_size,
+ float_field_encoding,
}
}
@@ -1233,6 +1272,7 @@ impl MemtableCompactor {
let encode_row_threshold = self.config.encode_row_threshold;
let encode_bytes_threshold = self.config.encode_bytes_threshold;
let row_group_size = self.row_group_size;
+ let float_field_encoding = self.float_field_encoding;
// Merge all groups in parallel
let merged_parts = collected
@@ -1247,6 +1287,7 @@ impl MemtableCompactor {
encode_row_threshold,
encode_bytes_threshold,
row_group_size,
+ float_field_encoding,
)
})
.collect::>>>()?;
@@ -1282,6 +1323,7 @@ impl MemtableCompactor {
encode_row_threshold: usize,
encode_bytes_threshold: usize,
row_group_size: usize,
+ float_field_encoding: FloatFieldEncoding,
) -> Result> {
if parts_to_merge.is_empty() {
return Ok(None);
@@ -1382,7 +1424,11 @@ impl MemtableCompactor {
if estimated_total_rows > encode_row_threshold
|| estimated_total_bytes > encode_bytes_threshold
{
- let encoder = BulkPartEncoder::new(metadata.clone(), row_group_size)?;
+ let encoder = BulkPartEncoder::new_with_float_field_encoding(
+ metadata.clone(),
+ row_group_size,
+ float_field_encoding,
+ )?;
let mut metrics = BulkPartEncodeMetrics::default();
let encoded_part = encoder.encode_record_batch_iter(
boxed_iter,
@@ -1508,6 +1554,7 @@ pub struct BulkMemtableBuilder {
merge_mode: MergeMode,
/// Max number of rows in a parquet row group for encoded parts.
row_group_size: usize,
+ float_field_encoding: FloatFieldEncoding,
}
impl Default for BulkMemtableBuilder {
@@ -1519,6 +1566,7 @@ impl Default for BulkMemtableBuilder {
append_mode: false,
merge_mode: MergeMode::default(),
row_group_size: DEFAULT_ROW_GROUP_SIZE,
+ float_field_encoding: FloatFieldEncoding::default(),
}
}
}
@@ -1550,6 +1598,11 @@ impl BulkMemtableBuilder {
self
}
+ pub(super) fn with_float_field_encoding(mut self, encoding: FloatFieldEncoding) -> Self {
+ self.float_field_encoding = encoding;
+ self
+ }
+
/// Sets the compact dispatcher.
pub fn with_compact_dispatcher(mut self, compact_dispatcher: Arc) -> Self {
self.compact_dispatcher = Some(compact_dispatcher);
@@ -1564,7 +1617,7 @@ impl BulkMemtableBuilder {
impl MemtableBuilder for BulkMemtableBuilder {
fn build(&self, id: MemtableId, metadata: &RegionMetadataRef) -> MemtableRef {
- Arc::new(BulkMemtable::new_with_row_group_size(
+ Arc::new(BulkMemtable::new_with_row_group_size_and_encoding(
id,
self.config.clone(),
metadata.clone(),
@@ -1573,6 +1626,7 @@ impl MemtableBuilder for BulkMemtableBuilder {
self.append_mode,
self.merge_mode,
self.row_group_size,
+ self.float_field_encoding,
))
}
@@ -1597,6 +1651,7 @@ mod tests {
use datatypes::schema::ColumnSchema;
use datatypes::types::json_type::{JsonNativeType, JsonObjectType};
use mito_codec::row_converter::build_primary_key_codec;
+ use parquet::basic::{Encoding, Type as PhysicalType};
use serde_json::json;
use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder, RegionMetadataRef};
@@ -1807,7 +1862,7 @@ mod tests {
let second = mock_bulk_part_with_json2_values(
&metadata,
vec![3000, 4000],
- vec![json!({"b": 3}), json!({"b": 4})],
+ vec![json!({"a": 3, "b": 3}), json!({"a": 4, "b": 4})],
200,
)?;
let parts = vec![
@@ -1829,6 +1884,7 @@ mod tests {
usize::MAX,
usize::MAX,
DEFAULT_ROW_GROUP_SIZE,
+ FloatFieldEncoding::default(),
)?
.unwrap();
let MergedPart::Multi(part) = merged else {
@@ -1879,9 +1935,18 @@ mod tests {
unreachable!()
};
assert_eq!(
- vec![JSON2_REMAINDER_FIELD_NAME, "a", "b"],
+ vec![JSON2_REMAINDER_FIELD_NAME],
fields.iter().map(|x| x.name().as_str()).collect::>()
);
+ // Neither path is explicit in every source; keep both opaque without losing values.
+ assert_eq!(4, batch.num_rows());
+ let json = datatypes::vectors::json::array::JsonArray::from(batch.column(0));
+ let restored = json.project_to_v2(schema.field(0), &ArrowDataType::Binary)?;
+ let restored = datatypes::vectors::json::array::JsonArray::from(&restored);
+ assert_eq!(json!({"a": 1}), restored.try_get_value(0)?);
+ assert_eq!(json!({"a": 2}), restored.try_get_value(1)?);
+ assert_eq!(json!({"b": 3}), restored.try_get_value(2)?);
+ assert_eq!(json!({"b": 4}), restored.try_get_value(3)?);
Ok(())
}
@@ -2134,7 +2199,7 @@ mod tests {
#[test]
fn test_bulk_memtable_fork() {
let metadata = metadata_for_test();
- let original_memtable = BulkMemtable::new(
+ let original_memtable = BulkMemtable::new_with_row_group_size_and_encoding(
333,
BulkMemtableConfig::default(),
metadata.clone(),
@@ -2142,6 +2207,8 @@ mod tests {
None,
false,
MergeMode::LastRow,
+ DEFAULT_ROW_GROUP_SIZE,
+ FloatFieldEncoding::ByteStreamSplit,
);
let bulk_part =
@@ -2150,9 +2217,25 @@ mod tests {
original_memtable.write_bulk(bulk_part).unwrap();
- let forked_memtable = original_memtable.fork(444, &metadata);
+ let forked_memtable = original_memtable.fork_inner(444, &metadata);
assert_eq!(forked_memtable.id(), 444);
+ assert_eq!(
+ FloatFieldEncoding::ByteStreamSplit,
+ original_memtable.float_field_encoding
+ );
+ assert_eq!(
+ original_memtable.float_field_encoding,
+ forked_memtable.float_field_encoding
+ );
+ assert_eq!(
+ original_memtable.float_field_encoding,
+ forked_memtable
+ .compactor
+ .lock()
+ .unwrap()
+ .float_field_encoding
+ );
assert!(forked_memtable.is_empty());
assert_eq!(0, forked_memtable.stats().num_rows);
@@ -2771,6 +2854,42 @@ mod tests {
assert!(!BulkParts::is_merge_candidate_by_size(true, 9, 8));
}
+ #[test]
+ fn test_bulk_part_encoder_preserves_byte_stream_split() -> WhateverResult<()> {
+ let metadata = metadata_for_test();
+ let bulk_part = create_bulk_part_with_converter(
+ "byte_stream_split",
+ 0,
+ vec![1000, 2000],
+ vec![Some(10.0), Some(20.0)],
+ 100,
+ )?;
+ let encoder = BulkPartEncoder::new_with_float_field_encoding(
+ metadata,
+ DEFAULT_ROW_GROUP_SIZE,
+ FloatFieldEncoding::ByteStreamSplit,
+ )?;
+ let encoded_part = encoder.encode_part(&bulk_part)?.unwrap();
+ let field_column = encoded_part
+ .metadata()
+ .parquet_metadata
+ .row_groups()
+ .iter()
+ .flat_map(|row_group| row_group.columns())
+ .find(|column| {
+ column.column_path().string() == "v1"
+ && column.column_type() == PhysicalType::DOUBLE
+ })
+ .expect("Float64 field should be present in encoded bulk part");
+
+ assert!(
+ field_column
+ .encodings()
+ .any(|encoding| encoding == Encoding::BYTE_STREAM_SPLIT)
+ );
+ Ok(())
+ }
+
#[test]
fn test_encoded_part_batch_size_uses_largest_uncompressed_row_group() {
const NUM_ROWS: usize = 14;
diff --git a/src/mito2/src/memtable/bulk/part.rs b/src/mito2/src/memtable/bulk/part.rs
index 2a0c42b7035..f0af666a644 100644
--- a/src/mito2/src/memtable/bulk/part.rs
+++ b/src/mito2/src/memtable/bulk/part.rs
@@ -53,6 +53,7 @@ use smallvec::SmallVec;
use snafu::{OptionExt, ResultExt};
use store_api::codec::PrimaryKeyEncoding;
use store_api::metadata::{RegionMetadata, RegionMetadataRef};
+use store_api::mito_engine_options::FloatFieldEncoding;
use store_api::storage::consts::PRIMARY_KEY_COLUMN_NAME;
use store_api::storage::{ColumnId, FileId, SequenceNumber, SequenceRange};
@@ -70,7 +71,7 @@ use crate::sst::SeriesEstimator;
use crate::sst::index::IndexOutput;
use crate::sst::parquet::flat_format::primary_key_column_index;
use crate::sst::parquet::format::{PrimaryKeyArray, PrimaryKeyArrayBuilder};
-use crate::sst::parquet::{PARQUET_METADATA_KEY, SstInfo};
+use crate::sst::parquet::{PARQUET_METADATA_KEY, SstInfo, apply_float_field_encoding};
const INIT_DICT_VALUE_CAPACITY: usize = 8;
@@ -1306,22 +1307,29 @@ pub struct BulkPartEncoder {
impl BulkPartEncoder {
pub fn new(metadata: RegionMetadataRef, row_group_size: usize) -> Result {
+ Self::new_with_float_field_encoding(metadata, row_group_size, FloatFieldEncoding::default())
+ }
+
+ pub(super) fn new_with_float_field_encoding(
+ metadata: RegionMetadataRef,
+ row_group_size: usize,
+ float_field_encoding: FloatFieldEncoding,
+ ) -> Result {
// TODO(yingwen): Skip arrow schema if needed.
let json = metadata.to_json().context(InvalidMetadataSnafu)?;
let key_value_meta =
parquet::file::metadata::KeyValue::new(PARQUET_METADATA_KEY.to_string(), json);
// TODO(yingwen): Do we need compression?
- let writer_props = Some(
- WriterProperties::builder()
- .set_key_value_metadata(Some(vec![key_value_meta]))
- .set_write_batch_size(row_group_size)
- .set_max_row_group_row_count(Some(row_group_size))
- .set_compression(Compression::ZSTD(ZstdLevel::default()))
- .set_column_index_truncate_length(None)
- .set_statistics_truncate_length(None)
- .build(),
- );
+ let mut props = WriterProperties::builder()
+ .set_key_value_metadata(Some(vec![key_value_meta]))
+ .set_write_batch_size(row_group_size)
+ .set_max_row_group_row_count(Some(row_group_size))
+ .set_compression(Compression::ZSTD(ZstdLevel::default()))
+ .set_column_index_truncate_length(None)
+ .set_statistics_truncate_length(None);
+ props = apply_float_field_encoding(props, &metadata, float_field_encoding);
+ let writer_props = Some(props.build());
Ok(Self {
metadata,
diff --git a/src/mito2/src/read/flat_projection.rs b/src/mito2/src/read/flat_projection.rs
index 19dc962c56e..2abf6aa48af 100644
--- a/src/mito2/src/read/flat_projection.rs
+++ b/src/mito2/src/read/flat_projection.rs
@@ -667,6 +667,77 @@ mod tests {
);
}
+ #[test]
+ fn test_single_value_dictionary_preserves_string_type() {
+ use datatypes::arrow::array::{DictionaryArray, TimestampMillisecondArray, UInt32Array};
+ use datatypes::arrow::datatypes::UInt32Type;
+
+ for data_type in [
+ ConcreteDataType::large_string_datatype(),
+ ConcreteDataType::utf8_view_datatype(),
+ ] {
+ let mut builder = RegionMetadataBuilder::new(RegionId::new(1024, 0));
+ builder
+ .push_column_metadata(ColumnMetadata {
+ column_schema: ColumnSchema::new("tag", data_type.clone(), true),
+ semantic_type: SemanticType::Tag,
+ column_id: 0,
+ })
+ .push_column_metadata(ColumnMetadata {
+ column_schema: ColumnSchema::new(
+ "ts",
+ ConcreteDataType::timestamp_millisecond_datatype(),
+ false,
+ ),
+ semantic_type: SemanticType::Timestamp,
+ column_id: 1,
+ })
+ .primary_key(vec![0]);
+ let metadata = Arc::new(builder.build().unwrap());
+ let mapper = FlatProjectionMapper::new(&metadata, [0, 1]).unwrap();
+
+ for value in [Value::from("greptime"), Value::Null] {
+ let mut values = data_type.create_mutable_vector(1);
+ values.try_push_value_ref(&value.as_value_ref()).unwrap();
+ let dictionary = Arc::new(
+ DictionaryArray::::try_new(
+ UInt32Array::from(vec![0, 0, 0]),
+ values.to_vector().to_arrow_array(),
+ )
+ .unwrap(),
+ );
+ let arrays: Vec> = mapper
+ .batch_schema()
+ .iter()
+ .map(|(id, _)| match id {
+ 0 => dictionary.clone() as Arc,
+ 1 => Arc::new(TimestampMillisecondArray::from(vec![1, 2, 3])),
+ _ => unreachable!(),
+ })
+ .collect();
+ let fields = mapper
+ .batch_schema()
+ .iter()
+ .zip(&arrays)
+ .map(|((id, _), array)| {
+ Field::new(id.to_string(), array.data_type().clone(), true)
+ })
+ .collect::>();
+ let batch = DfRecordBatch::try_new(
+ Arc::new(datatypes::arrow::datatypes::Schema::new(fields)),
+ arrays,
+ )
+ .unwrap();
+ let output = mapper.convert(&batch, &CacheStrategy::Disabled).unwrap();
+ let vector = Helper::try_into_vector(output.column(0)).unwrap();
+ assert_eq!(data_type, vector.data_type());
+ for row in 0..3 {
+ assert_eq!(value, vector.get(row));
+ }
+ }
+ }
+ }
+
#[test]
fn test_json_type_hint_does_not_concretize_legacy_json() {
let metadata = metadata_with_legacy_json();
diff --git a/src/mito2/src/read/projection.rs b/src/mito2/src/read/projection.rs
index c6d4baee3ac..fbf6a29b73d 100644
--- a/src/mito2/src/read/projection.rs
+++ b/src/mito2/src/read/projection.rs
@@ -19,7 +19,7 @@ use std::cmp::Ordering;
use common_recordbatch::error::DataTypesSnafu;
use datatypes::prelude::{ConcreteDataType, DataType};
use datatypes::value::Value;
-use datatypes::vectors::VectorRef;
+use datatypes::vectors::{Helper, VectorRef};
use snafu::{OptionExt, ResultExt};
use store_api::metadata::RegionMetadataRef;
use store_api::storage::ColumnId;
@@ -82,12 +82,21 @@ pub(crate) fn new_repeated_vector(
value: &Value,
num_rows: usize,
) -> common_recordbatch::error::Result {
- let mut mutable_vector = data_type.create_mutable_vector(1);
- mutable_vector
- .try_push_value_ref(&value.as_value_ref())
- .context(DataTypesSnafu)?;
- let base_vector = mutable_vector.to_vector();
- Ok(base_vector.replicate(&[num_rows]))
+ if let Ok(vector) = value
+ .try_to_scalar_value(data_type)
+ .and_then(|scalar| Helper::try_from_scalar_value(scalar, num_rows, Some(data_type)))
+ {
+ return Ok(vector);
+ }
+
+ // Preserve extension types that cannot safely round-trip through ScalarValue.
+ let mut mutable_vector = data_type.create_mutable_vector(num_rows);
+ for _ in 0..num_rows {
+ mutable_vector
+ .try_push_value_ref(&value.as_value_ref())
+ .context(DataTypesSnafu)?;
+ }
+ Ok(mutable_vector.to_vector())
}
#[cfg(test)]
@@ -110,6 +119,51 @@ mod tests {
use crate::read::flat_projection::FlatProjectionMapper;
use crate::read::read_columns::ReadColumns;
+ #[test]
+ fn test_repeated_struct_null_fields_and_json() {
+ use datatypes::types::{StructField, StructType};
+ use datatypes::value::StructValue;
+
+ let inner_type = StructType::from([StructField::new(
+ "x",
+ ConcreteDataType::int32_datatype(),
+ true,
+ )]);
+ let inner = Value::Struct(StructValue::new(vec![Value::Null], inner_type));
+ let json = datatypes::json::JsonSettings::default()
+ .encode(serde_json::json!({"answer": 42}))
+ .unwrap();
+ let nested_type = StructType::from([StructField::new("nested", inner.data_type(), true)]);
+ let json_type = StructType::from([StructField::new("json", json.data_type(), true)]);
+ let values = [
+ inner.clone(),
+ Value::Struct(StructValue::new(vec![inner], nested_type)),
+ Value::Struct(StructValue::new(vec![], StructType::default())),
+ Value::Struct(StructValue::new(vec![json], json_type)),
+ ];
+ for value in values {
+ let data_type = value.data_type();
+ // JSON children are read back as their underlying struct values.
+ let expected = serde_json::Value::try_from(value.clone()).unwrap();
+ for num_rows in [0, 1, 3] {
+ let vector = new_repeated_vector(&data_type, &value, num_rows).unwrap();
+ assert_eq!(data_type, vector.data_type());
+ assert_eq!(
+ data_type.as_arrow_type(),
+ *vector.to_arrow_array().data_type()
+ );
+ assert_eq!(num_rows, vector.len());
+ assert_eq!(0, vector.null_count());
+ for row in 0..num_rows {
+ assert_eq!(
+ expected,
+ serde_json::Value::try_from(vector.get(row)).unwrap()
+ );
+ }
+ }
+ }
+ }
+
fn print_record_batch(record_batch: RecordBatch) -> String {
pretty::pretty_format_batches(&[record_batch.into_df_record_batch()])
.unwrap()
diff --git a/src/mito2/src/region/opener.rs b/src/mito2/src/region/opener.rs
index ebabbbd295e..0d758f676ce 100644
--- a/src/mito2/src/region/opener.rs
+++ b/src/mito2/src/region/opener.rs
@@ -953,6 +953,7 @@ where
OptionOutputTx::none(),
// We should respect the sequence in WAL during replay.
Some(mutation.sequence),
+ false,
);
}
@@ -974,7 +975,8 @@ where
region_write_ctx.push_bulk(
OptionOutputTx::none(),
part,
- Some(bulk_sequence_from_wal)
+ Some(bulk_sequence_from_wal),
+ false
),
RegionCorruptedSnafu {
region_id,
diff --git a/src/mito2/src/region/options.rs b/src/mito2/src/region/options.rs
index 9a8d17c954c..a2a165c12af 100644
--- a/src/mito2/src/region/options.rs
+++ b/src/mito2/src/region/options.rs
@@ -32,7 +32,9 @@ use store_api::codec::PrimaryKeyEncoding;
use store_api::metric_engine_consts::{
MEMTABLE_PARTITION_TREE_PRIMARY_KEY_ENCODING, PRIMARY_KEY_ENCODING,
};
-use store_api::mito_engine_options::{COMPACTION_OVERRIDE, MAX_ROW_GROUP_ROW_COUNT_LIMIT};
+use store_api::mito_engine_options::{
+ COMPACTION_OVERRIDE, FloatFieldEncoding, MAX_ROW_GROUP_ROW_COUNT_LIMIT,
+};
use store_api::storage::{ColumnId, RegionId};
use strum::EnumString;
@@ -128,6 +130,9 @@ pub struct RegionOptions {
/// sequence metadata.
#[serde(default, skip_serializing_if = "is_false")]
pub preserve_row_sequence: bool,
+ /// Encoding for direct floating-point field columns in Parquet SSTs.
+ #[serde(default)]
+ pub float_field_encoding: FloatFieldEncoding,
}
fn is_false(value: &bool) -> bool {
@@ -287,6 +292,8 @@ impl RegionOptions {
sst_format = Some(FormatType::Flat);
}
+ let float_field_encoding = options.float_field_encoding.unwrap_or_default();
+
let compaction_override_flag = options_map
.get(COMPACTION_OVERRIDE)
.map(|v| matches!(v.to_lowercase().as_str(), "true" | "1"))
@@ -322,6 +329,7 @@ impl RegionOptions {
primary_key_encoding,
write_buffer_size: options.write_buffer_size,
preserve_row_sequence: options.preserve_row_sequence,
+ float_field_encoding,
};
opts.validate()?;
@@ -469,6 +477,9 @@ struct RegionOptionsWithoutEnum {
max_row_group_row_count: Option,
#[serde_as(as = "DisplayFromStr")]
preserve_row_sequence: bool,
+ #[serde(rename = "experimental_sst_float_field_encoding")]
+ #[serde_as(as = "NoneAsEmptyString")]
+ float_field_encoding: Option,
}
impl Default for RegionOptionsWithoutEnum {
@@ -485,6 +496,7 @@ impl Default for RegionOptionsWithoutEnum {
sst_format: options.sst_format,
max_row_group_row_count: options.max_row_group_row_count,
preserve_row_sequence: options.preserve_row_sequence,
+ float_field_encoding: Some(options.float_field_encoding),
}
}
}
@@ -628,7 +640,9 @@ mod tests {
use common_error::ext::ErrorExt;
use common_error::status_code::StatusCode;
use common_wal::options::KafkaWalOptions;
- use store_api::mito_engine_options::{SKIP_WAL_KEY, WRITE_BUFFER_SIZE_KEY};
+ use store_api::mito_engine_options::{
+ EXPERIMENTAL_SST_FLOAT_FIELD_ENCODING, SKIP_WAL_KEY, WRITE_BUFFER_SIZE_KEY,
+ };
use super::*;
@@ -646,6 +660,26 @@ mod tests {
assert_eq!(RegionOptions::default(), options);
}
+ #[test]
+ fn test_float_field_encoding_defaults_and_parses() {
+ let options = RegionOptions::try_from_options(RegionId::new(0, 0), &make_map(&[])).unwrap();
+ assert_eq!(FloatFieldEncoding::Default, options.float_field_encoding);
+
+ let map = make_map(&[(EXPERIMENTAL_SST_FLOAT_FIELD_ENCODING, "default")]);
+ let options = RegionOptions::try_from_options(RegionId::new(0, 0), &map).unwrap();
+ assert_eq!(FloatFieldEncoding::Default, options.float_field_encoding);
+
+ let map = make_map(&[(EXPERIMENTAL_SST_FLOAT_FIELD_ENCODING, "byte_stream_split")]);
+ let options = RegionOptions::try_from_options(RegionId::new(0, 0), &map).unwrap();
+ assert_eq!(
+ FloatFieldEncoding::ByteStreamSplit,
+ options.float_field_encoding
+ );
+
+ let map = make_map(&[(EXPERIMENTAL_SST_FLOAT_FIELD_ENCODING, "invalid")]);
+ assert!(RegionOptions::try_from_options(RegionId::new(0, 0), &map).is_err());
+ }
+
#[test]
fn test_with_ttl() {
let map = make_map(&[("ttl", "7d")]);
@@ -1103,6 +1137,7 @@ mod tests {
primary_key_encoding: None,
write_buffer_size: None,
preserve_row_sequence: false,
+ float_field_encoding: FloatFieldEncoding::default(),
};
assert_eq!(expect, options);
}
@@ -1161,6 +1196,7 @@ mod tests {
primary_key_encoding: None,
write_buffer_size: Some(ReadableSize::mb(128)),
preserve_row_sequence: true,
+ float_field_encoding: FloatFieldEncoding::default(),
};
let region_options_json_str = serde_json::to_string(&options).unwrap();
assert!(region_options_json_str.contains("preserve_row_sequence"));
@@ -1172,6 +1208,7 @@ mod tests {
let got: RegionOptions = serde_json::from_str(old_region_options_json_str).unwrap();
assert_eq!(None, got.write_buffer_size);
assert!(!got.preserve_row_sequence);
+ assert_eq!(FloatFieldEncoding::Default, got.float_field_encoding);
let CompactionOptions::Twcs(twcs) = got.compaction;
assert_eq!(16, twcs.active_window_l1_merge_trigger);
assert_eq!(8, twcs.inactive_window_l1_merge_trigger);
@@ -1238,6 +1275,7 @@ mod tests {
primary_key_encoding: None,
write_buffer_size: None,
preserve_row_sequence: false,
+ float_field_encoding: FloatFieldEncoding::default(),
};
assert_eq!(options, got);
}
diff --git a/src/mito2/src/region_write_ctx.rs b/src/mito2/src/region_write_ctx.rs
index 400f7db1fc4..4bc0164cc01 100644
--- a/src/mito2/src/region_write_ctx.rs
+++ b/src/mito2/src/region_write_ctx.rs
@@ -89,12 +89,14 @@ pub(crate) struct RegionWriteCtx {
/// We keep [WalEntry] instead of mutations to avoid taking mutations
/// out of the context to construct the wal entry when we write to the wal.
wal_entry: WalEntry,
+ /// Mutations that skip WAL, paired with their write notifiers.
+ memtable_mutations: Vec<(Mutation, WriteNotify)>,
/// Wal options of the region being written to.
provider: Provider,
/// Notifiers to send write results to waiters.
///
- /// The i-th notify is for i-th mutation.
- notifiers: Vec,
+ /// The i-th notify is for the i-th mutation in `wal_entry`.
+ wal_notifiers: Vec,
/// Notifiers for bulk requests.
bulk_notifiers: Vec,
/// Pending bulk write requests
@@ -133,8 +135,9 @@ impl RegionWriteCtx {
next_sequence: committed_sequence + 1,
next_entry_id: last_entry_id + 1,
wal_entry: WalEntry::default(),
+ memtable_mutations: Vec::new(),
provider,
- notifiers: Vec::new(),
+ wal_notifiers: Vec::new(),
bulk_notifiers: vec![],
failed: false,
put_num: 0,
@@ -153,21 +156,28 @@ impl RegionWriteCtx {
write_hint: Option,
tx: OptionOutputTx,
sequence: Option,
+ skip_wal: bool,
) {
if let Some(sequence) = sequence {
self.next_sequence = sequence;
}
let num_rows = rows.as_ref().map(|rows| rows.rows.len()).unwrap_or(0);
- self.wal_entry.mutations.push(Mutation {
+ let mutation = Mutation {
op_type,
sequence: self.next_sequence,
rows,
write_hint,
- });
+ };
+ // Assign sequences before routing so concurrent memtable writes retain
+ // their logical order regardless of the WAL policy.
let notify = WriteNotify::new(tx, num_rows);
- // Notifiers are 1:1 map to mutations.
- self.notifiers.push(notify);
+ if skip_wal {
+ self.memtable_mutations.push((mutation, notify));
+ } else {
+ self.wal_entry.mutations.push(mutation);
+ self.wal_notifiers.push(notify);
+ }
// Increase sequence number.
self.next_sequence += num_rows as u64;
@@ -206,13 +216,19 @@ impl RegionWriteCtx {
/// Returns whether writes in this context should skip WAL.
pub(crate) fn skip_wal(&self) -> bool {
- self.provider == Provider::Noop || self.version.options.skip_wal
+ self.provider == Provider::Noop
+ || self.version.options.skip_wal
+ || (self.wal_entry.mutations.is_empty() && self.wal_entry.bulk_entries.is_empty())
}
/// Sets error and marks all write operations are failed.
pub(crate) fn set_error(&mut self, err: Arc) {
// Set error for all notifiers.
- for notify in &mut self.notifiers {
+ for notify in self
+ .wal_notifiers
+ .iter_mut()
+ .chain(self.memtable_mutations.iter_mut().map(|(_, notify)| notify))
+ {
notify.err = Some(err.clone());
}
for notify in &mut self.bulk_notifiers {
@@ -241,7 +257,7 @@ impl RegionWriteCtx {
/// Consumes mutations and writes them into mutable memtable.
pub(crate) async fn write_memtable(&mut self) {
- debug_assert_eq!(self.notifiers.len(), self.wal_entry.mutations.len());
+ debug_assert_eq!(self.wal_notifiers.len(), self.wal_entry.mutations.len());
if self.failed {
return;
@@ -254,34 +270,38 @@ impl RegionWriteCtx {
None
};
- let mutations = mem::take(&mut self.wal_entry.mutations)
+ let mut mutations = mem::take(&mut self.wal_entry.mutations)
.into_iter()
- .enumerate()
- .filter_map(|(i, mutation)| {
+ .zip(&mut self.wal_notifiers)
+ .chain(
+ self.memtable_mutations
+ .iter_mut()
+ // Keep notifiers in the context until all writes complete.
+ .map(|(mutation, notify)| (mem::take(mutation), notify)),
+ )
+ .filter_map(|(mutation, notify)| {
let kvs = KeyValues::new(&self.version.metadata, mutation)?;
- Some((i, kvs))
+ Some((notify, kvs))
})
.collect::>();
if mutations.len() == 1 {
if let Err(err) = mutable_memtable.write(&mutations[0].1) {
- self.notifiers[mutations[0].0].err = Some(Arc::new(err));
+ mutations[0].0.err = Some(Arc::new(err));
}
} else {
let mut tasks = FuturesUnordered::new();
- for (i, kvs) in mutations {
+ for (notify, kvs) in mutations {
let mutable = mutable_memtable.clone();
// use tokio runtime to schedule tasks.
- tasks.push(common_runtime::spawn_blocking_global(move || {
- (i, mutable.write(&kvs))
- }));
+ let task = common_runtime::spawn_blocking_global(move || mutable.write(&kvs));
+ tasks.push(async move { (notify, task.await) });
}
- while let Some(result) = tasks.next().await {
- // first unwrap the result from `spawn` above
- let (i, result) = result.unwrap();
- if let Err(err) = result {
- self.notifiers[i].err = Some(Arc::new(err));
+ while let Some((notify, result)) = tasks.next().await {
+ // First unwrap the result from `spawn` above.
+ if let Err(err) = result.unwrap() {
+ notify.err = Some(Arc::new(err));
}
}
}
@@ -298,24 +318,26 @@ impl RegionWriteCtx {
sender: OptionOutputTx,
mut bulk: BulkPart,
sequence: Option,
+ skip_wal: bool,
) -> bool {
if let Some(sequence) = sequence {
self.next_sequence = sequence;
}
bulk.sequence = self.next_sequence;
- let entry = match BulkWalEntry::try_from(&bulk) {
- Ok(entry) => entry,
- Err(e) => {
- sender.send(Err(e));
- return false;
- }
- };
+ if !skip_wal {
+ let entry = match BulkWalEntry::try_from(&bulk) {
+ Ok(entry) => entry,
+ Err(e) => {
+ sender.send(Err(e));
+ return false;
+ }
+ };
+ self.wal_entry.bulk_entries.push(entry);
+ }
self.bulk_notifiers
.push(WriteNotify::new(sender, bulk.num_rows()));
- // Add bulk wal entry
- self.wal_entry.bulk_entries.push(entry);
self.next_sequence += bulk.num_rows() as u64;
self.bulk_parts.push(bulk);
true
@@ -523,6 +545,7 @@ mod tests {
use common_recordbatch::DfRecordBatch;
use datatypes::arrow::array::{ArrayRef, TimestampMillisecondArray};
use datatypes::arrow::datatypes::{DataType, Field, Schema};
+ use prost::Message;
use store_api::logstore::provider::Provider;
use tokio::sync::oneshot;
@@ -531,8 +554,219 @@ mod tests {
use crate::memtable::bulk::part::BulkPart;
use crate::test_util::version_util::VersionControlBuilder;
+ #[test]
+ fn test_request_skip_wal_preserves_sequences_and_other_writes() {
+ // Ablate only the request flag: the workload and sequence allocation stay identical.
+ check_request_skip_wal_preserves_sequences_and_other_writes(false, false);
+ check_request_skip_wal_preserves_sequences_and_other_writes(false, true);
+ check_request_skip_wal_preserves_sequences_and_other_writes(true, false);
+ check_request_skip_wal_preserves_sequences_and_other_writes(true, true);
+ }
+
+ fn check_request_skip_wal_preserves_sequences_and_other_writes(
+ skip_wal: bool,
+ bulk_skip_wal: bool,
+ ) {
+ let builder = VersionControlBuilder::new();
+ let region_id = builder.region_id();
+ let version_control = Arc::new(builder.build());
+ let mut ctx = RegionWriteCtx::new(
+ region_id,
+ &version_control,
+ Provider::raft_engine_provider(region_id.as_u64()),
+ None,
+ );
+ for (op_type, skip, num_rows) in [
+ (OpType::Put, skip_wal, 2),
+ (OpType::Put, false, 3),
+ (OpType::Delete, false, 1),
+ ] {
+ ctx.push_mutation(
+ op_type as i32,
+ Some(Rows {
+ schema: vec![],
+ rows: vec![api::v1::Row::default(); num_rows],
+ }),
+ None,
+ OptionOutputTx::none(),
+ None,
+ skip,
+ );
+ }
+ // Unequal row counts make a notifier/mutation pairing mismatch visible.
+ for (mutation, notify) in ctx
+ .wal_entry
+ .mutations
+ .iter()
+ .zip(&ctx.wal_notifiers)
+ .chain(
+ ctx.memtable_mutations
+ .iter()
+ .map(|(mutation, notify)| (mutation, notify)),
+ )
+ {
+ assert_eq!(mutation.rows.as_ref().unwrap().rows.len(), notify.num_rows);
+ }
+ assert!(ctx.push_bulk(OptionOutputTx::none(), new_bulk_part(), None, bulk_skip_wal));
+ assert!(!ctx.skip_wal());
+ assert_eq!(ctx.next_sequence, 9);
+ assert_eq!(
+ ctx.wal_entry.bulk_entries.len(),
+ usize::from(!bulk_skip_wal)
+ );
+ assert_eq!(ctx.bulk_parts[0].sequence, 7);
+ let sequences: Vec<_> = ctx.wal_entry.mutations.iter().map(|m| m.sequence).collect();
+ assert_eq!(sequences, if skip_wal { vec![3, 6] } else { vec![1, 3, 6] });
+ // Check the actual WAL bytes after routing the mutations.
+ let encoded = crate::wal::encoder::WalEntryEncoder::new().encode_to_vec(&ctx.wal_entry);
+ let decoded = WalEntry::decode(encoded.as_slice()).unwrap();
+ assert_eq!(
+ decoded
+ .mutations
+ .iter()
+ .map(|m| m.sequence)
+ .collect::>(),
+ sequences
+ );
+ assert_eq!(decoded.bulk_entries, ctx.wal_entry.bulk_entries);
+ assert_eq!(ctx.wal_entry.mutations.len(), if skip_wal { 2 } else { 3 });
+ assert_eq!(ctx.memtable_mutations.len(), usize::from(skip_wal));
+ if skip_wal {
+ assert_eq!(ctx.memtable_mutations[0].0.sequence, 1);
+ }
+ assert_eq!(
+ ctx.wal_entry.mutations.last().unwrap().op_type,
+ OpType::Delete as i32
+ );
+ }
+
+ #[test]
+ fn test_internal_delete_respects_skip_wal_flag() {
+ check_internal_delete_respects_skip_wal_flag(false);
+ check_internal_delete_respects_skip_wal_flag(true);
+ }
+
+ fn check_internal_delete_respects_skip_wal_flag(skip_wal: bool) {
+ let builder = VersionControlBuilder::new();
+ let region_id = builder.region_id();
+ let version_control = Arc::new(builder.build());
+ let mut ctx = RegionWriteCtx::new(
+ region_id,
+ &version_control,
+ Provider::raft_engine_provider(region_id.as_u64()),
+ None,
+ );
+ ctx.push_mutation(
+ OpType::Delete as i32,
+ Some(Rows {
+ schema: vec![],
+ rows: vec![api::v1::Row::default(); 2],
+ }),
+ None,
+ OptionOutputTx::none(),
+ None,
+ skip_wal,
+ );
+ assert_eq!(ctx.skip_wal(), skip_wal);
+ assert_eq!(ctx.next_sequence, 3);
+ assert_eq!(ctx.delete_num, 2);
+ assert_eq!(ctx.wal_entry.mutations.len(), usize::from(!skip_wal));
+ assert_eq!(ctx.memtable_mutations.len(), usize::from(skip_wal));
+ let encoded = crate::wal::encoder::WalEntryEncoder::new().encode_to_vec(&ctx.wal_entry);
+ let decoded = WalEntry::decode(encoded.as_slice()).unwrap();
+ if skip_wal {
+ assert!(decoded.mutations.is_empty());
+ } else {
+ assert_eq!(decoded, ctx.wal_entry);
+ }
+ }
+
+ #[test]
+ fn test_all_request_skip_wal_keeps_entry_id_and_propagates_errors() {
+ let builder = VersionControlBuilder::new();
+ let region_id = builder.region_id();
+ let version_control = Arc::new(builder.build());
+ let mut ctx = RegionWriteCtx::new(
+ region_id,
+ &version_control,
+ Provider::raft_engine_provider(region_id.as_u64()),
+ None,
+ );
+ let (tx, rx) = oneshot::channel();
+ ctx.push_mutation(
+ OpType::Put as i32,
+ Some(Rows {
+ schema: vec![],
+ rows: vec![api::v1::Row::default(); 2],
+ }),
+ None,
+ OptionOutputTx::from(tx),
+ None,
+ true,
+ );
+ assert!(ctx.skip_wal());
+ assert!(ctx.wal_entry.mutations.is_empty());
+ assert_eq!(ctx.memtable_mutations.len(), 1);
+ assert_eq!(ctx.next_entry_id(), 1);
+ assert_eq!(ctx.next_sequence, 3);
+ ctx.set_error(Arc::new(
+ UnexpectedSnafu {
+ reason: "wal failed".to_string(),
+ }
+ .build(),
+ ));
+ drop(ctx);
+ assert!(rx.blocking_recv().unwrap().is_err());
+ }
+
+ #[test]
+ fn test_bulk_skip_wal_preserves_sequences() {
+ check_bulk_skip_wal_preserves_sequences(false);
+ check_bulk_skip_wal_preserves_sequences(true);
+ }
+
+ fn check_bulk_skip_wal_preserves_sequences(skip_wal: bool) {
+ let builder = VersionControlBuilder::new();
+ let region_id = builder.region_id();
+ let version_control = Arc::new(builder.build());
+ let mut ctx = RegionWriteCtx::new(
+ region_id,
+ &version_control,
+ Provider::raft_engine_provider(region_id.as_u64()),
+ None,
+ );
+ // Alternate policies in one context, retaining all parts for the memtable.
+ for skip in [skip_wal, false, skip_wal] {
+ assert!(ctx.push_bulk(OptionOutputTx::none(), new_bulk_part(), None, skip));
+ }
+ assert_eq!(ctx.next_sequence, 7);
+ assert_eq!(ctx.bulk_notifiers.len(), 3);
+ assert_eq!(
+ ctx.bulk_parts
+ .iter()
+ .map(|p| p.sequence)
+ .collect::>(),
+ vec![1, 3, 5]
+ );
+ let encoded = crate::wal::encoder::WalEntryEncoder::new().encode_to_vec(&ctx.wal_entry);
+ let decoded = WalEntry::decode(encoded.as_slice()).unwrap();
+ assert_eq!(
+ decoded
+ .bulk_entries
+ .iter()
+ .map(|e| e.sequence)
+ .collect::>(),
+ if skip_wal { vec![3] } else { vec![1, 3, 5] }
+ );
+ }
+
#[test]
fn test_set_error_marks_bulk_notifiers_failed() {
+ check_set_error_marks_bulk_notifiers_failed(false);
+ check_set_error_marks_bulk_notifiers_failed(true);
+ }
+
+ fn check_set_error_marks_bulk_notifiers_failed(skip_wal: bool) {
let builder = VersionControlBuilder::new();
let region_id = builder.region_id();
let version_control = Arc::new(builder.build());
@@ -540,7 +774,9 @@ mod tests {
RegionWriteCtx::new(region_id, &version_control, Provider::noop_provider(), None);
let (tx, rx) = oneshot::channel();
- assert!(ctx.push_bulk(OptionOutputTx::from(tx), new_bulk_part(), None));
+ assert!(ctx.push_bulk(OptionOutputTx::from(tx), new_bulk_part(), None, skip_wal));
+ assert_eq!(ctx.wal_entry.bulk_entries.len(), usize::from(!skip_wal));
+ assert_eq!(ctx.bulk_parts.len(), 1);
ctx.set_error(Arc::new(
UnexpectedSnafu {
reason: "wal failed".to_string(),
diff --git a/src/mito2/src/request.rs b/src/mito2/src/request.rs
index 94d442e261c..16b7eb4a6be 100644
--- a/src/mito2/src/request.rs
+++ b/src/mito2/src/request.rs
@@ -76,6 +76,8 @@ pub struct WriteRequest {
pub name_to_index: HashMap,
/// Whether each column has null.
pub has_null: Vec,
+ /// Whether this insert should skip WAL. Never applies to deletes.
+ pub skip_wal: bool,
/// Write hint.
pub hint: Option,
/// Region metadata on the time of this request is created.
@@ -137,11 +139,18 @@ impl WriteRequest {
name_to_index,
has_null,
hint: None,
+ skip_wal: false,
region_metadata,
partition_expr_version: None,
})
}
+ /// Sets the request-level WAL policy.
+ pub fn with_skip_wal(mut self, skip_wal: bool) -> Self {
+ self.skip_wal = skip_wal;
+ self
+ }
+
/// Sets the write hint.
pub fn with_hint(mut self, hint: Option) -> Self {
self.hint = hint;
@@ -551,6 +560,7 @@ pub(crate) struct SenderWriteRequest {
}
pub(crate) struct SenderBulkRequest {
+ pub(crate) skip_wal: bool,
pub(crate) sender: OptionOutputTx,
pub(crate) region_id: RegionId,
pub(crate) request: BulkPart,
@@ -672,6 +682,7 @@ impl WorkerRequest {
let mut write_request =
WriteRequest::new(region_id, OpType::Put, v.rows, region_metadata.clone())?
.with_hint(v.hint)
+ .with_skip_wal(v.skip_wal)
.with_partition_expr_version(v.partition_expr_version);
if write_request.primary_key_encoding() == PrimaryKeyEncoding::Dense
&& let Some(region_metadata) = ®ion_metadata
@@ -1873,6 +1884,25 @@ mod tests {
);
}
+ #[test]
+ fn test_delete_request_defaults_to_writing_wal() {
+ let (request, _receiver) = WorkerRequest::try_from_region_request(
+ RegionId::new(1, 1),
+ RegionRequest::Delete(store_api::region_request::RegionDeleteRequest {
+ rows: Rows::default(),
+ hint: None,
+ partition_expr_version: None,
+ }),
+ None,
+ )
+ .unwrap();
+ let WorkerRequest::Write(request) = request else {
+ panic!("expected a write request");
+ };
+ assert_eq!(request.request.op_type, OpType::Delete);
+ assert!(!request.request.skip_wal);
+ }
+
#[test]
fn test_write_request_metadata() {
let rows = Rows {
diff --git a/src/mito2/src/series_index.rs b/src/mito2/src/series_index.rs
index f08178456a5..8173050f2ab 100644
--- a/src/mito2/src/series_index.rs
+++ b/src/mito2/src/series_index.rs
@@ -17,10 +17,18 @@
// These components are consumed by the upcoming query and maintenance integration.
#[allow(dead_code)]
mod catalog;
+// Consumed by the follow-up background-maintenance integration.
+#[allow(dead_code)]
+mod bucket;
+// Consumed by the follow-up background-maintenance integration.
+#[allow(dead_code)]
+mod builder;
#[allow(dead_code)]
mod purger;
mod searcher;
mod task;
+#[cfg(test)]
+mod tests;
#[allow(dead_code)]
mod version;
mod writer;
diff --git a/src/mito2/src/series_index/bucket.rs b/src/mito2/src/series_index/bucket.rs
new file mode 100644
index 00000000000..7fecb253441
--- /dev/null
+++ b/src/mito2/src/series_index/bucket.rs
@@ -0,0 +1,877 @@
+// Copyright 2023 Greptime Team
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Event-time bucket planning and source coverage.
+
+use std::collections::BTreeMap;
+use std::time::Duration;
+
+use common_time::{TimeToLive, Timestamp};
+use smallvec::{SmallVec, smallvec};
+use store_api::storage::FileId;
+
+use crate::series_index::catalog::{SeriesIndexEntry, WindowSequence};
+use crate::sst::file::FileHandle;
+
+const SERIES_INDEX_TRIGGER_FILES: usize = 4;
+
+/// Index files sharing a non-overlapping, half-open time bucket.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) struct IndexBucket {
+ pub(crate) start: Timestamp,
+ pub(crate) end: Timestamp,
+ pub(crate) index_ids: SmallVec<[FileId; 2]>,
+ /// Zero means that merged indexes use incompatible window widths.
+ pub(crate) compaction_window_secs: i64,
+ /// Indexed SST summaries keyed by aligned start.
+ /// See [`SeriesIndexEntry::window_sequences`] for the layout and sequence assumption.
+ pub(crate) window_sequences: BTreeMap,
+}
+
+impl IndexBucket {
+ pub(crate) fn from_entry(entry: &SeriesIndexEntry) -> Self {
+ Self {
+ start: entry.bucket_start,
+ end: entry.bucket_end,
+ index_ids: smallvec![entry.index_uuid],
+ compaction_window_secs: entry.compaction_window_secs,
+ window_sequences: entry.window_sequences.clone(),
+ }
+ }
+
+ fn merge(&mut self, mut other: Self) {
+ self.start = self.start.min(other.start);
+ self.end = self.end.max(other.end);
+ if self.index_ids.is_empty() {
+ self.compaction_window_secs = other.compaction_window_secs;
+ self.window_sequences = std::mem::take(&mut other.window_sequences);
+ } else if !other.index_ids.is_empty() {
+ if self.compaction_window_secs == other.compaction_window_secs {
+ merge_window_sequences(&mut self.window_sequences, other.window_sequences);
+ } else {
+ // Do not compare maps with different window boundaries, including
+ // a previous incompatible merge, against current SST coverage.
+ self.compaction_window_secs = 0;
+ self.window_sequences.clear();
+ }
+ }
+ self.index_ids.append(&mut other.index_ids);
+ }
+
+ /// Inserts a bucket, consuming overlapping entries and expanding their interval.
+ /// Each map key equals the stored bucket start; adjacent intervals remain separate.
+ pub(crate) fn insert_into(mut self, buckets: &mut BTreeMap) {
+ if let Some((&previous_start, previous)) = buckets.range(..self.start).next_back()
+ && previous.end > self.start
+ {
+ self.start = previous_start;
+ }
+ // Expanding the end may expose more overlaps, so look up the next entry again.
+ while let Some((&next_start, _)) = buckets.range(self.start..self.end).next() {
+ if let Some(other) = buckets.remove(&next_start) {
+ self.merge(other);
+ }
+ }
+ buckets.insert(self.start, self);
+ }
+}
+
+/// SSTs grouped into a half-open time interval for an aggregate series-index build.
+///
+/// Reconciliation may expand the interval through existing index coverage. A changed
+/// bucket is rebuilt from all its SSTs. Unknown source sequences prevent builds,
+/// since their coverage cannot be determined safely.
+#[derive(Debug, Clone)]
+pub(crate) struct SeriesBucket {
+ pub(crate) start: Timestamp,
+ pub(crate) end: Timestamp,
+ pub(crate) files: SmallVec<[FileHandle; 2]>,
+ pub(crate) has_unknown_sequence: bool,
+ /// Maximum known source sequence, or zero when all sequences are unknown.
+ pub(crate) max_file_sequence: u64,
+ pub(crate) compaction_window_secs: i64,
+ /// SST summaries keyed by compaction-window-aligned start.
+ /// Each SST contributes one entry; equal starts merge by maximum end and sequence.
+ /// Ranges may overlap. See [`WindowSequence`] for the sequence assumption.
+ /// Missing SST sequences use zero as a placeholder and set `has_unknown_sequence`,
+ /// preventing index builds and reuse.
+ pub(crate) window_sequences: BTreeMap,
+}
+
+/// Next bucket coverage and the work required to publish it, computed without I/O.
+pub(crate) struct SeriesIndexPlan {
+ pub(crate) index_buckets: BTreeMap,
+ pub(crate) builds: Vec<(SeriesBucket, SeriesIndexEntry)>,
+ pub(crate) expired_index_ids: Vec,
+ /// Retire only after replacement builds and catalog publication succeed.
+ pub(crate) superseded_index_ids: Vec,
+ pub(crate) computed_buckets: usize,
+ pub(crate) skipped_buckets: usize,
+}
+
+/// Plans whole-bucket replacements when source SST summaries change. The returned bucket
+/// map is publishable only after every planned build and the catalog writes succeed.
+pub(crate) fn plan_series_indexes(
+ buckets: Vec,
+ mut index_buckets: BTreeMap,
+ ttl: Option,
+ now_ms: i64,
+) -> SeriesIndexPlan {
+ // Reconciliation changes geometry, not established coverage. In particular,
+ // a deferred bridge must not change the indexed snapshot.
+ let buckets = reconcile_series_buckets(buckets, &index_buckets);
+ let computed_buckets = buckets.len();
+ let expired = |end| {
+ ttl.is_some_and(|ttl| {
+ ttl.is_expired(&end, &Timestamp::new_millisecond(now_ms))
+ .unwrap_or(false)
+ })
+ };
+ let mut expired_index_ids = Vec::new();
+ index_buckets.retain(|_, bucket| {
+ if expired(bucket.end) {
+ expired_index_ids.extend_from_slice(&bucket.index_ids);
+ false
+ } else {
+ true
+ }
+ });
+ let mut builds = Vec::new();
+ let mut superseded_index_ids = Vec::new();
+ for bucket in buckets {
+ if expired(bucket.end) {
+ continue;
+ }
+ if bucket.has_unknown_sequence {
+ continue;
+ }
+ if index_buckets.get(&bucket.start).is_some_and(|indexed| {
+ indexed.end == bucket.end
+ && indexed.compaction_window_secs == bucket.compaction_window_secs
+ && indexed.window_sequences == bucket.window_sequences
+ }) {
+ continue;
+ }
+ if let Some(entry) = bucket.to_series_entry() {
+ index_buckets.retain(|_, indexed| {
+ if indexed.start < bucket.end && bucket.start < indexed.end {
+ superseded_index_ids.extend_from_slice(&indexed.index_ids);
+ false
+ } else {
+ true
+ }
+ });
+ IndexBucket::from_entry(&entry).insert_into(&mut index_buckets);
+ builds.push((bucket, entry));
+ }
+ }
+ index_buckets.retain(|_, bucket| !bucket.index_ids.is_empty());
+ SeriesIndexPlan {
+ index_buckets,
+ skipped_buckets: computed_buckets - builds.len(),
+ builds,
+ expired_index_ids,
+ superseded_index_ids,
+ computed_buckets,
+ }
+}
+
+pub(crate) fn rounded_bucket_width(
+ requested: Duration,
+ compaction_window: Duration,
+) -> Option {
+ let window_secs = i64::try_from(compaction_window.as_secs()).ok()?.max(1);
+ let requested_secs = i64::try_from(requested.as_secs())
+ .unwrap_or(i64::MAX)
+ .max(1);
+ let multiples = requested_secs / window_secs + i64::from(requested_secs % window_secs != 0);
+ multiples.checked_mul(window_secs)
+}
+
+/// Groups SSTs across levels into sorted, disjoint time buckets without planning builds.
+///
+/// Both widths must be positive, and `width_secs` must be a multiple of the compaction
+/// window width. Inclusive SST ranges are rounded outward to aligned,
+/// half-open intervals in seconds. Overlapping intervals merge; adjacent ones stay
+/// separate. Each merged bucket tracks its maximum sequence and any unknown sequence.
+pub(crate) fn group_files_into_series_buckets(
+ files: &[FileHandle],
+ width_secs: i64,
+ compaction_window_secs: i64,
+) -> Vec {
+ let mut spans = files
+ .iter()
+ .map(|file| {
+ let start = file.time_range().0.split().0;
+ let end = file.time_range().1.split().0;
+ let sequence = file
+ .meta_ref()
+ .sequence
+ .map_or(0, |sequence| sequence.get());
+ let first_window = start.div_euclid(compaction_window_secs);
+ let last_window = end.div_euclid(compaction_window_secs);
+ let window_sequences = BTreeMap::from([(
+ first_window.saturating_mul(compaction_window_secs),
+ WindowSequence {
+ start: first_window.saturating_mul(compaction_window_secs),
+ end: last_window
+ .saturating_add(1)
+ .saturating_mul(compaction_window_secs),
+ max_sequence: sequence,
+ },
+ )]);
+ SeriesBucket {
+ start: Timestamp::new_second(
+ start.div_euclid(width_secs).saturating_mul(width_secs),
+ ),
+ end: Timestamp::new_second(
+ end.div_euclid(width_secs)
+ .saturating_add(1)
+ .saturating_mul(width_secs),
+ ),
+ files: smallvec![file.clone()],
+ has_unknown_sequence: file.meta_ref().sequence.is_none(),
+ max_file_sequence: sequence,
+ compaction_window_secs,
+ window_sequences,
+ }
+ })
+ .collect::>();
+ spans.sort_unstable_by(|a, b| a.start.cmp(&b.start).then_with(|| b.end.cmp(&a.end)));
+ group_series_buckets(spans)
+}
+
+/// Expands SST buckets through existing index coverage before grouping build inputs.
+///
+/// `buckets` must be sorted by start and disjoint. `index_buckets` must contain
+/// disjoint intervals keyed by their starts. Expansion preserves start ordering,
+/// but may introduce overlaps, which are merged in the returned disjoint buckets.
+/// Established index coverage is borrowed so deferred builds cannot modify it.
+fn reconcile_series_buckets(
+ mut buckets: Vec,
+ index_buckets: &BTreeMap,
+) -> Vec {
+ for bucket in &mut buckets {
+ if let Some((&start, previous)) = index_buckets.range(..=bucket.start).next_back()
+ && previous.end > bucket.start
+ {
+ bucket.start = start;
+ bucket.end = bucket.end.max(previous.end);
+ }
+ // Disjoint index intervals make the last overlapping interval's end the
+ // furthest boundary. Expanding to it cannot expose another index interval.
+ if let Some((_, last)) = index_buckets.range(bucket.start..bucket.end).next_back() {
+ bucket.end = bucket.end.max(last.end);
+ }
+ }
+ group_series_buckets(buckets)
+}
+
+/// Merges overlapping spans sorted by nondecreasing start; equal starts are allowed.
+/// Returns sorted, disjoint buckets. Adjacent half-open intervals remain separate.
+/// All spans must use the same compaction-window width for their SST summaries.
+fn group_series_buckets(spans: Vec) -> Vec {
+ let mut buckets: Vec = Vec::new();
+ for mut span in spans {
+ if let Some(last) = buckets.last_mut()
+ && span.start < last.end
+ {
+ last.end = last.end.max(span.end);
+ last.files.append(&mut span.files);
+ last.has_unknown_sequence |= span.has_unknown_sequence;
+ last.max_file_sequence = last.max_file_sequence.max(span.max_file_sequence);
+ merge_window_sequences(&mut last.window_sequences, span.window_sequences);
+ } else {
+ buckets.push(span);
+ }
+ }
+ buckets
+}
+
+/// Merges summaries sharing a start, relying on [`WindowSequence`]'s sequence assumption.
+fn merge_window_sequences(
+ target: &mut BTreeMap,
+ source: BTreeMap,
+) {
+ for (start, summary) in source {
+ target
+ .entry(start)
+ .and_modify(|current| {
+ current.end = current.end.max(summary.end);
+ current.max_sequence = current.max_sequence.max(summary.max_sequence);
+ })
+ .or_insert(summary);
+ }
+}
+
+impl SeriesBucket {
+ /// Creates entry metadata with a fresh UUID and sorted source file IDs.
+ /// Returns `None` for unknown sequences or too few files.
+ fn to_series_entry(&self) -> Option