diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml
index d03fbeff14..c3af006f54 100644
--- a/.github/workflows/dev-build.yml
+++ b/.github/workflows/dev-build.yml
@@ -30,7 +30,7 @@ on:
linux_arm64_runner:
type: choice
description: The runner uses to build linux-arm64 artifacts
- default: ec2-c6g.4xlarge-arm64
+ default: ec2-c6g.8xlarge-arm64
options:
- ec2-c6g.xlarge-arm64 # 4C8G
- ec2-c6g.2xlarge-arm64 # 8C16G
diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml
index 14ebb6e715..54af32a94b 100644
--- a/.github/workflows/nightly-build.yml
+++ b/.github/workflows/nightly-build.yml
@@ -27,7 +27,7 @@ on:
linux_arm64_runner:
type: choice
description: The runner uses to build linux-arm64 artifacts
- default: ec2-c6g.4xlarge-arm64
+ default: ec2-c6g.8xlarge-arm64
options:
- ec2-c6g.xlarge-arm64 # 4C8G
- ec2-c6g.2xlarge-arm64 # 8C16G
diff --git a/.github/workflows/nightly-jsonbench.yaml b/.github/workflows/nightly-jsonbench.yaml
new file mode 100644
index 0000000000..a9ce4dd363
--- /dev/null
+++ b/.github/workflows/nightly-jsonbench.yaml
@@ -0,0 +1,223 @@
+name: Nightly JSONBench
+
+on:
+ workflow_run:
+ workflows: [ "GreptimeDB Nightly Build" ]
+ types: [ completed ]
+ workflow_dispatch:
+ inputs:
+ run_id:
+ description: The nightly build workflow run id to download GreptimeDB artifacts from
+ required: true
+ type: string
+
+permissions:
+ actions: read
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ resolve-artifact:
+ name: Resolve GreptimeDB nightly artifact
+ if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success') }}
+ runs-on: ubuntu-latest
+ outputs:
+ artifact-name: ${{ steps.find-artifact.outputs.artifact-name }}
+ run-id: ${{ steps.resolve-run-id.outputs.run-id }}
+ steps:
+ - name: Resolve nightly build run id
+ id: resolve-run-id
+ shell: bash
+ env:
+ EVENT_NAME: ${{ github.event_name }}
+ WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
+ INPUT_RUN_ID: ${{ inputs.run_id }}
+ run: |
+ set -euo pipefail
+
+ if [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then
+ run_id="${INPUT_RUN_ID}"
+ else
+ run_id="${WORKFLOW_RUN_ID}"
+ fi
+
+ if [[ ! "${run_id}" =~ ^[0-9]+$ ]]; then
+ echo "Invalid workflow run id: ${run_id}"
+ exit 1
+ fi
+
+ echo "run-id=${run_id}" >> "${GITHUB_OUTPUT}"
+
+ - name: Find GreptimeDB nightly artifact
+ id: find-artifact
+ shell: bash
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ RUN_ID: ${{ steps.resolve-run-id.outputs.run-id }}
+ run: |
+ set -euo pipefail
+
+ artifact_name=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/artifacts" --paginate \
+ --jq '.artifacts[] | select(.name | test("^greptime-linux-arm64-nightly-[0-9]{8}-[0-9a-f]+$")) | .name' \
+ | head -n 1)
+
+ if [[ -z "${artifact_name}" ]]; then
+ echo "Cannot find linux arm64 nightly artifact in workflow run ${RUN_ID}."
+ exit 1
+ fi
+
+ echo "Download GreptimeDB artifact: ${artifact_name}"
+ echo "artifact-name=${artifact_name}" >> "${GITHUB_OUTPUT}"
+
+ allocate-runner:
+ name: Allocate runner
+ if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success') }}
+ needs: [ resolve-artifact ]
+ runs-on: ubuntu-latest
+ outputs:
+ linux-arm64-runner: ${{ steps.start-linux-arm64-runner.outputs.label }}
+
+ # The following EC2 resource id will be used for resource releasing.
+ linux-arm64-ec2-runner-label: ${{ steps.start-linux-arm64-runner.outputs.label }}
+ linux-arm64-ec2-runner-instance-id: ${{ steps.start-linux-arm64-runner.outputs.ec2-instance-id }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Allocate Linux ARM64 runner
+ uses: ./.github/actions/start-runner
+ id: start-linux-arm64-runner
+ with:
+ runner: ${{ vars.DEFAULT_ARM64_RUNNER }}
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ aws-region: ${{ vars.EC2_RUNNER_REGION }}
+ github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
+ image-id: ${{ vars.EC2_RUNNER_LINUX_ARM64_IMAGE_ID }}
+ security-group-id: ${{ vars.EC2_RUNNER_SECURITY_GROUP_ID }}
+ subnet-id: ${{ vars.EC2_RUNNER_SUBNET_ID }}
+
+ jsonbench:
+ name: Run JSONBench
+ if: ${{ github.repository == 'GreptimeTeam/greptimedb' && (github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success') }}
+ needs: [ resolve-artifact, allocate-runner ]
+ runs-on: ${{ needs.allocate-runner.outputs.linux-arm64-runner }}
+ timeout-minutes: 120
+ env:
+ JSONBENCH_OUTPUT_PREFIX: _linux-arm64
+ steps:
+ - name: Download GreptimeDB nightly artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: ${{ needs.resolve-artifact.outputs.artifact-name }}
+ path: greptimedb-artifact
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ run-id: ${{ needs.resolve-artifact.outputs.run-id }}
+
+ - name: Prepare GreptimeDB binary
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ tar -xzf "greptimedb-artifact/${{ needs.resolve-artifact.outputs.artifact-name }}.tar.gz"
+ cp "${{ needs.resolve-artifact.outputs.artifact-name }}/greptime" ./greptime
+ chmod +x ./greptime
+ rm -rf greptimedb-artifact "${{ needs.resolve-artifact.outputs.artifact-name }}"
+
+ - name: Run JSONBench
+ env:
+ # TODO(LFC): Change to "3" (100m) when JSON2 ingestion performance is optimized.
+ JSONBENCH_DATASET: 2
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ export JSONBENCH_DATA_DIR="/root/data/bluesky"
+ echo "Use JSONBench data directory ${JSONBENCH_DATA_DIR}"
+
+ echo "Cloning JSONBench"
+ git clone --branch greptimedb-new-json --depth 1 https://github.com/GreptimeTeam/JSONBench.git JSONBench
+
+ echo "Downloading JSONBench dataset choice ${JSONBENCH_DATASET} to ${JSONBENCH_DATA_DIR}"
+ mkdir -p "${JSONBENCH_DATA_DIR}"
+ printf "${JSONBENCH_DATASET}\n" | ./JSONBench/download_data.sh
+ downloaded_files=$(find "${JSONBENCH_DATA_DIR}" -type f | wc -l)
+ echo "Downloaded JSONBench dataset files: ${downloaded_files}"
+
+ export GREPTIMEDB_STANDALONE__WAL__DIR=greptimedb_data/wal
+ export GREPTIMEDB_STANDALONE__STORAGE__DATA_HOME=greptimedb_data
+ export GREPTIMEDB_STANDALONE__LOGGING__DIR=greptimedb_data/logs
+ export GREPTIMEDB_STANDALONE__LOGGING__APPEND_STDOUT=false
+ export GREPTIMEDB_STANDALONE__HTTP__BODY_LIMIT=1GB
+ export GREPTIMEDB_STANDALONE__HTTP__TIMEOUT=500s
+
+ echo "Starting GreptimeDB standalone"
+ ./greptime standalone start > greptimedb.log 2>&1 &
+ greptime_pid=$!
+ trap 'kill "${greptime_pid}" 2>/dev/null || true' EXIT
+
+ echo "Waiting for GreptimeDB health check"
+ until curl -s --fail -o /dev/null http://localhost:4000/health; do
+ if ! kill -0 "${greptime_pid}" 2>/dev/null; then
+ cat greptimedb.log
+ exit 1
+ fi
+ sleep 1
+ done
+ echo "GreptimeDB is ready"
+
+ cp ./greptime JSONBench/greptimedb/greptime
+
+ cd JSONBench/greptimedb
+ echo "Running JSONBench main.sh with dataset choice ${JSONBENCH_DATASET} and install=false"
+ ./main.sh ${JSONBENCH_DATASET} "${JSONBENCH_DATA_DIR}" success.log error.log "${JSONBENCH_OUTPUT_PREFIX}" false
+ echo "JSONBench finished"
+
+ - name: Upload JSONBench results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: jsonbench-results
+ path: |
+ ./greptimedb.log
+ ./JSONBench/greptimedb/*.log
+ ./JSONBench/greptimedb/*.total_size
+ ./JSONBench/greptimedb/*.data_size
+ ./JSONBench/greptimedb/*.index_size
+ ./JSONBench/greptimedb/*.count
+ ./JSONBench/greptimedb/*.results_runtime
+ ./JSONBench/greptimedb/*.query_results
+ if-no-files-found: ignore
+ retention-days: 7
+
+ stop-linux-arm64-runner:
+ name: Stop Linux ARM64 runner
+ # It's always run as the last job in the workflow to make sure that the runner is released.
+ if: ${{ always() && needs.allocate-runner.outputs.linux-arm64-ec2-runner-instance-id != '' }}
+ runs-on: ubuntu-latest
+ needs: [
+ allocate-runner,
+ jsonbench,
+ ]
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Stop Linux ARM64 runner
+ uses: ./.github/actions/stop-runner
+ with:
+ label: ${{ needs.allocate-runner.outputs.linux-arm64-ec2-runner-label }}
+ ec2-instance-id: ${{ needs.allocate-runner.outputs.linux-arm64-ec2-runner-instance-id }}
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ aws-region: ${{ vars.EC2_RUNNER_REGION }}
+ github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
diff --git a/Cargo.lock b/Cargo.lock
index 63ba289947..e3d0dfb69e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -79,8 +79,9 @@ checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"const-random",
- "getrandom 0.3.3",
+ "getrandom 0.3.4",
"once_cell",
+ "serde",
"version_check",
"zerocopy",
]
@@ -771,7 +772,7 @@ version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891"
dependencies = [
- "term",
+ "term 1.0.2",
]
[[package]]
@@ -1427,6 +1428,12 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "borrow-or-share"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
+
[[package]]
name = "borsh"
version = "1.5.7"
@@ -1525,6 +1532,12 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "bytecount"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
+
[[package]]
name = "bytemuck"
version = "1.23.1"
@@ -1635,7 +1648,7 @@ dependencies = [
"paste",
"prometheus 0.14.0",
"promql-parser",
- "rand 0.9.1",
+ "rand 0.9.4",
"serde",
"serde_json",
"session",
@@ -1973,7 +1986,7 @@ dependencies = [
"partition",
"paste",
"query",
- "rand 0.9.1",
+ "rand 0.9.4",
"reqwest 0.13.2",
"serde",
"serde_json",
@@ -2020,7 +2033,7 @@ dependencies = [
"prometheus 0.14.0",
"prost 0.14.1",
"query",
- "rand 0.9.1",
+ "rand 0.9.4",
"serde_json",
"snafu 0.8.6",
"store-api",
@@ -2031,6 +2044,15 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "clipboard-win"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
+dependencies = [
+ "error-code",
+]
+
[[package]]
name = "clocksource"
version = "0.8.1"
@@ -2123,7 +2145,7 @@ dependencies = [
"prometheus 0.14.0",
"prost 0.14.1",
"query",
- "rand 0.9.1",
+ "rand 0.9.4",
"regex",
"reqwest 0.13.2",
"serde",
@@ -2278,6 +2300,7 @@ dependencies = [
"futures",
"lazy_static",
"object-store",
+ "object_store_opendal",
"orc-rust",
"parquet",
"paste",
@@ -2461,7 +2484,7 @@ dependencies = [
"hyper-util",
"lazy_static",
"prost 0.14.1",
- "rand 0.9.1",
+ "rand 0.9.4",
"serde",
"serde_json",
"snafu 0.8.6",
@@ -2580,7 +2603,7 @@ dependencies = [
"prometheus 0.14.0",
"prost 0.14.1",
"prost-types 0.14.1",
- "rand 0.9.1",
+ "rand 0.9.4",
"regex",
"rskafka",
"rustls",
@@ -2648,7 +2671,7 @@ dependencies = [
"futures-util",
"humantime-serde",
"object-store",
- "rand 0.9.1",
+ "rand 0.9.4",
"serde",
"serde_json",
"smallvec",
@@ -2832,7 +2855,7 @@ dependencies = [
"common-query",
"common-recordbatch",
"once_cell",
- "rand 0.9.1",
+ "rand 0.9.4",
"tempfile",
]
@@ -2848,7 +2871,7 @@ dependencies = [
"humantime",
"humantime-serde",
"once_cell",
- "rand 0.9.1",
+ "rand 0.9.4",
"serde",
"serde_json",
"snafu 0.8.6",
@@ -3593,6 +3616,12 @@ dependencies = [
"parking_lot_core 0.9.11",
]
+[[package]]
+name = "data-encoding"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+
[[package]]
name = "datafusion"
version = "53.1.0"
@@ -3637,7 +3666,7 @@ dependencies = [
"object_store",
"parking_lot 0.12.4",
"parquet",
- "rand 0.9.1",
+ "rand 0.9.4",
"regex",
"sqlparser",
"tempfile",
@@ -3754,7 +3783,7 @@ dependencies = [
"liblzma",
"log",
"object_store",
- "rand 0.9.1",
+ "rand 0.9.4",
"tokio",
"tokio-util",
"url",
@@ -3880,7 +3909,7 @@ dependencies = [
"log",
"object_store",
"parking_lot 0.12.4",
- "rand 0.9.1",
+ "rand 0.9.4",
"tempfile",
"url",
]
@@ -3943,7 +3972,7 @@ dependencies = [
"md-5 0.10.6",
"memchr",
"num-traits",
- "rand 0.9.1",
+ "rand 0.9.4",
"regex",
"sha2 0.10.9",
"unicode-segmentation",
@@ -4233,7 +4262,7 @@ dependencies = [
"datafusion-proto-common",
"object_store",
"prost 0.14.1",
- "rand 0.9.1",
+ "rand 0.9.4",
]
[[package]]
@@ -4684,6 +4713,27 @@ dependencies = [
"crypto-common 0.2.1",
]
+[[package]]
+name = "dirs-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
+[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -4712,14 +4762,14 @@ dependencies = [
[[package]]
name = "dns-lookup"
-version = "2.0.4"
+version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5766087c2235fec47fafa4cfecc81e494ee679d0fd4a59887ea0919bfb0e4fc"
+checksum = "6e39034cee21a2f5bbb66ba0e3689819c4bb5d00382a282006e802a7ffa6c41d"
dependencies = [
"cfg-if",
"libc",
- "socket2 0.5.10",
- "windows-sys 0.48.0",
+ "socket2 0.6.0",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -4739,31 +4789,30 @@ dependencies = [
[[package]]
name = "domain"
-version = "0.11.0"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a11dd7f04a6a6d2aea0153c6e31f5ea7af8b2efdf52cdaeea7a9a592c7fefef9"
+checksum = "8c469892dddfeff64ecfdbc64cf059c77fb0decaeccd4d5d484394bdd6312bac"
dependencies = [
"bumpalo",
"bytes",
"domain-macros",
"futures-util",
- "hashbrown 0.14.5",
+ "hashbrown 0.17.1",
+ "jiff",
"log",
- "moka",
"octseq",
- "rand 0.8.5",
+ "rand 0.10.1",
"serde",
"smallvec",
- "time",
"tokio",
"tracing",
]
[[package]]
name = "domain-macros"
-version = "0.11.0"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e197fdfd2cdb5fdeb7f8ddcf3aed5d5d04ecde2890d448b14ffb716f7376b70"
+checksum = "6fef7ef74e413e36d5364db163ca577ccb56f2f74377705d5f920ee3e1544127"
dependencies = [
"proc-macro2",
"quote",
@@ -4848,6 +4897,15 @@ dependencies = [
"serde",
]
+[[package]]
+name = "email_address"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "ena"
version = "0.14.3"
@@ -4961,6 +5019,12 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "error-code"
+version = "3.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
+
[[package]]
name = "etcd-client"
version = "0.17.0"
@@ -5017,6 +5081,12 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "exitcode"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193"
+
[[package]]
name = "fail"
version = "0.5.1"
@@ -5042,9 +5112,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fancy-regex"
-version = "0.14.0"
+version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
+checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8"
dependencies = [
"bit-set",
"regex-automata",
@@ -5102,6 +5172,7 @@ dependencies = [
"datatypes",
"futures",
"object-store",
+ "object_store_opendal",
"serde",
"serde_json",
"snafu 0.8.6",
@@ -5272,7 +5343,7 @@ dependencies = [
"prometheus 0.14.0",
"prost 0.14.1",
"query",
- "rand 0.9.1",
+ "rand 0.9.4",
"serde",
"serde_json",
"servers",
@@ -5297,6 +5368,17 @@ dependencies = [
"bitflags 1.3.2",
]
+[[package]]
+name = "fluent-uri"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
+dependencies = [
+ "borrow-or-share",
+ "ref-cast",
+ "serde",
+]
+
[[package]]
name = "flume"
version = "0.11.1"
@@ -5335,6 +5417,16 @@ dependencies = [
"percent-encoding",
]
+[[package]]
+name = "fraction"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872"
+dependencies = [
+ "lazy_static",
+ "num",
+]
+
[[package]]
name = "fragile"
version = "2.0.1"
@@ -5403,7 +5495,7 @@ dependencies = [
"promql-parser",
"prost 0.14.1",
"query",
- "rand 0.9.1",
+ "rand 0.9.4",
"reqwest 0.13.2",
"serde",
"serde_json",
@@ -5754,21 +5846,21 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
- "wasi 0.11.1+wasi-snapshot-preview1",
+ "wasi",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
-version = "0.3.3"
+version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
- "wasi 0.14.2+wasi-0.2.4",
+ "wasip2",
"wasm-bindgen",
]
@@ -5826,7 +5918,7 @@ dependencies = [
[[package]]
name = "greptime-proto"
version = "0.1.0"
-source = "git+https://github.com/GreptimeTeam/greptime-proto.git?rev=7224c2ad6d11db612fbdb621c36135fc37ffce35#7224c2ad6d11db612fbdb621c36135fc37ffce35"
+source = "git+https://github.com/GreptimeTeam/greptime-proto.git?rev=912f6c21017d5ab6c529568d36e7b8c30a94d84b#912f6c21017d5ab6c529568d36e7b8c30a94d84b"
dependencies = [
"prost 0.14.1",
"prost-types 0.14.1",
@@ -5840,9 +5932,9 @@ dependencies = [
[[package]]
name = "grok"
-version = "2.1.0"
+version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c52724b609896f661a3f4641dd3a44dc602958ef615857c12d00756b4e9355b"
+checksum = "6ddab6a9c8bb998cb2fc3101fde8ef561b7c4970db3957be7a8eee1e168f666b"
dependencies = [
"glob",
"onig",
@@ -5982,6 +6074,9 @@ name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+dependencies = [
+ "allocator-api2",
+]
[[package]]
name = "hashlink"
@@ -6648,7 +6743,7 @@ dependencies = [
"pin-project",
"prost 0.14.1",
"puffin",
- "rand 0.9.1",
+ "rand 0.9.4",
"rand_chacha 0.9.0",
"regex",
"regex-automata",
@@ -6839,6 +6934,15 @@ dependencies = [
"derive_utils",
]
+[[package]]
+name = "ipcrypt-rs"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96e4f67dbfc0f75d7b65953ecf0be3fd84ee0cb1ae72a00a4aa9a2f5518a2c80"
+dependencies = [
+ "aes",
+]
+
[[package]]
name = "ipnet"
version = "2.11.0"
@@ -7014,6 +7118,36 @@ dependencies = [
"windows-sys 0.45.0",
]
+[[package]]
+name = "jni"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+dependencies = [
+ "cfg-if",
+ "combine",
+ "jni-macros",
+ "jni-sys 0.4.1",
+ "log",
+ "simd_cesu8",
+ "thiserror 2.0.17",
+ "walkdir",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "jni-macros"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn 2.0.117",
+]
+
[[package]]
name = "jni-sys"
version = "0.3.1"
@@ -7048,7 +7182,7 @@ version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
dependencies = [
- "getrandom 0.3.3",
+ "getrandom 0.3.4",
"libc",
]
@@ -7137,11 +7271,38 @@ version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c6e529149475ca0b2820835d3dce8fcc41c6b943ca608d32f35b449255e4627"
dependencies = [
- "fluent-uri",
+ "fluent-uri 0.1.4",
"serde",
"serde_json",
]
+[[package]]
+name = "jsonschema"
+version = "0.38.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89f50532ce4a0ba3ae930212908d8ec50e7806065c059fe9c75da2ece6132294"
+dependencies = [
+ "ahash 0.8.12",
+ "bytecount",
+ "data-encoding",
+ "email_address",
+ "fancy-regex",
+ "fraction",
+ "getrandom 0.3.4",
+ "idna",
+ "itoa",
+ "num-cmp",
+ "num-traits",
+ "percent-encoding",
+ "referencing",
+ "regex",
+ "regex-syntax",
+ "serde",
+ "serde_json",
+ "unicode-general-category",
+ "uuid-simd",
+]
+
[[package]]
name = "jsonwebtoken"
version = "10.3.0"
@@ -7335,7 +7496,7 @@ dependencies = [
"regex-syntax",
"sha3",
"string_cache",
- "term",
+ "term 1.0.2",
"unicode-xid",
"walkdir",
]
@@ -7734,7 +7895,7 @@ dependencies = [
"protobuf 2.28.0",
"protobuf-build",
"raft-engine",
- "rand 0.9.1",
+ "rand 0.9.4",
"rskafka",
"serde",
"serde_json",
@@ -8041,7 +8202,7 @@ dependencies = [
"futures-util",
"humantime-serde",
"meta-srv",
- "rand 0.9.1",
+ "rand 0.9.4",
"serde",
"serde_json",
"session",
@@ -8109,7 +8270,7 @@ dependencies = [
"partition",
"prometheus 0.14.0",
"prost 0.14.1",
- "rand 0.9.1",
+ "rand 0.9.4",
"regex",
"rskafka",
"serde",
@@ -8251,7 +8412,7 @@ checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"log",
- "wasi 0.11.1+wasi-snapshot-preview1",
+ "wasi",
"windows-sys 0.59.0",
]
@@ -8320,6 +8481,7 @@ dependencies = [
"datafusion-common",
"datafusion-expr",
"datatypes",
+ "derive_more",
"dotenv",
"either",
"futures",
@@ -8340,7 +8502,7 @@ dependencies = [
"prometheus 0.14.0",
"prost 0.14.1",
"puffin",
- "rand 0.9.1",
+ "rand 0.9.4",
"rayon",
"regex",
"roaring",
@@ -8653,6 +8815,12 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
[[package]]
name = "neli"
version = "0.6.5"
@@ -8755,6 +8923,15 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "nom-language"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2de2bc5b451bfedaef92c90b8939a8fff5770bdcc1fafd6239d086aab8fa6b29"
+dependencies = [
+ "nom 8.0.0",
+]
+
[[package]]
name = "notify"
version = "8.0.0"
@@ -8838,6 +9015,12 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "num-cmp"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa"
+
[[package]]
name = "num-complex"
version = "0.4.6"
@@ -8998,6 +9181,31 @@ dependencies = [
"libc",
]
+[[package]]
+name = "objc2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
+dependencies = [
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
+dependencies = [
+ "bitflags 2.11.1",
+ "objc2",
+]
+
[[package]]
name = "object"
version = "0.36.7"
@@ -9037,7 +9245,7 @@ dependencies = [
"object_store_opendal",
"opendal",
"prometheus 0.14.0",
- "rand 0.9.1",
+ "rand 0.9.4",
"reqwest 0.13.2",
"serde",
"snafu 0.8.6",
@@ -9074,8 +9282,9 @@ dependencies = [
[[package]]
name = "object_store_opendal"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eb12a624a41fce745838d0ef3701ff6c47797c13cd18ad3612fd2a3134fdbd8"
dependencies = [
"async-trait",
"bytes",
@@ -9090,9 +9299,9 @@ dependencies = [
[[package]]
name = "octseq"
-version = "0.5.2"
+version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "126c3ca37c9c44cec575247f43a3e4374d8927684f129d2beeb0d2cef262fe12"
+checksum = "182eab3e1cd9cdc0ecf1ce3342d9844f3dc7d098f0694569bfdf327b612d69fd"
dependencies = [
"bytes",
"serde",
@@ -9162,8 +9371,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "opendal"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c9c85ce253ff87225e7669979d877a20c98a06604ec9d6dd5f4473e08f1ae1"
dependencies = [
"ctor",
"opendal-core",
@@ -9183,8 +9393,9 @@ dependencies = [
[[package]]
name = "opendal-core"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4f8607c90e2c963a91467f50fb49fbc7fb3d573f88cea219ca59ccd3740b309"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -9210,8 +9421,9 @@ dependencies = [
[[package]]
name = "opendal-layer-concurrent-limit"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d6f81ba6960e3fae1882f253b114b21d7e444e1534f209c7737a79f6243eb6f"
dependencies = [
"futures",
"http 1.3.1",
@@ -9221,8 +9433,9 @@ dependencies = [
[[package]]
name = "opendal-layer-logging"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58ada45c6d81d1aa4c9305d0c7d4bc317c59c85866a0908a2d75a7a978aa5ee2"
dependencies = [
"log",
"opendal-core",
@@ -9230,8 +9443,9 @@ dependencies = [
[[package]]
name = "opendal-layer-observe-metrics-common"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "628b0228fdbd13c3d9d50eee4341f2eb82ca5b44991e4c68f07c84cc823e2d12"
dependencies = [
"futures",
"http 1.3.1",
@@ -9240,8 +9454,9 @@ dependencies = [
[[package]]
name = "opendal-layer-prometheus"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0487bdb1357097ec8654781bad03ef310282517738e2864ebde69e27aaafc5ec"
dependencies = [
"opendal-core",
"opendal-layer-observe-metrics-common",
@@ -9250,8 +9465,9 @@ dependencies = [
[[package]]
name = "opendal-layer-retry"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b2a25a718afb81fad81cb9a0580a1cb989221fa2317f888c6a37f8dad408eb7"
dependencies = [
"backon",
"log",
@@ -9260,8 +9476,9 @@ dependencies = [
[[package]]
name = "opendal-layer-timeout"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e91f731724c213af81e9d03517859c8fc47b4578e64ad61ae4f099f10fe36e3"
dependencies = [
"opendal-core",
"tokio",
@@ -9269,8 +9486,9 @@ dependencies = [
[[package]]
name = "opendal-layer-tracing"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90c6fc9df6da1f0dafbdf55fa48525f1643aefbe7da8f46936e869e2a5b8a34f"
dependencies = [
"futures",
"http 1.3.1",
@@ -9280,8 +9498,9 @@ dependencies = [
[[package]]
name = "opendal-service-azblob"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0030644366ef5d8cbe3a4a5822bf99a4aafddc1666e9d24b44d158d9062fc76a"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -9300,8 +9519,9 @@ dependencies = [
[[package]]
name = "opendal-service-azure-common"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b489f13c42e69d69bdd72952b634356ec43a7881a20259b38b540fcecdf4051"
dependencies = [
"http 1.3.1",
"opendal-core",
@@ -9309,8 +9529,9 @@ dependencies = [
[[package]]
name = "opendal-service-fs"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e89a665fef0e6bd249cf5ea47fc174b7ba892159bee4b9382528b1ca873a2c"
dependencies = [
"bytes",
"log",
@@ -9322,8 +9543,9 @@ dependencies = [
[[package]]
name = "opendal-service-gcs"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48de101aac565ed06af4b47903c24eafd249075553ec1fb18256751c45148d47"
dependencies = [
"async-trait",
"bytes",
@@ -9342,8 +9564,9 @@ dependencies = [
[[package]]
name = "opendal-service-http"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb6af628a0bf14075b957179444927e1df40dc7addef382b585a05ef015a077b"
dependencies = [
"http 1.3.1",
"log",
@@ -9353,8 +9576,9 @@ dependencies = [
[[package]]
name = "opendal-service-oss"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "328fa55e8888cbdfe00826bfea2a79042422b720e8369e9e021e46121dea5ace"
dependencies = [
"bytes",
"http 1.3.1",
@@ -9369,8 +9593,9 @@ dependencies = [
[[package]]
name = "opendal-service-s3"
-version = "0.56.0"
-source = "git+https://github.com/apache/opendal.git?rev=4ad2d85296ffa6fdc2882f97d3c760ee243913f7#4ad2d85296ffa6fdc2882f97d3c760ee243913f7"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "313d46c9f5ae70bca26b7c3e3fbb9b639292625f28af73aa016f47e788af9deb"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -9532,7 +9757,7 @@ dependencies = [
"futures-util",
"opentelemetry 0.30.0",
"percent-encoding",
- "rand 0.9.1",
+ "rand 0.9.4",
"serde_json",
"thiserror 2.0.17",
"tokio",
@@ -9550,7 +9775,7 @@ dependencies = [
"futures-util",
"opentelemetry 0.31.0",
"percent-encoding",
- "rand 0.9.1",
+ "rand 0.9.4",
"thiserror 2.0.17",
]
@@ -9728,7 +9953,7 @@ dependencies = [
"paste",
"prost 0.14.1",
"prost-build 0.14.1",
- "rand 0.9.1",
+ "rand 0.9.4",
"replace_with",
"serde",
"smallvec",
@@ -10134,7 +10359,7 @@ dependencies = [
"md5",
"pg_interval_2",
"postgres-types",
- "rand 0.10.0",
+ "rand 0.10.1",
"rust_decimal",
"rustls-pki-types",
"ryu",
@@ -10511,7 +10736,7 @@ dependencies = [
"hmac",
"md-5 0.10.6",
"memchr",
- "rand 0.9.1",
+ "rand 0.9.4",
"sha2 0.10.9",
"stringprep",
]
@@ -10660,6 +10885,19 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "prettytable-rs"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a"
+dependencies = [
+ "encode_unicode",
+ "is-terminal",
+ "lazy_static",
+ "term 0.7.0",
+ "unicode-width 0.1.14",
+]
+
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
@@ -10825,7 +11063,7 @@ checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40"
dependencies = [
"bitflags 2.11.1",
"num-traits",
- "rand 0.9.1",
+ "rand 0.9.4",
"rand_chacha 0.9.0",
"rand_xorshift",
"regex-syntax",
@@ -11252,7 +11490,7 @@ dependencies = [
"promql",
"promql-parser",
"prost 0.14.1",
- "rand 0.9.1",
+ "rand 0.9.4",
"regex",
"serde",
"serde_json",
@@ -11325,9 +11563,9 @@ checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
dependencies = [
"aws-lc-rs",
"bytes",
- "getrandom 0.3.3",
+ "getrandom 0.3.4",
"lru-slab",
- "rand 0.9.1",
+ "rand 0.9.4",
"ring",
"rustc-hash 2.1.1",
"rustls",
@@ -11433,9 +11671,9 @@ dependencies = [
[[package]]
name = "rand"
-version = "0.9.1"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
@@ -11443,9 +11681,9 @@ dependencies = [
[[package]]
name = "rand"
-version = "0.10.0"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
+checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20 0.10.0",
"getrandom 0.4.1",
@@ -11488,7 +11726,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
- "getrandom 0.3.3",
+ "getrandom 0.3.4",
]
[[package]]
@@ -11581,6 +11819,17 @@ dependencies = [
"bitflags 2.11.1",
]
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom 0.2.16",
+ "libredox",
+ "thiserror 1.0.69",
+]
+
[[package]]
name = "ref-cast"
version = "1.0.24"
@@ -11601,6 +11850,21 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "referencing"
+version = "0.38.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15a8af0c6bb8eaf8b07cb06fc31ff30ca6fe19fb99afa476c276d8b24f365b0b"
+dependencies = [
+ "ahash 0.8.12",
+ "fluent-uri 0.4.1",
+ "getrandom 0.3.4",
+ "hashbrown 0.16.1",
+ "parking_lot 0.12.4",
+ "percent-encoding",
+ "serde_json",
+]
+
[[package]]
name = "regex"
version = "1.12.2"
@@ -11666,6 +11930,15 @@ version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
+[[package]]
+name = "relative-path"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bca40a312222d8ba74837cb474edef44b37f561da5f773981007a10bbaa992b0"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "rend"
version = "0.4.2"
@@ -11805,6 +12078,7 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
+ "h2 0.4.11",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
@@ -11817,6 +12091,7 @@ dependencies = [
"pin-project-lite",
"quinn",
"rustls",
+ "rustls-native-certs 0.8.1",
"rustls-pki-types",
"serde",
"serde_json",
@@ -11878,6 +12153,50 @@ dependencies = [
"web-sys",
]
+[[package]]
+name = "reqwest-middleware"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "http 1.3.1",
+ "reqwest 0.12.28",
+ "serde",
+ "thiserror 1.0.69",
+ "tower-service",
+]
+
+[[package]]
+name = "reqwest-retry"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "105747e3a037fe5bf17458d794de91149e575b6183fc72c85623a44abb9683f5"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "futures",
+ "getrandom 0.2.16",
+ "http 1.3.1",
+ "hyper 1.6.0",
+ "reqwest 0.12.28",
+ "reqwest-middleware",
+ "retry-policies",
+ "thiserror 2.0.17",
+ "tokio",
+ "wasmtimer",
+]
+
+[[package]]
+name = "retry-policies"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc05fbf560421a0357a750cbe78c7ca19d4923918490daabba313d5dbc871e47"
+dependencies = [
+ "rand 0.10.1",
+]
+
[[package]]
name = "rgb"
version = "0.8.50"
@@ -11965,9 +12284,12 @@ dependencies = [
[[package]]
name = "roxmltree"
-version = "0.20.0"
+version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
+checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb"
+dependencies = [
+ "memchr",
+]
[[package]]
name = "rsa"
@@ -12022,7 +12344,7 @@ dependencies = [
"integer-encoding 4.0.2",
"lz4",
"parking_lot 0.12.4",
- "rand 0.9.1",
+ "rand 0.9.4",
"rsasl",
"rustls",
"snap",
@@ -12068,7 +12390,7 @@ dependencies = [
"proc-macro2",
"quote",
"regex",
- "relative-path",
+ "relative-path 1.9.3",
"rustc_version",
"syn 2.0.117",
"unicode-ident",
@@ -12289,7 +12611,7 @@ checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
- "jni",
+ "jni 0.21.1",
"log",
"once_cell",
"rustls",
@@ -12326,6 +12648,25 @@ version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+[[package]]
+name = "rustyline"
+version = "17.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e902948a25149d50edc1a8e0141aad50f54e22ba83ff988cf8f7c9ef07f50564"
+dependencies = [
+ "bitflags 2.11.1",
+ "cfg-if",
+ "clipboard-win",
+ "libc",
+ "log",
+ "memchr",
+ "nix 0.30.1",
+ "unicode-segmentation",
+ "unicode-width 0.2.1",
+ "utf8parse",
+ "windows-sys 0.60.2",
+]
+
[[package]]
name = "ryu"
version = "1.0.20"
@@ -12704,6 +13045,19 @@ dependencies = [
"unsafe-libyaml",
]
+[[package]]
+name = "serde_yaml_ng"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f"
+dependencies = [
+ "indexmap 2.13.0",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
[[package]]
name = "servers"
version = "1.1.0"
@@ -12800,7 +13154,7 @@ dependencies = [
"prost 0.14.1",
"query",
"quoted-string",
- "rand 0.9.1",
+ "rand 0.9.4",
"regex",
"reqwest 0.13.2",
"rust-embed",
@@ -12978,7 +13332,7 @@ version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c962f626b54771990066e5435ec8331d1462576cd2d1e62f24076ae014f92112"
dependencies = [
- "getrandom 0.3.3",
+ "getrandom 0.3.4",
"halfbrown",
"ref-cast",
"serde",
@@ -12987,6 +13341,16 @@ dependencies = [
"value-trait",
]
+[[package]]
+name = "simd_cesu8"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
[[package]]
name = "simdutf8"
version = "0.1.5"
@@ -13878,12 +14242,12 @@ dependencies = [
[[package]]
name = "syslog_loose"
-version = "0.21.0"
+version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "161028c00842709450114c39db3b29f44c898055ed8833bb9b535aba7facf30e"
+checksum = "d6ec4df26907adce53e94eac201a9ba38744baea3bc97f34ffd591d5646231a6"
dependencies = [
"chrono",
- "nom 7.1.3",
+ "nom 8.0.0",
]
[[package]]
@@ -14102,9 +14466,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tar"
-version = "0.4.45"
+version = "0.4.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
+checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
dependencies = [
"filetime",
"libc",
@@ -14127,12 +14491,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [
"fastrand",
- "getrandom 0.3.3",
+ "getrandom 0.3.4",
"once_cell",
"rustix 1.0.7",
"windows-sys 0.61.2",
]
+[[package]]
+name = "term"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
+dependencies = [
+ "dirs-next",
+ "rustversion",
+ "winapi",
+]
+
[[package]]
name = "term"
version = "1.0.2"
@@ -14185,7 +14560,7 @@ dependencies = [
"nix 0.28.0",
"partition",
"paste",
- "rand 0.9.1",
+ "rand 0.9.4",
"rand_chacha 0.9.0",
"reqwest 0.13.2",
"rustls",
@@ -14272,7 +14647,7 @@ dependencies = [
"plugins",
"prost 0.14.1",
"query",
- "rand 0.9.1",
+ "rand 0.9.4",
"rstest",
"rstest_reuse",
"sea-query",
@@ -14562,7 +14937,7 @@ dependencies = [
"pin-project-lite",
"postgres-protocol",
"postgres-types",
- "rand 0.9.1",
+ "rand 0.9.4",
"socket2 0.5.10",
"tokio",
"tokio-util",
@@ -15213,6 +15588,12 @@ version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
+[[package]]
+name = "unicode-general-category"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
+
[[package]]
name = "unicode-ident"
version = "1.0.22"
@@ -15346,11 +15727,21 @@ checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
dependencies = [
"getrandom 0.4.1",
"js-sys",
- "rand 0.9.1",
+ "rand 0.9.4",
"serde_core",
"wasm-bindgen",
]
+[[package]]
+name = "uuid-simd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
+dependencies = [
+ "outref",
+ "vsimd",
+]
+
[[package]]
name = "valuable"
version = "0.1.1"
@@ -15433,9 +15824,9 @@ dependencies = [
[[package]]
name = "vrl"
-version = "0.25.0"
+version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f49394b948406ea1564aa00152e011d87a38ad35d277ebddda257a9ee39c419"
+checksum = "925a4d3321b18a200c82c3ec02ee2be2b4bf16db07a5ce7e2a9a888b795ea862"
dependencies = [
"aes",
"aes-siv",
@@ -15465,8 +15856,10 @@ dependencies = [
"domain",
"dyn-clone",
"encoding_rs",
+ "exitcode",
"fancy-regex",
"flate2",
+ "getrandom 0.3.4",
"grok",
"hex",
"hmac",
@@ -15476,12 +15869,15 @@ dependencies = [
"indexmap 2.13.0",
"indoc",
"influxdb-line-protocol",
+ "ipcrypt-rs",
"itertools 0.14.0",
+ "jsonschema",
"lalrpop",
"lalrpop-util",
"lz4_flex 0.11.6",
"md-5 0.10.6",
- "nom 7.1.3",
+ "nom 8.0.0",
+ "nom-language",
"ofb",
"onig",
"ordered-float 4.6.0",
@@ -15490,20 +15886,27 @@ dependencies = [
"percent-encoding",
"pest",
"pest_derive",
+ "prettytable-rs",
"prost 0.13.5",
"prost-reflect",
"psl",
"psl-types",
"publicsuffix",
"quoted_printable",
- "rand 0.8.5",
+ "rand 0.9.4",
"regex",
+ "relative-path 2.0.1",
+ "reqwest 0.12.28",
+ "reqwest-middleware",
+ "reqwest-retry",
"roxmltree",
"rust_decimal",
+ "rustyline",
"seahash",
"serde",
"serde_json",
"serde_yaml",
+ "serde_yaml_ng",
"sha-1",
"sha2 0.10.9",
"sha3",
@@ -15511,6 +15914,8 @@ dependencies = [
"snafu 0.8.6",
"snap",
"strip-ansi-escapes",
+ "strum 0.26.3",
+ "strum_macros 0.26.4",
"syslog_loose",
"termcolor",
"thiserror 2.0.17",
@@ -15521,7 +15926,9 @@ dependencies = [
"url",
"utf8-width",
"uuid",
+ "webbrowser",
"woothee",
+ "xxhash-rust",
"zstd",
]
@@ -15565,15 +15972,6 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
-[[package]]
-name = "wasi"
-version = "0.14.2+wasi-0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
-dependencies = [
- "wit-bindgen-rt",
-]
-
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
@@ -15714,6 +16112,20 @@ dependencies = [
"semver",
]
+[[package]]
+name = "wasmtimer"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b"
+dependencies = [
+ "futures",
+ "js-sys",
+ "parking_lot 0.12.4",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen",
+]
+
[[package]]
name = "web-sys"
version = "0.3.95"
@@ -15734,6 +16146,22 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "webbrowser"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72"
+dependencies = [
+ "core-foundation 0.10.1",
+ "jni 0.22.4",
+ "log",
+ "ndk-context",
+ "objc2",
+ "objc2-foundation",
+ "url",
+ "web-sys",
+]
+
[[package]]
name = "webpki"
version = "0.22.4"
@@ -16020,6 +16448,15 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -16068,13 +16505,30 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
- "windows_i686_gnullvm",
+ "windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link 0.2.1",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
[[package]]
name = "windows-threading"
version = "0.1.0"
@@ -16102,6 +16556,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -16120,6 +16580,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -16138,12 +16604,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -16162,6 +16640,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -16180,6 +16664,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -16198,6 +16688,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@@ -16216,6 +16712,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
[[package]]
name = "winnow"
version = "0.5.40"
@@ -16263,15 +16765,6 @@ dependencies = [
"wit-parser",
]
-[[package]]
-name = "wit-bindgen-rt"
-version = "0.39.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
-dependencies = [
- "bitflags 2.11.1",
-]
-
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
@@ -16425,6 +16918,12 @@ dependencies = [
"rustix 1.0.7",
]
+[[package]]
+name = "xxhash-rust"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
+
[[package]]
name = "yaml-rust"
version = "0.4.5"
diff --git a/Cargo.toml b/Cargo.toml
index 32407f31cf..47b04be366 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -158,7 +158,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 = "7224c2ad6d11db612fbdb621c36135fc37ffce35" }
+greptime-proto = { git = "https://github.com/GreptimeTeam/greptime-proto.git", rev = "912f6c21017d5ab6c529568d36e7b8c30a94d84b" }
hex = "0.4"
http = "1"
humantime = "2.1"
@@ -178,7 +178,7 @@ nalgebra = "0.33"
nix = { version = "0.30.1", default-features = false, features = ["event", "fs", "process"] }
notify = "8.0"
num_cpus = "1.16"
-object_store_opendal = { git = "https://github.com/apache/opendal.git", rev = "4ad2d85296ffa6fdc2882f97d3c760ee243913f7" }
+object_store_opendal = "0.57"
once_cell = "1.18"
opentelemetry-proto = { version = "0.31", features = [
"gen-tonic",
@@ -259,7 +259,7 @@ tracing-opentelemetry = "0.31.0"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "fmt"] }
typetag = "0.2"
uuid = { version = "1.17", features = ["serde", "v4", "v7", "fast-rng"] }
-vrl = "0.25"
+vrl = "0.33"
zstd = "0.13"
# DO_NOT_REMOVE_THIS: END_OF_EXTERNAL_DEPENDENCIES
diff --git a/config/config.md b/config/config.md
index 0fae0caaa4..df06d2153c 100644
--- a/config/config.md
+++ b/config/config.md
@@ -14,6 +14,7 @@
| --- | -----| ------- | ----------- |
| `default_timezone` | String | Unset | The default timezone of the server. |
| `default_column_prefix` | String | Unset | The default column prefix for auto-created time index and value columns. |
+| `auto_create_table` | Bool | `true` | Server-side global switch for auto table creation on write.
When `false`, a missing table is never auto-created even if the request sets the `auto_create_table` hint to `true`. Default: `true`. |
| `user_provider` | String | Unset | The user provider for authentication.
Examples: "static_user_provider:file:/path/to/users", "static_user_provider:cmd:greptime_user=greptime_pwd" |
| `max_in_flight_write_bytes` | String | Unset | Maximum total memory for all concurrent write request bodies and messages (HTTP, gRPC, Flight).
Set to 0 to disable the limit. Default: "0" (unlimited) |
| `write_bytes_exhausted_policy` | String | Unset | Policy when write bytes quota is exhausted.
Options: "wait" (default, 10s timeout), "wait()" (e.g., "wait(30s)"), "fail" |
@@ -230,6 +231,7 @@
| --- | -----| ------- | ----------- |
| `default_timezone` | String | Unset | The default timezone of the server. |
| `default_column_prefix` | String | Unset | The default column prefix for auto-created time index and value columns. |
+| `auto_create_table` | Bool | `true` | Server-side global switch for auto table creation on write.
When `false`, a missing table is never auto-created even if the request sets the `auto_create_table` hint to `true`. Default: `true`. |
| `user_provider` | String | Unset | The user provider for authentication.
Examples: "static_user_provider:file:/path/to/users", "static_user_provider:cmd:greptime_user=greptime_pwd" |
| `max_in_flight_write_bytes` | String | Unset | Maximum total memory for all concurrent write request bodies and messages (HTTP, gRPC, Flight).
Set to 0 to disable the limit. Default: "0" (unlimited) |
| `write_bytes_exhausted_policy` | String | Unset | Policy when write bytes quota is exhausted.
Options: "wait" (default, 10s timeout), "wait()" (e.g., "wait(30s)"), "fail" |
@@ -449,6 +451,7 @@
| `init_regions_in_background` | Bool | `false` | Initialize all regions in the background during the startup.
By default, it provides services after all regions have been initialized. |
| `init_regions_parallelism` | Integer | `16` | Parallelism of initializing regions. |
| `max_concurrent_queries` | Integer | `0` | The maximum concurrent queries allowed to be executed. Zero means unlimited. |
+| `concurrent_query_limiter_timeout` | String | `100ms` | Timeout to acquire a permit from the concurrent query limiter when `max_concurrent_queries` is reached. |
| `enable_telemetry` | Bool | `true` | Enable telemetry to collect anonymous usage data. Enabled by default. |
| `http` | -- | -- | The HTTP server options. |
| `http.addr` | String | `127.0.0.1:4000` | The address to bind the HTTP server. |
@@ -628,6 +631,7 @@
| `flow.batching_mode.experimental_frontend_scan_timeout` | String | `30s` | Flow wait for available frontend timeout,
if failed to find available frontend after frontend_scan_timeout elapsed, return error
which prevent flownode from starting |
| `flow.batching_mode.experimental_max_filter_num_per_query` | Integer | `20` | Maximum number of filters allowed in a single query |
| `flow.batching_mode.experimental_time_window_merge_threshold` | Integer | `3` | Time window merge distance |
+| `flow.batching_mode.experimental_enable_incremental_read` | Bool | `false` | Whether to enable experimental flow incremental source reads.
When disabled, batching flows always execute full-snapshot queries.
Can be overridden per flow with WITH (experimental_enable_incremental_read = 'true'). |
| `flow.batching_mode.read_preference` | String | `Leader` | Read preference of the Frontend client. |
| `flow.batching_mode.frontend_tls` | -- | -- | -- |
| `flow.batching_mode.frontend_tls.enabled` | Bool | `false` | Whether to enable TLS for client. |
diff --git a/config/datanode.example.toml b/config/datanode.example.toml
index d558918daf..9351c4e85d 100644
--- a/config/datanode.example.toml
+++ b/config/datanode.example.toml
@@ -20,6 +20,9 @@ init_regions_parallelism = 16
## The maximum concurrent queries allowed to be executed. Zero means unlimited.
max_concurrent_queries = 0
+## Timeout to acquire a permit from the concurrent query limiter when `max_concurrent_queries` is reached.
+concurrent_query_limiter_timeout = "100ms"
+
## Enable telemetry to collect anonymous usage data. Enabled by default.
#+ enable_telemetry = true
diff --git a/config/flownode.example.toml b/config/flownode.example.toml
index 2c053e6e8c..ff8a9e4a50 100644
--- a/config/flownode.example.toml
+++ b/config/flownode.example.toml
@@ -31,6 +31,10 @@ node_id = 14
#+experimental_max_filter_num_per_query=20
## Time window merge distance
#+experimental_time_window_merge_threshold=3
+## Whether to enable experimental flow incremental source reads.
+## When disabled, batching flows always execute full-snapshot queries.
+## Can be overridden per flow with WITH (experimental_enable_incremental_read = 'true').
+#+experimental_enable_incremental_read=false
## Read preference of the Frontend client.
#+read_preference="Leader"
[flow.batching_mode.frontend_tls]
diff --git a/config/frontend.example.toml b/config/frontend.example.toml
index 39f38fbef9..a044aebda6 100644
--- a/config/frontend.example.toml
+++ b/config/frontend.example.toml
@@ -6,6 +6,10 @@ default_timezone = "UTC"
## @toml2docs:none-default
default_column_prefix = "greptime"
+## Server-side global switch for auto table creation on write.
+## When `false`, a missing table is never auto-created even if the request sets the `auto_create_table` hint to `true`. Default: `true`.
+#+ auto_create_table = true
+
## The user provider for authentication.
## Examples: "static_user_provider:file:/path/to/users", "static_user_provider:cmd:greptime_user=greptime_pwd"
## @toml2docs:none-default
diff --git a/config/standalone.example.toml b/config/standalone.example.toml
index d5c42e744c..5740e0e1cf 100644
--- a/config/standalone.example.toml
+++ b/config/standalone.example.toml
@@ -6,6 +6,10 @@ default_timezone = "UTC"
## @toml2docs:none-default
default_column_prefix = "greptime"
+## Server-side global switch for auto table creation on write.
+## When `false`, a missing table is never auto-created even if the request sets the `auto_create_table` hint to `true`. Default: `true`.
+#+ auto_create_table = true
+
## The user provider for authentication.
## Examples: "static_user_provider:file:/path/to/users", "static_user_provider:cmd:greptime_user=greptime_pwd"
## @toml2docs:none-default
diff --git a/docs/rfcs/2026-05-28-table-semantic-layer.md b/docs/rfcs/2026-05-28-table-semantic-layer.md
new file mode 100644
index 0000000000..e4d899d704
--- /dev/null
+++ b/docs/rfcs/2026-05-28-table-semantic-layer.md
@@ -0,0 +1,157 @@
+---
+Feature Name: Table Semantic Layer
+Tracking Issue: TBD
+Date: 2026-05-28
+Author: "Dennis Zhuang "
+---
+
+# Summary
+
+Attach a thin layer of semantic metadata to each table so machine consumers — LLM agents, alert generators, dashboard builders, MCP servers, ETL pipelines — can align it with the observability concepts they already know (OTel instrument kinds, Prometheus naming conventions, UCUM units, semantic conventions, severity numbers, OTel ↔ Prometheus translation rules).
+
+The mechanism reuses what already exists in `table_options` (the same slot that today carries `table_data_model` and `otlp_metric_compat`): a reserved `greptime.semantic.*` namespace, plus standard SQL column `COMMENT` for field-level supplements, plus an `information_schema.semantic_tables` view as the discovery entry point. No new protocol, no new DDL keyword.
+
+Per-table identity only. Cross-table relationships are deferred.
+
+# Motivation
+
+GreptimeDB already ingests OTLP metrics / traces / logs and Prometheus remote write. Each protocol carries rich metadata on the wire (instrument kind, temporality, unit, scope, resource, semantic-conventions version), and most of it is dropped when rows land in a table:
+
+- An `opentelemetry_traces` table looks like any wide table; signal type, source, and field provenance must be guessed from naming.
+- The OTel-to-Prometheus translation in v0.16+ actively drops scope attributes and most resource attributes; the table never records *what was dropped*.
+- Prometheus remote write v1 metadata is unreliable by protocol, but downstream tables do not flag whether `counter` typing was *declared* or *inferred* from the `_total` suffix.
+- Mixed-temporality data (OTel delta + Prometheus cumulative in the same table) is unrecoverable from schema alone.
+
+The audience is broader than LLM agents. Alert generators need to choose between `rate()` and absolute thresholds, and need units to pick sensible bounds. Dashboard builders pick visualisations by signal type. MCP servers surface a structured tool catalog instead of free-text descriptions. ETL pipelines need lineage to know whether a `service_name` column is `resource.service.name` or a free-form label. All of them currently guess from column names; the metadata to remove the guess already exists at ingest time, we just do not preserve it.
+
+# Goals
+
+1. Tag every ingested table with a stable identity using existing SQL surfaces — no new protocol, no new DDL keyword.
+2. Record the lossy transformations the ingestion path performs (dropped attributes, scope handling, type inference vs. declaration).
+3. Expose one `information_schema` view as the consumer-facing discovery entry point.
+4. Keep the layer optional and additive — tables without these options keep working unchanged.
+
+# Non-Goals
+
+- Cross-table relationship modelling. Deferred to a follow-up RFC.
+- Bespoke storage. Reuse `table_options` and column `COMMENT`.
+- Semantic enforcement at query time. The layer is descriptive, not coercive.
+- New wire protocol. Upstream standardisation is mentioned only as a future direction.
+
+# Proposal
+
+## Three mechanisms
+
+1. **`greptime.semantic.*` table options** — table-level identity and lineage. Carried inside the existing `table_options` blob. This is the same slot that today carries `table_data_model = 'greptime_trace_v1'` and `otlp_metric_compat = 'prom'`, so the mechanism is generalising what the OTLP trace auto-create path already does.
+2. **Column `COMMENT`** — column-level supplements ("this column is `resource.service.name`"; "this column carries delta values"). Standard SQL.
+3. **`information_schema.semantic_tables` view** — a denormalised projection of the options, registered through the existing `with_extra_table_factories()` hook. Tables without a `greptime.semantic.*` option do not appear in the view.
+
+## Vocabulary
+
+All keys are flat strings under the `greptime.semantic.` prefix; values are strings; unknown keys are tolerated so the vocabulary can grow without coordinated rollouts.
+
+**Common (all signals)**
+
+| Key | Example |
+| --- | --- |
+| `greptime.semantic.signal_type` | `trace` / `log` / `metric` / `event` |
+| `greptime.semantic.source` | `opentelemetry` / `prometheus` / `elasticsearch` / `loki` / `custom` |
+| `greptime.semantic.source_version` | protocol or SDK version, e.g. `v2` (Prom remote write), `1.30.0` (optional) |
+| `greptime.semantic.pipeline` | `greptime_trace_v1` (subsumes the existing `table_data_model` value) |
+
+**Trace**: `greptime.semantic.trace.conventions` (e.g. `otel-semconv-1.27`, lifted from `schema_url`, which is the version of the OpenTelemetry semantic conventions used in this table), `greptime.semantic.trace.has_events`, `greptime.semantic.trace.has_links`.
+
+**Metric** — v1 assumes one metric type per table, which is how both Prom RW and the post-v0.16 OTel ingestion path land data today; mixed-type tables are a follow-up.
+
+| Key | Example |
+| --- | --- |
+| `greptime.semantic.metric.type` | `counter` / `gauge` / `histogram` / `summary` / `updown_counter` / `gauge_histogram` / `info` / `stateset` |
+| `greptime.semantic.metric.unit` | UCUM, e.g. `s`, `By`, `{request}` |
+| `greptime.semantic.metric.temporality` | `cumulative` / `delta` (OTel only) |
+| `greptime.semantic.metric.monotonic` | `true` / `false` |
+| `greptime.semantic.metric.metadata_quality` | `declared` (OTLP / Prom RW v2 / exposition) or `inferred` (Prom RW v1, name-suffix guess) |
+| `greptime.semantic.metric.original_name` | Pre-translation OTel name when the table name was Prometheus-ised |
+
+`metadata_quality = inferred` is the load-bearing field for confidence-aware tooling: an inferred counter should be re-checked before betting on `rate()`-style semantics.
+
+**Log**: `greptime.semantic.log.severity_scheme` (`otlp` / `syslog` / `custom`), `greptime.semantic.log.body_format` (`string` / `json` / `mixed`).
+
+**Resource / scope preservation**: `greptime.semantic.resource.attributes_preserved` (JSON array string of attrs promoted to columns), `greptime.semantic.resource.attributes_dropped` (boolean), `greptime.semantic.scope.preserved` (boolean). These answer the most common downstream question: "is this data missing because it was dropped, or because it lives on a different column than I think?" List-shaped values use JSON array strings rather than comma-separated text to avoid escaping and ordering ambiguity.
+
+## Conflict and update semantics
+
+Two design decisions worth pinning down up front, because they constrain everything else:
+
+- **Conflict.** Some table-level keys (`trace.conventions` lifted from `schema_url`, `metric.temporality`, ...) cannot represent the truth when a long-lived table sees rows from multiple sources. v1 records `mixed` or `unknown` rather than a fictitious single value. Downstream consumers must treat any single-valued semantic key as best-effort, not strong evidence.
+- **Update.** Semantic options are stamped at table creation. v1 does not specify an update path; promoting `metadata_quality` from `inferred` to `declared`, refreshing `resource.attributes_preserved`, or revising `trace.conventions` on later writes is deferred. If real usage shows update is needed, it lands as a separate RFC.
+
+## `information_schema.semantic_tables`
+
+A consumer's first SQL on connect:
+
+```sql
+SELECT table_catalog, table_schema, table_name, signal_type, source, pipeline
+FROM information_schema.semantic_tables;
+```
+
+returns one row per semantic-tagged table. The view exposes a stable set of core columns (`table_catalog`, `table_schema`, `table_name`, `signal_type`, `source`, `source_version`, `pipeline`) plus a `semantic_options` JSON column carrying the rest of the `greptime.semantic.*` keys verbatim. Future keys appear inside `semantic_options` without forcing a view-schema change; only widely-used keys are ever promoted to first-class columns.
+
+# Implementation Plan
+
+Four phases, each independently shippable.
+
+1. **Identity.** Stamp `signal_type` and `source` on every auto-create path. The OTLP paths already have natural injection points; Prom remote write is the one non-trivial path because metric-engine logical tables share physical storage (see Open Question 2).
+2. **Metric specifics.** Add type / unit / temporality / monotonic / metadata_quality / original_name at OTel metric and Prom RW ingestion sites; the data is already at hand inside the OTel translator.
+3. **Resource / scope lineage.** Record what the OTel-to-Prometheus translation kept and dropped.
+4. **`information_schema.semantic_tables` view + documentation** as a stable user-facing contract.
+
+# Relationship to OpenTelemetry standardisation
+
+OTel today standardises what producers emit and how data collectors are managed; the read side — what a backend exposes back to clients — is deliberately vendor turf. OTLP is one-way; OpAMP is agent management; OTEP-0243 (App Telemetry Schema) is producer-side; `schema_url` is producer-stated with no reverse. Adjacent precedents — Prometheus `/api/v1/metadata`, Loki labels API, Tempo tags, Jaeger services, ad-hoc MCP servers — are all vendor-specific.
+
+This is a real gap. The shape we propose locally (signal-agnostic, `schema_url`-aware, structured around a small vocabulary) is deliberately close to what a future upstream OTEP for a backend-catalog read API could look like, with Weaver's *Resolved Telemetry Schema* as the natural data model. We do not commit to driving such an OTEP here; we do commit to keeping the local shape close enough that a future upstream proposal does not force a breaking migration.
+
+# Alternatives
+
+- **New DDL syntax (`SEMANTIC trace WITH (...)`).** Cleaner-looking but non-standard and forces every client to learn it. The metadata is not interesting enough to justify a new keyword.
+- **Dedicated `_semantic` system table.** Doubles the storage path for what is static per-table KV and adds lifecycle questions (drop, backfill). A view over `table_options` covers the same access pattern.
+- **Column comments only.** Discovery (`WHERE signal_type = 'trace'`) becomes a full-text problem. Comments are good for column-level supplements, not for identity.
+- **Encode everything into the table name.** What we do today. Every new field becomes a new naming convention.
+
+# Open Questions
+
+1. **Namespace prefix.** `greptime.semantic.*` vs. bare `semantic.*`. v1 picks the vendored prefix; alias or migrate if a community standard later emerges.
+2. **Prom RW injection point.** Metric-engine logical tables share physical storage, so per-logical-table options need a hook that does not exist as cleanly as the OTLP trace branch. A short spike before Phase 1 lands for Prom RW.
+3. **Mixed-type metric tables.** When ingestion modes that pack multiple metric types into one table appear, `metric.type` migrates from table-level to row-level. v1 leaves a `metric.type = 'mixed'` marker and punts.
+4. **Stability surface.** Top-level keys (`signal_type`, `source`) are stable; sub-namespaces (`metric.*`, ...) are evolving until v1.0 of the layer is declared.
+
+# Future Work
+
+- **Cross-table relationships.** Paired trace/services tables, metric/info pairing, JOIN hints. Its own RFC.
+- **Producer SDK/client identity.** An optional `greptime.semantic.source.sdk` key recording the emitting client (e.g. `opentelemetry-go`, `opentelemetry-java`, `opentelemetry-collector`). Because a single table can receive data from multiple SDKs (a shared trace table is the common case), mixed producers collapse to `mixed`, following the same conflict rule as the table-level keys above.
+- **Backfill** for tables created before this feature shipped.
+- **Upstream proposal.** Carry the shape into a community proposal — likely an OTEP for an OTLP-Catalog read API plus an MCP binding — informed by Greptime's local usage data.
+
+# References
+
+OpenTelemetry:
+- [OTLP specification](https://opentelemetry.io/docs/specs/otlp/)
+- [OTel Schemas (`schema_url`)](https://opentelemetry.io/docs/specs/otel/schemas/)
+- [Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/)
+- [OTEP-0243: App Telemetry Schema](https://github.com/open-telemetry/oteps/blob/main/text/0243-app-telemetry-schema-vision-roadmap.md)
+- [OpAMP specification](https://github.com/open-telemetry/opamp-spec/blob/main/specification.md)
+- [Weaver: Resolved Telemetry Schema](https://github.com/open-telemetry/weaver)
+- [2025 Stability Proposal](https://opentelemetry.io/blog/2025/stability-proposal-announcement/)
+
+Prometheus / OpenMetrics:
+- [Prometheus Remote Write 1.0](https://prometheus.io/docs/specs/prw/remote_write_spec/)
+- [Prometheus Remote Write 2.0](https://prometheus.io/docs/specs/prw/remote_write_spec_2_0/)
+- [Prometheus exposition formats](https://prometheus.io/docs/instrumenting/exposition_formats/)
+- [Prometheus HTTP API: `/api/v1/metadata`](https://prometheus.io/docs/prometheus/latest/querying/api/#querying-metric-metadata)
+
+Units and conventions:
+- [UCUM — Unified Code for Units of Measure](https://ucum.org/)
+
+GreptimeDB:
+- [OTLP ingestion guide](https://docs.greptime.com/user-guide/ingest-data/for-observability/opentelemetry/)
+- [Trace data model](https://docs.greptime.com/user-guide/traces/data-model/)
diff --git a/src/auth/src/permission.rs b/src/auth/src/permission.rs
index 88adfda633..8914635290 100644
--- a/src/auth/src/permission.rs
+++ b/src/auth/src/permission.rs
@@ -16,6 +16,7 @@ use std::fmt::Debug;
use std::sync::Arc;
use api::v1::greptime_request::Request;
+use api::v1::query_request::Query;
use common_telemetry::debug;
use sql::statements::statement::Statement;
@@ -42,10 +43,12 @@ impl<'a> PermissionReq<'a> {
/// Returns true if the permission request is for read operations.
pub fn is_readonly(&self) -> bool {
match self {
- PermissionReq::GrpcRequest(Request::Query(_))
- | PermissionReq::PromQuery
- | PermissionReq::LogQuery
- | PermissionReq::PromStoreRead => true,
+ PermissionReq::GrpcRequest(Request::Query(query_request)) => {
+ !matches!(query_request.query, Some(Query::InsertIntoPlan(_)))
+ }
+ PermissionReq::PromQuery | PermissionReq::LogQuery | PermissionReq::PromStoreRead => {
+ true
+ }
PermissionReq::SqlStatement(stmt) => stmt.is_readonly(),
PermissionReq::GrpcRequest(_)
@@ -196,4 +199,14 @@ mod tests {
assert!(matches!(read_result, PermissionResp::Reject));
assert!(matches!(write_result, PermissionResp::Allow));
}
+
+ #[test]
+ fn test_grpc_insert_into_plan_is_write_request() {
+ let request = Request::Query(api::v1::QueryRequest {
+ query: Some(Query::InsertIntoPlan(api::v1::InsertIntoPlan::default())),
+ });
+ let req = PermissionReq::GrpcRequest(&request);
+
+ assert!(req.is_write());
+ }
}
diff --git a/src/catalog/src/kvbackend/table_cache.rs b/src/catalog/src/kvbackend/table_cache.rs
index 42b3fbc74b..13f74a48c9 100644
--- a/src/catalog/src/kvbackend/table_cache.rs
+++ b/src/catalog/src/kvbackend/table_cache.rs
@@ -14,7 +14,9 @@
use std::sync::Arc;
-use common_meta::cache::{CacheContainer, Initializer, TableInfoCacheRef, TableNameCacheRef};
+use common_meta::cache::{
+ CacheContainer, InitStrategy, Initializer, TableInfoCacheRef, TableNameCacheRef,
+};
use common_meta::error::{Result as MetaResult, ValueNotExistSnafu};
use common_meta::instruction::CacheIdent;
use futures::future::BoxFuture;
@@ -38,7 +40,14 @@ pub fn new_table_cache(
) -> TableCache {
let init = init_factory(table_info_cache, table_name_cache);
- CacheContainer::new(name, cache, Box::new(invalidator), init, filter)
+ CacheContainer::with_strategy(
+ name,
+ cache,
+ Box::new(invalidator),
+ init,
+ filter,
+ InitStrategy::VersionChecked,
+ )
}
fn init_factory(
diff --git a/src/cli/src/data/export_v2/command.rs b/src/cli/src/data/export_v2/command.rs
index db0f576a4e..bb027bbef1 100644
--- a/src/cli/src/data/export_v2/command.rs
+++ b/src/cli/src/data/export_v2/command.rs
@@ -1077,7 +1077,9 @@ async fn verify_snapshot(storage: &OpenDalStorage) -> Result {
));
}
let data_files = storage.list_files_recursive("data/").await?;
- if let Some(path) = data_files.first() {
+ // Report the lexicographically smallest path so the message is stable
+ // regardless of listing order across backends.
+ if let Some(path) = data_files.iter().min() {
report.push_error(format!(
"Schema-only snapshot should not contain data files (found '{}')",
path
@@ -1103,75 +1105,113 @@ fn summarize_chunks(manifest: &Manifest) -> VerifyChunkSummary {
}
}
+/// A data file declared by a completed chunk that is expected to exist in storage.
+#[derive(Debug)]
+struct ChunkFile {
+ chunk_id: u32,
+ path: String,
+}
+
+/// Expected snapshot contents derived purely from the manifest (no object-store IO).
+///
+/// Separating planning from scanning makes it obvious which problems come from
+/// the manifest alone and which require comparing against actual storage.
+#[derive(Debug, Default)]
+struct VerifyPlan {
+ /// Valid data files declared by completed chunks; each must exist in storage.
+ files_to_check: Vec,
+ /// All syntactically-safe data paths declared by any chunk, regardless of
+ /// status. Used as the orphan-detection baseline so a listed-but-invalid
+ /// file is not also reported as unexpected.
+ claimed_data_files: HashSet,
+ /// Total data-file references in completed chunks (valid + invalid).
+ data_files_total: usize,
+ /// Problems detectable from the manifest alone.
+ problems: Vec,
+}
+
+/// Actual data files discovered under `data/` (the only object-store IO in
+/// chunk/data-file verification).
+#[derive(Debug)]
+struct VerifyDataScan {
+ existing_data_files: HashSet,
+}
+
+/// Result of reconciling the manifest plan against the storage scan.
+#[derive(Debug, Default)]
+struct VerifyOutcome {
+ data_files_total: usize,
+ data_files_verified: usize,
+ problems: Vec,
+}
+
async fn verify_chunks_and_data_files(
storage: &OpenDalStorage,
report: &mut VerifyReport,
) -> Result<()> {
- let existing_files: HashSet<_> = storage
- .list_files_recursive("data/")
- .await?
- .into_iter()
- .collect();
- let mut data_files_total = 0;
- let mut data_files_verified = 0;
- let mut problems = Vec::new();
- let mut seen_chunk_ids = HashSet::new();
- let mut claimed_data_files = HashSet::new();
+ let plan = build_verify_plan(&report.manifest);
+ let scan = scan_data_files(storage).await?;
+ let outcome = reconcile_plan_with_scan(plan, &scan);
- for chunk in &report.manifest.chunks {
+ report.data_files_total = outcome.data_files_total;
+ report.data_files_verified = outcome.data_files_verified;
+ report.problems.extend(outcome.problems);
+
+ Ok(())
+}
+
+/// Builds the expected-state plan from the manifest. Pure; performs no IO.
+fn build_verify_plan(manifest: &Manifest) -> VerifyPlan {
+ let mut plan = VerifyPlan::default();
+ let mut seen_chunk_ids = HashSet::new();
+
+ for chunk in &manifest.chunks {
if !seen_chunk_ids.insert(chunk.id) {
- problems.push(VerifyProblem {
+ plan.problems.push(VerifyProblem {
severity: VerifySeverity::Error,
message: format!("Chunk {}: duplicate chunk id", chunk.id),
});
}
for file in &chunk.files {
if let Some(path) = safe_manifest_data_file_path(file) {
- claimed_data_files.insert(path.to_string());
+ plan.claimed_data_files.insert(path.to_string());
}
}
match chunk.status {
ChunkStatus::Completed => {
if chunk.files.is_empty() {
- problems.push(VerifyProblem {
+ plan.problems.push(VerifyProblem {
severity: VerifySeverity::Error,
message: format!("Chunk {}: completed chunk has no data files", chunk.id),
});
continue;
}
- let allowed_prefixes = report
- .manifest
+ let allowed_prefixes = manifest
.schemas
.iter()
.map(|schema| data_dir_for_schema_chunk(schema, chunk.id))
.collect::>();
for file in &chunk.files {
- data_files_total += 1;
- let Some(path) = valid_manifest_data_file_path(file, &allowed_prefixes) else {
- problems.push(VerifyProblem {
+ plan.data_files_total += 1;
+ match valid_manifest_data_file_path(file, &allowed_prefixes) {
+ Some(path) => plan.files_to_check.push(ChunkFile {
+ chunk_id: chunk.id,
+ path: path.to_string(),
+ }),
+ None => plan.problems.push(VerifyProblem {
severity: VerifySeverity::Error,
message: format!(
"Chunk {}: invalid data file path '{}'",
chunk.id, file
),
- });
- continue;
- };
-
- if existing_files.contains(path) {
- data_files_verified += 1;
- } else {
- problems.push(VerifyProblem {
- severity: VerifySeverity::Error,
- message: format!("Chunk {}: missing file '{}'", chunk.id, path),
- });
+ }),
}
}
}
ChunkStatus::Skipped => {
if !chunk.files.is_empty() {
- problems.push(VerifyProblem {
+ plan.problems.push(VerifyProblem {
severity: VerifySeverity::Error,
message: format!(
"Chunk {}: skipped chunk should not list data files",
@@ -1181,20 +1221,20 @@ async fn verify_chunks_and_data_files(
}
}
ChunkStatus::Pending => {
- problems.push(VerifyProblem {
+ plan.problems.push(VerifyProblem {
severity: VerifySeverity::Error,
message: format!("Chunk {}: status is 'pending'", chunk.id),
});
}
ChunkStatus::InProgress => {
- problems.push(VerifyProblem {
+ plan.problems.push(VerifyProblem {
severity: VerifySeverity::Error,
message: format!("Chunk {}: status is 'in_progress'", chunk.id),
});
}
ChunkStatus::Failed => {
let reason = chunk.error.as_deref().unwrap_or("unknown error");
- problems.push(VerifyProblem {
+ plan.problems.push(VerifyProblem {
severity: VerifySeverity::Error,
message: format!("Chunk {}: status is 'failed' (error: {})", chunk.id, reason),
});
@@ -1202,20 +1242,60 @@ async fn verify_chunks_and_data_files(
}
}
- for path in &existing_files {
- if !claimed_data_files.contains(path) {
+ plan
+}
+
+/// Lists all data files under `data/`. This is the only object-store IO in
+/// chunk/data-file verification.
+async fn scan_data_files(storage: &OpenDalStorage) -> Result {
+ let existing_data_files = storage
+ .list_files_recursive("data/")
+ .await?
+ .into_iter()
+ .collect();
+ Ok(VerifyDataScan {
+ existing_data_files,
+ })
+}
+
+/// Reconciles the manifest plan against the storage scan. Pure; performs no IO.
+///
+/// Emits missing-file problems for expected files absent from storage and
+/// unexpected-file problems for storage files no chunk claims. Unexpected files
+/// are sorted by path so output is deterministic regardless of listing order.
+fn reconcile_plan_with_scan(plan: VerifyPlan, scan: &VerifyDataScan) -> VerifyOutcome {
+ let mut problems = plan.problems;
+ let mut data_files_verified = 0;
+
+ for file in &plan.files_to_check {
+ if scan.existing_data_files.contains(&file.path) {
+ data_files_verified += 1;
+ } else {
problems.push(VerifyProblem {
severity: VerifySeverity::Error,
- message: format!("Unexpected data file '{}' is not listed in manifest", path),
+ message: format!("Chunk {}: missing file '{}'", file.chunk_id, file.path),
});
}
}
- report.data_files_total = data_files_total;
- report.data_files_verified = data_files_verified;
- report.problems.extend(problems);
+ let mut orphans: Vec<&String> = scan
+ .existing_data_files
+ .iter()
+ .filter(|path| !plan.claimed_data_files.contains(*path))
+ .collect();
+ orphans.sort();
+ for path in orphans {
+ problems.push(VerifyProblem {
+ severity: VerifySeverity::Error,
+ message: format!("Unexpected data file '{}' is not listed in manifest", path),
+ });
+ }
- Ok(())
+ VerifyOutcome {
+ data_files_total: plan.data_files_total,
+ data_files_verified,
+ problems,
+ }
}
fn valid_manifest_data_file_path<'a>(
@@ -2294,6 +2374,90 @@ mod tests {
);
}
+ #[test]
+ fn test_build_verify_plan_classifies_chunks_without_io() {
+ let mut manifest = test_manifest(
+ chrono::Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(),
+ false,
+ true,
+ );
+ // test_manifest(complete) gives: chunk 1 completed (1 file), chunk 2 skipped.
+ let mut failed = ChunkMeta::new(3, TimeRange::unbounded());
+ failed.mark_failed("boom".to_string());
+ manifest.chunks.push(failed);
+ manifest
+ .chunks
+ .push(ChunkMeta::new(4, TimeRange::unbounded()));
+
+ let plan = build_verify_plan(&manifest);
+
+ assert_eq!(plan.files_to_check.len(), 1);
+ assert_eq!(plan.files_to_check[0].chunk_id, 1);
+ assert_eq!(plan.files_to_check[0].path, "data/public/1/file.parquet");
+ assert_eq!(plan.data_files_total, 1);
+ assert!(
+ plan.claimed_data_files
+ .contains("data/public/1/file.parquet")
+ );
+ assert_eq!(plan.problems.len(), 2);
+ assert!(
+ plan.problems
+ .iter()
+ .any(|problem| problem.message.contains("status is 'failed'"))
+ );
+ assert!(
+ plan.problems
+ .iter()
+ .any(|problem| problem.message.contains("status is 'pending'"))
+ );
+ }
+
+ #[tokio::test]
+ async fn test_verify_snapshot_produces_deterministic_problem_output() {
+ let dir = tempdir().unwrap();
+ let manifest = test_manifest(
+ chrono::Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(),
+ false,
+ true,
+ );
+ write_root_manifest(dir.path(), manifest);
+ write_snapshot_file(dir.path(), "schema/schemas.json", b"[]");
+ write_default_ddl_files(dir.path());
+ write_snapshot_file(dir.path(), "data/public/1/file.parquet", b"data");
+ // Many orphan files under a known chunk prefix to stress ordering.
+ for i in 0..50 {
+ write_snapshot_file(
+ dir.path(),
+ &format!("data/public/1/orphan_{:02}.parquet", i),
+ b"x",
+ );
+ }
+
+ let storage = file_storage_for_dir(dir.path());
+ let messages = |report: &VerifyReport| {
+ report
+ .problems
+ .iter()
+ .map(|problem| problem.message.clone())
+ .collect::>()
+ };
+ let first = messages(&verify_snapshot(&storage).await.unwrap());
+ let second = messages(&verify_snapshot(&storage).await.unwrap());
+
+ // Output is identical across runs despite HashSet-based scanning.
+ assert_eq!(first, second);
+
+ let orphans = first
+ .iter()
+ .filter(|message| message.contains("Unexpected data file"))
+ .cloned()
+ .collect::>();
+ assert_eq!(orphans.len(), 50);
+ let mut sorted = orphans.clone();
+ sorted.sort();
+ assert_eq!(orphans, sorted);
+ }
+
fn write_test_manifest(root: &std::path::Path, dir: &str, manifest: Manifest) {
let snapshot_dir = root.join(dir);
std::fs::create_dir_all(&snapshot_dir).unwrap();
diff --git a/src/cmd/src/datanode.rs b/src/cmd/src/datanode.rs
index 3c106bd43f..7f2057bc97 100644
--- a/src/cmd/src/datanode.rs
+++ b/src/cmd/src/datanode.rs
@@ -79,7 +79,7 @@ impl App for Instance {
}
async fn start(&mut self) -> Result<()> {
- plugins::start_datanode_plugins(self.datanode.plugins())
+ plugins::start_datanode_plugins(&self.datanode)
.await
.context(StartDatanodeSnafu)?;
diff --git a/src/cmd/src/datanode/scanbench.rs b/src/cmd/src/datanode/scanbench.rs
index b26705991c..b2a715ad31 100644
--- a/src/cmd/src/datanode/scanbench.rs
+++ b/src/cmd/src/datanode/scanbench.rs
@@ -524,6 +524,7 @@ impl ScanbenchCommand {
options: HashMap::default(),
skip_wal_replay: !self.enable_wal,
checkpoint: None,
+ requirements: Default::default(),
};
engine
diff --git a/src/cmd/src/flownode.rs b/src/cmd/src/flownode.rs
index 6228cbd3f3..32d9070ec6 100644
--- a/src/cmd/src/flownode.rs
+++ b/src/cmd/src/flownode.rs
@@ -90,7 +90,7 @@ impl App for Instance {
}
async fn start(&mut self) -> Result<()> {
- plugins::start_flownode_plugins(self.flownode.flow_engine().plugins().clone())
+ plugins::start_flownode_plugins(&self.flownode)
.await
.context(StartFlownodeSnafu)?;
diff --git a/src/cmd/src/frontend.rs b/src/cmd/src/frontend.rs
index da2c111e7c..cbc07d10e9 100644
--- a/src/cmd/src/frontend.rs
+++ b/src/cmd/src/frontend.rs
@@ -95,8 +95,7 @@ impl App for Instance {
}
async fn start(&mut self) -> Result<()> {
- let plugins = self.frontend.instance.plugins().clone();
- plugins::start_frontend_plugins(plugins)
+ plugins::start_frontend_plugins(&self.frontend.instance)
.await
.context(error::StartFrontendSnafu)?;
diff --git a/src/cmd/src/metasrv.rs b/src/cmd/src/metasrv.rs
index bf3cb2f5e7..e30b115ada 100644
--- a/src/cmd/src/metasrv.rs
+++ b/src/cmd/src/metasrv.rs
@@ -68,7 +68,7 @@ impl App for Instance {
}
async fn start(&mut self) -> Result<()> {
- plugins::start_metasrv_plugins(self.instance.plugins())
+ plugins::start_metasrv_plugins(&self.instance)
.await
.context(StartMetaServerSnafu)?;
diff --git a/src/cmd/src/standalone.rs b/src/cmd/src/standalone.rs
index e0f2c673ff..7d99e99554 100644
--- a/src/cmd/src/standalone.rs
+++ b/src/cmd/src/standalone.rs
@@ -164,7 +164,7 @@ impl App for Instance {
.start(self.leader_services_context.clone())
.await?;
- plugins::start_frontend_plugins(self.frontend.instance.plugins().clone())
+ plugins::start_frontend_plugins(&self.frontend.instance)
.await
.context(error::StartFrontendSnafu)?;
diff --git a/src/cmd/tests/load_config_test.rs b/src/cmd/tests/load_config_test.rs
index 6cffcd67c2..cee29e4456 100644
--- a/src/cmd/tests/load_config_test.rs
+++ b/src/cmd/tests/load_config_test.rs
@@ -114,6 +114,7 @@ fn test_load_frontend_example_config() {
component: FrontendOptions {
default_timezone: Some("UTC".to_string()),
default_column_prefix: Some("greptime".to_string()),
+ auto_create_table: true,
meta_client: Some(MetaClientOptions {
metasrv_addrs: vec!["127.0.0.1:3002".to_string()],
timeout: Duration::from_secs(3),
@@ -267,6 +268,7 @@ fn test_load_standalone_example_config() {
component: StandaloneOptions {
default_timezone: Some("UTC".to_string()),
default_column_prefix: Some("greptime".to_string()),
+ auto_create_table: true,
wal: DatanodeWalConfig::RaftEngine(RaftEngineConfig {
dir: Some(format!("{}/{}", DEFAULT_DATA_HOME, WAL_DIR)),
sync_period: Some(Duration::from_secs(10)),
diff --git a/src/common/datasource/Cargo.toml b/src/common/datasource/Cargo.toml
index 470b5371f7..8b4053db2f 100644
--- a/src/common/datasource/Cargo.toml
+++ b/src/common/datasource/Cargo.toml
@@ -33,6 +33,7 @@ datatypes.workspace = true
futures.workspace = true
lazy_static.workspace = true
object-store.workspace = true
+object_store_opendal.workspace = true
orc-rust = { version = "0.8", default-features = false, features = ["async"] }
parquet.workspace = true
paste.workspace = true
diff --git a/src/common/datasource/src/file_format.rs b/src/common/datasource/src/file_format.rs
index a6a358c9e4..d9d7b8b648 100644
--- a/src/common/datasource/src/file_format.rs
+++ b/src/common/datasource/src/file_format.rs
@@ -61,6 +61,7 @@ pub const FORMAT_COMPRESSION_TYPE: &str = "compression_type";
pub const FORMAT_DELIMITER: &str = "delimiter";
pub const FORMAT_SCHEMA_INFER_MAX_RECORD: &str = "schema_infer_max_record";
pub const FORMAT_HAS_HEADER: &str = "has_header";
+pub const FORMAT_SKIP_BAD_RECORDS: &str = "skip_bad_records";
pub const FORMAT_TYPE: &str = "format";
pub const FILE_PATTERN: &str = "pattern";
pub const TIMESTAMP_FORMAT: &str = "timestamp_format";
@@ -316,7 +317,7 @@ pub async fn file_to_stream(
.with_file_compression_type(df_compression)
.build();
- let store = Arc::new(object_store::compat::OpendalStore::new(store.clone()));
+ let store = Arc::new(object_store_opendal::OpendalStore::new(store.clone()));
let file_opener = config.file_source().create_file_opener(store, &config, 0)?;
let stream = FileStream::new(&config, 0, file_opener, &ExecutionPlanMetricsSet::new())?;
diff --git a/src/common/datasource/src/file_format/csv.rs b/src/common/datasource/src/file_format/csv.rs
index 77ea553f35..2b39051b48 100644
--- a/src/common/datasource/src/file_format/csv.rs
+++ b/src/common/datasource/src/file_format/csv.rs
@@ -13,15 +13,24 @@
// limitations under the License.
use std::collections::HashMap;
+use std::io;
use std::str::FromStr;
+use std::sync::Arc;
+use std::task::Poll;
use arrow::csv::reader::Format;
use arrow::csv::{self, WriterBuilder};
+use arrow::error::ArrowError;
use arrow::record_batch::RecordBatch;
-use arrow_schema::Schema;
+use arrow_schema::{Schema, SchemaRef};
use async_trait::async_trait;
+use bytes::{Buf, Bytes};
use common_runtime;
+use common_telemetry::warn;
use datafusion::physical_plan::SendableRecordBatchStream;
+use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
+use futures::StreamExt;
+use futures::stream::BoxStream;
use object_store::ObjectStore;
use snafu::ResultExt;
use tokio_util::compat::FuturesAsyncReadCompatExt;
@@ -34,9 +43,12 @@ use crate::file_format::{self, FileFormat, stream_to_file};
use crate::share_buffer::SharedBuffer;
use crate::util::normalize_infer_schema;
+const SKIP_BAD_RECORDS_BATCH_SIZE: usize = 1;
+
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CsvFormat {
pub has_header: bool,
+ pub skip_bad_records: bool,
pub delimiter: u8,
pub schema_infer_max_record: Option,
pub compression_type: CompressionType,
@@ -76,13 +88,11 @@ impl TryFrom<&HashMap> for CsvFormat {
})?);
};
if let Some(has_header) = value.get(file_format::FORMAT_HAS_HEADER) {
- format.has_header = has_header.parse().map_err(|_| {
- error::ParseFormatSnafu {
- key: file_format::FORMAT_HAS_HEADER,
- value: has_header,
- }
- .build()
- })?;
+ format.has_header = parse_bool(file_format::FORMAT_HAS_HEADER, has_header)?;
+ };
+ if let Some(skip_bad_records) = value.get(file_format::FORMAT_SKIP_BAD_RECORDS) {
+ format.skip_bad_records =
+ parse_bool(file_format::FORMAT_SKIP_BAD_RECORDS, skip_bad_records)?;
};
if let Some(timestamp_format) = value.get(file_format::TIMESTAMP_FORMAT) {
format.timestamp_format = Some(timestamp_format.clone());
@@ -97,10 +107,17 @@ impl TryFrom<&HashMap> for CsvFormat {
}
}
+fn parse_bool(key: &'static str, value: &str) -> Result {
+ value
+ .parse()
+ .map_err(|_| error::ParseFormatSnafu { key, value }.build())
+}
+
impl Default for CsvFormat {
fn default() -> Self {
Self {
has_header: true,
+ skip_bad_records: false,
delimiter: b',',
schema_infer_max_record: Some(file_format::DEFAULT_SCHEMA_INFER_MAX_RECORD),
compression_type: CompressionType::Uncompressed,
@@ -189,10 +206,136 @@ impl DfRecordBatchEncoder for csv::Writer {
}
}
+/// Builds a CSV stream that can skip selected record-level parse/cast errors.
+///
+/// This recovery path intentionally uses one-record batches. It is slower than
+/// normal CSV scanning, but keeps each parse/cast failure isolated to a single
+/// record. Arrow's CSV decoder clears buffered rows before type parsing, so a
+/// failed multi-row flush cannot be safely retried row by row without replaying
+/// input bytes.
+pub async fn tolerant_csv_stream(
+ store: &ObjectStore,
+ path: &str,
+ schema: SchemaRef,
+ projection: Vec,
+ format: &CsvFormat,
+) -> Result {
+ let meta = store
+ .stat(path)
+ .await
+ .context(error::ReadObjectSnafu { path })?;
+
+ let reader = store
+ .reader(path)
+ .await
+ .context(error::ReadObjectSnafu { path })?
+ .into_bytes_stream(0..meta.content_length())
+ .await
+ .context(error::ReadObjectSnafu { path })?;
+
+ let reader = format.compression_type.convert_stream(reader).boxed();
+ tolerant_csv_stream_from_reader(
+ reader,
+ path,
+ schema,
+ projection,
+ format.has_header,
+ format.delimiter,
+ )
+}
+
+fn tolerant_csv_stream_from_reader(
+ reader: BoxStream<'static, io::Result>,
+ path: &str,
+ schema: SchemaRef,
+ projection: Vec,
+ has_header: bool,
+ delimiter: u8,
+) -> Result {
+ let projected_schema = Arc::new(
+ schema
+ .project(&projection)
+ .context(error::InferSchemaSnafu)?,
+ );
+ let mut decoder = csv::ReaderBuilder::new(schema)
+ .with_header(has_header)
+ .with_delimiter(delimiter)
+ .with_batch_size(SKIP_BAD_RECORDS_BATCH_SIZE)
+ .with_projection(projection)
+ .build_decoder();
+
+ let path = path.to_string();
+ let mut upstream = reader.fuse();
+ let mut buffered = Bytes::new();
+ let mut input_finished = false;
+ let stream = futures::stream::poll_fn(move |cx| {
+ loop {
+ while !input_finished {
+ if buffered.is_empty() {
+ match futures::ready!(upstream.poll_next_unpin(cx)) {
+ Some(Ok(bytes)) if bytes.is_empty() => continue,
+ Some(Ok(bytes)) => buffered = bytes,
+ Some(Err(error)) => return Poll::Ready(Some(Err(error.into()))),
+ None => input_finished = true,
+ }
+ }
+
+ let decoded = decoder.decode(buffered.as_ref())?;
+ if decoded > 0 {
+ buffered.advance(decoded);
+ continue;
+ }
+
+ if decoder.capacity() == 0 || input_finished {
+ break;
+ }
+
+ if buffered.is_empty() {
+ continue;
+ }
+
+ return Poll::Ready(Some(Err(ArrowError::ParseError(
+ "CSV decoder made no progress while input bytes remain".to_string(),
+ ))));
+ }
+
+ match decoder.flush() {
+ Ok(Some(batch)) => return Poll::Ready(Some(Ok(batch))),
+ Ok(None) if input_finished => return Poll::Ready(None),
+ Ok(None) => continue,
+ Err(error) if is_skippable_arrow_error(&error) => {
+ warn!(
+ "Skipping bad CSV record while copying from {}: {}",
+ path, error
+ );
+ }
+ Err(error) => return Poll::Ready(Some(Err(error))),
+ }
+ }
+ })
+ .map(|result: std::result::Result| result.map_err(Into::into));
+
+ Ok(Box::pin(RecordBatchStreamAdapter::new(
+ projected_schema,
+ stream,
+ )))
+}
+
+pub fn is_skippable_arrow_error(error: &ArrowError) -> bool {
+ matches!(
+ error,
+ ArrowError::ParseError(_)
+ | ArrowError::CastError(_)
+ | ArrowError::ComputeError(_)
+ | ArrowError::InvalidArgumentError(_)
+ )
+}
+
#[cfg(test)]
mod tests {
use std::sync::Arc;
+ use arrow_schema::{DataType, Field};
use common_recordbatch::adapter::DfRecordBatchStreamAdapter;
use common_recordbatch::{RecordBatch, RecordBatches};
use common_test_util::find_workspace_path;
@@ -205,7 +348,7 @@ mod tests {
use super::*;
use crate::file_format::{
FORMAT_COMPRESSION_TYPE, FORMAT_DELIMITER, FORMAT_HAS_HEADER,
- FORMAT_SCHEMA_INFER_MAX_RECORD, FileFormat, file_to_stream,
+ FORMAT_SCHEMA_INFER_MAX_RECORD, FORMAT_SKIP_BAD_RECORDS, FileFormat, file_to_stream,
};
use crate::test_util::{format_schema, test_store};
@@ -331,11 +474,29 @@ mod tests {
schema_infer_max_record: Some(2000),
delimiter: b'\t',
has_header: false,
+ skip_bad_records: false,
timestamp_format: None,
time_format: None,
date_format: None
}
);
+
+ let map = HashMap::from([(FORMAT_SKIP_BAD_RECORDS.to_string(), "true".to_string())]);
+ let format = CsvFormat::try_from(&map).unwrap();
+
+ assert_eq!(
+ format,
+ CsvFormat {
+ skip_bad_records: true,
+ ..CsvFormat::default()
+ }
+ );
+ }
+
+ #[test]
+ fn test_try_from_rejects_invalid_bool_options() {
+ let map = HashMap::from([(FORMAT_SKIP_BAD_RECORDS.to_string(), "yes".to_string())]);
+ assert!(CsvFormat::try_from(&map).is_err());
}
#[tokio::test]
@@ -496,4 +657,63 @@ mod tests {
assert_eq!(expected, pretty_print);
}
}
+
+ #[tokio::test]
+ async fn test_tolerant_csv_stream_continues_after_parse_error() {
+ let temp_dir = common_test_util::temp_dir::create_temp_dir("test_tolerant_csv_stream");
+ let csv_file_path = temp_dir.path().join("input.csv");
+ std::fs::write(
+ &csv_file_path,
+ "id,name,value\n1,Alice,10.5\nbad,Bad,20.0\nworse,Bad,21.0\n2,Bob,30.5",
+ )
+ .unwrap();
+
+ let store = test_store("/");
+ let schema = Arc::new(arrow_schema::Schema::new(vec![
+ Field::new("id", DataType::UInt32, false),
+ Field::new("name", DataType::Utf8, false),
+ Field::new("value", DataType::Float64, false),
+ ]));
+ let path = csv_file_path.to_str().unwrap();
+
+ let stream =
+ tolerant_csv_stream(&store, path, schema, vec![0, 1, 2], &CsvFormat::default())
+ .await
+ .unwrap();
+ let batches = stream.try_collect::>().await.unwrap();
+ let pretty_print = arrow::util::pretty::pretty_format_batches(&batches)
+ .unwrap()
+ .to_string();
+ let expected = r#"+----+-------+-------+
+| id | name | value |
++----+-------+-------+
+| 1 | Alice | 10.5 |
+| 2 | Bob | 30.5 |
++----+-------+-------+"#;
+ assert_eq!(expected, pretty_print);
+ }
+
+ #[tokio::test]
+ async fn test_tolerant_csv_stream_fails_on_structural_csv_error() {
+ let temp_dir =
+ common_test_util::temp_dir::create_temp_dir("test_tolerant_csv_stream_csv_error");
+ let csv_file_path = temp_dir.path().join("input.csv");
+ std::fs::write(&csv_file_path, "id,name,value\n1,Alice,10.5\n2,Bob\n").unwrap();
+
+ let store = test_store("/");
+ let schema = Arc::new(arrow_schema::Schema::new(vec![
+ Field::new("id", DataType::UInt32, false),
+ Field::new("name", DataType::Utf8, false),
+ Field::new("value", DataType::Float64, false),
+ ]));
+ let path = csv_file_path.to_str().unwrap();
+
+ let stream =
+ tolerant_csv_stream(&store, path, schema, vec![0, 1, 2], &CsvFormat::default())
+ .await
+ .unwrap();
+ let error = stream.try_collect::>().await.unwrap_err();
+
+ assert!(error.to_string().contains("incorrect number of fields"));
+ }
}
diff --git a/src/common/datasource/src/file_format/tests.rs b/src/common/datasource/src/file_format/tests.rs
index 93ab3b4409..a925f73d48 100644
--- a/src/common/datasource/src/file_format/tests.rs
+++ b/src/common/datasource/src/file_format/tests.rs
@@ -44,7 +44,7 @@ struct Test<'a> {
impl Test<'_> {
async fn run(self, store: &ObjectStore) {
- let store = Arc::new(object_store::compat::OpendalStore::new(store.clone()));
+ let store = Arc::new(object_store_opendal::OpendalStore::new(store.clone()));
let file_opener = self
.file_source
.create_file_opener(store, &self.config, 0)
diff --git a/src/common/datasource/src/object_store/oss.rs b/src/common/datasource/src/object_store/oss.rs
index aded3eca2c..aacc17ac5e 100644
--- a/src/common/datasource/src/object_store/oss.rs
+++ b/src/common/datasource/src/object_store/oss.rs
@@ -27,12 +27,14 @@ const ACCESS_KEY_ID: &str = "access_key_id";
const ACCESS_KEY_SECRET: &str = "access_key_secret";
const ROOT: &str = "root";
const ALLOW_ANONYMOUS: &str = "allow_anonymous";
+const SKIP_SIGNATURE: &str = "skip_signature";
/// Check if the key is supported in OSS configuration.
pub fn is_supported_in_oss(key: &str) -> bool {
[
ROOT,
ALLOW_ANONYMOUS,
+ SKIP_SIGNATURE,
BUCKET,
ENDPOINT,
ACCESS_KEY_ID,
@@ -61,18 +63,23 @@ pub fn build_oss_backend(
builder = builder.access_key_secret(access_key_secret);
}
- if let Some(allow_anonymous) = connection.get(ALLOW_ANONYMOUS) {
- let allow = allow_anonymous.as_str().parse::().map_err(|e| {
+ if let Some((key, value)) = connection
+ .get(SKIP_SIGNATURE)
+ .map(|value| (SKIP_SIGNATURE, value))
+ .or_else(|| {
+ connection
+ .get(ALLOW_ANONYMOUS)
+ .map(|value| (ALLOW_ANONYMOUS, value))
+ })
+ {
+ let skip_signature = value.as_str().parse::().map_err(|e| {
error::InvalidConnectionSnafu {
- msg: format!(
- "failed to parse the option {}={}, {}",
- ALLOW_ANONYMOUS, allow_anonymous, e
- ),
+ msg: format!("failed to parse the option {}={}, {}", key, value, e),
}
.build()
})?;
- if allow {
- builder = builder.allow_anonymous();
+ if skip_signature {
+ builder = builder.skip_signature();
}
}
@@ -93,6 +100,7 @@ mod tests {
fn test_is_supported_in_oss() {
assert!(is_supported_in_oss(ROOT));
assert!(is_supported_in_oss(ALLOW_ANONYMOUS));
+ assert!(is_supported_in_oss(SKIP_SIGNATURE));
assert!(is_supported_in_oss(BUCKET));
assert!(is_supported_in_oss(ENDPOINT));
assert!(is_supported_in_oss(ACCESS_KEY_ID));
diff --git a/src/common/datasource/src/test_util.rs b/src/common/datasource/src/test_util.rs
index 0a13d9c6e8..ea2b0c768c 100644
--- a/src/common/datasource/src/test_util.rs
+++ b/src/common/datasource/src/test_util.rs
@@ -103,7 +103,7 @@ pub async fn setup_stream_to_json_test(origin_path: &str, threshold: impl Fn(usi
test_util::TEST_BATCH_SIZE,
schema.clone(),
FileCompressionType::UNCOMPRESSED,
- Arc::new(object_store::compat::OpendalStore::new(store.clone())),
+ Arc::new(object_store_opendal::OpendalStore::new(store.clone())),
true,
);
@@ -157,7 +157,7 @@ pub async fn setup_stream_to_csv_test(
let csv_opener = csv_source
.create_file_opener(
- Arc::new(object_store::compat::OpendalStore::new(store.clone())),
+ Arc::new(object_store_opendal::OpendalStore::new(store.clone())),
&config,
0,
)
diff --git a/src/common/meta/src/cache.rs b/src/common/meta/src/cache.rs
index f16290937a..c26a0fab76 100644
--- a/src/common/meta/src/cache.rs
+++ b/src/common/meta/src/cache.rs
@@ -17,7 +17,7 @@ mod flow;
mod registry;
mod table;
-pub use container::{CacheContainer, Initializer, Invalidator, TokenFilter};
+pub use container::{CacheContainer, InitStrategy, Initializer, Invalidator, TokenFilter};
pub use flow::{TableFlownodeSetCache, TableFlownodeSetCacheRef, new_table_flownode_set_cache};
pub use registry::{
CacheRegistry, CacheRegistryBuilder, CacheRegistryRef, LayeredCacheRegistry,
diff --git a/src/common/meta/src/cache/flow/table_flownode.rs b/src/common/meta/src/cache/flow/table_flownode.rs
index ebe3664202..4d3513a21d 100644
--- a/src/common/meta/src/cache/flow/table_flownode.rs
+++ b/src/common/meta/src/cache/flow/table_flownode.rs
@@ -210,7 +210,7 @@ mod tests {
use crate::cache::flow::table_flownode::{FlowIdent, new_table_flownode_set_cache};
use crate::instruction::{CacheIdent, CreateFlow, DropFlow};
use crate::key::flow::FlowMetadataManager;
- use crate::key::flow::flow_info::FlowInfoValue;
+ use crate::key::flow::flow_info::{FlowInfoValue, FlowStatus};
use crate::key::flow::flow_route::FlowRouteValue;
use crate::kv_backend::memory::MemoryKvBackend;
use crate::peer::Peer;
@@ -242,11 +242,14 @@ mod tests {
catalog_name: DEFAULT_CATALOG_NAME.to_string(),
query_context: None,
flow_name: "my_flow".to_string(),
+ all_source_table_names: vec![],
+ unresolved_source_table_names: vec![],
raw_sql: "sql".to_string(),
expire_after: Some(300),
eval_interval_secs: None,
comment: "comment".to_string(),
options: Default::default(),
+ status: FlowStatus::Active,
created_time: chrono::Utc::now(),
updated_time: chrono::Utc::now(),
},
diff --git a/src/common/meta/src/ddl/create_flow.rs b/src/common/meta/src/ddl/create_flow.rs
index 7120e50425..8a419176c9 100644
--- a/src/common/meta/src/ddl/create_flow.rs
+++ b/src/common/meta/src/ddl/create_flow.rs
@@ -14,7 +14,7 @@
mod metadata;
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, HashMap};
use std::fmt;
use api::v1::ExpireAfter;
@@ -34,13 +34,14 @@ use serde::{Deserialize, Serialize};
use snafu::{ResultExt, ensure};
use strum::AsRefStr;
use table::metadata::TableId;
+use table::table_name::TableName;
use crate::cache_invalidator::Context;
use crate::ddl::DdlContext;
use crate::ddl::utils::{add_peer_context_if_needed, map_to_procedure_error};
use crate::error::{self, Result, UnexpectedSnafu};
use crate::instruction::{CacheIdent, CreateFlow, DropFlow};
-use crate::key::flow::flow_info::FlowInfoValue;
+use crate::key::flow::flow_info::{FlowInfoValue, FlowStatus};
use crate::key::flow::flow_route::FlowRouteValue;
use crate::key::table_name::TableNameKey;
use crate::key::{DeserializedValueWithBytes, FlowId, FlowPartitionId};
@@ -67,6 +68,7 @@ impl CreateFlowProcedure {
flow_id: None,
peers: vec![],
source_table_ids: vec![],
+ unresolved_source_table_names: vec![],
flow_context: query_context.into(), // Convert to FlowQueryContext
state: CreateFlowState::Prepare,
prev_flow_info_value: None,
@@ -89,6 +91,8 @@ impl CreateFlowProcedure {
let create_if_not_exists = self.data.task.create_if_not_exists;
let or_replace = self.data.task.or_replace;
+ validate_flow_options(&self.data.task)?;
+
let flow_name_value = self
.context
.flow_metadata_manager
@@ -167,6 +171,21 @@ impl CreateFlowProcedure {
}
self.collect_source_tables().await?;
+ ensure!(
+ self.data.unresolved_source_table_names.is_empty()
+ || defer_on_missing_source(&self.data.task)?,
+ error::UnsupportedSnafu {
+ operation: format!(
+ "Create flow with missing source tables requires WITH ('{DEFER_ON_MISSING_SOURCE_KEY}'='true'): {}",
+ self.data
+ .unresolved_source_table_names
+ .iter()
+ .map(ToString::to_string)
+ .join(", ")
+ )
+ }
+ );
+ self.ensure_supported_replace_transition()?;
// Validate that source and sink tables are not the same
let sink_table_name = &self.data.task.sink_table_name;
@@ -189,13 +208,38 @@ impl CreateFlowProcedure {
if self.data.flow_id.is_none() {
self.allocate_flow_id().await?;
}
- self.data.state = CreateFlowState::CreateFlows;
- // determine flow type
self.data.flow_type = Some(get_flow_type_from_options(&self.data.task)?);
+ self.data.state = if self.data.is_pending() {
+ self.data.peers.clear();
+ CreateFlowState::CreateMetadata
+ } else {
+ CreateFlowState::CreateFlows
+ };
+
Ok(Status::executing(true))
}
+ fn ensure_supported_replace_transition(&self) -> Result<()> {
+ if !self.data.task.or_replace {
+ return Ok(());
+ }
+
+ let Some(prev_flow_info) = self.data.prev_flow_info_value.as_ref() else {
+ return Ok(());
+ };
+ let prev_pending = prev_flow_info.get_inner_ref().is_pending();
+ let new_pending = self.data.is_pending();
+ ensure!(
+ prev_pending == new_pending,
+ error::UnsupportedSnafu {
+ operation: "Replacing between pending and active flow states is not supported yet"
+ }
+ );
+
+ Ok(())
+ }
+
async fn on_flownode_create_flows(&mut self) -> Result {
// Safety: must be allocated.
let mut create_flow = Vec::with_capacity(self.data.peers.len());
@@ -365,6 +409,63 @@ pub fn get_flow_type_from_options(flow_task: &CreateFlowTask) -> Result Result {
+ flow_task
+ .flow_options
+ .get(DEFER_ON_MISSING_SOURCE_KEY)
+ .map(|value| {
+ value
+ .trim()
+ .to_ascii_lowercase()
+ .parse::()
+ .map_err(|_| {
+ error::UnexpectedSnafu {
+ err_msg: format!(
+ "Invalid flow option '{DEFER_ON_MISSING_SOURCE_KEY}': {value}"
+ ),
+ }
+ .build()
+ })
+ })
+ .transpose()
+ .map(|value| value.unwrap_or(false))
+}
+
+pub fn validate_flow_options(flow_task: &CreateFlowTask) -> Result<()> {
+ for key in flow_task.flow_options.keys() {
+ match key.as_str() {
+ DEFER_ON_MISSING_SOURCE_KEY
+ | FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY
+ | FlowType::FLOW_TYPE_KEY => {}
+ unknown => {
+ return UnexpectedSnafu {
+ err_msg: format!(
+ "Unknown flow option '{unknown}', supported user options: {DEFER_ON_MISSING_SOURCE_KEY}, {FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY}"
+ ),
+ }
+ .fail();
+ }
+ }
+ }
+
+ defer_on_missing_source(flow_task)?;
+ get_flow_type_from_options(flow_task)?;
+ Ok(())
+}
+
+fn user_runtime_flow_options(options: &HashMap) -> HashMap {
+ let mut options = options.clone();
+ options.remove(DEFER_ON_MISSING_SOURCE_KEY);
+ options
+}
+
+fn metadata_flow_options(options: &HashMap) -> HashMap {
+ options.clone()
+}
+
/// The state of [CreateFlowProcedure].
#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, PartialEq)]
pub enum CreateFlowState {
@@ -388,6 +489,9 @@ pub enum FlowType {
Streaming,
}
+pub const FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY: &str =
+ "experimental_enable_incremental_read";
+
impl FlowType {
pub const BATCHING: &str = "batching";
pub const STREAMING: &str = "streaming";
@@ -411,6 +515,8 @@ pub struct CreateFlowData {
pub(crate) flow_id: Option,
pub(crate) peers: Vec,
pub(crate) source_table_ids: Vec,
+ #[serde(default)]
+ pub(crate) unresolved_source_table_names: Vec,
/// Use alias for backward compatibility with QueryContext serialized data
#[serde(alias = "query_context")]
pub(crate) flow_context: FlowQueryContext,
@@ -424,6 +530,16 @@ pub struct CreateFlowData {
pub(crate) flow_type: Option,
}
+impl CreateFlowData {
+ pub(crate) fn is_pending(&self) -> bool {
+ !self.unresolved_source_table_names.is_empty()
+ }
+
+ pub(crate) fn is_active(&self) -> bool {
+ !self.is_pending()
+ }
+}
+
impl From<&CreateFlowData> for CreateRequest {
fn from(value: &CreateFlowData) -> Self {
let flow_id = value.flow_id.unwrap();
@@ -446,7 +562,7 @@ impl From<&CreateFlowData> for CreateRequest {
.map(|seconds| api::v1::EvalInterval { seconds }),
comment: value.task.comment.clone(),
sql: value.task.sql.clone(),
- flow_options: value.task.flow_options.clone(),
+ flow_options: user_runtime_flow_options(&value.task.flow_options),
};
let flow_type = value.flow_type.unwrap_or_default().to_string();
@@ -466,9 +582,9 @@ impl From<&CreateFlowData> for (FlowInfoValue, Vec<(FlowPartitionId, FlowRouteVa
eval_interval_secs: eval_interval,
comment,
sql,
- flow_options: mut options,
..
} = value.task.clone();
+ let mut options = metadata_flow_options(&value.task.flow_options);
let flownode_ids = value
.peers
@@ -484,7 +600,7 @@ impl From<&CreateFlowData> for (FlowInfoValue, Vec<(FlowPartitionId, FlowRouteVa
.collect::>();
let flow_type = value.flow_type.unwrap_or_default().to_string();
- options.insert("flow_type".to_string(), flow_type);
+ options.insert(FlowType::FLOW_TYPE_KEY.to_string(), flow_type);
let mut create_time = chrono::Utc::now();
if let Some(prev_flow_value) = value.prev_flow_info_value.as_ref()
@@ -495,6 +611,8 @@ impl From<&CreateFlowData> for (FlowInfoValue, Vec<(FlowPartitionId, FlowRouteVa
let flow_info: FlowInfoValue = FlowInfoValue {
source_table_ids: value.source_table_ids.clone(),
+ all_source_table_names: value.task.source_table_names.clone(),
+ unresolved_source_table_names: value.unresolved_source_table_names.clone(),
sink_table_name,
flownode_ids,
catalog_name,
@@ -506,6 +624,11 @@ impl From<&CreateFlowData> for (FlowInfoValue, Vec<(FlowPartitionId, FlowRouteVa
eval_interval_secs: eval_interval,
comment,
options,
+ status: if value.is_active() {
+ FlowStatus::Active
+ } else {
+ FlowStatus::PendingSources
+ },
created_time: create_time,
updated_time: chrono::Utc::now(),
};
diff --git a/src/common/meta/src/ddl/create_flow/metadata.rs b/src/common/meta/src/ddl/create_flow/metadata.rs
index 27b85b7946..f97ecfdf4a 100644
--- a/src/common/meta/src/ddl/create_flow/metadata.rs
+++ b/src/common/meta/src/ddl/create_flow/metadata.rs
@@ -12,10 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-use snafu::OptionExt;
-
use crate::ddl::create_flow::CreateFlowProcedure;
-use crate::error::{self, Result};
+use crate::error::Result;
use crate::key::table_name::TableNameKey;
impl CreateFlowProcedure {
@@ -34,9 +32,8 @@ impl CreateFlowProcedure {
Ok(())
}
- /// Ensures all source tables exist and collects source table ids
+ /// Collects source table ids and keeps track of missing tables.
pub(crate) async fn collect_source_tables(&mut self) -> Result<()> {
- // Ensures all source tables exist.
let keys = self
.data
.task
@@ -52,22 +49,24 @@ impl CreateFlowProcedure {
.batch_get(keys)
.await?;
- let source_table_ids = self
+ let mut resolved = Vec::with_capacity(self.data.task.source_table_names.len());
+ let mut unresolved = Vec::new();
+
+ for (name, table_id) in self
.data
.task
.source_table_names
.iter()
.zip(source_table_ids)
- .map(|(name, table_id)| {
- Ok(table_id
- .with_context(|| error::TableNotFoundSnafu {
- table_name: name.to_string(),
- })?
- .table_id())
- })
- .collect::>>()?;
+ {
+ match table_id {
+ Some(table_id) => resolved.push(table_id.table_id()),
+ None => unresolved.push(name.clone()),
+ }
+ }
- self.data.source_table_ids = source_table_ids;
+ self.data.source_table_ids = resolved;
+ self.data.unresolved_source_table_names = unresolved;
Ok(())
}
}
diff --git a/src/common/meta/src/ddl/drop_flow/metadata.rs b/src/common/meta/src/ddl/drop_flow/metadata.rs
index 0437098be3..7afd00f9d5 100644
--- a/src/common/meta/src/ddl/drop_flow/metadata.rs
+++ b/src/common/meta/src/ddl/drop_flow/metadata.rs
@@ -43,7 +43,7 @@ impl DropFlowProcedure {
.map(|(_, value)| value)
.collect::>();
ensure!(
- !flow_route_values.is_empty(),
+ flow_info_value.is_pending() || !flow_route_values.is_empty(),
error::FlowRouteNotFoundSnafu {
flow_name: format_full_flow_name(catalog_name, flow_name),
}
diff --git a/src/common/meta/src/ddl/tests/create_flow.rs b/src/common/meta/src/ddl/tests/create_flow.rs
index 344fc05024..7150be39cb 100644
--- a/src/common/meta/src/ddl/tests/create_flow.rs
+++ b/src/common/meta/src/ddl/tests/create_flow.rs
@@ -16,12 +16,18 @@ use std::assert_matches;
use std::collections::HashMap;
use std::sync::Arc;
+use api::v1::flow::CreateRequest;
use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
+use common_procedure::Status;
use common_procedure_test::execute_procedure_until_done;
use table::table_name::TableName;
use crate::ddl::DdlContext;
-use crate::ddl::create_flow::{CreateFlowData, CreateFlowProcedure, CreateFlowState, FlowType};
+use crate::ddl::create_flow::{
+ CreateFlowData, CreateFlowProcedure, CreateFlowState, DEFER_ON_MISSING_SOURCE_KEY,
+ FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY, FlowType, defer_on_missing_source,
+ validate_flow_options,
+};
use crate::ddl::test_util::create_table::test_create_table_task;
use crate::ddl::test_util::flownode_handler::NaiveFlownodeHandler;
use crate::error;
@@ -63,6 +69,11 @@ pub(crate) fn test_create_flow_task(
}
}
+fn enable_defer_on_missing_source(task: &mut CreateFlowTask) {
+ task.flow_options
+ .insert(DEFER_ON_MISSING_SOURCE_KEY.to_string(), "true".to_string());
+}
+
#[tokio::test]
async fn test_create_flow_source_table_not_found() {
let source_table_names = vec![TableName::new(
@@ -78,7 +89,277 @@ async fn test_create_flow_source_table_not_found() {
let query_ctx = test_query_context();
let mut procedure = CreateFlowProcedure::new(task, query_ctx, ddl_context);
let err = procedure.on_prepare().await.unwrap_err();
- assert_matches!(err, error::Error::TableNotFound { .. });
+ assert_matches!(err, error::Error::Unsupported { .. });
+ assert!(
+ err.to_string()
+ .contains("requires WITH ('defer_on_missing_source'='true')")
+ );
+}
+
+#[tokio::test]
+async fn test_create_pending_flow_source_table_not_found_with_defer() {
+ let source_table_names = vec![TableName::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ "my_table",
+ )];
+ let sink_table_name =
+ TableName::new(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, "my_sink_table");
+ let mut task = test_create_flow_task("my_flow", source_table_names, sink_table_name, false);
+ enable_defer_on_missing_source(&mut task);
+ let node_manager = Arc::new(MockFlownodeManager::new(NaiveFlownodeHandler));
+ let ddl_context = new_ddl_context(node_manager);
+ let query_ctx = test_query_context();
+ let mut procedure = CreateFlowProcedure::new(task, query_ctx, ddl_context.clone());
+ let status = procedure.on_prepare().await.unwrap();
+ assert_matches!(status, Status::Executing { persist: true, .. });
+ assert_eq!(procedure.data.unresolved_source_table_names.len(), 1);
+ assert_eq!(procedure.data.source_table_ids, Vec::::new());
+
+ let output = execute_procedure_until_done(&mut procedure).await.unwrap();
+ let flow_id = *output.downcast_ref::().unwrap();
+ let flow_info = ddl_context
+ .flow_metadata_manager
+ .flow_info_manager()
+ .get(flow_id)
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(flow_info.source_table_ids(), Vec::::new());
+ assert_eq!(
+ flow_info
+ .options()
+ .get(DEFER_ON_MISSING_SOURCE_KEY)
+ .map(String::as_str),
+ Some("true")
+ );
+}
+
+#[tokio::test]
+async fn test_create_pending_flow_source_table_not_found_with_defer_false() {
+ let source_table_names = vec![TableName::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ "my_table",
+ )];
+ let sink_table_name =
+ TableName::new(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, "my_sink_table");
+ let mut task = test_create_flow_task("my_flow", source_table_names, sink_table_name, false);
+ task.flow_options
+ .insert(DEFER_ON_MISSING_SOURCE_KEY.to_string(), "false".to_string());
+ let node_manager = Arc::new(MockFlownodeManager::new(NaiveFlownodeHandler));
+ let ddl_context = new_ddl_context(node_manager);
+ let query_ctx = test_query_context();
+ let mut procedure = CreateFlowProcedure::new(task, query_ctx, ddl_context);
+ let err = procedure.on_prepare().await.unwrap_err();
+ assert_matches!(err, error::Error::Unsupported { .. });
+ assert!(
+ err.to_string()
+ .contains("requires WITH ('defer_on_missing_source'='true')")
+ );
+}
+
+#[tokio::test]
+async fn test_create_pending_flow_records_partial_source_resolution() {
+ let existing_source = TableName::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ "partial_existing_source_table",
+ );
+ let missing_source = TableName::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ "partial_missing_source_table",
+ );
+ let sink_table_name = TableName::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ "partial_pending_sink_table",
+ );
+ let node_manager = Arc::new(MockFlownodeManager::new(NaiveFlownodeHandler));
+ let ddl_context = new_ddl_context(node_manager);
+
+ let existing_table_id = 3026;
+ let create_table_task =
+ test_create_table_task("partial_existing_source_table", existing_table_id);
+ ddl_context
+ .table_metadata_manager
+ .create_table_metadata(
+ create_table_task.table_info.clone(),
+ TableRouteValue::physical(vec![]),
+ HashMap::new(),
+ )
+ .await
+ .unwrap();
+
+ let mut task = test_create_flow_task(
+ "partial_pending_flow",
+ vec![existing_source.clone(), missing_source.clone()],
+ sink_table_name,
+ false,
+ );
+ enable_defer_on_missing_source(&mut task);
+ let query_ctx = test_query_context();
+ let mut procedure = CreateFlowProcedure::new(task, query_ctx, ddl_context.clone());
+ let status = procedure.on_prepare().await.unwrap();
+ assert_matches!(status, Status::Executing { persist: true, .. });
+ assert_eq!(procedure.data.source_table_ids, vec![existing_table_id]);
+ assert_eq!(
+ procedure.data.unresolved_source_table_names,
+ vec![missing_source.clone()]
+ );
+
+ let output = execute_procedure_until_done(&mut procedure).await.unwrap();
+ let flow_id = *output.downcast_ref::().unwrap();
+ let flow_info = ddl_context
+ .flow_metadata_manager
+ .flow_info_manager()
+ .get(flow_id)
+ .await
+ .unwrap()
+ .unwrap();
+
+ assert!(flow_info.is_pending());
+ assert_eq!(flow_info.source_table_ids(), &[existing_table_id]);
+ let expected_all_sources = vec![existing_source, missing_source.clone()];
+ assert_eq!(
+ flow_info.all_source_table_names(),
+ expected_all_sources.as_slice()
+ );
+ assert_eq!(flow_info.unresolved_source_table_names(), &[missing_source]);
+ assert!(flow_info.flownode_ids().is_empty());
+}
+
+#[test]
+fn test_defer_on_missing_source_defaults_false() {
+ let task = test_create_flow_task(
+ "my_flow",
+ vec![],
+ TableName::new(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, "my_sink_table"),
+ false,
+ );
+
+ assert!(!defer_on_missing_source(&task).unwrap());
+}
+
+#[test]
+fn test_defer_on_missing_source_true() {
+ let mut task = test_create_flow_task(
+ "my_flow",
+ vec![],
+ TableName::new(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, "my_sink_table"),
+ false,
+ );
+ task.flow_options
+ .insert(DEFER_ON_MISSING_SOURCE_KEY.to_string(), "true".to_string());
+
+ assert!(defer_on_missing_source(&task).unwrap());
+}
+
+#[test]
+fn test_defer_on_missing_source_invalid_value() {
+ let mut task = test_create_flow_task(
+ "my_flow",
+ vec![],
+ TableName::new(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, "my_sink_table"),
+ false,
+ );
+ task.flow_options.insert(
+ DEFER_ON_MISSING_SOURCE_KEY.to_string(),
+ "invalid".to_string(),
+ );
+
+ let err = defer_on_missing_source(&task).unwrap_err();
+ assert!(
+ err.to_string()
+ .contains("Invalid flow option 'defer_on_missing_source': invalid")
+ );
+}
+
+#[test]
+fn test_validate_flow_options_allows_incremental_read_option() {
+ let mut task = test_create_flow_task(
+ "my_flow",
+ vec![],
+ TableName::new(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, "my_sink_table"),
+ false,
+ );
+ task.flow_options.insert(
+ FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY.to_string(),
+ "true".to_string(),
+ );
+
+ validate_flow_options(&task).unwrap();
+}
+
+#[tokio::test]
+async fn test_create_flow_rejects_unknown_option_in_meta_task() {
+ let mut task = test_create_flow_task(
+ "my_flow",
+ vec![],
+ TableName::new(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, "my_sink_table"),
+ false,
+ );
+ task.flow_options
+ .insert("unknown_option".to_string(), "value".to_string());
+ let node_manager = Arc::new(MockFlownodeManager::new(NaiveFlownodeHandler));
+ let ddl_context = new_ddl_context(node_manager);
+ let query_ctx = test_query_context();
+ let mut procedure = CreateFlowProcedure::new(task, query_ctx, ddl_context);
+
+ let err = procedure.on_prepare().await.unwrap_err();
+ assert_matches!(err, error::Error::Unexpected { .. });
+ assert!(
+ err.to_string()
+ .contains("Unknown flow option 'unknown_option'")
+ );
+}
+
+#[test]
+fn test_create_request_strips_defer_on_missing_source_runtime_option() {
+ let mut task = test_create_flow_task(
+ "my_flow",
+ vec![],
+ TableName::new(DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, "my_sink_table"),
+ false,
+ );
+ enable_defer_on_missing_source(&mut task);
+
+ let data = CreateFlowData {
+ state: CreateFlowState::CreateFlows,
+ task,
+ flow_id: Some(1024),
+ peers: vec![],
+ source_table_ids: vec![],
+ unresolved_source_table_names: vec![],
+ flow_context: FlowQueryContext {
+ catalog: DEFAULT_CATALOG_NAME.to_string(),
+ schema: DEFAULT_SCHEMA_NAME.to_string(),
+ timezone: "UTC".to_string(),
+ extensions: HashMap::new(),
+ channel: 0,
+ snapshot_seqs: HashMap::new(),
+ sst_min_sequences: HashMap::new(),
+ },
+ prev_flow_info_value: None,
+ did_replace: false,
+ flow_type: Some(FlowType::Batching),
+ };
+
+ let request: CreateRequest = (&data).into();
+
+ assert!(
+ !request
+ .flow_options
+ .contains_key(DEFER_ON_MISSING_SOURCE_KEY)
+ );
+ assert_eq!(
+ request
+ .flow_options
+ .get(FlowType::FLOW_TYPE_KEY)
+ .map(String::as_str),
+ Some(FlowType::BATCHING)
+ );
}
pub(crate) async fn create_test_flow(
@@ -101,6 +382,27 @@ pub(crate) async fn create_test_flow(
*flow_id
}
+pub(crate) async fn create_test_pending_flow(
+ ddl_context: &DdlContext,
+ flow_name: &str,
+ source_table_names: Vec,
+ sink_table_name: TableName,
+) -> FlowId {
+ let mut task = test_create_flow_task(
+ flow_name,
+ source_table_names.clone(),
+ sink_table_name.clone(),
+ false,
+ );
+ enable_defer_on_missing_source(&mut task);
+ let query_ctx = test_query_context();
+ let mut procedure = CreateFlowProcedure::new(task, query_ctx, ddl_context.clone());
+ let output = execute_procedure_until_done(&mut procedure).await.unwrap();
+ let flow_id = output.downcast_ref::().unwrap();
+
+ *flow_id
+}
+
#[tokio::test]
async fn test_create_flow() {
let table_id = 1024;
@@ -154,6 +456,201 @@ async fn test_create_flow() {
assert_matches!(err, error::Error::FlowAlreadyExists { .. });
}
+#[tokio::test]
+async fn test_replace_pending_flow_with_active_flow_is_unsupported() {
+ let source_table_name = TableName::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ "replace_pending_source_table",
+ );
+ let sink_table_name = TableName::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ "replace_pending_sink_table",
+ );
+ let node_manager = Arc::new(MockFlownodeManager::new(NaiveFlownodeHandler));
+ let ddl_context = new_ddl_context(node_manager);
+
+ let pending_flow_id = create_test_pending_flow(
+ &ddl_context,
+ "replace_pending_flow",
+ vec![source_table_name.clone()],
+ sink_table_name.clone(),
+ )
+ .await;
+
+ let pending_flow = ddl_context
+ .flow_metadata_manager
+ .flow_info_manager()
+ .get(pending_flow_id)
+ .await
+ .unwrap()
+ .unwrap();
+ assert!(pending_flow.is_pending());
+ assert!(pending_flow.flownode_ids().is_empty());
+
+ let create_table_task = test_create_table_task("replace_pending_source_table", 1026);
+ ddl_context
+ .table_metadata_manager
+ .create_table_metadata(
+ create_table_task.table_info.clone(),
+ TableRouteValue::physical(vec![]),
+ HashMap::new(),
+ )
+ .await
+ .unwrap();
+
+ let mut replace_task = test_create_flow_task(
+ "replace_pending_flow",
+ vec![source_table_name],
+ sink_table_name,
+ false,
+ );
+ replace_task.or_replace = true;
+ let query_ctx = test_query_context();
+ let mut procedure = CreateFlowProcedure::new(replace_task, query_ctx, ddl_context.clone());
+ let err = procedure.on_prepare().await.unwrap_err();
+ assert_matches!(err, error::Error::Unsupported { .. });
+ assert!(
+ err.to_string()
+ .contains("Replacing between pending and active flow states")
+ );
+}
+
+#[tokio::test]
+async fn test_replace_active_flow_with_pending_flow_is_unsupported() {
+ let existing_source_table = TableName::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ "replace_active_source_table",
+ );
+ let missing_source_table = TableName::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ "replace_missing_source_table",
+ );
+ let sink_table_name = TableName::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ "replace_active_sink_table",
+ );
+
+ let node_manager = Arc::new(MockFlownodeManager::new(NaiveFlownodeHandler));
+ let ddl_context = new_ddl_context(node_manager);
+
+ let create_table_task = test_create_table_task("replace_active_source_table", 2026);
+ ddl_context
+ .table_metadata_manager
+ .create_table_metadata(
+ create_table_task.table_info.clone(),
+ TableRouteValue::physical(vec![]),
+ HashMap::new(),
+ )
+ .await
+ .unwrap();
+
+ let _flow_id = create_test_flow(
+ &ddl_context,
+ "replace_active_flow_to_pending",
+ vec![existing_source_table],
+ sink_table_name.clone(),
+ )
+ .await;
+
+ let mut replace_task = test_create_flow_task(
+ "replace_active_flow_to_pending",
+ vec![missing_source_table],
+ sink_table_name,
+ false,
+ );
+ enable_defer_on_missing_source(&mut replace_task);
+ replace_task.or_replace = true;
+ let query_ctx = test_query_context();
+ let mut procedure = CreateFlowProcedure::new(replace_task, query_ctx, ddl_context.clone());
+ let err = procedure.on_prepare().await.unwrap_err();
+ assert_matches!(err, error::Error::Unsupported { .. });
+ assert!(
+ err.to_string()
+ .contains("Replacing between pending and active flow states")
+ );
+}
+
+#[tokio::test]
+async fn test_replace_pending_flow_with_pending_flow_updates_metadata() {
+ let first_missing_source = TableName::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ "replace_pending_first_missing_source",
+ );
+ let second_missing_source = TableName::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ "replace_pending_second_missing_source",
+ );
+ let sink_table_name = TableName::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ "replace_pending_to_pending_sink_table",
+ );
+ let node_manager = Arc::new(MockFlownodeManager::new(NaiveFlownodeHandler));
+ let ddl_context = new_ddl_context(node_manager);
+
+ let original_flow_id = create_test_pending_flow(
+ &ddl_context,
+ "replace_pending_to_pending_flow",
+ vec![first_missing_source.clone()],
+ sink_table_name.clone(),
+ )
+ .await;
+
+ let original_flow = ddl_context
+ .flow_metadata_manager
+ .flow_info_manager()
+ .get(original_flow_id)
+ .await
+ .unwrap()
+ .unwrap();
+ assert!(original_flow.is_pending());
+ assert_eq!(
+ original_flow.unresolved_source_table_names(),
+ &[first_missing_source]
+ );
+ assert!(original_flow.flownode_ids().is_empty());
+
+ let mut replace_task = test_create_flow_task(
+ "replace_pending_to_pending_flow",
+ vec![second_missing_source.clone()],
+ sink_table_name,
+ false,
+ );
+ enable_defer_on_missing_source(&mut replace_task);
+ replace_task.or_replace = true;
+ let query_ctx = test_query_context();
+ let mut procedure = CreateFlowProcedure::new(replace_task, query_ctx, ddl_context.clone());
+ let output = execute_procedure_until_done(&mut procedure).await.unwrap();
+ let replaced_flow_id = *output.downcast_ref::().unwrap();
+ assert_eq!(replaced_flow_id, original_flow_id);
+
+ let replaced_flow = ddl_context
+ .flow_metadata_manager
+ .flow_info_manager()
+ .get(replaced_flow_id)
+ .await
+ .unwrap()
+ .unwrap();
+ assert!(replaced_flow.is_pending());
+ assert_eq!(replaced_flow.source_table_ids(), Vec::::new());
+ assert_eq!(
+ replaced_flow.unresolved_source_table_names(),
+ std::slice::from_ref(&second_missing_source)
+ );
+ assert_eq!(
+ replaced_flow.all_source_table_names(),
+ &[second_missing_source]
+ );
+ assert!(replaced_flow.flownode_ids().is_empty());
+}
+
#[tokio::test]
async fn test_create_flow_same_source_and_sink_table() {
let table_id = 1024;
@@ -228,6 +725,7 @@ fn test_create_flow_data_serialization_backward_compatibility() {
"flow_id": null,
"peers": [],
"source_table_ids": [],
+ "unresolved_source_table_names": [],
"query_context": {
"current_catalog": "old_catalog",
"current_schema": "old_schema",
@@ -265,6 +763,7 @@ fn test_create_flow_data_new_format_serialization() {
flow_id: None,
peers: vec![],
source_table_ids: vec![],
+ unresolved_source_table_names: vec![],
flow_context,
prev_flow_info_value: None,
did_replace: false,
@@ -327,6 +826,7 @@ fn test_flow_info_conversion_with_flow_context() {
flow_id: Some(123),
peers: vec![],
source_table_ids: vec![456, 789],
+ unresolved_source_table_names: vec![],
flow_context,
prev_flow_info_value: None,
did_replace: false,
diff --git a/src/common/meta/src/ddl/tests/drop_flow.rs b/src/common/meta/src/ddl/tests/drop_flow.rs
index af34da4809..400fd2e118 100644
--- a/src/common/meta/src/ddl/tests/drop_flow.rs
+++ b/src/common/meta/src/ddl/tests/drop_flow.rs
@@ -23,7 +23,7 @@ use table::table_name::TableName;
use crate::ddl::drop_flow::DropFlowProcedure;
use crate::ddl::test_util::create_table::test_create_table_task;
use crate::ddl::test_util::flownode_handler::NaiveFlownodeHandler;
-use crate::ddl::tests::create_flow::create_test_flow;
+use crate::ddl::tests::create_flow::{create_test_flow, create_test_pending_flow};
use crate::error;
use crate::key::table_route::TableRouteValue;
use crate::rpc::ddl::DropFlowTask;
@@ -91,3 +91,45 @@ async fn test_drop_flow() {
let err = procedure.on_prepare().await.unwrap_err();
assert_matches!(err, error::Error::FlowNotFound { .. });
}
+
+#[tokio::test]
+async fn test_drop_pending_flow_without_routes() {
+ let source_table_name = TableName::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ "drop_pending_missing_source_table",
+ );
+ let sink_table_name = TableName::new(
+ DEFAULT_CATALOG_NAME,
+ DEFAULT_SCHEMA_NAME,
+ "drop_pending_sink_table",
+ );
+ let node_manager = Arc::new(MockFlownodeManager::new(NaiveFlownodeHandler));
+ let ddl_context = new_ddl_context(node_manager);
+
+ let flow_id = create_test_pending_flow(
+ &ddl_context,
+ "drop_pending_flow",
+ vec![source_table_name],
+ sink_table_name,
+ )
+ .await;
+ let flow_info = ddl_context
+ .flow_metadata_manager
+ .flow_info_manager()
+ .get(flow_id)
+ .await
+ .unwrap()
+ .unwrap();
+ assert!(flow_info.is_pending());
+ assert!(flow_info.flownode_ids().is_empty());
+
+ let task = test_drop_flow_task("drop_pending_flow", flow_id, false);
+ let mut procedure = DropFlowProcedure::new(task, ddl_context.clone());
+ execute_procedure_until_done(&mut procedure).await;
+
+ let task = test_drop_flow_task("drop_pending_flow", flow_id, false);
+ let mut procedure = DropFlowProcedure::new(task, ddl_context);
+ let err = procedure.on_prepare().await.unwrap_err();
+ assert_matches!(err, error::Error::FlowNotFound { .. });
+}
diff --git a/src/common/meta/src/instruction.rs b/src/common/meta/src/instruction.rs
index 3fa6b1bad0..6872b9ad55 100644
--- a/src/common/meta/src/instruction.rs
+++ b/src/common/meta/src/instruction.rs
@@ -18,7 +18,7 @@ use std::time::Duration;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use store_api::region_engine::SyncRegionFromRequest;
-use store_api::region_request::RegionFlushReason;
+use store_api::region_request::{RegionFlushReason, RegionRequirements};
use store_api::storage::{FileRefsManifest, GcReport, RegionId, RegionNumber};
use strum::Display;
use table::metadata::TableId;
@@ -179,12 +179,24 @@ impl Display for OpenRegion {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
- "OpenRegion(region_ident={}, region_storage_path={})",
- self.region_ident, self.region_storage_path
+ "OpenRegion(region_ident={}, region_storage_path={}, reason={:?})",
+ self.region_ident, self.region_storage_path, self.reason
)
}
}
+/// The reason why an open region instruction is triggered.
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
+pub enum OpenRegionReason {
+ /// Open triggered before region migration.
+ RegionMigration,
+ /// Open triggered by region failover.
+ RegionFailover,
+ /// Open triggered when adding a follower region.
+ #[cfg(feature = "enterprise")]
+ RegionFollower,
+}
+
#[serde_with::serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OpenRegion {
@@ -196,6 +208,10 @@ pub struct OpenRegion {
pub region_wal_options: HashMap,
#[serde(default)]
pub skip_wal_replay: bool,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub reason: Option,
+ #[serde(default)]
+ pub requirements: RegionRequirements,
}
impl OpenRegion {
@@ -205,6 +221,8 @@ impl OpenRegion {
region_options: HashMap,
region_wal_options: HashMap,
skip_wal_replay: bool,
+ reason: Option,
+ requirements: RegionRequirements,
) -> Self {
Self {
region_ident,
@@ -212,6 +230,8 @@ impl OpenRegion {
region_options,
region_wal_options,
skip_wal_replay,
+ reason,
+ requirements,
}
}
}
@@ -1126,11 +1146,13 @@ mod tests {
HashMap::new(),
HashMap::new(),
false,
+ None,
+ RegionRequirements::empty(),
)]);
let serialized = serde_json::to_string(&open_region).unwrap();
assert_eq!(
- r#"{"OpenRegions":[{"region_ident":{"datanode_id":2,"table_id":1024,"region_number":1,"engine":"mito2"},"region_storage_path":"test/foo","region_options":{},"region_wal_options":{},"skip_wal_replay":false}]}"#,
+ r#"{"OpenRegions":[{"region_ident":{"datanode_id":2,"table_id":1024,"region_number":1,"engine":"mito2"},"region_storage_path":"test/foo","region_options":{},"region_wal_options":{},"skip_wal_replay":false,"requirements":{"object_storage":false}}]}"#,
serialized
);
@@ -1213,6 +1235,8 @@ mod tests {
HashMap::new(),
HashMap::new(),
false,
+ None,
+ RegionRequirements::empty(),
)]);
assert_eq!(open_region_instruction, open_region);
@@ -1368,10 +1392,41 @@ mod tests {
region_options,
region_wal_options: HashMap::new(),
skip_wal_replay: false,
+ reason: None,
+ requirements: RegionRequirements::empty(),
};
assert_eq!(expected, deserialized);
}
+ #[test]
+ fn test_serialize_open_region_with_reason_and_requirements() {
+ let open_region = OpenRegion::new(
+ RegionIdent {
+ datanode_id: 2,
+ table_id: 1024,
+ region_number: 1,
+ engine: "mito2".to_string(),
+ },
+ "test/foo",
+ HashMap::new(),
+ HashMap::new(),
+ false,
+ Some(OpenRegionReason::RegionMigration),
+ RegionRequirements::object_storage(),
+ );
+
+ let serialized = serde_json::to_string(&open_region).unwrap();
+ assert!(serialized.contains(r#""reason":"RegionMigration""#));
+ assert!(serialized.contains(r#""object_storage":true"#));
+
+ let deserialized: OpenRegion = serde_json::from_str(&serialized).unwrap();
+ assert_eq!(Some(OpenRegionReason::RegionMigration), deserialized.reason);
+ assert_eq!(
+ RegionRequirements::object_storage(),
+ deserialized.requirements
+ );
+ }
+
#[test]
fn test_flush_regions_creation() {
let region_id = RegionId::new(1024, 1);
diff --git a/src/common/meta/src/key/flow.rs b/src/common/meta/src/key/flow.rs
index d581b92685..bc9aaaa6b3 100644
--- a/src/common/meta/src/key/flow.rs
+++ b/src/common/meta/src/key/flow.rs
@@ -459,6 +459,7 @@ mod tests {
use super::*;
use crate::FlownodeId;
+ use crate::key::flow::flow_info::FlowStatus;
use crate::key::flow::table_flow::TableFlowKey;
use crate::key::node_address::{NodeAddressKey, NodeAddressValue};
use crate::key::{FlowPartitionId, MetadataValue};
@@ -522,6 +523,8 @@ mod tests {
query_context: None,
flow_name: flow_name.to_string(),
source_table_ids,
+ all_source_table_names: vec![],
+ unresolved_source_table_names: vec![],
sink_table_name,
flownode_ids,
raw_sql: "raw".to_string(),
@@ -529,6 +532,7 @@ mod tests {
eval_interval_secs: None,
comment: "hi".to_string(),
options: Default::default(),
+ status: FlowStatus::Active,
created_time: chrono::Utc::now(),
updated_time: chrono::Utc::now(),
}
@@ -774,6 +778,8 @@ mod tests {
query_context: None,
flow_name: "flow".to_string(),
source_table_ids: vec![1024, 1025, 1026],
+ all_source_table_names: vec![],
+ unresolved_source_table_names: vec![],
sink_table_name: another_sink_table_name,
flownode_ids: [(0, 1u64)].into(),
raw_sql: "raw".to_string(),
@@ -781,6 +787,7 @@ mod tests {
eval_interval_secs: None,
comment: "hi".to_string(),
options: Default::default(),
+ status: FlowStatus::Active,
created_time: chrono::Utc::now(),
updated_time: chrono::Utc::now(),
};
@@ -1151,6 +1158,8 @@ mod tests {
query_context: None,
flow_name: "flow".to_string(),
source_table_ids: vec![1024, 1025, 1026],
+ all_source_table_names: vec![],
+ unresolved_source_table_names: vec![],
sink_table_name: another_sink_table_name,
flownode_ids: [(0, 1u64)].into(),
raw_sql: "raw".to_string(),
@@ -1158,6 +1167,7 @@ mod tests {
eval_interval_secs: None,
comment: "hi".to_string(),
options: Default::default(),
+ status: FlowStatus::Active,
created_time: chrono::Utc::now(),
updated_time: chrono::Utc::now(),
};
diff --git a/src/common/meta/src/key/flow/flow_info.rs b/src/common/meta/src/key/flow/flow_info.rs
index d501822c3c..b1056902da 100644
--- a/src/common/meta/src/key/flow/flow_info.rs
+++ b/src/common/meta/src/key/flow/flow_info.rs
@@ -16,6 +16,8 @@ use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
use chrono::{DateTime, Utc};
+use futures::TryStreamExt;
+use futures::stream::BoxStream;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
@@ -27,12 +29,27 @@ use crate::FlownodeId;
use crate::error::{self, Result};
use crate::key::flow::FlowScoped;
use crate::key::txn_helper::TxnOpGetResponseSet;
-use crate::key::{DeserializedValueWithBytes, FlowId, FlowPartitionId, MetadataKey, MetadataValue};
+use crate::key::{
+ BytesAdapter, DeserializedValueWithBytes, FlowId, FlowPartitionId, MetadataKey, MetadataValue,
+};
use crate::kv_backend::KvBackendRef;
use crate::kv_backend::txn::{Compare, CompareOp, Txn, TxnOp};
+use crate::range_stream::{DEFAULT_PAGE_SIZE, PaginationStream};
+use crate::rpc::KeyValue;
+use crate::rpc::store::RangeRequest;
pub const FLOW_INFO_KEY_PREFIX: &str = "info";
+/// The lifecycle status of a flow stored in metadata.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
+pub enum FlowStatus {
+ /// The flow metadata exists, but at least one source table did not exist at create time.
+ PendingSources,
+ /// The flow has resolved source tables and can be scheduled on flownodes.
+ #[default]
+ Active,
+}
+
lazy_static! {
static ref FLOW_INFO_KEY_PATTERN: Regex =
Regex::new(&format!("^{FLOW_INFO_KEY_PREFIX}/([0-9]+)$")).unwrap();
@@ -114,7 +131,12 @@ impl<'a> MetadataKey<'a, FlowInfoKeyInner> for FlowInfoKeyInner {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FlowInfoValue {
/// The source tables used by the flow.
+ #[serde(default)]
pub source_table_ids: Vec,
+ #[serde(default)]
+ pub all_source_table_names: Vec,
+ #[serde(default)]
+ pub unresolved_source_table_names: Vec,
/// The sink table used by the flow.
pub sink_table_name: TableName,
/// Which flow nodes this flow is running on.
@@ -145,6 +167,8 @@ pub struct FlowInfoValue {
pub comment: String,
/// The options.
pub options: HashMap,
+ #[serde(default)]
+ pub status: FlowStatus,
/// The created time
#[serde(default)]
pub created_time: DateTime,
@@ -154,6 +178,14 @@ pub struct FlowInfoValue {
}
impl FlowInfoValue {
+ pub fn is_pending(&self) -> bool {
+ self.status == FlowStatus::PendingSources
+ }
+
+ pub fn is_active(&self) -> bool {
+ self.status == FlowStatus::Active
+ }
+
/// Returns the `flownode_id`.
pub fn flownode_ids(&self) -> &BTreeMap {
&self.flownode_ids
@@ -173,6 +205,14 @@ impl FlowInfoValue {
&self.source_table_ids
}
+ pub fn all_source_table_names(&self) -> &[TableName] {
+ &self.all_source_table_names
+ }
+
+ pub fn unresolved_source_table_names(&self) -> &[TableName] {
+ &self.unresolved_source_table_names
+ }
+
pub fn catalog_name(&self) -> &String {
&self.catalog_name
}
@@ -209,6 +249,10 @@ impl FlowInfoValue {
&self.options
}
+ pub fn status(&self) -> &FlowStatus {
+ &self.status
+ }
+
pub fn created_time(&self) -> &DateTime {
&self.created_time
}
@@ -225,6 +269,12 @@ pub struct FlowInfoManager {
kv_backend: KvBackendRef,
}
+pub fn flow_info_decoder(kv: KeyValue) -> Result<(FlowInfoKey, FlowInfoValue)> {
+ let key = FlowInfoKey::from_bytes(&kv.key)?;
+ let value = FlowInfoValue::try_from_raw_value(&kv.value)?;
+ Ok((key, value))
+}
+
impl FlowInfoManager {
/// Returns a new [FlowInfoManager].
pub fn new(kv_backend: KvBackendRef) -> Self {
@@ -254,6 +304,23 @@ impl FlowInfoManager {
.transpose()
}
+ pub fn flow_infos(&self) -> BoxStream<'static, Result<(FlowId, FlowInfoValue)>> {
+ let start_key = FlowScoped::new(BytesAdapter::from(
+ format!("{FLOW_INFO_KEY_PREFIX}/").into_bytes(),
+ ))
+ .to_bytes();
+ let req = RangeRequest::new().with_prefix(start_key);
+ let stream = PaginationStream::new(
+ self.kv_backend.clone(),
+ req,
+ DEFAULT_PAGE_SIZE,
+ flow_info_decoder,
+ )
+ .into_stream();
+
+ Box::pin(stream.map_ok(|(key, value)| (key.flow_id(), value)))
+ }
+
/// Builds a create flow transaction.
/// It is expected that the `__flow/info/{flow_id}` wasn't occupied.
/// Otherwise, the transaction will retrieve existing value.
diff --git a/src/common/meta/src/key/topic_name.rs b/src/common/meta/src/key/topic_name.rs
index 99ae631a72..79454c7cd2 100644
--- a/src/common/meta/src/key/topic_name.rs
+++ b/src/common/meta/src/key/topic_name.rs
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+use std::collections::HashMap;
use std::fmt::{self, Display};
use serde::{Deserialize, Serialize};
@@ -27,7 +28,7 @@ use crate::key::{
use crate::kv_backend::KvBackendRef;
use crate::kv_backend::txn::{Txn, TxnOp};
use crate::rpc::KeyValue;
-use crate::rpc::store::{BatchPutRequest, RangeRequest};
+use crate::rpc::store::{BatchGetRequest, BatchPutRequest, RangeRequest};
#[derive(Debug, Clone, PartialEq)]
pub struct TopicNameKey<'a> {
@@ -205,6 +206,25 @@ impl TopicNameManager {
.transpose()
}
+ /// Batch get values for specific topics.
+ pub async fn batch_get(
+ &self,
+ topics: Vec>,
+ ) -> Result> {
+ let raw_keys = topics.iter().map(|key| key.to_bytes()).collect::>();
+ let req = BatchGetRequest { keys: raw_keys };
+ let resp = self.kv_backend.batch_get(req).await?;
+
+ resp.kvs
+ .into_iter()
+ .map(|kv| {
+ let key = TopicNameKey::from_bytes(&kv.key)?;
+ let value = TopicNameValue::try_from_raw_value(&kv.value)?;
+ Ok((key.topic.to_string(), value))
+ })
+ .collect::>>()
+ }
+
/// Update the topic name key and value in the kv backend.
pub async fn update(
&self,
@@ -295,5 +315,17 @@ mod tests {
let err = manager.update(topic, 3, Some(value)).await.unwrap_err();
assert_matches!(err, error::Error::Unexpected { .. });
}
+
+ let batch_topics = topics
+ .iter()
+ .take(2)
+ .map(|topic| TopicNameKey::new(topic))
+ .chain(std::iter::once(TopicNameKey::new("missing-topic")))
+ .collect::>();
+ let values = manager.batch_get(batch_topics).await.unwrap();
+ assert_eq!(values.len(), 2);
+ for topic in topics.iter().take(2) {
+ assert_eq!(values.get(topic).unwrap().pruned_entry_id, 1);
+ }
}
}
diff --git a/src/common/meta/src/key/topic_region.rs b/src/common/meta/src/key/topic_region.rs
index 9feac10804..e7db23cfc2 100644
--- a/src/common/meta/src/key/topic_region.rs
+++ b/src/common/meta/src/key/topic_region.rs
@@ -126,6 +126,34 @@ impl ReplayCheckpoint {
metadata_entry_id,
}
}
+
+ /// Merges the checkpoint with the topic pruned entry id.
+ pub fn merge_with_topic_pruned_entry_id(
+ checkpoint: Option,
+ pruned_entry_id: Option,
+ is_metric_engine: bool,
+ ) -> Option {
+ match (checkpoint, pruned_entry_id) {
+ (Some(checkpoint), Some(pruned_entry_id)) => Some(Self {
+ entry_id: checkpoint.entry_id.max(pruned_entry_id),
+ metadata_entry_id: if is_metric_engine {
+ Some(
+ checkpoint
+ .metadata_entry_id
+ .unwrap_or_default()
+ .max(pruned_entry_id),
+ )
+ } else {
+ checkpoint.metadata_entry_id
+ },
+ }),
+ (None, Some(pruned_entry_id)) => Some(Self {
+ entry_id: pruned_entry_id,
+ metadata_entry_id: is_metric_engine.then_some(pruned_entry_id),
+ }),
+ (checkpoint, None) => checkpoint,
+ }
+ }
}
impl TopicRegionValue {
@@ -369,6 +397,58 @@ mod tests {
use super::*;
use crate::kv_backend::memory::MemoryKvBackend;
+ #[test]
+ fn test_merge_checkpoint_with_topic_pruned_entry_id_missing_pruned() {
+ let checkpoint = Some(ReplayCheckpoint::new(10, None));
+
+ assert_eq!(
+ ReplayCheckpoint::merge_with_topic_pruned_entry_id(checkpoint, None, true),
+ checkpoint
+ );
+ }
+
+ #[test]
+ fn test_merge_checkpoint_with_topic_pruned_entry_id_creates_checkpoint() {
+ assert_eq!(
+ ReplayCheckpoint::merge_with_topic_pruned_entry_id(None, Some(10), true),
+ Some(ReplayCheckpoint::new(10, Some(10)))
+ );
+ }
+
+ #[test]
+ fn test_merge_checkpoint_with_topic_pruned_entry_id_updates_both_ids() {
+ let checkpoint = ReplayCheckpoint::new(10, Some(5));
+
+ assert_eq!(
+ ReplayCheckpoint::merge_with_topic_pruned_entry_id(Some(checkpoint), Some(20), true),
+ Some(ReplayCheckpoint::new(20, Some(20)))
+ );
+ }
+
+ #[test]
+ fn test_merge_checkpoint_with_topic_pruned_entry_id_preserves_larger_ids() {
+ let checkpoint = ReplayCheckpoint::new(30, Some(40));
+
+ assert_eq!(
+ ReplayCheckpoint::merge_with_topic_pruned_entry_id(Some(checkpoint), Some(20), true),
+ Some(checkpoint)
+ );
+ }
+
+ #[test]
+ fn test_merge_checkpoint_with_topic_pruned_entry_id_for_mito() {
+ assert_eq!(
+ ReplayCheckpoint::merge_with_topic_pruned_entry_id(None, Some(10), false),
+ Some(ReplayCheckpoint::new(10, None))
+ );
+
+ let checkpoint = ReplayCheckpoint::new(5, Some(8));
+ assert_eq!(
+ ReplayCheckpoint::merge_with_topic_pruned_entry_id(Some(checkpoint), Some(10), false),
+ Some(ReplayCheckpoint::new(10, Some(8)))
+ );
+ }
+
#[tokio::test]
async fn test_topic_region_manager() {
let kv_backend = Arc::new(MemoryKvBackend::default());
diff --git a/src/datanode/src/config.rs b/src/datanode/src/config.rs
index 2ce306006b..b757c95121 100644
--- a/src/datanode/src/config.rs
+++ b/src/datanode/src/config.rs
@@ -14,6 +14,8 @@
//! Datanode configurations
+use std::time::Duration;
+
use common_base::readable_size::ReadableSize;
use common_config::{Configurable, DEFAULT_DATA_HOME};
use common_options::memory::MemoryOptions;
@@ -75,6 +77,10 @@ pub struct DatanodeOptions {
pub wal: DatanodeWalConfig,
pub storage: StorageConfig,
pub max_concurrent_queries: usize,
+ /// Timeout to acquire a permit from the concurrent query limiter when
+ /// `max_concurrent_queries` is reached. Only effective when the limiter is enabled.
+ #[serde(with = "humantime_serde")]
+ pub concurrent_query_limiter_timeout: Duration,
/// Options for different store engines.
pub region_engine: Vec,
pub logging: LoggingOptions,
@@ -131,6 +137,7 @@ impl Default for DatanodeOptions {
wal: DatanodeWalConfig::default(),
storage: StorageConfig::default(),
max_concurrent_queries: 0,
+ concurrent_query_limiter_timeout: Duration::from_millis(100),
region_engine: vec![
RegionEngineConfig::Mito(MitoConfig::default()),
RegionEngineConfig::File(FileEngineConfig::default()),
diff --git a/src/datanode/src/datanode.rs b/src/datanode/src/datanode.rs
index 9a2fe3d982..12d7c5109c 100644
--- a/src/datanode/src/datanode.rs
+++ b/src/datanode/src/datanode.rs
@@ -445,8 +445,7 @@ impl DatanodeBuilder {
event_listener,
table_provider_factory,
opts.max_concurrent_queries,
- //TODO: revaluate the hardcoded timeout on the next version of datanode concurrency limiter.
- Duration::from_millis(100),
+ opts.concurrent_query_limiter_timeout,
opts.grpc.flight_compression,
);
diff --git a/src/datanode/src/heartbeat/handler.rs b/src/datanode/src/heartbeat/handler.rs
index 10948a3e7c..79e0baaef3 100644
--- a/src/datanode/src/heartbeat/handler.rs
+++ b/src/datanode/src/heartbeat/handler.rs
@@ -313,7 +313,7 @@ mod tests {
use mito2::test_util::{CreateRequestBuilder, TestEnv};
use store_api::path_utils::table_dir;
use store_api::region_engine::RegionRole;
- use store_api::region_request::{RegionCloseRequest, RegionRequest};
+ use store_api::region_request::{RegionCloseRequest, RegionRequest, RegionRequirements};
use store_api::storage::RegionId;
use tokio::sync::mpsc::{self, Receiver};
@@ -442,6 +442,8 @@ mod tests {
HashMap::new(),
HashMap::new(),
false,
+ None,
+ RegionRequirements::empty(),
)])
}
diff --git a/src/datanode/src/heartbeat/handler/open_region.rs b/src/datanode/src/heartbeat/handler/open_region.rs
index 56c07a3efe..9c483e588d 100644
--- a/src/datanode/src/heartbeat/handler/open_region.rs
+++ b/src/datanode/src/heartbeat/handler/open_region.rs
@@ -14,6 +14,7 @@
use common_meta::instruction::{InstructionReply, OpenRegion, SimpleReply};
use common_meta::wal_provider::prepare_wal_options;
+use common_telemetry::info;
use store_api::path_utils::table_dir;
use store_api::region_request::{PathType, RegionOpenRequest};
use store_api::storage::RegionId;
@@ -41,8 +42,13 @@ impl InstructionHandler for OpenRegionsHandler {
mut region_options,
region_wal_options,
skip_wal_replay,
+ reason,
+ requirements,
} = open_region;
let region_id = RegionId::new(region_ident.table_id, region_ident.region_number);
+ info!(
+ "Received open region instruction, region_id: {region_id}, reason: {reason:?}"
+ );
prepare_wal_options(&mut region_options, region_id, ®ion_wal_options);
let request = RegionOpenRequest {
engine: region_ident.engine,
@@ -51,6 +57,7 @@ impl InstructionHandler for OpenRegionsHandler {
options: region_options,
skip_wal_replay,
checkpoint: None,
+ requirements,
};
(region_id, request)
})
@@ -85,7 +92,7 @@ mod tests {
use mito2::engine::MITO_ENGINE_NAME;
use mito2::test_util::{CreateRequestBuilder, TestEnv};
use store_api::path_utils::table_dir;
- use store_api::region_request::{RegionCloseRequest, RegionRequest};
+ use store_api::region_request::{RegionCloseRequest, RegionRequest, RegionRequirements};
use store_api::storage::RegionId;
use crate::heartbeat::handler::RegionHeartbeatResponseHandler;
@@ -98,17 +105,21 @@ mod tests {
) -> Instruction {
let region_idents = region_ids
.into_iter()
- .map(|region_id| OpenRegion {
- region_ident: RegionIdent {
- datanode_id: 0,
- table_id: region_id.table_id(),
- region_number: region_id.region_number(),
- engine: MITO_ENGINE_NAME.to_string(),
- },
- region_storage_path: storage_path.to_string(),
- region_options: HashMap::new(),
- region_wal_options: HashMap::new(),
- skip_wal_replay: false,
+ .map(|region_id| {
+ OpenRegion::new(
+ RegionIdent {
+ datanode_id: 0,
+ table_id: region_id.table_id(),
+ region_number: region_id.region_number(),
+ engine: MITO_ENGINE_NAME.to_string(),
+ },
+ storage_path,
+ HashMap::new(),
+ HashMap::new(),
+ false,
+ None,
+ RegionRequirements::empty(),
+ )
})
.collect();
diff --git a/src/datanode/src/region_server.rs b/src/datanode/src/region_server.rs
index d5711e1761..ce831353d1 100644
--- a/src/datanode/src/region_server.rs
+++ b/src/datanode/src/region_server.rs
@@ -49,6 +49,7 @@ use common_telemetry::{debug, error, info, warn};
use dashmap::DashMap;
use datafusion::datasource::TableProvider;
use datafusion_common::tree_node::TreeNode;
+use datatypes::schema::SchemaRef;
use either::Either;
use futures_util::Stream;
use futures_util::future::try_join_all;
@@ -82,7 +83,7 @@ use store_api::region_request::{
RegionOpenRequest, RegionRequest,
};
use store_api::storage::RegionId;
-use tokio::sync::{Semaphore, SemaphorePermit};
+use tokio::sync::{OwnedSemaphorePermit, Semaphore};
use tokio::time::timeout;
use tonic::{Request, Response, Result as TonicResult};
@@ -257,7 +258,7 @@ impl RegionServer {
request: api::v1::region::QueryRequest,
query_ctx: QueryContextRef,
) -> Result {
- let _permit = if let Some(p) = &self.inner.parallelism {
+ let permit = if let Some(p) = &self.inner.parallelism {
Some(p.acquire().await?)
} else {
None
@@ -298,14 +299,13 @@ impl RegionServer {
)
.await?;
- Ok(wrap_flow_region_watermark_stream(
- stream, region_id, &query_ctx,
- ))
+ let stream = wrap_flow_region_watermark_stream(stream, region_id, &query_ctx);
+ Ok(maybe_guard_stream(stream, permit))
}
#[tracing::instrument(skip_all)]
pub async fn handle_read(&self, request: QueryRequest) -> Result {
- let _permit = if let Some(p) = &self.inner.parallelism {
+ let permit = if let Some(p) = &self.inner.parallelism {
Some(p.acquire().await?)
} else {
None
@@ -332,9 +332,8 @@ impl RegionServer {
.handle_read(QueryRequest { plan, ..request }, query_ctx.clone())
.await?;
- Ok(wrap_flow_region_watermark_stream(
- stream, region_id, &query_ctx,
- ))
+ let stream = wrap_flow_region_watermark_stream(stream, region_id, &query_ctx);
+ Ok(maybe_guard_stream(stream, permit))
}
/// Returns all opened and reportable regions.
@@ -1058,7 +1057,7 @@ struct RegionServerInner {
}
struct RegionServerParallelism {
- semaphore: Semaphore,
+ semaphore: Arc,
timeout: Duration,
}
@@ -1071,19 +1070,68 @@ impl RegionServerParallelism {
return None;
}
Some(RegionServerParallelism {
- semaphore: Semaphore::new(max_concurrent_queries),
+ semaphore: Arc::new(Semaphore::new(max_concurrent_queries)),
timeout: concurrent_query_limiter_timeout,
})
}
- pub async fn acquire(&self) -> Result> {
- timeout(self.timeout, self.semaphore.acquire())
+ pub async fn acquire(&self) -> Result {
+ timeout(self.timeout, self.semaphore.clone().acquire_owned())
.await
.context(ConcurrentQueryLimiterTimeoutSnafu)?
.context(ConcurrentQueryLimiterClosedSnafu)
}
}
+/// Wraps a record batch stream and holds a concurrency permit until the stream is
+/// fully consumed (dropped), so `max_concurrent_queries` bounds the number of
+/// in-flight read streams, not just query planning.
+struct PermitGuardedStream {
+ inner: SendableRecordBatchStream,
+ _permit: OwnedSemaphorePermit,
+}
+
+impl RecordBatchStream for PermitGuardedStream {
+ fn name(&self) -> &str {
+ self.inner.name()
+ }
+
+ fn schema(&self) -> SchemaRef {
+ self.inner.schema()
+ }
+
+ fn output_ordering(&self) -> Option<&[OrderOption]> {
+ self.inner.output_ordering()
+ }
+
+ fn metrics(&self) -> Option {
+ self.inner.metrics()
+ }
+}
+
+impl Stream for PermitGuardedStream {
+ type Item = common_recordbatch::error::Result;
+
+ fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll