mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-11-13 22:36:57 +00:00
fix(mito2): sync main and verify two-phase data cache replay
Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) —
|
||||
|
||||
Generated
+148
-73
@@ -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]]
|
||||
|
||||
+23
-16
@@ -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
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -13,95 +13,65 @@ One engine, on your infrastructure.</h2>
|
||||
> Apache-2.0 licensed core.
|
||||
|
||||
<div align="center">
|
||||
<h3 align="center">
|
||||
<a href="https://docs.greptime.com/user-guide/overview/">User Guide</a> |
|
||||
<a href="https://greptimedb.rs/">API Docs</a> |
|
||||
<a href="https://github.com/GreptimeTeam/greptimedb/issues/7685">Roadmap 2026</a>
|
||||
</h3>
|
||||
|
||||
<a href="https://github.com/GreptimeTeam/greptimedb/releases/latest">
|
||||
<img src="https://img.shields.io/github/v/release/GreptimeTeam/greptimedb?filter=!*-*&label=stable&color=brightgreen" alt="Stable"/>
|
||||
</a>
|
||||
<a href="https://github.com/GreptimeTeam/greptimedb/releases">
|
||||
<img src="https://img.shields.io/github/v/release/GreptimeTeam/greptimedb?include_prereleases&filter=!*-*-*&label=canary&color=blueviolet" alt="Canary"/>
|
||||
</a>
|
||||
<a href="https://github.com/GreptimeTeam/greptimedb/releases">
|
||||
<img src="https://img.shields.io/github/v/release/GreptimeTeam/greptimedb?include_prereleases&filter=*-nightly-*&label=nightly&color=orange" alt="Nightly"/>
|
||||
</a>
|
||||
<a href="https://docs.greptime.com/user-guide/overview/">User Guide</a> ·
|
||||
<a href="https://greptimedb.rs/">API Docs</a> ·
|
||||
<a href="https://github.com/GreptimeTeam/greptimedb/issues/7685">Roadmap 2026</a> ·
|
||||
<a href="https://greptime.com/slack">Slack</a>
|
||||
|
||||
<a href="https://github.com/GreptimeTeam/greptimedb/releases/latest"><img src="https://img.shields.io/github/v/release/GreptimeTeam/greptimedb?filter=!*-*&sort=semver&label=stable&color=brightgreen" alt="Stable"/></a>
|
||||
<a href="https://github.com/GreptimeTeam/greptimedb/releases"><img src="https://img.shields.io/github/v/release/GreptimeTeam/greptimedb?include_prereleases&filter=!*-*-*&sort=semver&label=canary&color=blueviolet" alt="Canary"/></a>
|
||||
<a href="https://github.com/GreptimeTeam/greptimedb/releases"><img src="https://img.shields.io/github/v/release/GreptimeTeam/greptimedb?include_prereleases&filter=*-nightly-*&label=nightly&color=orange" alt="Nightly"/></a>
|
||||
<a href="https://hub.docker.com/r/greptime/greptimedb/"><img src="https://img.shields.io/docker/pulls/greptime/greptimedb.svg" alt="Docker Pulls"/></a>
|
||||
<a href="https://github.com/GreptimeTeam/greptimedb/blob/main/LICENSE"><img src="https://img.shields.io/github/license/GreptimeTeam/greptimedb" alt="License"/></a>
|
||||
|
||||
<sub><b>stable</b> for production · <b>canary</b> includes pre-releases · <b>nightly</b> is a weekly snapshot of <code>main</code></sub>
|
||||
|
||||
<a href="https://hub.docker.com/r/greptime/greptimedb/">
|
||||
<img src="https://img.shields.io/docker/pulls/greptime/greptimedb.svg" alt="Docker Pulls"/>
|
||||
</a>
|
||||
<a href="https://github.com/GreptimeTeam/greptimedb/actions/workflows/integration.yml">
|
||||
<img src="https://github.com/GreptimeTeam/greptimedb/actions/workflows/integration.yml/badge.svg" alt="GitHub Actions"/>
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/GreptimeTeam/greptimedb">
|
||||
<img src="https://codecov.io/gh/GreptimeTeam/greptimedb/branch/main/graph/badge.svg?token=FITFDI3J3C" alt="Codecov"/>
|
||||
</a>
|
||||
<a href="https://github.com/GreptimeTeam/greptimedb/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/GreptimeTeam/greptimedb" alt="License"/>
|
||||
</a>
|
||||
|
||||
<br/>
|
||||
|
||||
<a href="https://greptime.com/slack">
|
||||
<img src="https://img.shields.io/badge/slack-GreptimeDB-0abd59?logo=slack&style=for-the-badge" alt="Slack"/>
|
||||
</a>
|
||||
<a href="https://twitter.com/greptime">
|
||||
<img src="https://img.shields.io/badge/twitter-follow_us-1d9bf0.svg?style=for-the-badge" alt="Twitter"/>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/greptime/">
|
||||
<img src="https://img.shields.io/badge/linkedin-connect_with_us-0a66c2.svg?style=for-the-badge" alt="LinkedIn"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
- [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.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/GreptimeTeam/greptimedb/raw/main/docs/overview.png" target="_blank" rel="noopener">
|
||||
<img alt="GreptimeDB Overview" src="docs/overview.png" width="900px">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 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.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/GreptimeTeam/greptimedb/raw/main/docs/overview.png" target="_blank" rel="noopener">
|
||||
<img alt="GreptimeDB Overview" src="docs/overview.png" width="900px">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 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.
|
||||
|
||||
<img alt="Known Users" src="https://greptime.com/logo/img/users.png"/>
|
||||
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.
|
||||
|
||||
<img alt="Known Users" src="https://greptime.com/logo/img/users.png"/>
|
||||
|
||||
## 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).
|
||||
|
||||
+2
-2
@@ -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`,<br/>`alter_database`, `drop_database`, `create_flow`, `drop_flow`,<br/>`create_table`, `create_logical_tables`, `alter_table`, `alter_logical_tables`,<br/>`drop_table`, `undrop_table`, `purge_dropped_table`, `truncate_table`,<br/>`create_view`, `drop_view`, `admin_function`, `reconcile_table`,<br/>`reconcile_logical_tables`.<br/>When omitted, all current and future event types are recorded.<br/>Set to an empty array to disable event recording. |
|
||||
| `event_recorder.event_types` | Array | -- | Event types to record. Current available event types: `create_database`,<br/>`alter_database`, `drop_database`, `create_flow`, `drop_flow`,<br/>`create_table`, `create_logical_tables`, `alter_table`, `alter_logical_tables`,<br/>`drop_table`, `undrop_table`, `purge_dropped_table`, `truncate_table`,<br/>`create_view`, `drop_view`, `admin_function`, `reconcile_catalog`,<br/>`reconcile_database`, `reconcile_table`, `reconcile_logical_tables`.<br/>When omitted, all current and future event types are recorded.<br/>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.<br/>When enabled, heap profiling will be activated if the `MALLOC_CONF` environment variable<br/>is set to "prof:true,prof_active:false". The official image adds this env variable.<br/>Default is true. |
|
||||
|
||||
@@ -477,7 +477,7 @@
|
||||
| `wal.create_topic_timeout` | String | `30s` | The timeout for creating a Kafka topic.<br/>**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`,<br/>`create_database`, `alter_database`, `drop_database`, `create_flow`,<br/>`drop_flow`, `create_table`, `create_logical_tables`, `alter_table`,<br/>`alter_logical_tables`, `drop_table`, `undrop_table`, `purge_dropped_table`,<br/>`truncate_table`, `create_view`, `drop_view`, `repartition`,<br/>`repartition_group`, `wal_prune`, `batch_gc`, `reconcile_table`,<br/>`reconcile_logical_tables`.<br/>When omitted, all current and future event types are recorded.<br/>Set to an empty array to disable event recording. |
|
||||
| `event_recorder.event_types` | Array | -- | Event types to record. Current available event types: `region_migration`,<br/>`create_database`, `alter_database`, `drop_database`, `create_flow`,<br/>`drop_flow`, `create_table`, `create_logical_tables`, `alter_table`,<br/>`alter_logical_tables`, `drop_table`, `undrop_table`, `purge_dropped_table`,<br/>`truncate_table`, `create_view`, `drop_view`, `repartition`,<br/>`repartition_group`, `wal_prune`, `batch_gc`, `reconcile_catalog`,<br/>`reconcile_database`, `reconcile_table`, `reconcile_logical_tables`.<br/>When omitted, all current and future event types are recorded.<br/>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.<br/>Set to `0s` to disable stats persistence.<br/>Default is `0s`.<br/>If you want to enable stats persistence, set the TTL to a value greater than 0.<br/>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`.<br/>The minimum value is `10m`, if the value is less than `10m`, it will be overridden to `10m`. |
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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<RecordBatch> {
|
||||
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<VectorRef> = vec![
|
||||
Arc::new(self.catalog_names.finish()),
|
||||
|
||||
@@ -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<RecordBatch> {
|
||||
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<VectorRef> = vec![
|
||||
Arc::new(self.constraint_catalog.finish()),
|
||||
Arc::new(self.constraint_schema.finish()),
|
||||
|
||||
@@ -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<RecordBatch> {
|
||||
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<VectorRef> = vec![
|
||||
Arc::new(self.catalog_names.finish()),
|
||||
|
||||
@@ -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<RecordBatch> {
|
||||
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<VectorRef> = vec![
|
||||
constraint_catalogs,
|
||||
|
||||
@@ -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<DoPutResponseStream> {
|
||||
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<DoPutResponseStream> {
|
||||
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 })),
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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<String>,
|
||||
#[serde(default)]
|
||||
pub(super) candidate_setup_sql: Vec<String>,
|
||||
#[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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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::<Vec<_>>();
|
||||
|
||||
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::<Vec<_>>();
|
||||
monolithic.value_seed = 43;
|
||||
assert_ne!(
|
||||
original,
|
||||
(0..600)
|
||||
.map(|sample_idx| deterministic_prom_value(&monolithic, 19, sample_idx))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
@@ -116,6 +116,10 @@ pub(super) struct RemoteWrite {
|
||||
pub(super) sample_chunk_size: Option<u64>,
|
||||
pub(super) flush_every_sample_chunks: u64,
|
||||
pub(super) visibility_timeout_seconds: u64,
|
||||
#[serde(default)]
|
||||
pub(super) base_setup_sql: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub(super) candidate_setup_sql: Vec<String>,
|
||||
pub(super) prom_store: PromStore,
|
||||
pub(super) value: RemoteValue,
|
||||
pub(super) storage: Option<StorageConfig>,
|
||||
|
||||
@@ -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<Value> {
|
||||
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<Vec<Value>> {
|
||||
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<u8> {
|
||||
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::<usize>()
|
||||
.unwrap();
|
||||
if request.len() >= header_end + 4 + content_length {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn spawn_sql_server(
|
||||
responses: Vec<String>,
|
||||
) -> (u16, tokio::task::JoinHandle<Vec<String>>) {
|
||||
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,
|
||||
|
||||
@@ -306,13 +306,25 @@ fn run_bench_command(command: Vec<String>, mut run: Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn parse_average_duration(stdout: &str) -> Option<f64> {
|
||||
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::<f64>().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<f64> {
|
||||
@@ -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(),
|
||||
|
||||
@@ -50,6 +50,22 @@ pub const AZBLOB_SCHEMA: &str = "AZBLOB";
|
||||
pub struct BuiltBackend {
|
||||
pub object_store: ObjectStore,
|
||||
pub object_path: Option<String>,
|
||||
local_root: Option<SecureFsRoot>,
|
||||
}
|
||||
|
||||
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<bool> {
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ArrayRef> = 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!(
|
||||
|
||||
@@ -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<ArrayRef> = 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();
|
||||
|
||||
@@ -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<ArrayRef> = 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!(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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: Scalar, R: Scalar, O: Scalar, F>(
|
||||
l: &VectorRef,
|
||||
r: &VectorRef,
|
||||
f: F,
|
||||
ctx: &mut EvalContext,
|
||||
) -> Result<<O as Scalar>::VectorType>
|
||||
where
|
||||
F: Fn(Option<L::RefType<'_>>, Option<R::RefType<'_>>, &mut EvalContext) -> Option<O>,
|
||||
{
|
||||
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: &<L as Scalar>::VectorType = unsafe { Helper::static_cast(l) };
|
||||
let right: &ConstantVector = unsafe { Helper::static_cast(r) };
|
||||
let right: &<R as Scalar>::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));
|
||||
<O as Scalar>::VectorType::from_owned_iterator(it)
|
||||
}
|
||||
|
||||
(false, false) => {
|
||||
let left: &<L as Scalar>::VectorType = unsafe { Helper::static_cast(l) };
|
||||
let right: &<R as Scalar>::VectorType = unsafe { Helper::static_cast(r) };
|
||||
|
||||
let it = left
|
||||
.iter_data()
|
||||
.zip(right.iter_data())
|
||||
.map(|(a, b)| f(a, b, ctx));
|
||||
<O as Scalar>::VectorType::from_owned_iterator(it)
|
||||
}
|
||||
|
||||
(true, false) => {
|
||||
let left: &ConstantVector = unsafe { Helper::static_cast(l) };
|
||||
let left: &<L as Scalar>::VectorType = unsafe { Helper::static_cast(left.inner()) };
|
||||
let a = left.get_data(0);
|
||||
|
||||
let right: &<R as Scalar>::VectorType = unsafe { Helper::static_cast(r) };
|
||||
let it = right.iter_data().map(|b| f(a.clone(), b, ctx));
|
||||
<O as Scalar>::VectorType::from_owned_iterator(it)
|
||||
}
|
||||
|
||||
(true, true) => {
|
||||
let left: &ConstantVector = unsafe { Helper::static_cast(l) };
|
||||
let left: &<L as Scalar>::VectorType = unsafe { Helper::static_cast(left.inner()) };
|
||||
let a = left.get_data(0);
|
||||
|
||||
let right: &ConstantVector = unsafe { Helper::static_cast(r) };
|
||||
let right: &<R as Scalar>::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());
|
||||
<O as Scalar>::VectorType::from_owned_iterator(it)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(error) = ctx.error.take() {
|
||||
return Err(error);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
@@ -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: Scalar, R: Scalar, O: Scalar, F>(
|
||||
l: &VectorRef,
|
||||
r: &VectorRef,
|
||||
f: F,
|
||||
ctx: &mut EvalContext,
|
||||
) -> Result<<O as Scalar>::VectorType>
|
||||
where
|
||||
F: Fn(Option<L::RefType<'_>>, Option<R::RefType<'_>>, &mut EvalContext) -> Option<O>,
|
||||
{
|
||||
debug_assert!(
|
||||
l.len() == r.len(),
|
||||
"Size of vectors must match to apply binary expression"
|
||||
);
|
||||
|
||||
let left: &<L as Scalar>::VectorType = unsafe { Helper::static_cast(l) };
|
||||
let right: &<R as Scalar>::VectorType = unsafe { Helper::static_cast(r) };
|
||||
let result = <O as Scalar>::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 {
|
||||
|
||||
@@ -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<TableMutationHandlerRef>,
|
||||
// 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)),
|
||||
|
||||
@@ -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<Item = &'a str>,
|
||||
keys: impl Iterator<Item = &'a str>,
|
||||
) -> Result<Option<AnnotationFamily>> {
|
||||
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]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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<ColumnarValue, DataFusionError>`. 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)
|
||||
|
||||
@@ -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<Ident> = None;
|
||||
let mut display_name: Option<Ident> = None;
|
||||
let mut ret: Option<Ident> = None;
|
||||
let mut evaluator: Option<Path> = 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<Type>,
|
||||
fn_name: Ident,
|
||||
ret_type: Ident,
|
||||
evaluator: Option<Path>,
|
||||
) -> TokenStream {
|
||||
if let Some(evaluator) = evaluator {
|
||||
return quote! {
|
||||
impl #name {
|
||||
fn calc(input: &[ColumnarValue]) -> Result<ColumnarValue, DataFusionError> {
|
||||
#evaluator(input, Self::name())
|
||||
}
|
||||
}
|
||||
}
|
||||
.into();
|
||||
}
|
||||
|
||||
let param_names = param_types
|
||||
.iter()
|
||||
.enumerate()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 { .. }
|
||||
|
||||
@@ -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<Mutex<Vec<String>>>);
|
||||
|
||||
impl<S> tracing_subscriber::Layer<S> 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<String>,
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<RegionId>,
|
||||
/// 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<RegionId, HashSet<RegionId>>,
|
||||
}
|
||||
|
||||
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<RegionId>,
|
||||
/// 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<RegionId, PackedRegionFileRefs>,
|
||||
#[serde(default)]
|
||||
pub manifest_version: HashMap<RegionId, u64>,
|
||||
#[serde(default)]
|
||||
pub cross_region_refs: HashMap<RegionId, HashSet<RegionId>>,
|
||||
}
|
||||
|
||||
/// 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<FileRefsManifest> {
|
||||
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<FileRef>) -> 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<HashSet<FileRef>> {
|
||||
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<InstructionError>,
|
||||
}
|
||||
|
||||
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<RegionId>,
|
||||
related_regions: HashMap<RegionId, HashSet<RegionId>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LegacyGetFileRefsReply {
|
||||
file_refs_manifest: FileRefsManifest,
|
||||
success: bool,
|
||||
error: Option<InstructionError>,
|
||||
}
|
||||
|
||||
#[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::<LegacyInstruction>(&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::<Instruction>(&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::<PackedFileRefsManifest>(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::<PackedFileRefsManifest>(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::<base64::DecodeError>())
|
||||
);
|
||||
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();
|
||||
|
||||
@@ -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<ReconcileCatalogPayload>,
|
||||
}
|
||||
|
||||
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<serde_json::Value> {
|
||||
match &self.payload {
|
||||
Some(payload) => serde_json::to_value(payload).context(SerializeEventSnafu),
|
||||
None => Ok(serde_json::Value::Null),
|
||||
}
|
||||
}
|
||||
|
||||
fn extra_schema(&self) -> Vec<ColumnSchema> {
|
||||
ReconciliationLocator::schema()
|
||||
}
|
||||
|
||||
fn extra_rows(&self) -> Result<Vec<Row>> {
|
||||
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<ReconcileDatabasePayload>,
|
||||
}
|
||||
|
||||
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<serde_json::Value> {
|
||||
match &self.payload {
|
||||
Some(payload) => serde_json::to_value(payload).context(SerializeEventSnafu),
|
||||
None => Ok(serde_json::Value::Null),
|
||||
}
|
||||
}
|
||||
|
||||
fn extra_schema(&self) -> Vec<ColumnSchema> {
|
||||
ReconciliationLocator::schema()
|
||||
}
|
||||
|
||||
fn extra_rows(&self) -> Result<Vec<Row>> {
|
||||
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,
|
||||
|
||||
@@ -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<Box<dyn common_event_recorder::Event>> {
|
||||
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<Item = &'static str>) -> 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<Box<dyn common_event_recorder::Event>> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Box<dyn common_event_recorder::Event>> {
|
||||
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<Item = &'static str>) -> 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<Box<dyn common_event_recorder::Event>> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<InstructionReply> {
|
||||
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(),
|
||||
)),
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<InstructionReply> {
|
||||
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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -26,7 +26,7 @@ pub fn cast(src_value: Value, dest_type: &ConcreteDataType) -> Result<Value> {
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -1085,11 +1085,21 @@ impl StructValue {
|
||||
}
|
||||
|
||||
fn try_to_scalar_value(&self, output_type: &StructType) -> Result<ScalarValue> {
|
||||
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::<Result<Vec<Arc<dyn Array>>>>()?;
|
||||
|
||||
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!(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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<Vec<bool>> 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());
|
||||
|
||||
@@ -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<VectorRef> {
|
||||
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<VectorRef> {
|
||||
Ok(Arc::new(ConstantVector::new(
|
||||
self.inner().cast(to_type)?,
|
||||
self.length,
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) fn take_vector(&self, indices: &UInt32Vector) -> Result<VectorRef> {
|
||||
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::<UInt32Array>().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<dyn Array> {
|
||||
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<Vec<serde_json::Value>> {
|
||||
std::iter::repeat_n(self.get(0), self.len())
|
||||
.map(serde_json::Value::try_from)
|
||||
.collect::<serde_json::Result<_>>()
|
||||
.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]");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<K: ArrowDictionaryKeyType> VectorOp for DictionaryVector<K> {
|
||||
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<VectorRef> {
|
||||
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();
|
||||
|
||||
@@ -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::<ConstantVector>()
|
||||
.unwrap()
|
||||
.inner(),
|
||||
&**rhs
|
||||
.as_any()
|
||||
.downcast_ref::<ConstantVector>()
|
||||
.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(
|
||||
|
||||
+163
-143
@@ -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<VectorRef> {
|
||||
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<VectorRef> {
|
||||
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::<Result<Vec<Value>>>()?;
|
||||
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();
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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<VectorRef> {
|
||||
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<VectorRef> {
|
||||
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<VectorRef> {
|
||||
filter::filter_non_constant!(self, Decimal128Vector, filter)
|
||||
}
|
||||
@@ -128,10 +106,6 @@ impl VectorOp for Decimal128Vector {
|
||||
}
|
||||
|
||||
impl<T: LogicalPrimitiveType> VectorOp for PrimitiveVector<T> {
|
||||
fn replicate(&self, offsets: &[usize]) -> VectorRef {
|
||||
std::sync::Arc::new(replicate::replicate_primitive(self, offsets))
|
||||
}
|
||||
|
||||
fn filter(&self, filter: &BooleanVector) -> Result<VectorRef> {
|
||||
filter::filter_non_constant!(self, PrimitiveVector<T>, filter)
|
||||
}
|
||||
@@ -146,10 +120,6 @@ impl<T: LogicalPrimitiveType> VectorOp for PrimitiveVector<T> {
|
||||
}
|
||||
|
||||
impl VectorOp for NullVector {
|
||||
fn replicate(&self, offsets: &[usize]) -> VectorRef {
|
||||
replicate::replicate_null(self, offsets)
|
||||
}
|
||||
|
||||
fn filter(&self, filter: &BooleanVector) -> Result<VectorRef> {
|
||||
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<VectorRef> {
|
||||
self.filter_vector(filter)
|
||||
}
|
||||
|
||||
fn cast(&self, to_type: &ConcreteDataType) -> Result<VectorRef> {
|
||||
self.cast_vector(to_type)
|
||||
}
|
||||
|
||||
fn take(&self, indices: &UInt32Vector) -> Result<VectorRef> {
|
||||
self.take_vector(indices)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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: ScalarVector>(c: &C, offsets: &[usize]) -> VectorRef {
|
||||
assert_eq!(offsets.len(), c.len());
|
||||
|
||||
if offsets.is_empty() {
|
||||
return c.slice(0, 0);
|
||||
}
|
||||
let mut builder = <<C as ScalarVector>::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::[<Timestamp $unit Vector>];
|
||||
use $crate::timestamp::[<Timestamp $unit>];
|
||||
let v = [<Timestamp $unit Vector>]::from_iterator((0..5).map([<Timestamp $unit>]::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);
|
||||
}
|
||||
}
|
||||
@@ -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<T>(
|
||||
@@ -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() {
|
||||
|
||||
@@ -366,40 +366,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn replicate_primitive<T: LogicalPrimitiveType>(
|
||||
vector: &PrimitiveVector<T>,
|
||||
offsets: &[usize],
|
||||
) -> PrimitiveVector<T> {
|
||||
assert_eq!(offsets.len(), vector.len());
|
||||
|
||||
if offsets.is_empty() {
|
||||
return vector.get_slice(0, 0);
|
||||
}
|
||||
|
||||
let mut builder = PrimitiveVectorBuilder::<T>::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());
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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<VectorRef> {
|
||||
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::<Int8Type>::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
|
||||
|
||||
@@ -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<Option<tokio::sync::oneshot::Sender<()>>>,
|
||||
release: Arc<tokio::sync::Notify>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LateStreamErrorFlight;
|
||||
|
||||
struct WaitForConcurrentFlight {
|
||||
barrier: Arc<tokio::sync::Barrier>,
|
||||
}
|
||||
@@ -870,6 +888,45 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FlightCraft for DelayedEofFlight {
|
||||
async fn do_get(
|
||||
&self,
|
||||
_request: TonicRequest<Ticket>,
|
||||
) -> std::result::Result<TonicResponse<TonicStream<FlightData>>, 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<FlightData, Status>, _)>
|
||||
});
|
||||
|
||||
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<Ticket>,
|
||||
) -> std::result::Result<TonicResponse<TonicStream<FlightData>>, 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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -453,6 +453,7 @@ impl Instance {
|
||||
request.flight_data,
|
||||
request.record_batch,
|
||||
request.schema_bytes,
|
||||
ctx.skip_wal(),
|
||||
)
|
||||
.await
|
||||
.context(TableOperationSnafu)?;
|
||||
|
||||
@@ -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::<Int64Array>()
|
||||
.unwrap()
|
||||
.values()
|
||||
.iter()
|
||||
.copied()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(values, expected, "{name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<MailboxReceiver> {
|
||||
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<GetFileRefsReply> {
|
||||
) -> Result<GetPackedFileRefsReply> {
|
||||
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<MailboxReceiver> {
|
||||
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<RegionId>,
|
||||
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<GcReport> {
|
||||
@@ -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<RegionId>,
|
||||
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<RegionId>,
|
||||
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<RegionId, HashSet<_>> = HashMap::new();
|
||||
let mut all_manifest_versions = HashMap::new();
|
||||
let mut all_cross_region_refs = HashMap::new();
|
||||
let mut all_cross_region_refs: HashMap<RegionId, HashSet<RegionId>> = 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<Status> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MailboxMessage>) -> 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(())
|
||||
|
||||
@@ -87,7 +87,9 @@ impl FromStr for LeaseValue {
|
||||
type Err = error::Error;
|
||||
|
||||
fn from_str(value: &str) -> crate::Result<Self> {
|
||||
serde_json::from_str(value).context(error::DeserializeFromJsonSnafu { input: value })
|
||||
serde_json::from_str(value).context(error::DeserializeFromJsonSnafu {
|
||||
input_len: value.len(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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::<usize>(), 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::<usize>(),
|
||||
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::<usize>(), 4);
|
||||
assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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::<TimestampMillisecondArray>()
|
||||
.unwrap();
|
||||
let values = batch
|
||||
.column(value_index)
|
||||
.as_any()
|
||||
.downcast_ref::<Float64Array>()
|
||||
.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<u64>; 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::<Vec<_>>()
|
||||
};
|
||||
// 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<String> = 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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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::<Vec<_>>();
|
||||
assert_eq!(request.rows.rows, expected_rows);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concat_table_key() {
|
||||
let region_id = RegionId::new(1234, 7844);
|
||||
|
||||
+3
-1
@@ -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.
|
||||
|
||||
|
||||
@@ -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<ObjectStore> {
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -52,6 +52,8 @@ pub(crate) type Json2RewritePlans = HashMap<String, Json2RewritePlan>;
|
||||
#[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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
+4
-45
@@ -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);
|
||||
|
||||
@@ -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::<RegionHookRef>();
|
||||
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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user