From d38fe5b1738bf2447e33c8f6e58226cfe4f7ee55 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 4 Aug 2026 17:40:58 +0800 Subject: [PATCH] feat(tdengine): replace Java agent with native Rust driver --- .github/scripts/bump-agent-versions.mjs | 3 +- .github/scripts/bump-agent-versions.test.mjs | 14 + .../scripts/reuse-agent-release-assets.mjs | 2 +- .../reuse-agent-release-assets.test.mjs | 18 + .github/workflows/agents-release.yml | 117 +- .github/workflows/ci.yml | 49 + agents/README.md | 13 +- agents/README.zh-CN.md | 13 +- agents/build.gradle | 2 +- .../agent/MetadataConstraintCoverageTest.java | 11 + agents/drivers/tdengine/.gitignore | 1 + agents/drivers/tdengine/Cargo.lock | 2842 +++++++++++++++++ agents/drivers/tdengine/Cargo.toml | 30 + agents/drivers/tdengine/build.gradle | 10 - agents/drivers/tdengine/src/config.rs | 274 ++ agents/drivers/tdengine/src/driver.rs | 1123 +++++++ agents/drivers/tdengine/src/lib.rs | 7 + agents/drivers/tdengine/src/main.rs | 7 + .../com/dbx/agent/tdengine/TDengineAgent.java | 980 ------ agents/drivers/tdengine/src/model.rs | 279 ++ agents/drivers/tdengine/src/runtime.rs | 662 ++++ .../tdengine/TDengineAgentPagingTest.java | 178 -- .../dbx/agent/tdengine/TDengineAgentTest.java | 584 ---- agents/drivers/tdengine/src/value.rs | 124 + agents/drivers/tdengine/tests/live.rs | 219 ++ agents/drivers/tdengine/tests/protocol.rs | 91 + agents/metadata-constraint-coverage.tsv | 2 +- .../scripts/driver_release_packages_test.py | 46 +- agents/scripts/validate_agents.py | 2 +- agents/scripts/validate_agents_test.py | 13 +- agents/scripts/version_agent_artifacts.py | 2 +- agents/settings.gradle | 2 +- 32 files changed, 5943 insertions(+), 1777 deletions(-) create mode 100644 agents/drivers/tdengine/.gitignore create mode 100644 agents/drivers/tdengine/Cargo.lock create mode 100644 agents/drivers/tdengine/Cargo.toml delete mode 100644 agents/drivers/tdengine/build.gradle create mode 100644 agents/drivers/tdengine/src/config.rs create mode 100644 agents/drivers/tdengine/src/driver.rs create mode 100644 agents/drivers/tdengine/src/lib.rs create mode 100644 agents/drivers/tdengine/src/main.rs delete mode 100644 agents/drivers/tdengine/src/main/java/com/dbx/agent/tdengine/TDengineAgent.java create mode 100644 agents/drivers/tdengine/src/model.rs create mode 100644 agents/drivers/tdengine/src/runtime.rs delete mode 100644 agents/drivers/tdengine/src/test/java/com/dbx/agent/tdengine/TDengineAgentPagingTest.java delete mode 100644 agents/drivers/tdengine/src/test/java/com/dbx/agent/tdengine/TDengineAgentTest.java create mode 100644 agents/drivers/tdengine/src/value.rs create mode 100644 agents/drivers/tdengine/tests/live.rs create mode 100644 agents/drivers/tdengine/tests/protocol.rs diff --git a/.github/scripts/bump-agent-versions.mjs b/.github/scripts/bump-agent-versions.mjs index b3f92a0875..ea0686cd75 100644 --- a/.github/scripts/bump-agent-versions.mjs +++ b/.github/scripts/bump-agent-versions.mjs @@ -51,8 +51,9 @@ const nativeDriverDirectories = { kingbase: "kingbase-go", vastbase: "vastbase-go", rabbitmq: "rabbitmq", + tdengine: "tdengine", }; -const nativeDriverModules = new Set(["cassandra", "duckdb", "oracle", "xugu", "kingbase", "vastbase", "rabbitmq"]); +const nativeDriverModules = new Set(["cassandra", "duckdb", "oracle", "xugu", "kingbase", "vastbase", "rabbitmq", "tdengine"]); function resolveAgentModule(moduleName, { legacyStandaloneModules, moduleExists, readModuleFile }) { let checkDir = null; diff --git a/.github/scripts/bump-agent-versions.test.mjs b/.github/scripts/bump-agent-versions.test.mjs index 98c5bc74a6..dee7627262 100644 --- a/.github/scripts/bump-agent-versions.test.mjs +++ b/.github/scripts/bump-agent-versions.test.mjs @@ -34,6 +34,20 @@ test("bumps DuckDB after its initial release", () => { assert.equal(result.versions.duckdb, "0.1.1"); }); +test("classifies TDengine Rust changes as native-only", () => { + const result = evaluateAgentVersionBump({ + versions: { tdengine: "0.1.39" }, + changedFiles: ["agents/drivers/tdengine/src/driver.rs"], + moduleExists: (path) => path === "agents/drivers/tdengine", + readModuleFile: () => "", + }); + + assert.equal(result.versions.tdengine, "0.1.40"); + assert.deepEqual(result.changedModules, ["tdengine"]); + assert.deepEqual(result.javaModules, []); + assert.deepEqual(result.nativeModules, ["tdengine"]); +}); + test("bumps the native RabbitMQ agent from its Go directory", () => { const result = evaluateAgentVersionBump({ versions: { rabbitmq: "0.1.0" }, diff --git a/.github/scripts/reuse-agent-release-assets.mjs b/.github/scripts/reuse-agent-release-assets.mjs index 8a342adf93..bbf2a1a57a 100644 --- a/.github/scripts/reuse-agent-release-assets.mjs +++ b/.github/scripts/reuse-agent-release-assets.mjs @@ -15,7 +15,7 @@ import { basename, join } from "node:path"; import { tmpdir } from "node:os"; const REGISTRY_ASSET = "agent-registry.json"; -const NATIVE_MODULES = new Set(["duckdb", "oracle", "xugu", "kingbase", "vastbase", "rabbitmq"]); +const NATIVE_MODULES = new Set(["duckdb", "oracle", "xugu", "kingbase", "vastbase", "rabbitmq", "tdengine"]); const PLATFORMS = [ "macos-aarch64", "macos-x64", diff --git a/.github/scripts/reuse-agent-release-assets.test.mjs b/.github/scripts/reuse-agent-release-assets.test.mjs index b99b7655b7..dda1aec367 100644 --- a/.github/scripts/reuse-agent-release-assets.test.mjs +++ b/.github/scripts/reuse-agent-release-assets.test.mjs @@ -60,6 +60,24 @@ test("rejects an incomplete reusable native platform set", () => { ); }); +test("requires all TDengine native platforms when reusing a release", () => { + const native = Object.fromEntries( + platforms.slice(0, -1).map((platform, index) => [platform, artifact(`dbx-agent-tdengine-0.1.40-${platform}.tar.zst`, String(index + 1))]), + ); + const registry = { drivers: { tdengine: { version: "0.1.40", native } }, jres: {} }; + + assert.throws( + () => collectReusableAssetPlan({ + registry, + release: releaseFor(Object.values(native)), + versions: { tdengine: "0.1.40" }, + modules: ["tdengine"], + reuseJre: false, + }), + /missing=windows-x64/, + ); +}); + test("ignores zero-size legacy JAR placeholders for native-only modules", () => { const native = Object.fromEntries( platforms.map((platform, index) => [platform, artifact(`dbx-agent-duckdb-0.1.2-${platform}.tar.zst`, String(index + 1))]), diff --git a/.github/workflows/agents-release.yml b/.github/workflows/agents-release.yml index 2e4f844074..99f809c1c9 100644 --- a/.github/workflows/agents-release.yml +++ b/.github/workflows/agents-release.yml @@ -515,6 +515,117 @@ jobs: name: duckdb-native-${{ matrix.platform }} path: "release-native/dbx-agent-duckdb-*" + build-tdengine-native: + needs: [bump-versions] + if: ${{ contains(fromJSON(needs.bump-versions.outputs.native_modules), 'tdengine') }} + strategy: + fail-fast: false + matrix: + include: + - runner: macos-latest + target: aarch64-apple-darwin + platform: macos-aarch64 + extension: "" + smoke: true + - runner: macos-15-intel + target: x86_64-apple-darwin + platform: macos-x64 + extension: "" + smoke: true + - runner: ubuntu-22.04-arm + target: aarch64-unknown-linux-gnu + platform: linux-aarch64 + extension: "" + manylinux_image: quay.io/pypa/manylinux_2_28_aarch64 + - runner: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + platform: linux-x64 + extension: "" + manylinux_image: quay.io/pypa/manylinux_2_28_x86_64 + - runner: windows-2022 + target: aarch64-pc-windows-msvc + platform: windows-aarch64 + extension: .exe + - runner: windows-2022 + target: x86_64-win7-windows-msvc + platform: windows-x64 + extension: .exe + smoke: true + win7: true + runs-on: ${{ matrix.runner }} + env: + CARGO_INCREMENTAL: "0" + CARGO_TARGET_DIR: ${{ github.workspace }}/target/tdengine-driver + RUSTC_WRAPPER: sccache + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@1.97.1 + if: runner.os != 'Linux' && matrix.win7 != true + with: + targets: ${{ matrix.target }} + - name: Setup Rust for Windows 7 + if: matrix.win7 == true + uses: dtolnay/rust-toolchain@nightly + with: + toolchain: nightly-2026-07-22 + components: rust-src + - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 + with: + version: "v0.10.0" + - name: Build TDengine native driver + shell: bash + run: | + if [ -n "${{ matrix.manylinux_image }}" ]; then + docker run --rm \ + --user "$(id -u):$(id -g)" \ + -e HOME=/tmp/dbx-rust-home \ + -e CARGO_INCREMENTAL=0 \ + -e CARGO_TARGET_DIR=/workspace/target/tdengine-driver \ + -v "${{ github.workspace }}:/workspace" \ + -w /workspace \ + "${{ matrix.manylinux_image }}" \ + bash -lc ' + mkdir -p "$HOME" + curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.97.1 + source "$HOME/.cargo/env" + cargo build \ + --manifest-path agents/drivers/tdengine/Cargo.toml \ + --locked \ + --release \ + --bin dbx-tdengine-driver \ + --target "${{ matrix.target }}" + ' + else + BUILD_STD=() + if [ "${{ matrix.win7 }}" = "true" ]; then + BUILD_STD=(-Z build-std=std,panic_abort) + fi + cargo build \ + --manifest-path agents/drivers/tdengine/Cargo.toml \ + --locked \ + --release \ + --bin dbx-tdengine-driver \ + --target "${{ matrix.target }}" \ + "${BUILD_STD[@]}" + fi + mkdir -p release-native + cp \ + "target/tdengine-driver/${{ matrix.target }}/release/dbx-tdengine-driver${{ matrix.extension }}" \ + "release-native/dbx-agent-tdengine-${{ matrix.platform }}${{ matrix.extension }}" + if [ -n "${{ matrix.manylinux_image }}" ]; then + docker run --rm \ + --user "$(id -u):$(id -g)" \ + -v "${{ github.workspace }}/release-native:/driver:ro" \ + "${{ matrix.manylinux_image }}" \ + "/driver/dbx-agent-tdengine-${{ matrix.platform }}" < /dev/null + elif [ "${{ matrix.smoke }}" = "true" ]; then + "release-native/dbx-agent-tdengine-${{ matrix.platform }}${{ matrix.extension }}" < /dev/null + fi + - uses: actions/upload-artifact@v4 + with: + name: tdengine-native-${{ matrix.platform }} + path: "release-native/dbx-agent-tdengine-*" + build-jre: needs: [bump-versions] if: ${{ needs.bump-versions.outputs.build_jre == 'true' }} @@ -624,7 +735,7 @@ jobs: retention-days: 1 release: - needs: [bump-versions, commit-versions, build-agents, build-oracle-native, build-xugu-native, build-rabbitmq-native, build-cassandra-native, build-kingbase-native, build-vastbase-native, build-duckdb-native, build-jre, reuse-previous-assets] + needs: [bump-versions, commit-versions, build-agents, build-oracle-native, build-xugu-native, build-rabbitmq-native, build-cassandra-native, build-kingbase-native, build-vastbase-native, build-duckdb-native, build-tdengine-native, build-jre, reuse-previous-assets] if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} runs-on: ubuntu-latest steps: @@ -747,6 +858,7 @@ jobs: xugu) echo "虚谷 XuguDB" ;; rabbitmq) echo "RabbitMQ" ;; cassandra) echo "Apache Cassandra" ;; + tdengine) echo "TDengine" ;; *) echo "$name" ;; esac } @@ -810,7 +922,7 @@ jobs: [ -n "$DRIVERS" ] && DRIVERS="${DRIVERS},"$'\n' DRIVERS="${DRIVERS}$(generate_jar_entry "$name" "$label" "$f" "$jre_key" "$version" "$external_driver" "$native_json")" done - for name in oracle xugu kingbase vastbase duckdb rabbitmq cassandra; do + for name in oracle xugu kingbase vastbase duckdb rabbitmq cassandra tdengine; do version=$(get_module_version "$name") [ -f "release/dbx-agent-${name}-${version}.jar" ] && continue native_json=$(generate_native_platforms "$name" "$version") @@ -881,6 +993,7 @@ jobs: xugu) echo "虚谷 XuguDB" ;; rabbitmq) echo "RabbitMQ" ;; cassandra) echo "Apache Cassandra" ;; + tdengine) echo "TDengine" ;; *) echo "$name" ;; esac } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7d7025ef6..dae8f4571f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -575,6 +575,9 @@ jobs: - name: DuckDB driver tests run: cargo test --manifest-path drivers/duckdb/Cargo.toml --locked + - name: TDengine driver tests + run: cargo test --manifest-path drivers/tdengine/Cargo.toml --locked + - name: Agent validation run: python3 scripts/validate_agents.py @@ -618,6 +621,52 @@ jobs: run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o /tmp/dbx-agent-vastbase-linux-x64 . working-directory: agents/drivers/vastbase-go + - name: TDengine native agent build + run: cargo build --manifest-path drivers/tdengine/Cargo.toml --locked --release --bin dbx-tdengine-driver + + - name: TDengine native agent integration tests + shell: bash + run: | + set -euo pipefail + while IFS='|' read -r version image; do + name="dbx-tdengine-${version//./-}" + docker rm -fv "$name" >/dev/null 2>&1 || true + docker run -d --name "$name" \ + --memory 4g \ + --ulimit nofile=65535:65535 \ + -p 6030:6030 \ + -p 6041:6041 \ + "$image" + cleanup() { + docker rm -fv "$name" >/dev/null 2>&1 || true + } + trap cleanup EXIT + ready=false + for _ in $(seq 1 90); do + if docker exec "$name" taos -s 'SELECT server_version()' >/dev/null 2>&1; then + ready=true + break + fi + sleep 2 + done + if [ "$ready" != "true" ]; then + docker logs "$name" + exit 1 + fi + TDENGINE_INTEGRATION=1 \ + TDENGINE_TEST_HOST=127.0.0.1 \ + TDENGINE_TEST_PORT=6041 \ + cargo test --manifest-path drivers/tdengine/Cargo.toml --locked --test live -- --nocapture + cleanup + trap - EXIT + done <<'EOF' + 2.4.0.14|tdengine/tdengine:2.4.0.14 + 2.6.0.34|tdengine/tdengine:2.6.0.34 + 3.0.7.1|tdengine/tdengine:3.0.7.1 + 3.3.6.13|tdengine/tdengine:3.3.6.13 + 3.4.2.2|tdengine/tsdb:3.4.2.2 + EOF + - name: Cassandra native agent integration tests shell: bash working-directory: agents/drivers/cassandra-go diff --git a/agents/README.md b/agents/README.md index 46d66c5695..024fb6b2c2 100644 --- a/agents/README.md +++ b/agents/README.md @@ -38,7 +38,7 @@ Each agent runs as a standalone process and communicates with DBX via stdin/stdo | bigquery | Google BigQuery | BigQuery JDBC | | kylin | Apache Kylin | Kylin JDBC | | sundb | SunDB | SunDB JDBC | -| tdengine | TDengine | taos-jdbcdriver (WebSocket, REST fallback) | +| tdengine | TDengine 2.4+ | taos-connector-rust native WebSocket agent | | yashandb | 崖山 YashanDB | YashanDB JDBC | | xugu | 虚谷 XuguDB | XuguDB Go native agent | | iotdb | Apache IoTDB | IoTDB JDBC | @@ -49,7 +49,7 @@ Each agent runs as a standalone process and communicates with DBX via stdin/stdo ## Multi-JRE Support -Most Java agents target JRE 21. Native agents, such as `cassandra`, `duckdb`, `oracle`, `kingbase`, `xugu`, and `rabbitmq`, do not require a JRE. DBX downloads and manages the JRE 21 installation automatically for Java agents. +Most Java agents target JRE 21. Native agents, such as `cassandra`, `duckdb`, `oracle`, `kingbase`, `tdengine`, `xugu`, and `rabbitmq`, do not require a JRE. DBX downloads and manages the JRE 21 installation automatically for Java agents. ## JDBC Connection Pooling @@ -75,7 +75,7 @@ Set `DBX_AGENT_JDBC_POOL_ENABLED=false` for a runtime-level compatibility fallba For new agents, prefer a **native (Go or Rust) driver** over a Java/JDBC agent whenever a mature, license-compatible native driver is available. Native agents ship as a single self-contained executable with no JRE, which significantly reduces memory footprint and startup time — the JVM baseline that every Java agent pays even when idle is avoided entirely. -- **Native (C++/Go/Rust)** — preferred when a usable native driver exists. See `drivers/cassandra-go` (Apache cassandra-gocql-driver), `drivers/duckdb`, `drivers/oracle-go` (go-ora), `drivers/kingbase-go` (gokb), `drivers/vastbase-go` (openGauss connector), `drivers/xugu`, and `drivers/rabbitmq` (amqp091-go) as reference implementations. No JRE download or management is needed. +- **Native (C++/Go/Rust)** — preferred when a usable native driver exists. See `drivers/cassandra-go` (Apache cassandra-gocql-driver), `drivers/duckdb`, `drivers/oracle-go` (go-ora), `drivers/kingbase-go` (gokb), `drivers/vastbase-go` (openGauss connector), `drivers/tdengine` (taos-connector-rust), `drivers/xugu`, and `drivers/rabbitmq` (amqp091-go) as reference implementations. No JRE download or management is needed. - **Java/JDBC** — the default fallback when only a JDBC driver exists for the database, or when the native driver is immature or unmaintained. Most agents still fall in this category. Native agents implement the same JSON-RPC contract and `versions.json` registration as Java agents; they ship an `agent` executable instead of `agent.jar`. If both native and Java source implementations exist for the same database, publish only the native artifact unless the Java variant has a separately registered compatibility profile, such as `oracle-legacy` / `oracle-10g`. @@ -90,11 +90,12 @@ Requires JDK 21 (Gradle toolchain auto-downloads if needed). (cd drivers/cassandra-go && go build -o agent .) (cd drivers/kingbase-go && go build -o agent .) (cd drivers/vastbase-go && go build -o agent .) +(cargo build --manifest-path drivers/tdengine/Cargo.toml --release --locked) (cd drivers/xugu && go build -o agent .) (cd drivers/rabbitmq && go build -o agent .) ``` -Output JARs are in `drivers/{module}/build/libs/`. Native agents build from `drivers/cassandra-go`, `drivers/oracle-go`, `drivers/kingbase-go`, `drivers/vastbase-go`, `drivers/xugu`, and `drivers/rabbitmq`. +Output JARs are in `drivers/{module}/build/libs/`. Native agents build from `drivers/cassandra-go`, `drivers/oracle-go`, `drivers/kingbase-go`, `drivers/vastbase-go`, `drivers/tdengine`, `drivers/xugu`, and `drivers/rabbitmq`. ### Local DBX Runtime Test @@ -108,14 +109,14 @@ cp agents/drivers//build/libs/*-all.jar ~/.dbx/agents/drivers/ Restart DBX or disconnect and reconnect the database so the new agent process loads the replacement JAR. -Native agents such as `cassandra`, `oracle`, `kingbase`, `xugu`, and `rabbitmq` use the `agent` executable in the driver directory instead of `agent.jar`. +Native agents such as `cassandra`, `oracle`, `kingbase`, `tdengine`, `xugu`, and `rabbitmq` use an `agent` executable instead of `agent.jar`. TDengine builds `target/release/dbx-tdengine-driver` from `drivers/tdengine/Cargo.toml`. ## Versioning Agent module versions are tracked in [`versions.json`](versions.json). - **Changing an existing driver** — do not edit `versions.json` manually. The release CI diffs each `drivers//` directory against the previous tag and auto-bumps the patch version for every changed module (see [`bump-agent-versions.mjs`](../.github/scripts/bump-agent-versions.mjs)). A change to the shared `agents/common` runtime bumps every module that packages it. -- **Adding a new driver** — add an entry to `versions.json`, e.g. `"rabbitmq": "0.1.0"`. The CI only bumps keys already present in the file, so a new module is invisible to versioning until it is registered here. In the same change, also add the module to `settings.gradle` and the support table above — `versions.json` keys must match the agent modules declared in `settings.gradle`, excluding the infrastructure modules `common` and `test-support`. +- **Adding a new driver** — add an entry to `versions.json`, e.g. `"rabbitmq": "0.1.0"`. The CI only bumps keys already present in the file, so a new module is invisible to versioning until it is registered here. Java modules must also be added to `settings.gradle`; native modules must be registered in the release version script and workflow. Update the support table in the same change. ## Development diff --git a/agents/README.zh-CN.md b/agents/README.zh-CN.md index d0990a585b..ff6d950df7 100644 --- a/agents/README.zh-CN.md +++ b/agents/README.zh-CN.md @@ -38,7 +38,7 @@ DBX 的 Agent 驱动 —— 通过 JDBC 和原生数据库驱动支持各种数 | bigquery | Google BigQuery | BigQuery JDBC | | kylin | Apache Kylin | Kylin JDBC | | sundb | SunDB | SunDB JDBC | -| tdengine | TDengine | taos-jdbcdriver(WebSocket,REST 兜底) | +| tdengine | TDengine 2.4+ | taos-connector-rust 原生 WebSocket agent | | yashandb | 崖山 YashanDB | YashanDB JDBC | | xugu | 虚谷 XuguDB | XuguDB Go 原生 agent | | iotdb | Apache IoTDB | IoTDB JDBC | @@ -49,7 +49,7 @@ DBX 的 Agent 驱动 —— 通过 JDBC 和原生数据库驱动支持各种数 ## 多 JRE 支持 -多数 Java agent 以 JRE 21 为目标。原生 agent(如 `cassandra`、`oracle`、`kingbase`、`xugu` 和 `rabbitmq`)不需要 JRE。对 Java agent,DBX 会自动下载并管理 JRE 21 安装。 +多数 Java agent 以 JRE 21 为目标。原生 agent(如 `cassandra`、`duckdb`、`oracle`、`kingbase`、`tdengine`、`xugu` 和 `rabbitmq`)不需要 JRE。对 Java agent,DBX 会自动下载并管理 JRE 21 安装。 ## JDBC 连接池 @@ -75,7 +75,7 @@ HikariCP 会直接打进启用连接池的 Agent JAR。已经使用 DBX 托管 J 对于新 agent,只要存在成熟、许可证兼容的原生驱动,优先选择**原生(Go 或 Rust)驱动**而非 Java/JDBC agent。原生 agent 以单一自包含可执行文件发布,无需 JRE,可显著降低内存占用和启动时间 —— 完全避开 Java agent 即便空闲也要付出的 JVM 基线开销。 -- **原生(Go/Rust)** —— 存在可用原生驱动时首选。参考 `drivers/cassandra-go`(Apache cassandra-gocql-driver)、`drivers/oracle-go`(go-ora)、`drivers/kingbase-go`(gokb)、`drivers/vastbase-go`(openGauss connector)、`drivers/xugu` 和 `drivers/rabbitmq`(amqp091-go)。无需 JRE 下载与管理。 +- **原生(Go/Rust)** —— 存在可用原生驱动时首选。参考 `drivers/cassandra-go`(Apache cassandra-gocql-driver)、`drivers/duckdb`、`drivers/oracle-go`(go-ora)、`drivers/kingbase-go`(gokb)、`drivers/vastbase-go`(openGauss connector)、`drivers/tdengine`(taos-connector-rust)、`drivers/xugu` 和 `drivers/rabbitmq`(amqp091-go)。无需 JRE 下载与管理。 - **Java/JDBC** —— 当某数据库只有 JDBC 驱动,或原生驱动不成熟、缺乏维护时的默认兜底方案。多数 agent 仍属此类。 原生 agent 实现与 Java agent 相同的 JSON-RPC 契约和 `versions.json` 登记;它发布的是 `agent` 可执行文件而非 `agent.jar`。若同一数据库同时保留原生和 Java 源码实现,默认只发布原生产物;只有 Java 变体以独立兼容配置登记时才同时发布,例如 `oracle-legacy` / `oracle-10g`。 @@ -90,11 +90,12 @@ HikariCP 会直接打进启用连接池的 Agent JAR。已经使用 DBX 托管 J (cd drivers/cassandra-go && go build -o agent .) (cd drivers/kingbase-go && go build -o agent .) (cd drivers/vastbase-go && go build -o agent .) +(cargo build --manifest-path drivers/tdengine/Cargo.toml --release --locked) (cd drivers/xugu && go build -o agent .) (cd drivers/rabbitmq && go build -o agent .) ``` -产物 JAR 在 `drivers/{module}/build/libs/`。原生 agent 从 `drivers/cassandra-go`、`drivers/oracle-go`、`drivers/kingbase-go`、`drivers/vastbase-go`、`drivers/xugu` 和 `drivers/rabbitmq` 构建。 +产物 JAR 在 `drivers/{module}/build/libs/`。原生 agent 从 `drivers/cassandra-go`、`drivers/duckdb`、`drivers/oracle-go`、`drivers/kingbase-go`、`drivers/vastbase-go`、`drivers/tdengine`、`drivers/xugu` 和 `drivers/rabbitmq` 构建。 ### 本地 DBX 运行时测试 @@ -108,14 +109,14 @@ cp agents/drivers//build/libs/*-all.jar ~/.dbx/agents/drivers/ 重启 DBX 或断开重连数据库,使新 agent 进程加载替换后的 JAR。 -`cassandra`、`oracle`、`kingbase`、`xugu` 和 `rabbitmq` 等原生 agent 使用驱动目录下的 `agent` 可执行文件而非 `agent.jar`。 +`cassandra`、`oracle`、`kingbase`、`tdengine`、`xugu` 和 `rabbitmq` 等原生 agent 使用可执行文件而非 `agent.jar`。TDengine 从 `drivers/tdengine/Cargo.toml` 构建 `target/release/dbx-tdengine-driver`。 ## 版本管理 Agent 模块的版本记录在 [`versions.json`](versions.json) 中,遵循以下规则: - **修改现有驱动**:无需手动编辑 `versions.json`。发版 CI 会把每个 `drivers//` 目录与上一个 tag 做对比,对有变更的模块自动 bump patch 版本号(见 [`bump-agent-versions.mjs`](../.github/scripts/bump-agent-versions.mjs))。若改动的是共享运行时 `agents/common`,所有依赖它的模块会一并 bump。 -- **新增驱动**:在 `versions.json` 中新增一行,例如 `"rabbitmq": "0.1.0"`。CI 只 bump 文件里已存在的 key,所以新模块在登记到这里之前对版本管理完全不可见。同一次改动中,还要把模块加进 `settings.gradle` 并更新上方的支持表 —— `versions.json` 的 key 必须与 `settings.gradle` 声明的 agent 模块一致(不含 `common`、`test-support` 这类基础设施模块)。 +- **新增驱动**:在 `versions.json` 中新增一行,例如 `"rabbitmq": "0.1.0"`。CI 只 bump 文件里已存在的 key,所以新模块在登记到这里之前对版本管理完全不可见。Java 模块还要加入 `settings.gradle`;原生模块要在发版版本脚本与 workflow 中登记,并同步更新上方支持表。 ## 开发 diff --git a/agents/build.gradle b/agents/build.gradle index f26d57a1f2..69446e11f0 100644 --- a/agents/build.gradle +++ b/agents/build.gradle @@ -8,7 +8,7 @@ def pooledJdbcProjects = [ 'access', 'bigquery', 'dameng', 'databend', 'databricks', 'db2', 'exasol', 'firebird', 'gbase8a', 'gbase8s', 'goldendb', 'h2', 'h2-legacy', 'highgo', 'hive', 'informix', 'iotdb', 'iris', 'kylin', 'neo4j', 'oceanbase-oracle', 'oscar', 'saphana', - 'snowflake', 'spark', 'sqlserver-legacy', 'sundb', 'tdengine', 'teradata', 'trino', 'uxdb', + 'snowflake', 'spark', 'sqlserver-legacy', 'sundb', 'teradata', 'trino', 'uxdb', 'vertica', 'yashandb' ] as Set def agentProjects = subprojects.findAll { !infrastructureProjects.contains(it.name) } diff --git a/agents/common/src/test/java/com/dbx/agent/MetadataConstraintCoverageTest.java b/agents/common/src/test/java/com/dbx/agent/MetadataConstraintCoverageTest.java index be8498017f..ea15d6ac12 100644 --- a/agents/common/src/test/java/com/dbx/agent/MetadataConstraintCoverageTest.java +++ b/agents/common/src/test/java/com/dbx/agent/MetadataConstraintCoverageTest.java @@ -62,6 +62,8 @@ class MetadataConstraintCoverageTest { result.add(driversRoot.relativize(file).getName(0).toString()); } else if ("main.go".equals(fileName) && hasGoMetadataDispatcher(file)) { result.add(driversRoot.relativize(file).getName(0).toString()); + } else if (fileName.endsWith(".rs") && hasRustMetadataDispatcher(file)) { + result.add(driversRoot.relativize(file).getName(0).toString()); } }); } @@ -85,6 +87,15 @@ class MetadataConstraintCoverageTest { } } + private static boolean hasRustMetadataDispatcher(Path file) { + try { + String source = readUtf8(file); + return source.contains("\"list_tables\"") || source.contains("\"list_objects\""); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + private static Map readCoverageMatrix(Path matrix) throws IOException { Map result = new HashMap<>(); for (String line : Files.readAllLines(matrix, StandardCharsets.UTF_8)) { diff --git a/agents/drivers/tdengine/.gitignore b/agents/drivers/tdengine/.gitignore new file mode 100644 index 0000000000..b83d22266a --- /dev/null +++ b/agents/drivers/tdengine/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/agents/drivers/tdengine/Cargo.lock b/agents/drivers/tdengine/Cargo.lock new file mode 100644 index 0000000000..40a9b8e2a3 --- /dev/null +++ b/agents/drivers/tdengine/Cargo.lock @@ -0,0 +1,2842 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bigdecimal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", +] + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "dbx-tdengine-driver" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "chrono", + "chrono-tz", + "futures", + "percent-encoding", + "serde", + "serde_json", + "taos", + "tokio", + "tokio-util", + "url", + "uuid", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling 0.14.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +dependencies = [ + "serde", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "faststr" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca7d44d22004409a61c393afb3369c8f7bb74abcae49fe249ee01dcc3002113" +dependencies = [ + "bytes", + "rkyv", + "serde", + "simdutf8", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "is_debug" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe266d2e243c931d8190177f20bf7f24eed45e96f39e87dc49a27b32d12d407" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "mdsn" +version = "0.2.25" +source = "git+https://github.com/taosdata/taos-connector-rust.git?rev=f49c37185987a3f5a893a02d04fe5db662a79c37#f49c37185987a3f5a893a02d04fe5db662a79c37" +dependencies = [ + "itertools", + "lazy_static", + "regex", + "thiserror", + "urlencoding", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettytable" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46480520d1b77c9a3482d39939fcf96831537a250ec62d4fd8fbdf8e0302e781" +dependencies = [ + "csv", + "encode_unicode", + "is-terminal", + "lazy_static", + "term", + "unicode-width", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rancor" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daff8b7b3ccf5f7ba270b3e7a0a4d4c701c5797e38dec27c7e2c3dbb830fed1c" +dependencies = [ + "ptr_meta", +] + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rend" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "663ba70707f96e871406fe10d68128412e619b06d1d47cb91c3a4c6501176240" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "815cc8a37159a463064825246cadb07961e25cd9885908606f6d08a98d8f8874" +dependencies = [ + "bytes", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ed1a78a1b19d184b0daa629dd9a024573173ec7d485b287cb369fb3607cc1c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shadow-rs" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3978e7641f86a78c2f619f9b13bdffb4e43a1d5119f39eb03d1bc5ac61daaf" +dependencies = [ + "const_format", + "is_debug", + "time", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "taos" +version = "0.12.4" +source = "git+https://github.com/taosdata/taos-connector-rust.git?rev=f49c37185987a3f5a893a02d04fe5db662a79c37#f49c37185987a3f5a893a02d04fe5db662a79c37" +dependencies = [ + "async-trait", + "bytes", + "chrono-tz", + "futures", + "shadow-rs", + "taos-query", + "taos-ws", + "tracing", +] + +[[package]] +name = "taos-error" +version = "0.12.4" +source = "git+https://github.com/taosdata/taos-connector-rust.git?rev=f49c37185987a3f5a893a02d04fe5db662a79c37#f49c37185987a3f5a893a02d04fe5db662a79c37" +dependencies = [ + "anyhow", + "derive_more", + "mdsn", + "rustc_version", + "serde", + "thiserror", +] + +[[package]] +name = "taos-query" +version = "0.12.4" +source = "git+https://github.com/taosdata/taos-connector-rust.git?rev=f49c37185987a3f5a893a02d04fe5db662a79c37#f49c37185987a3f5a893a02d04fe5db662a79c37" +dependencies = [ + "anyhow", + "async-trait", + "bigdecimal", + "bitflags", + "bitvec", + "byteorder", + "bytes", + "chrono", + "chrono-tz", + "derive_builder", + "derive_more", + "either", + "faststr", + "futures", + "itertools", + "lazy_static", + "mdsn", + "nom", + "num_enum", + "once_cell", + "paste", + "prettytable", + "rayon", + "rustc_version", + "rustversion", + "serde", + "serde_json", + "serde_repr", + "serde_with", + "taos-error", + "thiserror", + "tokio", + "tracing", + "url", + "uuid", + "zerocopy", +] + +[[package]] +name = "taos-ws" +version = "0.12.4" +source = "git+https://github.com/taosdata/taos-connector-rust.git?rev=f49c37185987a3f5a893a02d04fe5db662a79c37#f49c37185987a3f5a893a02d04fe5db662a79c37" +dependencies = [ + "anyhow", + "async-trait", + "byteorder", + "bytes", + "cfg-if", + "chrono-tz", + "crossbeam", + "dashmap", + "faststr", + "flume", + "futures", + "futures-util", + "itertools", + "once_cell", + "rand 0.9.5", + "rustls", + "scc", + "serde", + "serde_json", + "serde_repr", + "serde_with", + "taos-query", + "thiserror", + "tokio", + "tokio-tungstenite", + "tokio-util", + "tracing", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[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 = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.23.1" +source = "git+https://github.com/taosdata/tokio-tungstenite.git?rev=3420353#3420353b475ed1162c248cab51cd314e103bb48a" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "git+https://github.com/taosdata/tungstenite-rs.git?branch=feat%2Ftaosdata%2Fpermessage-deflate#e4d146b831fe8255c669ce9984267ba3204999c5" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "flate2", + "headers", + "http", + "httparse", + "log", + "rand 0.8.7", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "rand 0.10.2", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[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_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/agents/drivers/tdengine/Cargo.toml b/agents/drivers/tdengine/Cargo.toml new file mode 100644 index 0000000000..fe973805ff --- /dev/null +++ b/agents/drivers/tdengine/Cargo.toml @@ -0,0 +1,30 @@ +[workspace] + +[package] +name = "dbx-tdengine-driver" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +publish = false + +[dependencies] +anyhow = "1" +base64 = "0.22" +chrono = "0.4" +chrono-tz = "0.10" +futures = "0.3" +percent-encoding = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +taos = { git = "https://github.com/taosdata/taos-connector-rust.git", rev = "f49c37185987a3f5a893a02d04fe5db662a79c37", default-features = false, features = ["ws-rustls"] } +tokio = { version = "1", features = ["full"] } +tokio-util = { version = "0.7", features = ["rt"] } +url = "2" +uuid = { version = "1", features = ["v4"] } + +[profile.release] +panic = "abort" +strip = true +lto = true +codegen-units = 1 +opt-level = "s" diff --git a/agents/drivers/tdengine/build.gradle b/agents/drivers/tdengine/build.gradle deleted file mode 100644 index 3d537919af..0000000000 --- a/agents/drivers/tdengine/build.gradle +++ /dev/null @@ -1,10 +0,0 @@ -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'com.taosdata.jdbc:taos-jdbcdriver:3.6.3' -} - -tasks.named('shadowJar') { - manifest { - attributes('Agent-Label': 'TDengine', 'Main-Class': 'com.dbx.agent.tdengine.TDengineAgent') - } -} diff --git a/agents/drivers/tdengine/src/config.rs b/agents/drivers/tdengine/src/config.rs new file mode 100644 index 0000000000..3697ecc2dc --- /dev/null +++ b/agents/drivers/tdengine/src/config.rs @@ -0,0 +1,274 @@ +use anyhow::{bail, Context, Result}; +use percent_encoding::percent_decode_str; +use std::net::IpAddr; +use url::Url; + +use crate::model::ConnectParams; + +const DEFAULT_HOST: &str = "127.0.0.1"; +const DEFAULT_PORT: u16 = 6041; +const DEFAULT_USER: &str = "root"; +const DEFAULT_PASSWORD: &str = "taosdata"; + +#[derive(Debug)] +pub struct BuiltDsn { + pub value: String, + pub database: String, +} + +pub fn build_dsn(params: &ConnectParams) -> Result { + if !params.client_cert_path.trim().is_empty() || !params.client_key_path.trim().is_empty() { + bail!("TDengine Rust WebSocket connector does not support client certificate authentication"); + } + + let mut url = if params.connection_string.trim().is_empty() { + build_from_fields(params)? + } else { + normalize_connection_string(params.connection_string.trim(), params.ssl)? + }; + + apply_connection_fields(&mut url, params)?; + merge_query_params(&mut url, ¶ms.url_params); + if (params.ssl || !params.ca_cert_path.trim().is_empty()) && url.scheme() == "ws" { + url.set_scheme("wss").map_err(|_| anyhow::anyhow!("failed to enable TLS in TDengine connection URL"))?; + } + if !params.ca_cert_path.trim().is_empty() { + set_query_param(&mut url, "tls_mode", "verify_identity"); + set_query_param(&mut url, "tls_ca", params.ca_cert_path.trim()); + } + let database = url + .path_segments() + .and_then(|mut segments| segments.find(|segment| !segment.is_empty())) + .map(|segment| percent_decode_str(segment).decode_utf8_lossy().into_owned()) + .unwrap_or_default(); + Ok(BuiltDsn { value: url.into(), database }) +} + +fn build_from_fields(params: &ConnectParams) -> Result { + let scheme = if params.ssl { "wss" } else { "ws" }; + let host = if params.host.trim().is_empty() { DEFAULT_HOST } else { params.host.trim() }; + let port = if params.port == 0 { DEFAULT_PORT } else { params.port }; + let username = if params.username.is_empty() { DEFAULT_USER } else { ¶ms.username }; + let password = if params.password.is_empty() { DEFAULT_PASSWORD } else { ¶ms.password }; + let mut url = Url::parse(&format!("{scheme}://{DEFAULT_HOST}:{port}/"))?; + if let Ok(address) = host.parse::() { + url.set_ip_host(address).map_err(|_| anyhow::anyhow!("invalid TDengine host"))?; + } else { + url.set_host(Some(host)).map_err(|_| anyhow::anyhow!("invalid TDengine host"))?; + } + url.set_username(username).map_err(|_| anyhow::anyhow!("invalid TDengine username"))?; + url.set_password(Some(password)).map_err(|_| anyhow::anyhow!("invalid TDengine password"))?; + if !params.database.trim().is_empty() { + url.set_path(&format!("/{}", params.database.trim())); + } + Ok(url) +} + +fn apply_connection_fields(url: &mut Url, params: &ConnectParams) -> Result<()> { + if url.username().is_empty() { + let username = if params.username.is_empty() { DEFAULT_USER } else { ¶ms.username }; + url.set_username(username).map_err(|_| anyhow::anyhow!("invalid TDengine username"))?; + } + if url.password().is_none() { + let password = if params.password.is_empty() { DEFAULT_PASSWORD } else { ¶ms.password }; + url.set_password(Some(password)).map_err(|_| anyhow::anyhow!("invalid TDengine password"))?; + } + if url.path().trim_matches('/').is_empty() && !params.database.trim().is_empty() { + url.set_path(&format!("/{}", params.database.trim())); + } + Ok(()) +} + +fn normalize_connection_string(raw: &str, ssl: bool) -> Result { + let trimmed = raw.trim(); + let normalized = if let Some(value) = strip_prefix_ignore_ascii_case(trimmed, "jdbc:TAOS-WS://") { + format!("{}://{value}", if ssl { "wss" } else { "ws" }) + } else if let Some(value) = strip_prefix_ignore_ascii_case(trimmed, "jdbc:TAOS-RS://") { + format!("{}://{value}", if ssl { "wss" } else { "ws" }) + } else if let Some(value) = strip_prefix_ignore_ascii_case(trimmed, "tdengine://") { + format!("{}://{value}", if ssl { "wss" } else { "ws" }) + } else if let Some(value) = strip_prefix_ignore_ascii_case(trimmed, "taosws://") { + format!("{}://{value}", if ssl { "wss" } else { "ws" }) + } else if let Some(value) = strip_prefix_ignore_ascii_case(trimmed, "taoswss://") { + format!("wss://{value}") + } else { + trimmed.to_string() + }; + let mut url = Url::parse(&normalized).with_context(|| "invalid TDengine connection string")?; + if !matches!(url.scheme(), "ws" | "wss" | "http" | "https") { + bail!("TDengine native agent supports only WebSocket connection strings"); + } + if matches!(url.scheme(), "http") { + url.set_scheme("ws").map_err(|_| anyhow::anyhow!("invalid TDengine HTTP connection string"))?; + } else if matches!(url.scheme(), "https") { + url.set_scheme("wss").map_err(|_| anyhow::anyhow!("invalid TDengine HTTPS connection string"))?; + } + remove_control_params(&mut url); + Ok(url) +} + +fn merge_query_params(url: &mut Url, raw: &str) { + let raw = raw.trim().trim_start_matches('?'); + if raw.is_empty() { + return; + } + let additions = url::form_urlencoded::parse(raw.as_bytes()) + .filter(|(key, _)| !is_control_param(key)) + .map(|(key, value)| (key.into_owned(), value.into_owned())) + .collect::>(); + let mut query = url.query_pairs_mut(); + for (key, value) in additions { + query.append_pair(&key, &value); + } +} + +fn remove_control_params(url: &mut Url) { + let kept = url + .query_pairs() + .filter(|(key, _)| !is_control_param(key)) + .map(|(key, value)| (key.into_owned(), value.into_owned())) + .collect::>(); + url.set_query(None); + if kept.is_empty() { + return; + } + let mut query = url.query_pairs_mut(); + for (key, value) in kept { + query.append_pair(&key, &value); + } +} + +fn set_query_param(url: &mut Url, key: &str, value: &str) { + let mut pairs = url + .query_pairs() + .filter(|(existing, _)| !existing.eq_ignore_ascii_case(key)) + .map(|(existing, value)| (existing.into_owned(), value.into_owned())) + .collect::>(); + pairs.push((key.to_string(), value.to_string())); + url.set_query(None); + let mut query = url.query_pairs_mut(); + for (key, value) in pairs { + query.append_pair(&key, &value); + } +} + +fn is_control_param(key: &str) -> bool { + key.eq_ignore_ascii_case("transport") || key.eq_ignore_ascii_case("dbx.transport") +} + +fn strip_prefix_ignore_ascii_case<'a>(value: &'a str, prefix: &str) -> Option<&'a str> { + value.get(..prefix.len()).filter(|head| head.eq_ignore_ascii_case(prefix))?; + value.get(prefix.len()..) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builds_ws_dsn_from_connection_fields() { + let dsn = build_dsn(&ConnectParams { + host: "db.example.com".into(), + port: 6041, + database: "power_data".into(), + username: "root".into(), + password: "secret".into(), + url_params: "timezone=UTC&dbx.transport=rest".into(), + ..Default::default() + }) + .unwrap(); + assert_eq!(dsn.value, "ws://root:secret@db.example.com:6041/power_data?timezone=UTC"); + assert_eq!(dsn.database, "power_data"); + } + + #[test] + fn accepts_legacy_jdbc_urls_without_transport_controls() { + let dsn = build_dsn(&ConnectParams { + connection_string: "jdbc:TAOS-WS://127.0.0.1:6041/db?transport=ws&timezone=UTC".into(), + ..Default::default() + }) + .unwrap(); + assert_eq!(dsn.value, "ws://root:taosdata@127.0.0.1:6041/db?timezone=UTC"); + assert_eq!(dsn.database, "db"); + } + + #[test] + fn fills_connection_string_credentials_and_database_from_fields() { + let dsn = build_dsn(&ConnectParams { + username: "reader".into(), + password: "secret".into(), + database: "metrics".into(), + connection_string: "jdbc:TAOS-WS://td.example.com:6041?transport=ws".into(), + ..Default::default() + }) + .unwrap(); + assert_eq!(dsn.value, "ws://reader:secret@td.example.com:6041/metrics"); + assert_eq!(dsn.database, "metrics"); + } + + #[test] + fn preserves_explicit_connection_string_credentials_and_database() { + let dsn = build_dsn(&ConnectParams { + username: "ignored".into(), + password: "ignored".into(), + database: "ignored".into(), + connection_string: "ws://url-user:url-pass@td.example.com:6041/url-db".into(), + ..Default::default() + }) + .unwrap(); + assert_eq!(dsn.value, "ws://url-user:url-pass@td.example.com:6041/url-db"); + assert_eq!(dsn.database, "url-db"); + } + + #[test] + fn passes_ca_certificate_path_to_the_websocket_connector() { + let dsn = + build_dsn(&ConnectParams { ssl: true, ca_cert_path: "/tmp/tdengine-ca.pem".into(), ..Default::default() }) + .unwrap(); + let url = Url::parse(&dsn.value).unwrap(); + assert_eq!(url.scheme(), "wss"); + let query = url.query_pairs().collect::>(); + assert_eq!(query.get("tls_mode").map(|value| value.as_ref()), Some("verify_identity")); + assert_eq!(query.get("tls_ca").map(|value| value.as_ref()), Some("/tmp/tdengine-ca.pem")); + } + + #[test] + fn rejects_mutual_tls_paths_instead_of_ignoring_them() { + let error = + build_dsn(&ConnectParams { client_cert_path: "/tmp/client.pem".into(), ..Default::default() }).unwrap_err(); + assert!(error.to_string().contains("client certificate")); + } + + #[test] + fn ca_certificate_enables_tls_and_replaces_conflicting_query_values() { + let dsn = build_dsn(&ConnectParams { + connection_string: "ws://td.example.com:6041/db?tls_mode=verify_ca&tls_ca=old.pem".into(), + ca_cert_path: "/tmp/new-ca.pem".into(), + ..Default::default() + }) + .unwrap(); + let url = Url::parse(&dsn.value).unwrap(); + assert_eq!(url.scheme(), "wss"); + let tls_mode = url.query_pairs().filter(|(key, _)| key == "tls_mode").collect::>(); + let tls_ca = url.query_pairs().filter(|(key, _)| key == "tls_ca").collect::>(); + assert_eq!(tls_mode.as_slice(), &[("tls_mode".into(), "verify_identity".into())]); + assert_eq!(tls_ca.as_slice(), &[("tls_ca".into(), "/tmp/new-ca.pem".into())]); + } + + #[test] + fn supports_ipv6_hosts_and_decodes_connection_database() { + let fields = + build_dsn(&ConnectParams { host: "::1".into(), database: "power data".into(), ..Default::default() }) + .unwrap(); + assert_eq!(fields.value, "ws://root:taosdata@[::1]:6041/power%20data"); + assert_eq!(fields.database, "power data"); + + let connection = build_dsn(&ConnectParams { + connection_string: "taoswss://td.example.com:6041/power%20data".into(), + ..Default::default() + }) + .unwrap(); + assert_eq!(connection.value, "wss://root:taosdata@td.example.com:6041/power%20data"); + assert_eq!(connection.database, "power data"); + } +} diff --git a/agents/drivers/tdengine/src/driver.rs b/agents/drivers/tdengine/src/driver.rs new file mode 100644 index 0000000000..9d26b6c34a --- /dev/null +++ b/agents/drivers/tdengine/src/driver.rs @@ -0,0 +1,1123 @@ +use std::collections::{HashMap, HashSet}; +use std::future::Future; +use std::time::{Duration, Instant}; + +use anyhow::{anyhow, bail, Context, Result}; +use futures::future::poll_fn; +use serde_json::Value; +use taos::{AsyncFetchable, AsyncQueryable, AsyncTBuilder, RawBlock, ResultSet, Taos, TaosBuilder}; +use tokio::time::timeout; +use tokio_util::sync::CancellationToken; +use uuid::Uuid; + +use crate::config::build_dsn; +use crate::model::{ + AgentConnectionInfo, ColumnInfo, CompletionAssistantCandidate, CompletionAssistantRequest, + CompletionAssistantResponse, ConnectParams, DatabaseConnectionInfo, DatabaseInfo, MetadataListConstraints, + ObjectInfo, ObjectSource, QueryOptions, QueryPageResult, QueryResult, TableInfo, DEFAULT_MAX_ROWS, + TDENGINE_DATA_TYPES, +}; +use crate::value::borrowed_value_to_json; + +const TABLE_CACHE_TTL: Duration = Duration::from_secs(10); +const QUERY_SESSION_IDLE_TIMEOUT: Duration = Duration::from_secs(10 * 60); +const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(15); + +pub struct TdengineDriver { + connection: Option, + params: ConnectParams, + server_version: Option, + current_database: String, + query_sessions: HashMap, + table_cache: Option, +} + +struct TableCache { + database: String, + loaded_at: Instant, + tables: Vec, +} + +struct QueryCursor { + result_set: ResultSet, + columns: Vec, + column_types: Vec, + affected_rows: i64, + max_rows: usize, + rows_read: usize, + pending_block: Option, + pending_row_index: usize, + pending_row: Option>, + last_accessed_at: Instant, +} + +impl TdengineDriver { + pub fn new() -> Self { + Self { + connection: None, + params: ConnectParams::default(), + server_version: None, + current_database: String::new(), + query_sessions: HashMap::new(), + table_cache: None, + } + } + + pub async fn connect(&mut self, params: ConnectParams) -> Result<()> { + self.disconnect().await; + let dsn = build_dsn(¶ms)?; + let builder = + TaosBuilder::from_dsn(&dsn.value).with_context(|| "failed to configure TDengine WebSocket connector")?; + let connect_timeout = connect_timeout(¶ms); + let connection = timeout(connect_timeout, builder.build()) + .await + .map_err(|_| anyhow!("TDengine connection timed out after {} seconds", connect_timeout.as_secs()))??; + let token = CancellationToken::new(); + let server_version = query_scalar_string(&connection, "SELECT server_version()", &token, 5).await.ok(); + self.current_database = dsn.database; + self.server_version = server_version; + self.params = params; + self.connection = Some(connection); + Ok(()) + } + + pub async fn test_connection(params: ConnectParams) -> Result { + let mut driver = Self::new(); + driver.connect(params).await?; + let token = CancellationToken::new(); + driver.validate_connection(&token).await?; + driver.connection_info()?.database_info.ok_or_else(|| anyhow!("TDengine connection metadata is unavailable")) + } + + pub async fn disconnect(&mut self) { + self.query_sessions.clear(); + self.table_cache = None; + self.server_version = None; + self.current_database.clear(); + self.connection = None; + } + + pub async fn validate_connection(&self, token: &CancellationToken) -> Result<()> { + query_scalar_string(self.require_connection()?, "SELECT server_version()", token, 3).await?; + Ok(()) + } + + pub fn connection_info(&self) -> Result { + self.require_connection()?; + Ok(AgentConnectionInfo { + identifier_quote: "`".into(), + compatibility_mode: None, + database_info: Some(DatabaseConnectionInfo { + product_name: Some("TDengine".into()), + product_version: self.server_version.clone(), + current_database: non_empty(&self.current_database), + driver_name: Some("taos-connector-rust".into()), + driver_version: Some("0.12.4-git-f49c3718".into()), + unquoted_identifier_case: Some("lower"), + quoted_identifier_case: Some("lower"), + }), + }) + } + + pub async fn list_databases(&self, token: &CancellationToken) -> Result> { + let rows = self.query_rows("SHOW DATABASES", token, 0).await?; + Ok(rows + .into_iter() + .filter_map(|row| row.first().and_then(json_text).map(str::to_string)) + .filter(|name| !is_system_database(name)) + .map(|name| DatabaseInfo { name }) + .collect()) + } + + pub fn list_schemas(&self) -> Vec { + Vec::new() + } + + pub fn list_data_types(&self) -> Vec { + TDENGINE_DATA_TYPES.iter().map(|value| (*value).to_string()).collect() + } + + pub async fn list_tables( + &mut self, + database: &str, + constraints: MetadataListConstraints, + token: &CancellationToken, + ) -> Result> { + if !table_type_allowed(&constraints.object_types, "TABLE") { + return Ok(Vec::new()); + } + let database = effective_database(database, &self.current_database)?.to_string(); + if constraints.limit > 0 && constraints.filter.trim().is_empty() { + return self.list_tables_page(&database, constraints.offset, constraints.limit, token).await; + } + let tables = self.all_tables(&database, token).await?; + Ok(filter_tables(tables, &constraints)) + } + + pub async fn list_objects( + &mut self, + database: &str, + constraints: MetadataListConstraints, + token: &CancellationToken, + ) -> Result> { + let database = effective_database(database, &self.current_database)?.to_string(); + let tables = self.list_tables(&database, constraints, token).await?; + Ok(tables + .into_iter() + .map(|table| ObjectInfo { + name: table.name, + object_type: table.table_type, + schema: database.clone(), + comment: table.comment, + }) + .collect()) + } + + pub async fn get_columns(&self, database: &str, table: &str, token: &CancellationToken) -> Result> { + let database = effective_database(database, &self.current_database)?; + let rows = self.query_rows(&format!("DESCRIBE {}", qualified_name(database, table)), token, 0).await?; + Ok(parse_describe_columns(rows)) + } + + pub async fn get_table_comment(&self, _database: &str, _table: &str) -> Result> { + Ok(None) + } + + pub async fn get_object_source( + &self, + database: &str, + name: &str, + object_type: &str, + token: &CancellationToken, + ) -> Result { + let database = effective_database(database, &self.current_database)?; + let mut source = self.get_create_sql(database, name, object_type, token).await?; + if source.is_empty() { + let columns = self.get_columns(database, name, token).await?; + source = build_fallback_table_ddl(database, name, &columns); + } + Ok(ObjectSource { + name: name.to_string(), + object_type: object_type.to_string(), + schema: database.to_string(), + source, + editable: true, + }) + } + + pub async fn get_table_ddl(&self, database: &str, table: &str, token: &CancellationToken) -> Result { + let stable = self.get_create_sql(database, table, "STABLE", token).await?; + if !stable.is_empty() { + return Ok(stable); + } + let table_source = self.get_create_sql(database, table, "TABLE", token).await?; + if !table_source.is_empty() { + return Ok(table_source); + } + let columns = self.get_columns(database, table, token).await?; + Ok(build_fallback_table_ddl(database, table, &columns)) + } + + pub async fn completion_assistant_search( + &mut self, + request: CompletionAssistantRequest, + token: &CancellationToken, + ) -> Result { + let allowed = request.object_kinds.iter().map(|kind| kind.to_ascii_lowercase()).collect::>(); + let include_databases = allowed.contains("database") || allowed.contains("schema"); + let include_tables = allowed.is_empty() || allowed.contains("table") || allowed.contains("view"); + let include_columns = allowed.contains("column"); + let max_results = if request.max_results == 0 { 100 } else { request.max_results.min(1000) }; + let mut candidates = Vec::new(); + let requested_database = if !request.parent_schema.trim().is_empty() { + request.parent_schema.trim() + } else if !request.schema.trim().is_empty() { + request.schema.trim() + } else { + request.database.trim() + }; + let databases = if request.global_search || (include_databases && requested_database.is_empty()) { + self.list_databases(token).await?.into_iter().map(|database| database.name).collect() + } else { + vec![effective_database(requested_database, &self.current_database)?.to_string()] + }; + + if include_databases { + for database in &databases { + if completion_matches(database, &request) { + candidates.push(CompletionAssistantCandidate { + name: database.clone(), + kind: "schema".into(), + database: Some(database.clone()), + schema: Some(database.clone()), + parent_schema: None, + parent_name: None, + comment: None, + data_type: None, + }); + if candidates.len() >= max_results { + return Ok(CompletionAssistantResponse { candidates, incomplete: true, fallback_used: false }); + } + } + } + } + + for database in databases { + if include_columns && !request.parent_name.trim().is_empty() { + for column in self.get_columns(&database, request.parent_name.trim(), token).await? { + if completion_matches(&column.name, &request) { + candidates.push(CompletionAssistantCandidate { + name: column.name, + kind: "column".into(), + database: Some(database.clone()), + schema: Some(database.clone()), + parent_schema: Some(database.clone()), + parent_name: Some(request.parent_name.trim().to_string()), + comment: column.comment, + data_type: Some(column.data_type), + }); + } + if candidates.len() >= max_results { + return Ok(CompletionAssistantResponse { candidates, incomplete: true, fallback_used: false }); + } + } + continue; + } + if include_tables { + for table in self.all_tables(&database, token).await? { + if completion_matches(&table.name, &request) { + candidates.push(CompletionAssistantCandidate { + name: table.name, + kind: "table".into(), + database: Some(database.clone()), + schema: Some(database.clone()), + parent_schema: None, + parent_name: table.parent_name, + comment: table.comment, + data_type: None, + }); + } + if candidates.len() >= max_results { + return Ok(CompletionAssistantResponse { candidates, incomplete: true, fallback_used: false }); + } + } + } + } + Ok(CompletionAssistantResponse { candidates, incomplete: false, fallback_used: false }) + } + + pub async fn execute_query(&mut self, options: QueryOptions, token: &CancellationToken) -> Result { + let start = Instant::now(); + self.prepare_database(&options, token).await?; + let max_rows = normalized_max_rows(options.max_rows); + let mut cursor = self.start_cursor(&options.sql, max_rows, token, options.timeout_secs).await?; + let mut page = read_cursor_page(&mut cursor, max_rows, token, options.timeout_secs).await?; + let (_, truncated) = cursor.prepare_next_page(token, options.timeout_secs).await?; + page.truncated = truncated; + if may_change_metadata(&options.sql) { + self.table_cache = None; + } + Ok(QueryResult { + columns: page.columns, + column_types: page.column_types, + rows: page.rows, + affected_rows: page.affected_rows, + execution_time_ms: start.elapsed().as_millis() as i64, + truncated: page.truncated, + }) + } + + pub async fn execute_query_page( + &mut self, + options: QueryOptions, + token: &CancellationToken, + ) -> Result { + let start = Instant::now(); + self.expire_query_sessions(); + self.prepare_database(&options, token).await?; + let max_rows = normalized_max_rows(options.max_rows); + let page_size = normalized_page_size(options.page_size, options.fetch_size, max_rows); + let mut cursor = self.start_cursor(&options.sql, max_rows, token, options.timeout_secs).await?; + let mut page = read_cursor_page(&mut cursor, page_size, token, options.timeout_secs).await?; + let (has_more, truncated) = cursor.prepare_next_page(token, options.timeout_secs).await?; + page.truncated = truncated; + if has_more { + let session_id = format!("tdengine-{}", Uuid::new_v4()); + page.session_id = Some(session_id.clone()); + page.has_more = true; + self.query_sessions.insert(session_id, cursor); + } + page.execution_time_ms = start.elapsed().as_millis() as i64; + if may_change_metadata(&options.sql) { + self.table_cache = None; + } + Ok(page) + } + + pub async fn fetch_query_page( + &mut self, + session_id: &str, + page_size: usize, + timeout_secs: u64, + token: &CancellationToken, + ) -> Result { + self.expire_query_sessions(); + let Some(mut cursor) = self.query_sessions.remove(session_id) else { + bail!("TDengine query session not found: {session_id}"); + }; + cursor.last_accessed_at = Instant::now(); + let page_size = normalized_page_size(page_size, 0, cursor.remaining_limit().max(1)); + let mut page = read_cursor_page(&mut cursor, page_size, token, timeout_secs).await?; + let (has_more, truncated) = cursor.prepare_next_page(token, timeout_secs).await?; + page.truncated = truncated; + if has_more { + page.session_id = Some(session_id.to_string()); + page.has_more = true; + self.query_sessions.insert(session_id.to_string(), cursor); + } + Ok(page) + } + + pub fn close_query_session(&mut self, session_id: &str) -> bool { + self.query_sessions.remove(session_id).is_some() + } + + fn expire_query_sessions(&mut self) { + self.query_sessions.retain(|_, cursor| cursor.last_accessed_at.elapsed() <= QUERY_SESSION_IDLE_TIMEOUT); + } + + pub async fn execute_statements( + &mut self, + statements: Vec, + database: &str, + timeout_secs: u64, + token: &CancellationToken, + ) -> Result { + let start = Instant::now(); + if !database.trim().is_empty() { + self.use_database(database, token, timeout_secs).await?; + } + let mut affected_rows = 0i64; + for statement in &statements { + let sql = trim_sql(statement); + if sql.is_empty() { + continue; + } + let result_set = cancellable(token, timeout_secs, self.require_connection()?.query(sql)).await?; + affected_rows += i64::from(result_set.affected_rows()); + } + if statements.iter().any(|statement| may_change_metadata(statement)) { + self.table_cache = None; + } + Ok(QueryResult { + columns: Vec::new(), + column_types: Vec::new(), + rows: Vec::new(), + affected_rows, + execution_time_ms: start.elapsed().as_millis() as i64, + truncated: false, + }) + } + + pub async fn get_explain_info(&self, sql: &str, token: &CancellationToken, timeout_secs: u64) -> Result { + let sql = trim_sql(sql); + if sql.is_empty() { + bail!("SQL is required"); + } + let rows = self.query_rows(&format!("EXPLAIN {sql}"), token, timeout_secs).await?; + Ok(rows + .into_iter() + .map(|row| row.iter().map(display_json_value).collect::>().join("\t")) + .collect::>() + .join("\n")) + } + + async fn prepare_database(&mut self, options: &QueryOptions, token: &CancellationToken) -> Result<()> { + let database = if options.schema.trim().is_empty() { options.database.trim() } else { options.schema.trim() }; + if database.is_empty() || database == self.current_database { + return Ok(()); + } + self.use_database(database, token, options.timeout_secs).await + } + + async fn use_database(&mut self, database: &str, token: &CancellationToken, timeout_secs: u64) -> Result<()> { + let database = validate_database_name(database)?; + let sql = format!("USE {database}"); + cancellable(token, timeout_secs, self.require_connection()?.exec(sql)).await?; + self.current_database = database.to_string(); + Ok(()) + } + + async fn start_cursor( + &self, + sql: &str, + max_rows: usize, + token: &CancellationToken, + timeout_secs: u64, + ) -> Result { + let sql = trim_sql(sql); + if sql.is_empty() { + bail!("SQL is required"); + } + let result_set = cancellable(token, timeout_secs, self.require_connection()?.query(sql)).await?; + let columns = result_set.fields().iter().map(|field| field.name().to_string()).collect(); + let column_types = result_set.fields().iter().map(|field| field.ty().name().to_string()).collect(); + let affected_rows = i64::from(result_set.affected_rows()); + Ok(QueryCursor { + result_set, + columns, + column_types, + affected_rows, + max_rows, + rows_read: 0, + pending_block: None, + pending_row_index: 0, + pending_row: None, + last_accessed_at: Instant::now(), + }) + } + + async fn query_rows(&self, sql: &str, token: &CancellationToken, timeout_secs: u64) -> Result>> { + let mut cursor = self.start_cursor(sql, usize::MAX, token, timeout_secs).await?; + let mut rows = Vec::new(); + while let Some(row) = cursor.next_row(token, timeout_secs).await? { + rows.push(row); + } + Ok(rows) + } + + async fn all_tables(&mut self, database: &str, token: &CancellationToken) -> Result> { + if let Some(cache) = &self.table_cache { + if cache.database == database && cache.loaded_at.elapsed() <= TABLE_CACHE_TTL { + return Ok(cache.tables.clone()); + } + } + let mut tables = self + .query_tables(&format!("SHOW {}STABLES", quote_qualified_prefix(database)), "STABLE", false, token) + .await?; + tables.extend( + self.query_tables(&format!("SHOW {}TABLES", quote_qualified_prefix(database)), "TABLE", true, token) + .await?, + ); + self.enrich_table_parents(database, &mut tables, token).await?; + let mut seen = HashSet::new(); + tables.retain(|table| seen.insert(format!("{}:{}", table.table_type, table.name))); + sort_tables_for_hierarchy(&mut tables); + self.table_cache = + Some(TableCache { database: database.to_string(), loaded_at: Instant::now(), tables: tables.clone() }); + Ok(tables) + } + + async fn list_tables_page( + &self, + database: &str, + offset: usize, + limit: usize, + token: &CancellationToken, + ) -> Result> { + let (mut stables, scanned) = self + .query_tables_page( + &format!("SHOW {}STABLES", quote_qualified_prefix(database)), + "STABLE", + false, + offset, + limit, + token, + ) + .await?; + if stables.len() >= limit { + return Ok(stables); + } + let table_offset = offset.saturating_sub(scanned); + let (tables, _) = self + .query_tables_page( + &format!("SHOW {}TABLES", quote_qualified_prefix(database)), + "TABLE", + true, + table_offset, + limit - stables.len(), + token, + ) + .await?; + stables.extend(tables); + self.enrich_table_parents(database, &mut stables, token).await?; + Ok(stables) + } + + async fn enrich_table_parents( + &self, + database: &str, + tables: &mut [TableInfo], + token: &CancellationToken, + ) -> Result<()> { + if !tables.iter().any(|table| table.table_type == "TABLE" && table.parent_name.is_none()) { + return Ok(()); + } + let sql = format!( + "SELECT table_name, stable_name FROM information_schema.ins_tables WHERE db_name = {}", + quote_literal(database) + ); + let rows = match self.query_rows(&sql, token, 0).await { + Ok(rows) => rows, + Err(error) if token.is_cancelled() => return Err(error), + Err(_) => return Ok(()), + }; + let parents = rows + .into_iter() + .filter_map(|row| { + let table = row.first().and_then(json_text)?.to_ascii_lowercase(); + let parent = row.get(1).and_then(json_text)?.trim(); + (!parent.is_empty()).then(|| (table, parent.to_string())) + }) + .collect::>(); + for table in tables { + if table.table_type == "TABLE" && table.parent_name.is_none() { + table.parent_name = parents.get(&table.name.to_ascii_lowercase()).cloned(); + } + } + Ok(()) + } + + async fn query_tables( + &self, + sql: &str, + table_type: &str, + includes_stable_name: bool, + token: &CancellationToken, + ) -> Result> { + let rows = self.query_rows(sql, token, 0).await?; + Ok(rows.into_iter().filter_map(|row| table_from_show_row(row, table_type, includes_stable_name)).collect()) + } + + async fn query_tables_page( + &self, + sql: &str, + table_type: &str, + includes_stable_name: bool, + offset: usize, + limit: usize, + token: &CancellationToken, + ) -> Result<(Vec, usize)> { + let mut cursor = self.start_cursor(sql, offset.saturating_add(limit), token, 0).await?; + let mut scanned = 0usize; + let mut result = Vec::with_capacity(limit); + while result.len() < limit { + let Some(row) = cursor.next_row(token, 0).await? else { + break; + }; + scanned += 1; + if scanned <= offset { + continue; + } + if let Some(table) = table_from_show_row(row, table_type, includes_stable_name) { + result.push(table); + } + } + Ok((result, scanned)) + } + + async fn get_create_sql( + &self, + database: &str, + name: &str, + object_type: &str, + token: &CancellationToken, + ) -> Result { + let show_type = match object_type.trim().to_ascii_uppercase().as_str() { + "STABLE" | "SUPER TABLE" | "SUPERTABLE" => "STABLE", + "TABLE" | "BASE TABLE" | "CHILD TABLE" => "TABLE", + _ => return Ok(String::new()), + }; + let database = effective_database(database, &self.current_database)?; + let rows = match self + .query_rows(&format!("SHOW CREATE {show_type} {}", qualified_name(database, name)), token, 0) + .await + { + Ok(rows) => rows, + Err(error) if token.is_cancelled() => return Err(error), + Err(_) => return Ok(String::new()), + }; + let Some(row) = rows.first() else { + return Ok(String::new()); + }; + for value in row.iter().skip(1) { + if let Some(value) = json_text(value) { + if value.to_ascii_uppercase().contains("CREATE") { + return Ok(value.to_string()); + } + } + } + Ok(row.last().and_then(json_text).unwrap_or_default().to_string()) + } + + fn require_connection(&self) -> Result<&Taos> { + self.connection.as_ref().ok_or_else(|| anyhow!("not connected")) + } +} + +impl QueryCursor { + async fn next_row(&mut self, token: &CancellationToken, timeout_secs: u64) -> Result>> { + if self.rows_read >= self.max_rows { + return Ok(None); + } + if let Some(row) = self.pending_row.take() { + self.rows_read += 1; + return Ok(Some(row)); + } + let row = self.next_raw_row(token, timeout_secs).await?; + if row.is_some() { + self.rows_read += 1; + } + Ok(row) + } + + async fn prepare_next_page(&mut self, token: &CancellationToken, timeout_secs: u64) -> Result<(bool, bool)> { + if self.rows_read >= self.max_rows { + return Ok((false, self.next_raw_row(token, timeout_secs).await?.is_some())); + } + if self.pending_row.is_some() { + return Ok((true, false)); + } + self.pending_row = self.next_raw_row(token, timeout_secs).await?; + Ok((self.pending_row.is_some(), false)) + } + + fn remaining_limit(&self) -> usize { + self.max_rows.saturating_sub(self.rows_read) + } + + async fn next_raw_row(&mut self, token: &CancellationToken, timeout_secs: u64) -> Result>> { + loop { + if let Some(block) = &self.pending_block { + if self.pending_row_index < block.nrows() { + let timezone = block.timezone(); + let row_index = self.pending_row_index; + self.pending_row_index += 1; + let row = (0..block.ncols()) + .map(|column| { + block + .get_ref(row_index, column) + .map(|value| borrowed_value_to_json(value, timezone)) + .unwrap_or(Value::Null) + }) + .collect(); + return Ok(Some(row)); + } + } + self.pending_block = None; + self.pending_row_index = 0; + let block = + cancellable(token, timeout_secs, poll_fn(|context| self.result_set.fetch_raw_block(context))).await?; + match block { + Some(block) if block.nrows() > 0 => self.pending_block = Some(block), + Some(_) => continue, + None => return Ok(None), + } + } + } +} + +async fn read_cursor_page( + cursor: &mut QueryCursor, + page_size: usize, + token: &CancellationToken, + timeout_secs: u64, +) -> Result { + let mut rows = Vec::with_capacity(page_size); + while rows.len() < page_size { + let Some(row) = cursor.next_row(token, timeout_secs).await? else { + break; + }; + rows.push(row); + } + Ok(QueryPageResult { + columns: cursor.columns.clone(), + column_types: cursor.column_types.clone(), + rows, + affected_rows: cursor.affected_rows, + execution_time_ms: 0, + truncated: false, + session_id: None, + has_more: false, + }) +} + +async fn query_scalar_string( + connection: &Taos, + sql: &str, + token: &CancellationToken, + timeout_secs: u64, +) -> Result { + let mut result = cancellable(token, timeout_secs, connection.query(sql)).await?; + loop { + let block = cancellable(token, timeout_secs, poll_fn(|context| result.fetch_raw_block(context))).await?; + let Some(block) = block else { + bail!("TDengine query returned no rows"); + }; + if block.nrows() == 0 || block.ncols() == 0 { + continue; + } + let value = block.get_ref(0, 0).ok_or_else(|| anyhow!("TDengine query returned an empty value"))?; + return match borrowed_value_to_json(value, block.timezone()) { + Value::String(value) => Ok(value), + value => Ok(value.to_string()), + }; + } +} + +async fn cancellable(token: &CancellationToken, timeout_secs: u64, future: F) -> Result +where + F: Future>, +{ + tokio::pin!(future); + let operation = async { + if timeout_secs == 0 { + future.await.map_err(anyhow::Error::from) + } else { + timeout(Duration::from_secs(timeout_secs), future) + .await + .map_err(|_| anyhow!("TDengine operation timed out after {timeout_secs} seconds"))? + .map_err(anyhow::Error::from) + } + }; + tokio::select! { + _ = token.cancelled() => bail!("TDengine operation was cancelled"), + result = operation => result, + } +} + +fn effective_database<'a>(requested: &'a str, current: &'a str) -> Result<&'a str> { + let requested = requested.trim(); + if !requested.is_empty() { + return validate_database_name(requested); + } + let current = current.trim(); + if current.is_empty() { + bail!("TDengine database is required"); + } + validate_database_name(current) +} + +fn validate_database_name(value: &str) -> Result<&str> { + let value = value.trim(); + let mut chars = value.chars(); + let valid_start = chars.next().is_some_and(|char| char == '_' || char.is_ascii_alphabetic()); + if !valid_start || !chars.all(|char| char == '_' || char.is_ascii_alphanumeric()) { + bail!("invalid TDengine database name: {value}"); + } + Ok(value) +} + +fn table_from_show_row(row: Vec, table_type: &str, includes_stable_name: bool) -> Option { + let name = row.first().and_then(json_text)?.to_string(); + let parent_name = includes_stable_name + .then(|| row.get(3).and_then(json_text).map(str::trim).filter(|value| !value.is_empty()).map(str::to_string)) + .flatten(); + Some(TableInfo { name, table_type: table_type.to_string(), comment: None, parent_schema: None, parent_name }) +} + +fn parse_describe_columns(rows: Vec>) -> Vec { + rows.into_iter() + .enumerate() + .filter_map(|(index, row)| { + let name = row.first().and_then(json_text)?.to_string(); + let data_type = row.get(1).and_then(json_text).unwrap_or_default().to_string(); + let note = row.get(3).and_then(json_text).map(str::to_string); + let is_tag = note.as_deref().is_some_and(|note| note.to_ascii_uppercase().contains("TAG")); + let is_primary_key = !is_tag + && (index == 0 + || note.as_deref().is_some_and(|note| note.to_ascii_uppercase().contains("COMPOSITE KEY"))); + Some(ColumnInfo { + name, + data_type: data_type.clone(), + is_nullable: !is_primary_key, + column_default: None, + is_primary_key, + extra: note, + comment: is_tag.then(|| "TAG".to_string()), + numeric_precision: type_numbers(&data_type, "decimal").map(|(precision, _)| precision), + numeric_scale: type_numbers(&data_type, "decimal").and_then(|(_, scale)| scale), + character_maximum_length: character_length(&data_type), + }) + }) + .collect() +} + +fn type_numbers(data_type: &str, expected: &str) -> Option<(u32, Option)> { + let lower = data_type.trim().to_ascii_lowercase(); + if !(lower.starts_with(expected) || (expected == "decimal" && lower.starts_with("numeric"))) { + return None; + } + let values = lower.strip_prefix(expected).or_else(|| lower.strip_prefix("numeric"))?.trim(); + let inner = values.strip_prefix('(')?.strip_suffix(')')?; + let mut values = inner.split(',').map(str::trim); + let precision = values.next()?.parse().ok()?; + let scale = values.next().and_then(|value| value.parse().ok()); + Some((precision, scale)) +} + +fn character_length(data_type: &str) -> Option { + let lower = data_type.trim().to_ascii_lowercase(); + ["binary", "nchar", "varchar", "varbinary", "geometry", "blob", "mediumblob"] + .iter() + .find_map(|prefix| type_numbers(&lower, prefix).map(|(length, _)| length)) +} + +fn filter_tables(tables: Vec, constraints: &MetadataListConstraints) -> Vec { + let filter = constraints.filter.trim().to_ascii_lowercase(); + let mut skipped = 0usize; + let limit = if constraints.limit == 0 { usize::MAX } else { constraints.limit }; + tables + .into_iter() + .filter(|table| table_type_allowed(&constraints.object_types, &table.table_type)) + .filter(|table| { + text_matches(&table.name, &filter) + || table.comment.as_deref().is_some_and(|comment| text_matches(comment, &filter)) + }) + .filter(|_| { + let include = skipped >= constraints.offset; + skipped += 1; + include + }) + .take(limit) + .collect() +} + +fn table_type_allowed(object_types: &[String], table_type: &str) -> bool { + object_types.is_empty() + || object_types + .iter() + .map(|value| normalize_object_type(value)) + .any(|value| value == normalize_object_type(table_type)) +} + +fn normalize_object_type(value: &str) -> String { + let upper = value.trim().to_ascii_uppercase().replace(' ', "_"); + if upper == "STABLE" || upper == "BASE_TABLE" || upper.contains("TABLE") { + "TABLE".into() + } else if upper.contains("VIEW") { + "VIEW".into() + } else { + upper + } +} + +fn text_matches(candidate: &str, filter: &str) -> bool { + if filter.is_empty() { + return true; + } + let candidate = candidate.to_ascii_lowercase(); + candidate.contains(filter) || (filter.chars().count() >= 2 && fuzzy_subsequence_matches(&candidate, filter)) +} + +fn fuzzy_subsequence_matches(candidate: &str, expected: &str) -> bool { + let mut candidate = candidate.chars(); + expected.chars().all(|expected| candidate.by_ref().any(|value| value == expected)) +} + +fn completion_matches(candidate: &str, request: &CompletionAssistantRequest) -> bool { + let mask = request.mask.trim(); + if mask.is_empty() { + return true; + } + if request.case_sensitive { + completion_match_mode(candidate, mask, &request.match_mode) + } else { + completion_match_mode(&candidate.to_ascii_lowercase(), &mask.to_ascii_lowercase(), &request.match_mode) + } +} + +fn completion_match_mode(candidate: &str, mask: &str, mode: &str) -> bool { + match mode.trim().to_ascii_lowercase().as_str() { + "prefix" => candidate.starts_with(mask), + "exact" => candidate == mask, + "subsequence" | "fuzzy" => fuzzy_subsequence_matches(candidate, mask), + _ => candidate.contains(mask) || fuzzy_subsequence_matches(candidate, mask), + } +} + +fn sort_tables_for_hierarchy(tables: &mut [TableInfo]) { + tables.sort_by(|left, right| { + let left_group = left.parent_name.as_deref().unwrap_or(&left.name).to_ascii_lowercase(); + let right_group = right.parent_name.as_deref().unwrap_or(&right.name).to_ascii_lowercase(); + left_group + .cmp(&right_group) + .then_with(|| left.parent_name.is_some().cmp(&right.parent_name.is_some())) + .then_with(|| left.name.to_ascii_lowercase().cmp(&right.name.to_ascii_lowercase())) + }); +} + +fn build_fallback_table_ddl(database: &str, table: &str, columns: &[ColumnInfo]) -> String { + let definitions = columns + .iter() + .filter(|column| column.comment.as_deref() != Some("TAG")) + .map(|column| format!(" {} {}", quote_identifier(&column.name), column.data_type)) + .collect::>() + .join(",\n"); + format!("CREATE TABLE {} (\n{}\n);", qualified_name(database, table), definitions) +} + +fn quote_identifier(value: &str) -> String { + format!("`{}`", value.replace('`', "``")) +} + +fn quote_literal(value: &str) -> String { + format!("'{}'", value.replace('\'', "''")) +} + +fn quote_qualified_prefix(database: &str) -> String { + if database.trim().is_empty() { + String::new() + } else { + format!("{}.", database.trim()) + } +} + +fn qualified_name(database: &str, name: &str) -> String { + if database.trim().is_empty() { + quote_identifier(name) + } else { + format!("{}.{}", database.trim(), quote_identifier(name)) + } +} + +fn json_text(value: &Value) -> Option<&str> { + match value { + Value::String(value) => Some(value), + _ => None, + } +} + +fn display_json_value(value: &Value) -> String { + match value { + Value::Null => String::new(), + Value::String(value) => value.clone(), + value => value.to_string(), + } +} + +fn non_empty(value: &str) -> Option { + (!value.trim().is_empty()).then(|| value.trim().to_string()) +} + +fn is_system_database(value: &str) -> bool { + value.eq_ignore_ascii_case("information_schema") || value.eq_ignore_ascii_case("performance_schema") +} + +fn may_change_metadata(sql: &str) -> bool { + let normalized = sql.trim().to_ascii_lowercase(); + ["create ", "drop ", "alter ", "rename ", "truncate "].iter().any(|prefix| normalized.starts_with(prefix)) +} + +fn trim_sql(sql: &str) -> &str { + sql.trim().trim_end_matches(';').trim_end() +} + +fn normalized_max_rows(max_rows: usize) -> usize { + if max_rows == 0 { + DEFAULT_MAX_ROWS + } else { + max_rows + } +} + +fn normalized_page_size(page_size: usize, fetch_size: usize, max_rows: usize) -> usize { + let page_size = if page_size > 0 { + page_size + } else if fetch_size > 0 { + fetch_size + } else { + 500 + }; + page_size.min(max_rows.max(1)) +} + +fn connect_timeout(params: &ConnectParams) -> Duration { + if params.connect_timeout_secs == 0 { + DEFAULT_CONNECT_TIMEOUT + } else { + Duration::from_secs(params.connect_timeout_secs) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn filters_names_with_substrings_and_subsequences() { + assert!(text_matches("sensor_metrics", "metrics")); + assert!(text_matches("sensor_metrics", "snm")); + assert!(!text_matches("sensor_metrics", "xyz")); + } + + #[test] + fn sorts_stables_before_their_children() { + let mut tables = vec![ + TableInfo { + name: "d2".into(), + table_type: "TABLE".into(), + comment: None, + parent_schema: None, + parent_name: Some("meters".into()), + }, + TableInfo { + name: "meters".into(), + table_type: "STABLE".into(), + comment: None, + parent_schema: None, + parent_name: None, + }, + TableInfo { + name: "d1".into(), + table_type: "TABLE".into(), + comment: None, + parent_schema: None, + parent_name: Some("meters".into()), + }, + ]; + sort_tables_for_hierarchy(&mut tables); + assert_eq!(tables.iter().map(|table| table.name.as_str()).collect::>(), vec!["meters", "d1", "d2"]); + } + + #[test] + fn parses_tdengine_describe_metadata() { + let columns = parse_describe_columns(vec![ + vec![json!("ts"), json!("TIMESTAMP"), json!(8), json!("")], + vec![json!("device"), json!("VARCHAR(64)"), json!(64), json!("COMPOSITE KEY")], + vec![json!("location"), json!("NCHAR(32)"), json!(32), json!("TAG")], + vec![json!("reading"), json!("DECIMAL(20, 4)"), json!(16), Value::Null], + ]); + assert!(columns[0].is_primary_key); + assert!(columns[1].is_primary_key); + assert_eq!(columns[2].comment.as_deref(), Some("TAG")); + assert_eq!(columns[2].character_maximum_length, Some(32)); + assert_eq!(columns[3].numeric_precision, Some(20)); + assert_eq!(columns[3].numeric_scale, Some(4)); + } + + #[test] + fn converts_legacy_show_rows_to_hierarchy() { + let table = + table_from_show_row(vec![json!("d1001"), json!("x"), json!("x"), json!("meters")], "TABLE", true).unwrap(); + assert_eq!(table.parent_name.as_deref(), Some("meters")); + } + + #[test] + fn validates_database_names_before_using_unquoted_show_syntax() { + assert_eq!(validate_database_name("Power_1").unwrap(), "Power_1"); + assert!(validate_database_name("power-data").is_err()); + assert!(validate_database_name("power; DROP DATABASE other").is_err()); + assert_eq!(quote_qualified_prefix("Power_1"), "Power_1."); + assert_eq!(qualified_name("Power_1", "meters"), "Power_1.`meters`"); + } + + #[tokio::test] + async fn cancellable_stops_pending_connector_operations() { + let token = CancellationToken::new(); + token.cancel(); + let error = cancellable(&token, 0, std::future::pending::>()).await.unwrap_err(); + assert!(error.to_string().contains("cancelled")); + } +} diff --git a/agents/drivers/tdengine/src/lib.rs b/agents/drivers/tdengine/src/lib.rs new file mode 100644 index 0000000000..c4ff87ec89 --- /dev/null +++ b/agents/drivers/tdengine/src/lib.rs @@ -0,0 +1,7 @@ +mod config; +mod driver; +mod model; +mod runtime; +mod value; + +pub use runtime::run; diff --git a/agents/drivers/tdengine/src/main.rs b/agents/drivers/tdengine/src/main.rs new file mode 100644 index 0000000000..2020ce7723 --- /dev/null +++ b/agents/drivers/tdengine/src/main.rs @@ -0,0 +1,7 @@ +#[tokio::main] +async fn main() { + if let Err(error) = dbx_tdengine_driver::run().await { + eprintln!("TDengine driver failed: {error:#}"); + std::process::exit(1); + } +} diff --git a/agents/drivers/tdengine/src/main/java/com/dbx/agent/tdengine/TDengineAgent.java b/agents/drivers/tdengine/src/main/java/com/dbx/agent/tdengine/TDengineAgent.java deleted file mode 100644 index 946c8aaa64..0000000000 --- a/agents/drivers/tdengine/src/main/java/com/dbx/agent/tdengine/TDengineAgent.java +++ /dev/null @@ -1,980 +0,0 @@ -package com.dbx.agent.tdengine; - -import com.dbx.agent.AbstractJdbcAgent; -import com.dbx.agent.ColumnInfo; -import com.dbx.agent.ConnectParams; -import com.dbx.agent.DatabaseInfo; -import com.dbx.agent.ExecuteQueryOptions; -import com.dbx.agent.ForeignKeyInfo; -import com.dbx.agent.IndexInfo; -import com.dbx.agent.JdbcExecutor; -import com.dbx.agent.MultiSessionJsonRpcServer; -import com.dbx.agent.MetadataListConstraints; -import com.dbx.agent.ObjectInfo; -import com.dbx.agent.ObjectSource; -import com.dbx.agent.QueryPageOptions; -import com.dbx.agent.QueryPageResult; -import com.dbx.agent.QueryResult; -import com.dbx.agent.TableInfo; -import com.dbx.agent.TriggerInfo; -import com.taosdata.jdbc.TSDBDriver; -import com.taosdata.jdbc.rs.RestfulConnection; -import java.nio.charset.StandardCharsets; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Timestamp; -import java.sql.Types; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; -import java.time.temporal.ChronoField; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public final class TDengineAgent extends AbstractJdbcAgent { - private static final long TABLE_CACHE_TTL_MILLIS = 10_000L; - private static final DateTimeFormatter TDENGINE_TIMESTAMP_FORMAT = - new DateTimeFormatterBuilder() - .appendPattern("yyyy-MM-dd HH:mm:ss") - .appendFraction(ChronoField.NANO_OF_SECOND, 3, 9, true) - .toFormatter(); - private static final Pattern NUMERIC_PRECISION_PATTERN = - Pattern.compile("(?i)^(decimal|numeric)\\((\\d+)(?:,\\s*\\d+)?\\)"); - private static final Pattern NUMERIC_SCALE_PATTERN = - Pattern.compile("(?i)^(decimal|numeric)\\(\\d+,\\s*(\\d+)\\)"); - private static final Pattern CHARACTER_LENGTH_PATTERN = - Pattern.compile("(?i)^(binary|nchar|varchar|varbinary)\\((\\d+)\\)"); - private static final Pattern COMPOSITE_KEY_PATTERN = - Pattern.compile("(?i)\\bCOMPOSITE\\s+KEY\\b"); - private static final Pattern TDENGINE_VERSION_PATTERN = - Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)(?:\\.|$)"); - - private final Object tableCacheLock = new Object(); - private String tableCacheSchema = ""; - private long tableCacheTimeMillis; - private List tableCache = Collections.emptyList(); - private Connection restfulStateConnection; - private String restfulOriginalCatalog; - private String restfulOriginalDatabase; - private boolean restInformationSchemaPagingEnabled; - - @Override - protected String driverClass() { - return TDengineTransport.WEBSOCKET.driverClass(); - } - - @Override - protected String buildJdbcUrl(ConnectParams params) { - String connectionString = params.getConnection_string() == null ? "" : params.getConnection_string().trim(); - if (!connectionString.isBlank()) { - return TDengineJdbcUrl.sanitizeConnectionString(connectionString); - } - TDengineJdbcUrl.TransportPreference preference = TDengineJdbcUrl.transportPreference(params.getUrl_params()); - TDengineTransport transport = preference == TDengineJdbcUrl.TransportPreference.REST - ? TDengineTransport.REST - : TDengineTransport.WEBSOCKET; - return TDengineJdbcUrl.from(params, transport); - } - - @Override - protected void loadDriver(ConnectParams params) { - } - - @Override - protected Connection openConnection(ConnectParams params) throws Exception { - return TDengineConnectionFactory.open(params); - } - - @Override - protected void afterConnect(ConnectParams params, Connection connection) { - clearTableCache(); - clearRestfulState(); - restInformationSchemaPagingEnabled = supportsRestInformationSchemaPaging(connection); - } - - @Override - protected void beforePooledConnectionReturn(Connection connection) throws Exception { - if (restfulStateConnection != connection) { - return; - } - try { - restoreRestfulConnectionState(connection, restfulOriginalCatalog, restfulOriginalDatabase); - } finally { - clearRestfulState(); - } - } - - @Override - protected void afterDisconnect() { - clearTableCache(); - clearRestfulState(); - restInformationSchemaPagingEnabled = false; - } - - @Override - public List listDatabases() { - return unchecked(() -> { - List result = new ArrayList<>(); - try (java.sql.Statement stmt = requireConnected().createStatement(); - ResultSet rs = stmt.executeQuery("SHOW DATABASES")) { - while (rs.next()) { - String name = rs.getString(1); - if (!isSystemDatabase(name)) { - result.add(new DatabaseInfo(name)); - } - } - } - return result; - }); - } - - @Override - public List listSchemas() { - return Collections.emptyList(); - } - - @Override - public List listTables(String schema) { - return listTables(schema, MetadataListConstraints.NONE); - } - - @Override - public List listTables(String schema, MetadataListConstraints constraints) { - MetadataListConstraints normalized = MetadataListConstraints.orNone(constraints); - if (!normalized.tableTypeAllowed("TABLE")) { - return Collections.emptyList(); - } - if (normalized.hasLimit() && !normalized.hasFilter()) { - if (restInformationSchemaPagingEnabled) { - try { - return listTablesPageFromRestInformationSchema(schema, normalized); - } catch (RuntimeException ignored) { - } - } - return listTablesPageFromShow(schema, normalized); - } - return normalized.filterTables(listTablesFromShow(schema)); - } - - private List listTablesPageFromRestInformationSchema( - String schema, - MetadataListConstraints constraints - ) { - return unchecked(() -> { - int limit = constraints.getLimit(); - int offset = constraints.getOffset() == null ? 0 : constraints.getOffset(); - List result = new ArrayList<>(limit); - - TablePageScan stablePage = queryTablesPage( - "SHOW " + quoteQualifiedPrefix(schema) + "STABLES", - "STABLE", - false, - offset, - limit - ); - result.addAll(stablePage.tables()); - if (result.size() >= limit) { - return result; - } - - int tableOffset = Math.max(0, offset - stablePage.scannedRows()); - result.addAll(queryInformationSchemaTablePage( - schema, - tableOffset, - limit - result.size() - )); - return result; - }); - } - - private List listTablesPageFromShow(String schema, MetadataListConstraints constraints) { - return unchecked(() -> { - int limit = constraints.getLimit(); - int offset = constraints.getOffset() == null ? 0 : constraints.getOffset(); - List result = new ArrayList<>(limit); - - TablePageScan stablePage = queryTablesPage( - "SHOW " + quoteQualifiedPrefix(schema) + "STABLES", - "STABLE", - false, - offset, - limit - ); - result.addAll(stablePage.tables()); - if (result.size() >= limit) { - return result; - } - - int tableOffset = Math.max(0, offset - stablePage.scannedRows()); - result.addAll(queryTablesPage( - "SHOW " + quoteQualifiedPrefix(schema) + "TABLES", - "TABLE", - true, - tableOffset, - limit - result.size() - ).tables()); - return result; - }); - } - - private List listTablesFromShow(String schema) { - List cached = cachedTables(schema); - if (cached != null) { - return cached; - } - return unchecked(() -> { - List result = new ArrayList<>(); - // Connector/J 3.6.3 ignores Statement#setMaxRows. Filtered and - // unbounded callers still need the complete result, so cache that - // scan briefly instead of repeating it for adjacent requests. - result.addAll(queryTables("SHOW " + quoteQualifiedPrefix(schema) + "STABLES", "STABLE", false)); - result.addAll(queryTables("SHOW " + quoteQualifiedPrefix(schema) + "TABLES", "TABLE", true)); - - Map distinct = new LinkedHashMap<>(); - for (TableInfo table : result) { - distinct.putIfAbsent(table.getTable_type() + ":" + table.getName(), table); - } - List sorted = new ArrayList<>(distinct.values()); - sortTablesForHierarchy(sorted); - cacheTables(schema, sorted); - return copyTables(sorted); - }); - } - - @Override - public List listObjects(String schema) { - List result = new ArrayList<>(); - for (TableInfo table : listTables(schema)) { - result.add(new ObjectInfo(table.getName(), table.getTable_type(), schema, table.getComment())); - } - return result; - } - - @Override - public List getColumns(String schema, String table) { - return unchecked(() -> { - try (java.sql.Statement stmt = requireConnected().createStatement(); - ResultSet rs = stmt.executeQuery("DESCRIBE " + qualifiedName(schema, table))) { - return readDescribeColumns(rs); - } - }); - } - - @Override - public ObjectSource getObjectSource(String schema, String name, String objectType) { - String source = getCreateSql(schema, name, objectType); - if (source.isBlank()) { - source = getTableDdl(schema, name); - } - return new ObjectSource(name, objectType, schema, source); - } - - @Override - public String getTableDdl(String schema, String table) { - String source = getCreateSql(schema, table, "STABLE"); - if (source.isBlank()) { - source = getCreateSql(schema, table, "TABLE"); - } - if (source.isBlank()) { - return super.getTableDdl(schema, table); - } - return source; - } - - @Override - public List listIndexes(String schema, String table) { - return Collections.emptyList(); - } - - @Override - public List listForeignKeys(String schema, String table) { - return Collections.emptyList(); - } - - @Override - public List listTriggers(String schema, String table) { - return Collections.emptyList(); - } - - @Override - public QueryResult executeQuery(String sql, String schema, ExecuteQueryOptions options) { - QueryResult result = JdbcExecutor.current().execute( - requireConnected(), - sql, - prepareExecutionSchema(schema), - this::setSchemaSQL, - options.getMaxRows(), - options.getFetchSize(), - options.getTimeoutSecs(), - this::resultValue - ); - if (mayChangeMetadata(sql)) { - clearTableCache(); - } - return result; - } - - @Override - public QueryPageResult executeQueryPage(String sql, String schema, QueryPageOptions options) { - return JdbcExecutor.current().executePage( - requireConnected(), - sql, - prepareExecutionSchema(schema), - this::setSchemaSQL, - options, - this::resultValue - ); - } - - @Override - public QueryPageResult startTableRead(String sql, String schema, QueryPageOptions options) { - return JdbcExecutor.current().startTableRead( - requireConnected(), - sql, - prepareExecutionSchema(schema), - this::setSchemaSQL, - options, - this::resultValue - ); - } - - @Override - public String setSchemaSQL(String schema) { - return "USE " + quoteIdentifier(schema); - } - - @Override - public QueryResult executeTransaction(List statements, String schema) { - QueryResult result = super.executeTransaction(statements, prepareExecutionSchema(schema)); - if (statements.stream().anyMatch(TDengineAgent::mayChangeMetadata)) { - clearTableCache(); - } - return result; - } - - @Override - public QueryResult executeBatch(List statements, String schema) { - QueryResult result = super.executeBatch(statements, prepareExecutionSchema(schema)); - if (statements.stream().anyMatch(TDengineAgent::mayChangeMetadata)) { - clearTableCache(); - } - return result; - } - - private String prepareExecutionSchema(String schema) { - return unchecked(() -> { - Connection connection = requireConnected(); - if (schema != null - && !schema.trim().isEmpty() - && unwrapConnection(connection, RestfulConnection.class) != null - && restfulStateConnection != connection) { - restfulStateConnection = connection; - restfulOriginalCatalog = connection.getCatalog(); - restfulOriginalDatabase = connection.getClientInfo(TSDBDriver.PROPERTY_KEY_DBNAME); - } - return prepareExecutionSchema(connection, schema); - }); - } - - static String prepareExecutionSchema(Connection connection, String schema) throws SQLException { - if (unwrapConnection(connection, RestfulConnection.class) == null || schema == null || schema.trim().isEmpty()) { - return schema; - } - - String database = schema.trim(); - // Connector/J 3.6.3 misparses quoted USE statements and puts the whole SQL in /rest/sql/. - connection.setCatalog(database); - connection.setClientInfo(TSDBDriver.PROPERTY_KEY_DBNAME, database); - return null; - } - - static void restoreRestfulConnectionState(Connection connection, String catalog, String database) throws SQLException { - connection.setCatalog(catalog); - if (database == null) { - connection.getClientInfo().remove(TSDBDriver.PROPERTY_KEY_DBNAME); - } else { - connection.setClientInfo(TSDBDriver.PROPERTY_KEY_DBNAME, database); - } - } - - private List queryTables(String sql, String tableType, boolean includesStableName) throws Exception { - List result = new ArrayList<>(); - try (java.sql.Statement stmt = requireConnected().createStatement()) { - try (ResultSet rs = stmt.executeQuery(sql)) { - while (rs.next()) { - result.add(readShowTable(rs, tableType, includesStableName)); - } - } - } - return result; - } - - private TablePageScan queryTablesPage( - String sql, - String tableType, - boolean includesStableName, - int offset, - int limit - ) throws Exception { - List result = new ArrayList<>(limit); - int scannedRows = 0; - try (java.sql.Statement stmt = requireConnected().createStatement(); - ResultSet rs = stmt.executeQuery(sql)) { - while (result.size() < limit && rs.next()) { - scannedRows += 1; - if (scannedRows <= offset) { - continue; - } - result.add(readShowTable(rs, tableType, includesStableName)); - } - } - return new TablePageScan(result, scannedRows); - } - - private List queryInformationSchemaTablePage(String database, int offset, int limit) throws Exception { - StringBuilder sql = new StringBuilder( - "SELECT table_name, stable_name FROM information_schema.ins_tables WHERE db_name = ? LIMIT " - ).append(limit); - if (offset > 0) { - sql.append(" OFFSET ").append(offset); - } - - List result = new ArrayList<>(limit); - try (java.sql.PreparedStatement stmt = requireConnected().prepareStatement(sql.toString())) { - stmt.setString(1, database); - try (ResultSet rs = stmt.executeQuery()) { - while (rs.next()) { - String parentName = optionalString(rs, 2); - if (parentName != null && parentName.trim().isEmpty()) { - parentName = null; - } - result.add(new TableInfo(rs.getString(1), "TABLE", null, null, parentName)); - } - } - } - return result; - } - - private static TableInfo readShowTable(ResultSet rs, String tableType, boolean includesStableName) throws Exception { - // SHOW TABLES returns the owning STABLE as its fourth column. It - // is absent for ordinary tables and older servers, where this - // best-effort read simply leaves the table at the root level. - String parentName = includesStableName ? optionalString(rs, 4) : null; - if (parentName != null && parentName.trim().isEmpty()) { - parentName = null; - } - return new TableInfo(rs.getString(1), tableType, null, null, parentName); - } - - private record TablePageScan(List tables, int scannedRows) { - } - - private static boolean supportsRestInformationSchemaPaging(Connection connection) { - if (unwrapConnection(connection, RestfulConnection.class) == null) { - return false; - } - try { - return supportsRestInformationSchemaPagingVersion(connection.getMetaData().getDatabaseProductVersion()); - } catch (Exception ignored) { - return false; - } - } - - static boolean supportsRestInformationSchemaPagingVersion(String version) { - Matcher matcher = TDENGINE_VERSION_PATTERN.matcher(version == null ? "" : version.trim()); - if (!matcher.find()) { - return false; - } - int major = Integer.parseInt(matcher.group(1)); - int minor = Integer.parseInt(matcher.group(2)); - int patch = Integer.parseInt(matcher.group(3)); - return major > 3 || (major == 3 && (minor > 3 || (minor == 3 && patch >= 8))); - } - - private List cachedTables(String schema) { - String normalizedSchema = normalizedSchema(schema); - synchronized (tableCacheLock) { - if (cacheFresh(tableCacheTimeMillis) && tableCacheSchema.equals(normalizedSchema)) { - return copyTables(tableCache); - } - } - return null; - } - - private void cacheTables(String schema, List tables) { - synchronized (tableCacheLock) { - tableCacheSchema = normalizedSchema(schema); - tableCache = copyTables(tables); - tableCacheTimeMillis = System.currentTimeMillis(); - } - } - - private void clearTableCache() { - synchronized (tableCacheLock) { - tableCacheSchema = ""; - tableCacheTimeMillis = 0L; - tableCache = Collections.emptyList(); - } - } - - private void clearRestfulState() { - restfulStateConnection = null; - restfulOriginalCatalog = null; - restfulOriginalDatabase = null; - } - - private static boolean cacheFresh(long cachedAtMillis) { - return cachedAtMillis > 0L && System.currentTimeMillis() - cachedAtMillis <= TABLE_CACHE_TTL_MILLIS; - } - - private static String normalizedSchema(String schema) { - return schema == null ? "" : schema.trim(); - } - - private static List copyTables(List tables) { - List copies = new ArrayList<>(tables.size()); - for (TableInfo table : tables) { - copies.add(new TableInfo( - table.getName(), - table.getTable_type(), - table.getComment(), - table.getParent_schema(), - table.getParent_name() - )); - } - return copies; - } - - private static boolean mayChangeMetadata(String sql) { - String normalized = sql == null ? "" : sql.trim().toLowerCase(Locale.ROOT); - return normalized.startsWith("create ") - || normalized.startsWith("drop ") - || normalized.startsWith("alter ") - || normalized.startsWith("rename ") - || normalized.startsWith("truncate "); - } - - static void sortTablesForHierarchy(List tables) { - tables.sort((left, right) -> { - String leftGroup = hierarchyGroupName(left); - String rightGroup = hierarchyGroupName(right); - int groupCompared = leftGroup.compareTo(rightGroup); - if (groupCompared != 0) { - return groupCompared; - } - - boolean leftIsParent = left.getParent_name() == null; - boolean rightIsParent = right.getParent_name() == null; - if (leftIsParent != rightIsParent) { - return leftIsParent ? -1 : 1; - } - return left.getName().toLowerCase(Locale.ROOT).compareTo(right.getName().toLowerCase(Locale.ROOT)); - }); - } - - private static String hierarchyGroupName(TableInfo table) { - String parentName = table.getParent_name(); - String groupName = parentName == null || parentName.trim().isEmpty() ? table.getName() : parentName; - return groupName.toLowerCase(Locale.ROOT); - } - - static List readDescribeColumns(ResultSet rs) throws Exception { - List result = new ArrayList<>(); - int ordinal = 0; - while (rs.next()) { - ordinal += 1; - String name = rs.getString(1); - String dataType = coalesce(rs.getString(2)); - String note = optionalString(rs, 4); - boolean isTag = note != null && note.toUpperCase(Locale.ROOT).contains("TAG"); - boolean isPrimaryKey = !isTag && (ordinal == 1 || (note != null && COMPOSITE_KEY_PATTERN.matcher(note).find())); - result.add(new ColumnInfo( - name, - dataType, - !isPrimaryKey, - null, - isPrimaryKey, - note, - isTag ? "TAG" : null, - parseNumericPrecision(dataType), - parseNumericScale(dataType), - parseCharacterMaximumLength(dataType) - )); - } - return result; - } - - private String getCreateSql(String schema, String name, String objectType) { - String showType = switch (objectType.toUpperCase(Locale.ROOT)) { - case "STABLE", "SUPER TABLE", "SUPERTABLE" -> "STABLE"; - case "TABLE", "BASE TABLE", "CHILD TABLE" -> "TABLE"; - default -> null; - }; - if (showType == null) { - return ""; - } - - try (java.sql.Statement stmt = requireConnected().createStatement(); - ResultSet rs = stmt.executeQuery("SHOW CREATE " + showType + " " + qualifiedName(schema, name))) { - if (!rs.next()) { - return ""; - } - int columnCount = rs.getMetaData().getColumnCount(); - for (int i = 2; i <= columnCount; i++) { - String value = rs.getString(i); - if (value != null && value.toUpperCase(Locale.ROOT).contains("CREATE")) { - return value; - } - } - String value = rs.getString(columnCount); - return value == null ? "" : value; - } catch (Exception e) { - return ""; - } - } - - @Override - protected Object resultValue(ResultSet rs, int index, int sqlType) { - return unchecked(() -> { - Object value = switch (sqlType) { - case Types.BIGINT -> rs.getLong(index); - case Types.INTEGER, Types.SMALLINT, Types.TINYINT -> rs.getInt(index); - case Types.FLOAT, Types.REAL -> rs.getFloat(index); - case Types.DOUBLE -> rs.getDouble(index); - case Types.DECIMAL, Types.NUMERIC -> rs.getBigDecimal(index); - case Types.BOOLEAN, Types.BIT -> rs.getBoolean(index); - default -> rs.getObject(index); - }; - return rs.wasNull() ? null : decodeTdengineValue(value); - }); - } - - public static Object decodeTdengineValue(Object value) { - if (value instanceof byte[] bytes) { - return new String(bytes, StandardCharsets.UTF_8); - } - if (value instanceof Timestamp timestamp) { - return TDENGINE_TIMESTAMP_FORMAT.format(timestamp.toLocalDateTime()); - } - return value; - } - - private static String quoteQualifiedPrefix(String schema) { - String trimmed = schema.trim(); - return trimmed.isEmpty() ? "" : quoteIdentifier(trimmed) + "."; - } - - private static String qualifiedName(String schema, String name) { - String table = quoteIdentifier(name); - String trimmed = schema.trim(); - return trimmed.isEmpty() ? table : quoteIdentifier(trimmed) + "." + table; - } - - private static String quoteIdentifier(String identifier) { - return "`" + identifier.replace("`", "``") + "`"; - } - - private static boolean isSystemDatabase(String name) { - if (name == null) { - return false; - } - String normalized = name.trim().toLowerCase(Locale.ROOT); - return "information_schema".equals(normalized) || "performance_schema".equals(normalized); - } - - private static Integer parseNumericPrecision(String dataType) { - return parseIntGroup(NUMERIC_PRECISION_PATTERN, dataType, 2); - } - - private static Integer parseNumericScale(String dataType) { - return parseIntGroup(NUMERIC_SCALE_PATTERN, dataType, 2); - } - - private static Integer parseCharacterMaximumLength(String dataType) { - return parseIntGroup(CHARACTER_LENGTH_PATTERN, dataType, 2); - } - - private static Integer parseIntGroup(Pattern pattern, String value, int group) { - Matcher matcher = pattern.matcher(value); - if (!matcher.find()) { - return null; - } - try { - return Integer.valueOf(matcher.group(group)); - } catch (NumberFormatException e) { - return null; - } - } - - private static String optionalString(ResultSet rs, int index) { - try { - return rs.getString(index); - } catch (Exception e) { - return null; - } - } - - private static String coalesce(String value) { - return value == null ? "" : value; - } - - public static void main(String[] args) { - new MultiSessionJsonRpcServer(TDengineAgent::new).run(); - } -} - -final class TDengineJdbcUrl { - enum TransportPreference { - AUTO, - WEBSOCKET, - REST - } - - private static final String PARAM_TRANSPORT = "transport"; - private static final String PARAM_DBX_TRANSPORT = "dbx.transport"; - - private TDengineJdbcUrl() { - } - - static String from(ConnectParams params) { - TransportPreference preference = transportPreference(params.getUrl_params()); - if (preference == TransportPreference.REST) { - return from(params, TDengineTransport.REST); - } - String host = params.getHost().isBlank() ? "localhost" : params.getHost(); - int port = params.getPort() > 0 ? params.getPort() : 6041; - String database = params.getDatabase().trim(); - String path = database.isBlank() ? "/" : "/" + database; - String query = normalizeQueryString(params.getUrl_params()); - String suffix = query.isBlank() ? "" : "?" + query; - return "jdbc:TAOS-WS://" + host + ":" + port + path + suffix; - } - - static String from(ConnectParams params, TDengineTransport transport) { - String host = params.getHost().isBlank() ? "localhost" : params.getHost(); - int port = params.getPort() > 0 ? params.getPort() : 6041; - String database = params.getDatabase().trim(); - String path = database.isBlank() ? "/" : "/" + database; - String query = normalizeQueryString(params.getUrl_params()); - String suffix = query.isBlank() ? "" : "?" + query; - return transport.urlPrefix() + host + ":" + port + path + suffix; - } - - static String sanitizeConnectionString(String connectionString) { - if (connectionString == null || connectionString.isBlank()) { - return ""; - } - int queryIndex = connectionString.indexOf('?'); - if (queryIndex < 0) { - return connectionString; - } - String base = connectionString.substring(0, queryIndex); - String rawQuery = connectionString.substring(queryIndex + 1); - int fragmentIndex = rawQuery.indexOf('#'); - String fragment = ""; - if (fragmentIndex >= 0) { - fragment = rawQuery.substring(fragmentIndex); - rawQuery = rawQuery.substring(0, fragmentIndex); - } - String query = normalizeQueryString(rawQuery); - if (query.isBlank()) { - return base + fragment; - } - return base + "?" + query + fragment; - } - - static TransportPreference transportPreference(String rawQuery) { - String query = rawQuery == null ? "" : rawQuery.trim(); - if (query.startsWith("?")) { - query = query.substring(1); - } - if (query.isBlank()) { - return TransportPreference.AUTO; - } - String[] parts = query.split("&"); - for (String part : parts) { - if (part == null || part.isBlank()) { - continue; - } - int index = part.indexOf('='); - String key = index < 0 ? part.trim() : part.substring(0, index).trim(); - if (!PARAM_TRANSPORT.equalsIgnoreCase(key) && !PARAM_DBX_TRANSPORT.equalsIgnoreCase(key)) { - continue; - } - String value = index < 0 ? "" : part.substring(index + 1).trim(); - if ("rest".equalsIgnoreCase(value) || "rs".equalsIgnoreCase(value)) { - return TransportPreference.REST; - } - if ("ws".equalsIgnoreCase(value) || "websocket".equalsIgnoreCase(value)) { - return TransportPreference.WEBSOCKET; - } - } - return TransportPreference.AUTO; - } - - private static String normalizeQueryString(String rawQuery) { - String query = rawQuery == null ? "" : rawQuery.trim(); - if (query.startsWith("?")) { - query = query.substring(1); - } - if (query.isBlank()) { - return ""; - } - String[] parts = query.split("&"); - List kept = new ArrayList<>(); - for (String part : parts) { - if (part == null || part.isBlank()) { - continue; - } - int index = part.indexOf('='); - String key = index < 0 ? part.trim() : part.substring(0, index).trim(); - if (PARAM_TRANSPORT.equalsIgnoreCase(key) || PARAM_DBX_TRANSPORT.equalsIgnoreCase(key)) { - continue; - } - kept.add(part); - } - return String.join("&", kept); - } -} - -enum TDengineTransport { - WEBSOCKET("jdbc:TAOS-WS://", "com.taosdata.jdbc.ws.WebSocketDriver"), - REST("jdbc:TAOS-RS://", "com.taosdata.jdbc.rs.RestfulDriver"); - - private final String urlPrefix; - private final String driverClass; - - TDengineTransport(String urlPrefix, String driverClass) { - this.urlPrefix = urlPrefix; - this.driverClass = driverClass; - } - - String urlPrefix() { - return urlPrefix; - } - - String driverClass() { - return driverClass; - } -} - -final class TDengineConnectionFactory { - private static final int AUTO_TRANSPORT_ATTEMPT_TIMEOUT_SECONDS = 8; - - private TDengineConnectionFactory() { - } - - static Connection open(ConnectParams params) throws Exception { - String explicitConnectionString = params.getConnection_string() == null ? "" : params.getConnection_string().trim(); - if (!explicitConnectionString.isBlank()) { - String sanitizedConnectionString = TDengineJdbcUrl.sanitizeConnectionString(explicitConnectionString); - Class.forName(driverClassFromConnectionString(sanitizedConnectionString)); - return DriverManager.getConnection(sanitizedConnectionString, params.getUsername(), params.getPassword()); - } - - TDengineJdbcUrl.TransportPreference preference = TDengineJdbcUrl.transportPreference(params.getUrl_params()); - TDengineTransport[] candidates = transportsFor(preference); - Exception firstError = null; - for (TDengineTransport transport : candidates) { - try { - Connection connection = preference == TDengineJdbcUrl.TransportPreference.AUTO - ? openWithAttemptTimeout(params, transport) - : openDirect(params, transport); - return connection; - } catch (Exception e) { - if (firstError == null) { - firstError = e; - } else { - firstError.addSuppressed(e); - } - } - } - if (firstError != null) { - throw new RuntimeException("Failed to connect TDengine using transports: " + candidateNames(candidates), firstError); - } - throw new IllegalStateException("No TDengine transport available."); - } - - private static Connection openDirect(ConnectParams params, TDengineTransport transport) throws Exception { - Class.forName(transport.driverClass()); - String url = TDengineJdbcUrl.from(params, transport); - return DriverManager.getConnection(url, params.getUsername(), params.getPassword()); - } - - private static Connection openWithAttemptTimeout(ConnectParams params, TDengineTransport transport) throws Exception { - String threadName = "tdengine-connect-" + transport.name().toLowerCase(Locale.ROOT); - ThreadFactory factory = runnable -> { - Thread thread = new Thread(runnable, threadName); - thread.setDaemon(true); - return thread; - }; - ExecutorService executor = Executors.newSingleThreadExecutor(factory); - try { - Future future = executor.submit(new Callable() { - @Override - public Connection call() throws Exception { - return openDirect(params, transport); - } - }); - return future.get(AUTO_TRANSPORT_ATTEMPT_TIMEOUT_SECONDS, TimeUnit.SECONDS); - } catch (TimeoutException e) { - throw new RuntimeException( - "TDengine " + transport.name().toLowerCase(Locale.ROOT) - + " transport connect timed out after " + AUTO_TRANSPORT_ATTEMPT_TIMEOUT_SECONDS + "s", - e - ); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Interrupted while connecting TDengine via " + transport.name().toLowerCase(Locale.ROOT), e); - } catch (ExecutionException e) { - Throwable cause = e.getCause(); - if (cause instanceof Exception exception) { - throw exception; - } - throw new RuntimeException(cause); - } finally { - executor.shutdownNow(); - } - } - - private static TDengineTransport[] transportsFor(TDengineJdbcUrl.TransportPreference preference) { - return switch (preference) { - case REST -> new TDengineTransport[] {TDengineTransport.REST}; - case WEBSOCKET -> new TDengineTransport[] {TDengineTransport.WEBSOCKET}; - case AUTO -> new TDengineTransport[] {TDengineTransport.WEBSOCKET, TDengineTransport.REST}; - }; - } - - private static String candidateNames(TDengineTransport[] candidates) { - List names = new ArrayList<>(); - for (TDengineTransport candidate : candidates) { - names.add(candidate.name().toLowerCase(Locale.ROOT)); - } - return String.join(" -> ", names); - } - - private static String driverClassFromConnectionString(String connectionString) { - if (connectionString.regionMatches(true, 0, TDengineTransport.REST.urlPrefix(), 0, TDengineTransport.REST.urlPrefix().length())) { - return TDengineTransport.REST.driverClass(); - } - if (connectionString.regionMatches(true, 0, TDengineTransport.WEBSOCKET.urlPrefix(), 0, TDengineTransport.WEBSOCKET.urlPrefix().length())) { - return TDengineTransport.WEBSOCKET.driverClass(); - } - return TDengineTransport.WEBSOCKET.driverClass(); - } -} diff --git a/agents/drivers/tdengine/src/model.rs b/agents/drivers/tdengine/src/model.rs new file mode 100644 index 0000000000..9646d8119c --- /dev/null +++ b/agents/drivers/tdengine/src/model.rs @@ -0,0 +1,279 @@ +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::Value; + +pub const PROTOCOL_VERSION: u32 = 2; +pub const LEGACY_SESSION_ID: &str = "__legacy__"; +pub const DEFAULT_MAX_ROWS: usize = 10_000; +pub const MAX_AGENT_SESSIONS: usize = 256; +pub const MAX_CONCURRENT_REQUESTS: usize = 64; + +#[derive(Debug, Deserialize)] +pub struct RpcRequest { + #[serde(default)] + pub id: Value, + pub method: String, + #[serde(default)] + pub params: Value, +} + +#[derive(Debug, Serialize)] +pub struct RpcResponse { + pub jsonrpc: &'static str, + pub id: Value, + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Debug, Serialize)] +pub struct RpcError { + pub code: i32, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StructuredError { + pub category: &'static str, + pub retryable: bool, + pub session_disposition: &'static str, + pub stage: &'static str, + pub contract_version: u32, + pub operation_outcome: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + pub agent_session_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub exception_class: Option, +} + +#[derive(Debug, Clone, Default, Deserialize)] +pub struct ConnectParams { + #[serde(default)] + pub host: String, + #[serde(default)] + pub port: u16, + #[serde(default)] + pub database: String, + #[serde(default)] + pub username: String, + #[serde(default)] + pub password: String, + #[serde(default)] + pub url_params: String, + #[serde(default)] + pub connection_string: String, + #[serde(default)] + pub ssl: bool, + #[serde(default)] + pub ca_cert_path: String, + #[serde(default)] + pub client_cert_path: String, + #[serde(default)] + pub client_key_path: String, + #[serde(default)] + pub connect_timeout_secs: u64, +} + +#[derive(Debug, Clone, Default, Deserialize)] +pub struct QueryOptions { + #[serde(default)] + pub sql: String, + #[serde(default)] + pub database: String, + #[serde(default)] + pub schema: String, + #[serde(default, rename = "maxRows")] + pub max_rows: usize, + #[serde(default, rename = "fetchSize")] + pub fetch_size: usize, + #[serde(default, rename = "timeoutSecs")] + pub timeout_secs: u64, + #[serde(default, rename = "pageSize")] + pub page_size: usize, +} + +#[derive(Debug, Clone, Default, Deserialize)] +pub struct MetadataListConstraints { + #[serde(default)] + pub filter: String, + #[serde(default)] + pub limit: usize, + #[serde(default)] + pub offset: usize, + #[serde(default)] + pub object_types: Vec, +} + +#[derive(Debug, Clone, Default, Deserialize)] +pub struct CompletionAssistantRequest { + #[serde(default)] + pub database: String, + #[serde(default, deserialize_with = "deserialize_null_default")] + pub schema: String, + #[serde(default)] + pub object_kinds: Vec, + #[serde(default)] + pub mask: String, + #[serde(default)] + pub case_sensitive: bool, + #[serde(default)] + pub global_search: bool, + #[serde(default, deserialize_with = "deserialize_null_default")] + pub max_results: usize, + #[serde(default, deserialize_with = "deserialize_null_default")] + pub parent_schema: String, + #[serde(default, deserialize_with = "deserialize_null_default")] + pub parent_name: String, + #[serde(default, deserialize_with = "deserialize_null_default")] + pub match_mode: String, +} + +fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: Deserialize<'de> + Default, +{ + Ok(Option::::deserialize(deserializer)?.unwrap_or_default()) +} + +#[derive(Debug, Serialize)] +pub struct HandshakeResult { + #[serde(rename = "protocolVersion")] + pub protocol_version: u32, + #[serde(rename = "agentProtocolVersion")] + pub agent_protocol_version: u32, + pub capabilities: Vec<&'static str>, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct DatabaseInfo { + pub name: String, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct TableInfo { + pub name: String, + pub table_type: String, + pub comment: Option, + pub parent_schema: Option, + pub parent_name: Option, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct ObjectInfo { + pub name: String, + pub object_type: String, + pub schema: String, + pub comment: Option, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct ColumnInfo { + pub name: String, + pub data_type: String, + pub is_nullable: bool, + pub column_default: Option, + pub is_primary_key: bool, + pub extra: Option, + pub comment: Option, + pub numeric_precision: Option, + pub numeric_scale: Option, + pub character_maximum_length: Option, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct ObjectSource { + pub name: String, + pub object_type: String, + pub schema: String, + pub source: String, + pub editable: bool, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct CompletionAssistantCandidate { + pub name: String, + pub kind: String, + pub database: Option, + pub schema: Option, + pub parent_schema: Option, + pub parent_name: Option, + pub comment: Option, + pub data_type: Option, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct CompletionAssistantResponse { + pub candidates: Vec, + pub incomplete: bool, + pub fallback_used: bool, +} + +#[derive(Debug, Clone, Default, Serialize, PartialEq)] +pub struct QueryResult { + pub columns: Vec, + pub column_types: Vec, + pub rows: Vec>, + pub affected_rows: i64, + pub execution_time_ms: i64, + pub truncated: bool, +} + +#[derive(Debug, Clone, Default, Serialize, PartialEq)] +pub struct QueryPageResult { + pub columns: Vec, + pub column_types: Vec, + pub rows: Vec>, + pub affected_rows: i64, + pub execution_time_ms: i64, + pub truncated: bool, + pub session_id: Option, + pub has_more: bool, +} + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DatabaseConnectionInfo { + pub product_name: Option, + pub product_version: Option, + pub current_database: Option, + pub driver_name: Option, + pub driver_version: Option, + pub unquoted_identifier_case: Option<&'static str>, + pub quoted_identifier_case: Option<&'static str>, +} + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentConnectionInfo { + pub identifier_quote: String, + pub compatibility_mode: Option, + pub database_info: Option, +} + +pub const TDENGINE_DATA_TYPES: &[&str] = &[ + "TIMESTAMP", + "BOOL", + "TINYINT", + "SMALLINT", + "INT", + "BIGINT", + "TINYINT UNSIGNED", + "SMALLINT UNSIGNED", + "INT UNSIGNED", + "BIGINT UNSIGNED", + "FLOAT", + "DOUBLE", + "BINARY", + "VARCHAR", + "NCHAR", + "JSON", + "VARBINARY", + "GEOMETRY", + "DECIMAL", + "BLOB", + "MEDIUMBLOB", +]; diff --git a/agents/drivers/tdengine/src/runtime.rs b/agents/drivers/tdengine/src/runtime.rs new file mode 100644 index 0000000000..1db3bfcbfb --- /dev/null +++ b/agents/drivers/tdengine/src/runtime.rs @@ -0,0 +1,662 @@ +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex as StdMutex}; + +use anyhow::{anyhow, bail, Context, Result}; +use serde::de::DeserializeOwned; +use serde_json::{json, Value}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::sync::{mpsc, Mutex, OwnedSemaphorePermit, RwLock, Semaphore}; +use tokio::task::JoinSet; +use tokio_util::sync::CancellationToken; + +use crate::driver::TdengineDriver; +use crate::model::{ + ConnectParams, HandshakeResult, RpcError, RpcRequest, RpcResponse, StructuredError, LEGACY_SESSION_ID, + MAX_AGENT_SESSIONS, MAX_CONCURRENT_REQUESTS, PROTOCOL_VERSION, +}; + +struct RuntimeServer { + sessions: RwLock>>, + session_slots: Arc, + request_slots: Arc, +} + +struct AgentSession { + driver: Mutex, + active: StdMutex>, + next_operation_id: AtomicU64, + _slot: OwnedSemaphorePermit, +} + +struct ActiveOperation { + id: u64, + token: CancellationToken, +} + +struct OperationGuard<'a> { + session: &'a AgentSession, + id: u64, +} + +impl Drop for OperationGuard<'_> { + fn drop(&mut self) { + let mut active = self.session.active.lock().expect("active operation lock poisoned"); + if active.as_ref().is_some_and(|operation| operation.id == self.id) { + *active = None; + } + } +} + +impl AgentSession { + fn begin_operation(&self) -> (CancellationToken, OperationGuard<'_>) { + let id = self.next_operation_id.fetch_add(1, Ordering::Relaxed); + let token = CancellationToken::new(); + *self.active.lock().expect("active operation lock poisoned") = + Some(ActiveOperation { id, token: token.clone() }); + (token, OperationGuard { session: self, id }) + } + + fn cancel(&self) { + if let Some(operation) = self.active.lock().expect("active operation lock poisoned").as_ref() { + operation.token.cancel(); + } + } +} + +pub async fn run() -> Result<()> { + let runtime = Arc::new(RuntimeServer::new()); + let (responses, mut response_rx) = mpsc::unbounded_channel::(); + let writer = tokio::spawn(async move { + let mut stdout = tokio::io::stdout(); + stdout.write_all(b"{\"ready\":true}\n").await?; + stdout.flush().await?; + while let Some(response) = response_rx.recv().await { + let line = serde_json::to_vec(&response)?; + stdout.write_all(&line).await?; + stdout.write_all(b"\n").await?; + stdout.flush().await?; + } + Ok::<(), anyhow::Error>(()) + }); + + let mut lines = BufReader::new(tokio::io::stdin()).lines(); + let mut requests = JoinSet::new(); + while let Some(line) = lines.next_line().await? { + while requests.try_join_next().is_some() {} + let line = line.trim(); + if line.is_empty() { + continue; + } + let parsed = serde_json::from_str::(line); + if parsed.as_ref().is_ok_and(|request| request.method == "shutdown") { + let response = match parsed { + Ok(request) => handle_request(runtime.clone(), request).await, + Err(error) => error_response(Value::Null, "request", None, error.into()), + }; + responses.send(response).map_err(|_| anyhow!("TDengine response writer stopped"))?; + while requests.join_next().await.is_some() {} + break; + } + let request_permit = if parsed.as_ref().is_ok_and(|request| is_capacity_exempt(&request.method)) { + None + } else { + match runtime.request_slots.clone().try_acquire_owned() { + Ok(permit) => Some(permit), + Err(_) => { + let response = match parsed { + Ok(request) => error_response( + if request.id.is_null() { json!(1) } else { request.id }, + &request.method, + session_id(&request.params), + anyhow!("agent request capacity is temporarily exhausted"), + ), + Err(error) => error_response(Value::Null, "request", None, error.into()), + }; + responses.send(response).map_err(|_| anyhow!("TDengine response writer stopped"))?; + continue; + } + } + }; + let runtime = runtime.clone(); + let responses = responses.clone(); + requests.spawn(async move { + let _request_permit = request_permit; + let response = match parsed { + Ok(request) => handle_request(runtime, request).await, + Err(error) => error_response(Value::Null, "request", None, error.into()), + }; + let _ = responses.send(response); + }); + } + runtime.close_all_sessions().await; + while requests.join_next().await.is_some() {} + drop(responses); + writer.await.context("TDengine response writer task failed")??; + Ok(()) +} + +async fn handle_request(runtime: Arc, request: RpcRequest) -> RpcResponse { + let id = if request.id.is_null() { json!(1) } else { request.id.clone() }; + let session_id = session_id(&request.params); + match runtime.dispatch(&request.method, request.params).await { + Ok(result) => RpcResponse { jsonrpc: "2.0", id, result: Some(result), error: None }, + Err(error) => error_response(id, &request.method, session_id, error), + } +} + +impl RuntimeServer { + fn new() -> Self { + Self { + sessions: RwLock::new(HashMap::new()), + session_slots: Arc::new(Semaphore::new(MAX_AGENT_SESSIONS)), + request_slots: Arc::new(Semaphore::new(MAX_CONCURRENT_REQUESTS)), + } + } + + async fn dispatch(&self, method: &str, params: Value) -> Result { + match method { + "handshake" => serialize(HandshakeResult { + protocol_version: PROTOCOL_VERSION, + agent_protocol_version: PROTOCOL_VERSION, + capabilities: vec![ + "connect", + "test_connection", + "metadata", + "query", + "paged_query", + "transaction", + "ddl", + "multi_session", + "structured_error_v1", + ], + }), + "open_session" => { + let id = required_string(¶ms, "agentSessionId")?; + let connect = decode::(¶ms)?; + self.open_session(&id, connect).await?; + Ok(json!({"ok": true})) + } + "close_session" => { + self.close_session(&required_string(¶ms, "agentSessionId")?).await?; + Ok(json!({"ok": true})) + } + "validate_session" => { + let session = self.session(&required_string(¶ms, "agentSessionId")?).await?; + let driver = session.driver.lock().await; + let (token, _operation) = session.begin_operation(); + driver.validate_connection(&token).await?; + Ok(json!({"ok": true})) + } + "cancel_session" => { + self.session(&required_string(¶ms, "agentSessionId")?).await?.cancel(); + Ok(json!({"ok": true})) + } + "test_connection" => { + let database_info = TdengineDriver::test_connection(decode::(¶ms)?).await?; + Ok(json!({"ok": true, "databaseInfo": database_info})) + } + "connect" => { + self.close_session(LEGACY_SESSION_ID).await?; + self.open_session(LEGACY_SESSION_ID, decode::(¶ms)?).await?; + Ok(json!({"ok": true})) + } + "disconnect" => { + self.close_session(LEGACY_SESSION_ID).await?; + Ok(json!({"ok": true})) + } + "shutdown" => { + self.close_all_sessions().await; + Ok(json!({"ok": true})) + } + _ => { + if !is_driver_method(method) { + bail!("unknown method: {method}"); + } + let id = session_id(¶ms).unwrap_or_else(|| LEGACY_SESSION_ID.to_string()); + let session = self.session(&id).await?; + let mut driver = session.driver.lock().await; + let (token, _operation) = session.begin_operation(); + dispatch_driver(&mut driver, method, ¶ms, &token).await + } + } + } + + async fn open_session(&self, id: &str, params: ConnectParams) -> Result<()> { + if id.trim().is_empty() { + bail!("agentSessionId is required"); + } + { + let sessions = self.sessions.read().await; + if sessions.contains_key(id) { + bail!("agent session already exists: {id}"); + } + } + let slot = self + .session_slots + .clone() + .try_acquire_owned() + .map_err(|_| anyhow!("agent session limit reached: {MAX_AGENT_SESSIONS}"))?; + let mut driver = TdengineDriver::new(); + driver.connect(params).await?; + let session = Arc::new(AgentSession { + driver: Mutex::new(driver), + active: StdMutex::new(None), + next_operation_id: AtomicU64::new(1), + _slot: slot, + }); + let mut sessions = self.sessions.write().await; + if sessions.contains_key(id) { + bail!("agent session already exists: {id}"); + } + sessions.insert(id.to_string(), session); + Ok(()) + } + + async fn session(&self, id: &str) -> Result> { + self.sessions.read().await.get(id).cloned().ok_or_else(|| anyhow!("agent session not found: {id}")) + } + + async fn close_session(&self, id: &str) -> Result<()> { + let session = self.sessions.write().await.remove(id); + let Some(session) = session else { + return Ok(()); + }; + session.cancel(); + session.driver.lock().await.disconnect().await; + Ok(()) + } + + async fn close_all_sessions(&self) { + let sessions = { + let mut sessions = self.sessions.write().await; + std::mem::take(&mut *sessions) + }; + for (_, session) in sessions { + session.cancel(); + session.driver.lock().await.disconnect().await; + } + } +} + +async fn dispatch_driver( + driver: &mut TdengineDriver, + method: &str, + params: &Value, + token: &CancellationToken, +) -> Result { + let scope = database_scope(params); + match method { + "validate_connection" => { + driver.validate_connection(token).await?; + Ok(json!({"ok": true})) + } + "connection_info" => serialize(driver.connection_info()?), + "list_databases" => serialize(driver.list_databases(token).await?), + "list_schemas" => serialize(driver.list_schemas()), + "list_tables" => serialize(driver.list_tables(&scope, decode(params)?, token).await?), + "get_table_comment" => serialize(driver.get_table_comment(&scope, &required_string(params, "table")?).await?), + "list_objects" => serialize(driver.list_objects(&scope, decode(params)?, token).await?), + "list_data_types" => serialize(driver.list_data_types()), + "completion_assistant_search_v1" => { + serialize(driver.completion_assistant_search(decode(params)?, token).await?) + } + "get_columns" => serialize(driver.get_columns(&scope, &required_string(params, "table")?, token).await?), + "list_indexes" | "list_foreign_keys" | "list_triggers" | "list_constraints" | "list_partitions" + | "list_subpartitions" => Ok(json!([])), + "get_object_source" => serialize( + driver + .get_object_source( + &scope, + &required_string(params, "name")?, + &required_string(params, "object_type")?, + token, + ) + .await?, + ), + "get_table_ddl" => serialize(driver.get_table_ddl(&scope, &required_string(params, "table")?, token).await?), + "get_explain_info" => Ok(json!({ + "plan": driver + .get_explain_info( + &required_string(params, "sql")?, + token, + optional_u64(params, "timeoutSecs").unwrap_or(0), + ) + .await?, + "has_actual_stats": false, + })), + "execute_query" => serialize(driver.execute_query(decode(params)?, token).await?), + "execute_query_page" | "start_table_read" => { + serialize(driver.execute_query_page(decode(params)?, token).await?) + } + "fetch_query_page" | "fetch_table_read_page" => serialize( + driver + .fetch_query_page( + &required_string(params, "sessionId")?, + optional_usize(params, "pageSize").unwrap_or(100), + optional_u64(params, "timeoutSecs").unwrap_or(0), + token, + ) + .await?, + ), + "close_query_session" | "close_table_read_session" => { + serialize(driver.close_query_session(&required_string(params, "sessionId")?)) + } + "execute_transaction" | "execute_batch" => serialize( + driver + .execute_statements( + string_array(params, "statements")?, + &scope, + optional_u64(params, "timeoutSecs").unwrap_or(0), + token, + ) + .await?, + ), + "disconnect" => { + driver.disconnect().await; + Ok(json!({"ok": true})) + } + "shutdown" => { + driver.disconnect().await; + Ok(json!({"ok": true})) + } + _ => bail!("unknown method: {method}"), + } +} + +fn decode(params: &Value) -> Result { + serde_json::from_value(params.clone()).with_context(|| "invalid TDengine agent request parameters") +} + +fn serialize(value: T) -> Result { + serde_json::to_value(value).map_err(anyhow::Error::from) +} + +fn required_string(params: &Value, key: &str) -> Result { + params + .get(key) + .and_then(Value::as_str) + .map(str::to_string) + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| anyhow!("{key} is required")) +} + +fn optional_string(params: &Value, key: &str) -> String { + params.get(key).and_then(Value::as_str).unwrap_or_default().to_string() +} + +fn optional_usize(params: &Value, key: &str) -> Option { + params.get(key).and_then(Value::as_u64).and_then(|value| usize::try_from(value).ok()) +} + +fn optional_u64(params: &Value, key: &str) -> Option { + params.get(key).and_then(Value::as_u64) +} + +fn string_array(params: &Value, key: &str) -> Result> { + params + .get(key) + .cloned() + .ok_or_else(|| anyhow!("{key} is required")) + .and_then(|value| serde_json::from_value(value).map_err(anyhow::Error::from)) +} + +fn database_scope(params: &Value) -> String { + let schema = optional_string(params, "schema"); + if schema.trim().is_empty() { + optional_string(params, "database") + } else { + schema + } +} + +fn session_id(params: &Value) -> Option { + params.get("agentSessionId").and_then(Value::as_str).map(str::to_string) +} + +fn is_driver_method(method: &str) -> bool { + matches!( + method, + "validate_connection" + | "connection_info" + | "list_databases" + | "list_schemas" + | "list_tables" + | "get_table_comment" + | "list_objects" + | "list_data_types" + | "completion_assistant_search_v1" + | "get_columns" + | "list_indexes" + | "list_foreign_keys" + | "list_triggers" + | "list_constraints" + | "list_partitions" + | "list_subpartitions" + | "get_object_source" + | "get_table_ddl" + | "get_explain_info" + | "execute_query" + | "execute_query_page" + | "start_table_read" + | "fetch_query_page" + | "fetch_table_read_page" + | "close_query_session" + | "close_table_read_session" + | "execute_transaction" + | "execute_batch" + ) +} + +fn is_capacity_exempt(method: &str) -> bool { + matches!(method, "handshake" | "cancel_session" | "close_session" | "disconnect") +} + +fn error_response(id: Value, method: &str, session_id: Option, error: anyhow::Error) -> RpcResponse { + let message = format!("{error:#}"); + let lower = message.to_ascii_lowercase(); + let stage = error_stage(method); + let (category, retryable, session_disposition) = if lower.contains("cancel") { + ("canceled", false, "quarantine") + } else if lower.contains("timed out") || lower.contains("timeout") { + ("timeout", false, "quarantine") + } else if lower.contains("memory exhausted") + || lower.contains("resource exhausted") + || lower.contains("session limit reached") + || lower.contains("request capacity") + { + ("resource", true, "keep") + } else if lower.contains("agent session not found") + || lower.contains("query session not found") + || lower.contains("agent session already exists") + || lower.contains("unknown method") + || lower.contains("invalid tdengine agent request parameters") + || lower.contains(" is required") + { + ("protocol", false, "keep") + } else if stage == "connect" || stage == "validate" || lower.contains("connection") || lower.contains("websocket") { + ( + "connection", + stage == "connect" || stage == "validate", + if stage == "connect" { "keep" } else { "quarantine" }, + ) + } else if matches!(stage, "execute" | "fetch") { + ("sql", false, "keep") + } else { + ("protocol", false, "keep") + }; + RpcResponse { + jsonrpc: "2.0", + id, + result: None, + error: Some(RpcError { + code: -1, + message, + data: Some(StructuredError { + category, + retryable, + session_disposition, + stage, + contract_version: 1, + operation_outcome: if matches!(stage, "request" | "connect" | "validate") { + "not_started" + } else { + "unknown" + }, + agent_session_id: session_id, + exception_class: Some("dbx_tdengine_driver::Error".into()), + }), + }), + } +} + +fn error_stage(method: &str) -> &'static str { + match method { + "request" | "handshake" => "request", + "connect" | "open_session" | "test_connection" => "connect", + "validate_connection" | "validate_session" => "validate", + "cancel_session" => "cancel", + "close_session" | "disconnect" | "close_query_session" | "close_table_read_session" | "shutdown" => "close", + "fetch_query_page" | "fetch_table_read_page" => "fetch", + _ => "execute", + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn structured_errors_keep_stage_and_outcome_consistent() { + let response = error_response(json!(1), "open_session", Some("s1".into()), anyhow!("connection refused")); + let data = response.error.unwrap().data.unwrap(); + assert_eq!(data.category, "connection"); + assert_eq!(data.stage, "connect"); + assert_eq!(data.operation_outcome, "not_started"); + assert_eq!(data.agent_session_id.as_deref(), Some("s1")); + } + + #[test] + fn classifies_tdengine_execution_failures_as_sql_errors() { + let response = error_response(json!(1), "execute_query", Some("s1".into()), anyhow!("invalid function")); + let data = response.error.unwrap().data.unwrap(); + assert_eq!(data.category, "sql"); + assert_eq!(data.stage, "execute"); + assert_eq!(data.operation_outcome, "unknown"); + } + + #[test] + fn keeps_invalid_request_parameters_as_protocol_errors() { + let response = error_response( + json!(1), + "execute_query", + Some("s1".into()), + anyhow!("invalid TDengine agent request parameters"), + ); + assert_eq!(response.error.unwrap().data.unwrap().category, "protocol"); + } + + #[test] + fn keeps_missing_query_sessions_as_protocol_errors() { + let response = + error_response(json!(1), "fetch_query_page", Some("s1".into()), anyhow!("query session not found")); + let data = response.error.unwrap().data.unwrap(); + assert_eq!(data.category, "protocol"); + assert_eq!(data.stage, "fetch"); + } + + #[test] + fn quarantines_connections_lost_during_execution() { + let response = error_response(json!(1), "execute_query", Some("s1".into()), anyhow!("connection reset")); + let data = response.error.unwrap().data.unwrap(); + assert_eq!(data.category, "connection"); + assert_eq!(data.session_disposition, "quarantine"); + assert!(!data.retryable); + } + + #[test] + fn tdengine_uses_database_when_schema_is_empty() { + assert_eq!(database_scope(&json!({"database": "power", "schema": ""})), "power"); + assert_eq!(database_scope(&json!({"database": "power", "schema": "metrics"})), "metrics"); + } + + #[test] + fn metadata_constraints_ignore_protocol_session_fields() { + let constraints: crate::model::MetadataListConstraints = decode(&json!({ + "agentSessionId": "session-1", + "filter": "meter", + "limit": 10, + })) + .unwrap(); + assert_eq!(constraints.filter, "meter"); + assert_eq!(constraints.limit, 10); + } + + #[test] + fn query_options_accept_protocol_camel_case_fields() { + let options: crate::model::QueryOptions = + decode(&json!({"sql": "SELECT 1", "pageSize": 25, "maxRows": 100})).unwrap(); + assert_eq!(options.page_size, 25); + assert_eq!(options.max_rows, 100); + } + + #[test] + fn completion_request_accepts_null_optional_fields() { + let request: crate::model::CompletionAssistantRequest = decode(&json!({ + "connection_id": "connection-1", + "database": "power", + "schema": null, + "object_kinds": ["table"], + "max_results": null, + "parent_schema": null, + "parent_name": null, + "match_mode": null, + })) + .unwrap(); + assert_eq!(request.schema, ""); + assert_eq!(request.max_results, 0); + assert_eq!(request.parent_schema, ""); + assert_eq!(request.parent_name, ""); + assert_eq!(request.match_mode, ""); + } + + #[test] + fn cancel_session_cancels_the_active_operation() { + let slot = Arc::new(Semaphore::new(1)).try_acquire_owned().unwrap(); + let session = AgentSession { + driver: Mutex::new(TdengineDriver::new()), + active: StdMutex::new(None), + next_operation_id: AtomicU64::new(1), + _slot: slot, + }; + let (token, operation) = session.begin_operation(); + session.cancel(); + assert!(token.is_cancelled()); + drop(operation); + assert!(session.active.lock().unwrap().is_none()); + } + + #[test] + fn session_slots_bound_connects_in_flight_and_open_sessions() { + let runtime = RuntimeServer::new(); + let permits = (0..MAX_AGENT_SESSIONS) + .map(|_| runtime.session_slots.clone().try_acquire_owned().unwrap()) + .collect::>(); + assert!(runtime.session_slots.clone().try_acquire_owned().is_err()); + drop(permits); + assert!(runtime.session_slots.clone().try_acquire_owned().is_ok()); + } + + #[test] + fn request_slots_bound_non_control_work_without_blocking_cancellation() { + let runtime = RuntimeServer::new(); + let permits = (0..MAX_CONCURRENT_REQUESTS) + .map(|_| runtime.request_slots.clone().try_acquire_owned().unwrap()) + .collect::>(); + assert!(runtime.request_slots.clone().try_acquire_owned().is_err()); + assert!(is_capacity_exempt("cancel_session")); + assert!(is_capacity_exempt("close_session")); + assert!(!is_capacity_exempt("execute_query")); + drop(permits); + } +} diff --git a/agents/drivers/tdengine/src/test/java/com/dbx/agent/tdengine/TDengineAgentPagingTest.java b/agents/drivers/tdengine/src/test/java/com/dbx/agent/tdengine/TDengineAgentPagingTest.java deleted file mode 100644 index 91ef0b9174..0000000000 --- a/agents/drivers/tdengine/src/test/java/com/dbx/agent/tdengine/TDengineAgentPagingTest.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.dbx.agent.tdengine; - -import com.dbx.agent.JsonRpcServer; -import com.dbx.agent.test.TestSupport; -import org.junit.jupiter.api.Test; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.nio.charset.StandardCharsets; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.Statement; -import java.sql.Types; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class TDengineAgentPagingTest { - @Test - void executeQueryPageDecodesBinaryVarcharValuesAsStrings() { - TDengineAgent agent = new TDengineAgent(); - TestSupport.setPrivateConnection(agent, binaryVarcharConnection()); - JsonRpcServer server = new JsonRpcServer(agent); - - String response = handleRequest(server, """ - { - "jsonrpc": "2.0", - "id": 1, - "method": "execute_query_page", - "params": { - "sql": "SELECT v FROM sample", - "schema": "power", - "pageSize": 100 - } - } - """); - - assertFalse(response.contains("\"error\""), response); - assertTrue(response.contains("\"hello\""), response); - assertFalse(response.contains("[104,101,108,108,111]"), response); - } - - private static Connection binaryVarcharConnection() { - Statement statement = binaryVarcharStatement(); - return proxy(Connection.class, (method, args) -> { - String name = method.getName(); - if ("createStatement".equals(name)) { - return statement; - } - if ("getAutoCommit".equals(name)) { - return true; - } - if ("setAutoCommit".equals(name) || "commit".equals(name) || "rollback".equals(name) || "close".equals(name)) { - return null; - } - if ("isClosed".equals(name)) { - return false; - } - return defaultValue(method.getReturnType()); - }); - } - - private static Statement binaryVarcharStatement() { - ResultSet resultSet = binaryVarcharResultSet(); - return proxy(Statement.class, (method, args) -> { - String name = method.getName(); - if ("execute".equals(name)) { - return true; - } - if ("getResultSet".equals(name)) { - return resultSet; - } - if ("getUpdateCount".equals(name)) { - return 0; - } - if ("setMaxRows".equals(name) || "setFetchSize".equals(name) || "close".equals(name)) { - return null; - } - return defaultValue(method.getReturnType()); - }); - } - - private static ResultSet binaryVarcharResultSet() { - final int[] index = {-1}; - ResultSetMetaData metadata = binaryVarcharMetadata(); - byte[] value = "hello".getBytes(StandardCharsets.UTF_8); - return proxy(ResultSet.class, (method, args) -> { - String name = method.getName(); - if ("next".equals(name)) { - index[0] += 1; - return index[0] == 0; - } - if ("getMetaData".equals(name)) { - return metadata; - } - if ("getObject".equals(name)) { - return value; - } - if ("wasNull".equals(name)) { - return false; - } - if ("close".equals(name)) { - return null; - } - return defaultValue(method.getReturnType()); - }); - } - - private static ResultSetMetaData binaryVarcharMetadata() { - return proxy(ResultSetMetaData.class, (method, args) -> { - String name = method.getName(); - if ("getColumnCount".equals(name)) { - return 1; - } - if ("getColumnLabel".equals(name)) { - return "v"; - } - if ("getColumnType".equals(name)) { - return Types.VARCHAR; - } - return defaultValue(method.getReturnType()); - }); - } - - private static String handleRequest(JsonRpcServer server, String request) { - try { - Method method = JsonRpcServer.class.getDeclaredMethod("handleRequest", String.class); - method.setAccessible(true); - return (String) method.invoke(server, request); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private static T proxy(Class type, MethodHandler handler) { - InvocationHandler invocationHandler = new InvocationHandler() { - @Override - public Object invoke(Object proxy, Method method, Object[] args) { - return handler.handle(method, args); - } - }; - return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, invocationHandler)); - } - - private static Object defaultValue(Class type) { - if (Boolean.TYPE.equals(type)) { - return false; - } - if (Byte.TYPE.equals(type)) { - return (byte) 0; - } - if (Short.TYPE.equals(type)) { - return (short) 0; - } - if (Integer.TYPE.equals(type)) { - return 0; - } - if (Long.TYPE.equals(type)) { - return 0L; - } - if (Float.TYPE.equals(type)) { - return 0f; - } - if (Double.TYPE.equals(type)) { - return 0.0d; - } - if (Character.TYPE.equals(type)) { - return '\0'; - } - return null; - } - - private interface MethodHandler { - Object handle(Method method, Object[] args); - } -} diff --git a/agents/drivers/tdengine/src/test/java/com/dbx/agent/tdengine/TDengineAgentTest.java b/agents/drivers/tdengine/src/test/java/com/dbx/agent/tdengine/TDengineAgentTest.java deleted file mode 100644 index 8c6c489b5c..0000000000 --- a/agents/drivers/tdengine/src/test/java/com/dbx/agent/tdengine/TDengineAgentTest.java +++ /dev/null @@ -1,584 +0,0 @@ -package com.dbx.agent.tdengine; - -import com.dbx.agent.ColumnInfo; -import com.dbx.agent.ConnectParams; -import com.dbx.agent.DatabaseAgent; -import com.dbx.agent.ExecuteQueryOptions; -import com.dbx.agent.MetadataListConstraints; -import com.dbx.agent.TableInfo; -import com.dbx.agent.test.JdbcAgentFake; -import com.dbx.agent.test.JdbcFakeExecutionBehaviorTest; -import com.dbx.agent.test.JdbcMetadataSqlFake; -import com.dbx.agent.test.TestSupport; -import com.taosdata.jdbc.TSDBDriver; -import com.taosdata.jdbc.rs.RestfulConnection; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Proxy; -import java.nio.charset.StandardCharsets; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.Timestamp; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Properties; -import javax.sql.DataSource; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -class TDengineAgentExecutionTest extends JdbcFakeExecutionBehaviorTest { - @Override - protected DatabaseAgent createAgent() { - return new TDengineAgent(); - } - - @Override - protected String resultSetSql() { - return "SHOW DATABASES"; - } -} - -class TDengineAgentMetadataTest { - @Test - void buildsWebsocketJdbcUrlWithDefaultPortAndDatabase() { - String url = TDengineJdbcUrl.from( - new ConnectParams("127.0.0.1", 0, "meters", "root", "taosdata", "", "", false) - ); - - Assertions.assertEquals("jdbc:TAOS-WS://127.0.0.1:6041/meters", url); - } - - @Test - void preservesCustomWebsocketPortAndUrlParams() { - String url = TDengineJdbcUrl.from( - new ConnectParams("td.local", 6042, "", "", "", "timezone=UTC&charset=UTF-8", "", false) - ); - - Assertions.assertEquals("jdbc:TAOS-WS://td.local:6042/?timezone=UTC&charset=UTF-8", url); - } - - @Test - void supportsRestTransportViaCompatibilityUrlParam() { - String url = TDengineJdbcUrl.from( - new ConnectParams("127.0.0.1", 0, "testdb", "", "", "dbx.transport=rest&charset=UTF-8", "", false) - ); - - Assertions.assertEquals("jdbc:TAOS-RS://127.0.0.1:6041/testdb?charset=UTF-8", url); - } - - @Test - void stripsTransportControlParamFromJdbcQuery() { - String url = TDengineJdbcUrl.from( - new ConnectParams("127.0.0.1", 6041, "", "", "", "transport=ws&timezone=UTC", "", false) - ); - - Assertions.assertEquals("jdbc:TAOS-WS://127.0.0.1:6041/?timezone=UTC", url); - } - - @Test - void usesTdengineShowStatementsForMetadata() { - TDengineAgent agent = new TDengineAgent(); - TestSupport.setPrivateConnection(agent, JdbcMetadataSqlFake.connection()); - - agent.listDatabases(); - agent.listTables("power"); - agent.getColumns("power", "meters"); - - Assertions.assertEquals("SHOW DATABASES", JdbcMetadataSqlFake.statements.get(0)); - Assertions.assertEquals("SHOW `power`.STABLES", JdbcMetadataSqlFake.statements.get(1)); - Assertions.assertEquals("SHOW `power`.TABLES", JdbcMetadataSqlFake.statements.get(2)); - Assertions.assertFalse(JdbcMetadataSqlFake.statements.stream().anyMatch(sql -> sql.contains("information_schema"))); - Assertions.assertTrue(JdbcMetadataSqlFake.statements.contains("DESCRIBE `power`.`meters`")); - } - - @Test - void showMetadataStopsReadingAtTheRequestedPageBoundary() { - List statements = new ArrayList<>(); - List maxRows = new ArrayList<>(); - List rowReads = new ArrayList<>(); - TDengineAgent agent = new TDengineAgent(); - TestSupport.setPrivateConnection(agent, showMetadataConnection(statements, maxRows, rowReads)); - - List firstPage = agent.listTables( - "dbx_alpha", - new MetadataListConstraints(null, 3, null, List.of("TABLE")) - ); - List secondPage = agent.listTables( - "dbx_alpha", - new MetadataListConstraints(null, 2, 3, List.of("TABLE")) - ); - - Assertions.assertEquals(List.of("meters", "weather", "device_a"), firstPage.stream().map(TableInfo::getName).toList()); - Assertions.assertEquals(List.of("device_b", "standalone"), secondPage.stream().map(TableInfo::getName).toList()); - Assertions.assertEquals("meters", firstPage.get(2).getParent_name()); - Assertions.assertEquals("meters", secondPage.get(0).getParent_name()); - Assertions.assertTrue(maxRows.isEmpty()); - Assertions.assertEquals( - List.of( - "SHOW `dbx_alpha`.STABLES", - "SHOW `dbx_alpha`.TABLES", - "SHOW `dbx_alpha`.STABLES", - "SHOW `dbx_alpha`.TABLES" - ), - statements - ); - Assertions.assertEquals(4, rowReads.stream().filter("STABLE"::equals).count()); - Assertions.assertEquals(4, rowReads.stream().filter("TABLE"::equals).count()); - } - - @Test - void showMetadataCachesOnlyUnboundedResults() { - List statements = new ArrayList<>(); - List maxRows = new ArrayList<>(); - TDengineAgent agent = new TDengineAgent(); - TestSupport.setPrivateConnection(agent, showMetadataConnection(statements, maxRows)); - - agent.listTables("dbx_alpha"); - agent.listTables("dbx_alpha"); - - Assertions.assertEquals( - List.of( - "SHOW `dbx_alpha`.STABLES", - "SHOW `dbx_alpha`.TABLES" - ), - statements - ); - } - - @Test - void showMetadataFiltersLocallyBeforeApplyingRowLimit() { - List statements = new ArrayList<>(); - List maxRows = new ArrayList<>(); - TDengineAgent agent = new TDengineAgent(); - TestSupport.setPrivateConnection(agent, showMetadataConnection(statements, maxRows)); - - List tables = agent.listTables( - "dbx_alpha", - new MetadataListConstraints("stand", 1, null, List.of("TABLE")) - ); - - Assertions.assertEquals(List.of("standalone"), tables.stream().map(TableInfo::getName).toList()); - Assertions.assertTrue(maxRows.isEmpty()); - Assertions.assertEquals( - List.of( - "SHOW `dbx_alpha`.STABLES", - "SHOW `dbx_alpha`.TABLES" - ), - statements - ); - } - - @Test - void showMetadataFallsBackToLocalFilteringForLongLikePatterns() { - List statements = new ArrayList<>(); - List maxRows = new ArrayList<>(); - TDengineAgent agent = new TDengineAgent(); - TestSupport.setPrivateConnection(agent, showMetadataConnection(statements, maxRows)); - - agent.listTables( - "dbx_alpha", - new MetadataListConstraints("a".repeat(50), 1, null, List.of("TABLE")) - ); - - Assertions.assertTrue(maxRows.isEmpty()); - Assertions.assertEquals( - List.of( - "SHOW `dbx_alpha`.STABLES", - "SHOW `dbx_alpha`.TABLES" - ), - statements - ); - } - - @Test - void showMetadataDoesNotLoadTableComments() { - List statements = new ArrayList<>(); - List maxRows = new ArrayList<>(); - TDengineAgent agent = new TDengineAgent(); - TestSupport.setPrivateConnection(agent, showMetadataConnection(statements, maxRows)); - - List tables = agent.listTables("dbx_alpha"); - - Assertions.assertNull(tables.get(0).getComment()); - Assertions.assertEquals( - List.of( - "SHOW `dbx_alpha`.STABLES", - "SHOW `dbx_alpha`.TABLES" - ), - statements - ); - } - - @Test - void showMetadataFiltersByNameWithoutInformationSchema() { - List statements = new ArrayList<>(); - List maxRows = new ArrayList<>(); - TDengineAgent agent = new TDengineAgent(); - TestSupport.setPrivateConnection(agent, showMetadataConnection(statements, maxRows)); - - List tables = agent.listTables( - "dbx_alpha", - new MetadataListConstraints("device_b", 1, null, List.of("TABLE")) - ); - - Assertions.assertEquals(List.of("device_b"), tables.stream().map(TableInfo::getName).toList()); - Assertions.assertTrue(maxRows.isEmpty()); - } - - @Test - void sortsSupertablesBeforeTheirChildTables() { - List tables = new ArrayList<>(Arrays.asList( - new TableInfo("device_b", "TABLE", null, null, "meters"), - new TableInfo("standalone", "TABLE", null), - new TableInfo("meters", "STABLE", null), - new TableInfo("device_a", "TABLE", null, null, "meters") - )); - - TDengineAgent.sortTablesForHierarchy(tables); - - Assertions.assertEquals( - Arrays.asList("meters", "device_a", "device_b", "standalone"), - tables.stream().map(TableInfo::getName).toList() - ); - Assertions.assertEquals("meters", tables.get(1).getParent_name()); - } - - @Test - void marksTimestampAndCompositeKeyDescribeColumnsAsPrimaryKeys() throws Exception { - ResultSet resultSet = describeResultSet(new String[][] { - {"ts", "TIMESTAMP", "8", ""}, - {"seq", "INT", "4", "COMPOSITE KEY"}, - {"voltage", "FLOAT", "4", ""}, - {"site", "VARCHAR(32)", "32", "TAG"} - }); - - List columns = TDengineAgent.readDescribeColumns(resultSet); - - Assertions.assertTrue(columns.get(0).getIs_primary_key()); - Assertions.assertFalse(columns.get(0).getIs_nullable()); - Assertions.assertTrue(columns.get(1).getIs_primary_key()); - Assertions.assertFalse(columns.get(1).getIs_nullable()); - Assertions.assertFalse(columns.get(2).getIs_primary_key()); - Assertions.assertFalse(columns.get(3).getIs_primary_key()); - Assertions.assertEquals("TAG", columns.get(3).getExtra()); - } - - @Test - void doesNotExposeDatabasesAsSchemas() { - TDengineAgent agent = new TDengineAgent(); - TestSupport.setPrivateConnection(agent, JdbcMetadataSqlFake.connection()); - - Assertions.assertTrue(agent.listSchemas().isEmpty()); - Assertions.assertTrue(JdbcMetadataSqlFake.statements.isEmpty()); - } - - private static Connection showMetadataConnection(List statements, List maxRows) { - return showMetadataConnection(statements, maxRows, null); - } - - private static Connection showMetadataConnection( - List statements, - List maxRows, - List rowReads - ) { - return proxy(Connection.class, (proxy, method, args) -> { - String name = method.getName(); - if ("createStatement".equals(name)) { - return showMetadataStatement(statements, maxRows, rowReads); - } - if ("isClosed".equals(name)) return false; - if ("close".equals(name)) return null; - return defaultValue(method.getReturnType()); - }); - } - - private static java.sql.Statement showMetadataStatement( - List statements, - List maxRows, - List rowReads - ) { - int[] activeMaxRows = {0}; - return proxy(java.sql.Statement.class, (proxy, method, args) -> { - String name = method.getName(); - if ("setMaxRows".equals(name)) { - activeMaxRows[0] = (Integer) args[0]; - maxRows.add(activeMaxRows[0]); - return null; - } - if ("executeQuery".equals(name)) { - String sql = (String) args[0]; - statements.add(sql); - if (sql.endsWith("STABLES")) { - return showTableResultSet( - List.of(new String[] {"meters"}, new String[] {"weather"}), - activeMaxRows[0], - "STABLE", - rowReads - ); - } - if (sql.endsWith("TABLES")) { - return showTableResultSet( - List.of( - new String[] {"device_a", "", "", "meters"}, - new String[] {"device_b", "", "", "meters"}, - new String[] {"standalone", "", "", ""} - ), - activeMaxRows[0], - "TABLE", - rowReads - ); - } - } - if ("close".equals(name)) return null; - return defaultValue(method.getReturnType()); - }); - } - - private static ResultSet showTableResultSet( - List rows, - int maxRows, - String tableType, - List rowReads - ) { - List limitedRows = maxRows > 0 ? rows.subList(0, Math.min(rows.size(), maxRows)) : rows; - int[] index = {-1}; - return proxy(ResultSet.class, (proxy, method, args) -> { - String name = method.getName(); - if ("next".equals(name)) { - index[0] += 1; - boolean hasNext = index[0] < limitedRows.size(); - if (hasNext && rowReads != null) { - rowReads.add(tableType); - } - return hasNext; - } - if ("getString".equals(name)) { - int column = (Integer) args[0]; - String[] row = limitedRows.get(index[0]); - return column <= row.length ? row[column - 1] : null; - } - if ("isClosed".equals(name)) return false; - if ("close".equals(name)) return null; - return defaultValue(method.getReturnType()); - }); - } - - private static T proxy(Class type, InvocationHandler handler) { - return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, handler)); - } - - private static Object defaultValue(Class type) { - if (Boolean.TYPE.equals(type)) return false; - if (Integer.TYPE.equals(type)) return 0; - if (Long.TYPE.equals(type)) return 0L; - if (Double.TYPE.equals(type)) return 0.0d; - if (Float.TYPE.equals(type)) return 0.0f; - if (Short.TYPE.equals(type)) return (short) 0; - if (Byte.TYPE.equals(type)) return (byte) 0; - if (Character.TYPE.equals(type)) return '\0'; - return null; - } - - @Test - void setsDatabaseBeforeExecutionWhenSchemaIsProvided() { - TDengineAgent agent = new TDengineAgent(); - TestSupport.setPrivateConnection(agent, JdbcAgentFake.connection()); - - agent.executeQuery("SELECT 1", "power", new ExecuteQueryOptions()); - - // JdbcSchemaSwitcher executes the USE statement (recorded as "execute") - // before the query runs, so the database is switched prior to execution. - Assertions.assertEquals( - Arrays.asList("execute", "setMaxRows:10001", "execute"), - JdbcAgentFake.calls - ); - } - - @Test - void restExecutionSwitchesCatalogWithoutGeneratingUseSql() throws Exception { - try (RestfulConnection connection = restfulConnection()) { - String executionSchema = TDengineAgent.prepareExecutionSchema(connection, " power-data "); - - Assertions.assertNull(executionSchema); - Assertions.assertEquals("power-data", connection.getCatalog()); - Assertions.assertEquals( - "power-data", - connection.getClientInfo(TSDBDriver.PROPERTY_KEY_DBNAME) - ); - } - } - - @Test - void restExecutionSupportsPooledWrappersAndRestoresOriginalDatabase() throws Exception { - Properties properties = new Properties(); - properties.setProperty(TSDBDriver.PROPERTY_KEY_DBNAME, "original"); - try (RestfulConnection physical = restfulConnection(properties)) { - physical.setCatalog("original"); - Connection pooled = wrapperConnection(physical); - - Assertions.assertNull(TDengineAgent.prepareExecutionSchema(pooled, "power")); - Assertions.assertEquals("power", pooled.getCatalog()); - Assertions.assertEquals("power", pooled.getClientInfo(TSDBDriver.PROPERTY_KEY_DBNAME)); - - TDengineAgent.restoreRestfulConnectionState(pooled, "original", "original"); - Assertions.assertEquals("original", pooled.getCatalog()); - Assertions.assertEquals("original", pooled.getClientInfo(TSDBDriver.PROPERTY_KEY_DBNAME)); - } - } - - @Test - void hikariCanManageRestfulConnectionsWithoutLosingNativeAccess() throws Exception { - RestfulConnection physical = restfulConnection(); - DataSource dataSource = proxy(DataSource.class, (proxy, method, args) -> { - if ("getConnection".equals(method.getName())) { - return physical; - } - return defaultValue(method.getReturnType()); - }); - HikariConfig config = new HikariConfig(); - config.setPoolName("tdengine-rest-test"); - config.setDataSource(dataSource); - config.setMaximumPoolSize(1); - config.setMinimumIdle(0); - config.setInitializationFailTimeout(-1L); - - try (HikariDataSource pool = new HikariDataSource(config); - Connection connection = pool.getConnection()) { - Assertions.assertTrue(connection.isWrapperFor(RestfulConnection.class)); - Assertions.assertSame(physical, connection.unwrap(RestfulConnection.class)); - Assertions.assertNull(TDengineAgent.prepareExecutionSchema(connection, "power")); - Assertions.assertEquals("power", connection.getCatalog()); - } - } - - @Test - void websocketExecutionKeepsSchemaForUseSqlSwitching() throws Exception { - Connection connection = JdbcAgentFake.connection(); - - Assertions.assertEquals( - "power", - TDengineAgent.prepareExecutionSchema(connection, "power") - ); - } - - @Test - void decodesTdengineByteArrayTextValues() { - Assertions.assertEquals( - "d1001", - TDengineAgent.decodeTdengineValue("d1001".getBytes(StandardCharsets.UTF_8)) - ); - } - - @Test - void formatsTdengineTimestampsAsSqlLiterals() { - Assertions.assertEquals( - "2026-05-16 09:35:58.123", - TDengineAgent.decodeTdengineValue(Timestamp.valueOf("2026-05-16 09:35:58.123")) - ); - Assertions.assertEquals( - "2026-05-16 09:35:58.000", - TDengineAgent.decodeTdengineValue(Timestamp.valueOf("2026-05-16 09:35:58")) - ); - Assertions.assertEquals( - "2026-05-16 09:35:58.123456", - TDengineAgent.decodeTdengineValue(Timestamp.valueOf("2026-05-16 09:35:58.123456")) - ); - Assertions.assertEquals( - "2026-05-16 09:35:58.123456789", - TDengineAgent.decodeTdengineValue(Timestamp.valueOf("2026-05-16 09:35:58.123456789")) - ); - } - - @Test - void unknownTransportValueDefaultsToWebsocketAndKeepsOtherParams() { - String url = TDengineJdbcUrl.from( - new ConnectParams("127.0.0.1", 6041, "", "", "", "transport=foo&timezone=UTC", "", false) - ); - - Assertions.assertEquals("jdbc:TAOS-WS://127.0.0.1:6041/?timezone=UTC", url); - } - - @Test - void sanitizeConnectionStringStripsTransportControlParams() { - String sanitized = TDengineJdbcUrl.sanitizeConnectionString( - "jdbc:TAOS-WS://127.0.0.1:6041/db?dbx.transport=rest&charset=UTF-8&transport=ws" - ); - - Assertions.assertEquals("jdbc:TAOS-WS://127.0.0.1:6041/db?charset=UTF-8", sanitized); - } - - @Test - void sanitizeConnectionStringKeepsFragmentAndNonControlParams() { - String sanitized = TDengineJdbcUrl.sanitizeConnectionString( - "jdbc:TAOS-RS://127.0.0.1:6041/db?timezone=UTC&dbx.transport=rest#anchor" - ); - - Assertions.assertEquals("jdbc:TAOS-RS://127.0.0.1:6041/db?timezone=UTC#anchor", sanitized); - } - - @Test - void enablesRestInformationSchemaPagingForTdengine338AndNewer() { - Assertions.assertFalse(TDengineAgent.supportsRestInformationSchemaPagingVersion(null)); - Assertions.assertFalse(TDengineAgent.supportsRestInformationSchemaPagingVersion("3.3.7.9")); - Assertions.assertTrue(TDengineAgent.supportsRestInformationSchemaPagingVersion("3.3.8.0")); - Assertions.assertTrue(TDengineAgent.supportsRestInformationSchemaPagingVersion("3.3.8.1")); - Assertions.assertTrue(TDengineAgent.supportsRestInformationSchemaPagingVersion("4.0.0.0")); - } - - private static ResultSet describeResultSet(String[][] rows) { - int[] rowIndex = {-1}; - return (ResultSet) Proxy.newProxyInstance( - ResultSet.class.getClassLoader(), - new Class[] {ResultSet.class}, - (proxy, method, args) -> { - if ("next".equals(method.getName())) { - rowIndex[0] += 1; - return rowIndex[0] < rows.length; - } - if ("getString".equals(method.getName())) { - return rows[rowIndex[0]][((Integer) args[0]) - 1]; - } - if ("close".equals(method.getName())) { - return null; - } - throw new UnsupportedOperationException(method.getName()); - } - ); - } - - private static RestfulConnection restfulConnection() { - return restfulConnection(new Properties()); - } - - private static RestfulConnection restfulConnection(Properties properties) { - return new RestfulConnection( - "127.0.0.1", - "6041", - properties, - "", - "jdbc:TAOS-RS://127.0.0.1:6041/", - null, - false, - null, - null - ); - } - - private static Connection wrapperConnection(Connection physical) { - return proxy(Connection.class, (proxy, method, args) -> { - if ("isWrapperFor".equals(method.getName())) { - return ((Class) args[0]).isInstance(physical); - } - if ("unwrap".equals(method.getName())) { - return ((Class) args[0]).cast(physical); - } - try { - return method.invoke(physical, args); - } catch (java.lang.reflect.InvocationTargetException error) { - throw error.getCause(); - } - }); - } -} diff --git a/agents/drivers/tdengine/src/value.rs b/agents/drivers/tdengine/src/value.rs new file mode 100644 index 0000000000..660d4e3600 --- /dev/null +++ b/agents/drivers/tdengine/src/value.rs @@ -0,0 +1,124 @@ +use base64::engine::general_purpose::STANDARD; +use base64::Engine; +use chrono::{NaiveDateTime, Timelike}; +use chrono_tz::Tz; +use serde_json::{json, Number, Value}; +use taos::taos_query::common::Timestamp; +use taos::BorrowedValue; + +pub fn borrowed_value_to_json(value: BorrowedValue<'_>, timezone: Option) -> Value { + match value { + BorrowedValue::Null(_) => Value::Null, + BorrowedValue::Bool(value) => Value::Bool(value), + BorrowedValue::TinyInt(value) => json!(value), + BorrowedValue::SmallInt(value) => json!(value), + BorrowedValue::Int(value) => json!(value), + BorrowedValue::BigInt(value) => json!(value), + BorrowedValue::UTinyInt(value) => json!(value), + BorrowedValue::USmallInt(value) => json!(value), + BorrowedValue::UInt(value) => json!(value), + BorrowedValue::UBigInt(value) => json!(value), + BorrowedValue::Float(value) => finite_number(value as f64), + BorrowedValue::Double(value) => finite_number(value), + BorrowedValue::VarChar(value) => Value::String(value.to_string()), + BorrowedValue::NChar(value) => Value::String(value.into_owned()), + BorrowedValue::Timestamp(value) => Value::String(format_timestamp(value, timezone)), + BorrowedValue::Json(value) => serde_json::from_slice(&value) + .unwrap_or_else(|_| Value::String(String::from_utf8_lossy(&value).into_owned())), + BorrowedValue::VarBinary(value) | BorrowedValue::Blob(value) | BorrowedValue::MediumBlob(value) => { + binary_json(&value) + } + BorrowedValue::Geometry(value) => geometry_json(&value), + BorrowedValue::Decimal(value) => Value::String(value.to_string()), + BorrowedValue::Decimal64(value) => Value::String(value.to_string()), + } +} + +fn finite_number(value: f64) -> Value { + Number::from_f64(value).map(Value::Number).unwrap_or_else(|| Value::String(value.to_string())) +} + +fn binary_json(value: &[u8]) -> Value { + json!({ "$binary": STANDARD.encode(value) }) +} + +fn geometry_json(value: &[u8]) -> Value { + if let Ok(text) = std::str::from_utf8(value) { + let trimmed = text.trim(); + if looks_like_wkt(trimmed) { + return Value::String(trimmed.to_string()); + } + } + binary_json(value) +} + +fn looks_like_wkt(value: &str) -> bool { + let upper = value.to_ascii_uppercase(); + ["POINT", "LINESTRING", "POLYGON", "MULTIPOINT", "MULTILINESTRING", "MULTIPOLYGON", "GEOMETRYCOLLECTION"] + .iter() + .any(|prefix| upper.starts_with(prefix)) +} + +fn format_timestamp(timestamp: Timestamp, timezone: Option) -> String { + let datetime = match timezone { + Some(timezone) => timestamp.to_datetime_with_custom_tz(&timezone).naive_local(), + None => timestamp.to_naive_datetime(), + }; + format_naive_datetime(datetime, timestamp) +} + +fn format_naive_datetime(datetime: NaiveDateTime, timestamp: Timestamp) -> String { + let digits = match timestamp { + Timestamp::Milliseconds(_) => 3, + Timestamp::Microseconds(_) => 6, + Timestamp::Nanoseconds(_) => 9, + }; + let mut fraction = format!("{:09}", datetime.nanosecond()); + fraction.truncate(digits); + while fraction.len() > 3 && fraction.ends_with('0') { + fraction.pop(); + } + format!("{}.{}", datetime.format("%Y-%m-%d %H:%M:%S"), fraction) +} + +#[cfg(test)] +mod tests { + use super::*; + use taos::Ty; + + #[test] + fn preserves_timestamp_precision_with_at_least_milliseconds() { + assert_eq!( + borrowed_value_to_json( + BorrowedValue::Timestamp(Timestamp::Milliseconds(1_704_067_200_123)), + Some(chrono_tz::UTC) + ), + Value::String("2024-01-01 00:00:00.123".into()) + ); + assert_eq!( + borrowed_value_to_json( + BorrowedValue::Timestamp(Timestamp::Microseconds(1_704_067_200_123_400)), + Some(chrono_tz::UTC) + ), + Value::String("2024-01-01 00:00:00.1234".into()) + ); + } + + #[test] + fn keeps_text_and_binary_values_distinct() { + assert_eq!(borrowed_value_to_json(BorrowedValue::VarChar("hello"), None), json!("hello")); + assert_eq!( + borrowed_value_to_json(BorrowedValue::VarBinary(vec![0, 255].into()), None), + json!({"$binary": "AP8="}) + ); + assert_eq!(borrowed_value_to_json(BorrowedValue::Null(Ty::Int), None), Value::Null); + } + + #[test] + fn returns_wkt_geometry_as_text() { + assert_eq!( + borrowed_value_to_json(BorrowedValue::Geometry(b"POINT (1 2)".as_slice().into()), None), + json!("POINT (1 2)") + ); + } +} diff --git a/agents/drivers/tdengine/tests/live.rs b/agents/drivers/tdengine/tests/live.rs new file mode 100644 index 0000000000..26856681ff --- /dev/null +++ b/agents/drivers/tdengine/tests/live.rs @@ -0,0 +1,219 @@ +use std::env; +use std::io::{BufRead, BufReader, Write}; +use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio}; + +use serde_json::{json, Value}; + +struct AgentProcess { + child: Child, + stdin: ChildStdin, + stdout: BufReader, + next_id: u64, +} + +impl AgentProcess { + fn spawn() -> Self { + let mut child = Command::new(env!("CARGO_BIN_EXE_dbx-tdengine-driver")) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + .expect("start TDengine agent"); + let stdin = child.stdin.take().expect("agent stdin"); + let mut stdout = BufReader::new(child.stdout.take().expect("agent stdout")); + let mut ready = String::new(); + stdout.read_line(&mut ready).expect("read ready line"); + assert_eq!(serde_json::from_str::(&ready).unwrap(), json!({"ready": true})); + Self { child, stdin, stdout, next_id: 1 } + } + + fn call(&mut self, method: &str, params: Value) -> Value { + let id = self.next_id; + self.next_id += 1; + let request = json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params}); + writeln!(self.stdin, "{request}").expect("write request"); + self.stdin.flush().expect("flush request"); + let mut line = String::new(); + self.stdout.read_line(&mut line).expect("read response"); + let response: Value = serde_json::from_str(&line).expect("valid JSON response"); + assert_eq!(response["id"], id, "unexpected response: {response}"); + if let Some(error) = response.get("error") { + panic!("{method} failed: {error}"); + } + response["result"].clone() + } + + fn shutdown(mut self) { + assert_eq!(self.call("shutdown", json!({})), json!({"ok": true})); + drop(self.stdin); + assert!(self.child.wait().expect("wait for agent").success()); + } +} + +fn integration_params() -> Option { + if env::var("TDENGINE_INTEGRATION").ok().as_deref() != Some("1") { + return None; + } + Some(json!({ + "host": env::var("TDENGINE_TEST_HOST").unwrap_or_else(|_| "127.0.0.1".into()), + "port": env::var("TDENGINE_TEST_PORT").ok().and_then(|value| value.parse::().ok()).unwrap_or(6041), + "username": env::var("TDENGINE_TEST_USERNAME").unwrap_or_else(|_| "root".into()), + "password": env::var("TDENGINE_TEST_PASSWORD").unwrap_or_else(|_| "taosdata".into()), + "connect_timeout_secs": 20, + })) +} + +#[test] +fn tdengine_websocket_live_compatibility() { + let Some(connect_params) = integration_params() else { + return; + }; + let database = format!("dbx_rust_live_{}", std::process::id()); + let session_id = "tdengine-live"; + let mut agent = AgentProcess::spawn(); + + let tested = agent.call("test_connection", connect_params.clone()); + assert_eq!(tested["ok"], true); + assert_eq!(tested["databaseInfo"]["productName"], "TDengine"); + assert!(tested["databaseInfo"]["productVersion"].as_str().is_some_and(|version| !version.is_empty())); + + let mut open_params = connect_params.clone(); + open_params["agentSessionId"] = json!(session_id); + assert_eq!(agent.call("open_session", open_params), json!({"ok": true})); + + agent.call( + "execute_batch", + json!({ + "agentSessionId": session_id, + "statements": [ + format!("CREATE DATABASE IF NOT EXISTS {database}"), + format!("CREATE STABLE IF NOT EXISTS {database}.meters (ts TIMESTAMP, current FLOAT, voltage INT, active BOOL, note NCHAR(32)) TAGS (location NCHAR(64), group_id INT)"), + format!("CREATE TABLE IF NOT EXISTS {database}.d1001 USING {database}.meters TAGS ('California.SanFrancisco', 2)"), + format!("INSERT INTO {database}.d1001 VALUES ('2026-08-04 10:00:00.001', 10.1, 220, true, 'first') ('2026-08-04 10:00:00.002', 10.2, 221, false, 'second') ('2026-08-04 10:00:00.003', 10.3, 222, true, 'third')") + ] + }), + ); + + let host = connect_params["host"].as_str().unwrap(); + let host = if host.contains(':') { format!("[{host}]") } else { host.to_string() }; + let port = connect_params["port"].as_u64().unwrap(); + let mut database_connect_params = connect_params.clone(); + database_connect_params["connection_string"] = json!(format!("ws://{host}:{port}/{database}")); + database_connect_params["database"] = json!(""); + let database_connection = agent.call("test_connection", database_connect_params); + assert_eq!(database_connection["databaseInfo"]["currentDatabase"], database); + + let databases = agent.call("list_databases", json!({"agentSessionId": session_id})); + assert!(databases.as_array().unwrap().iter().any(|item| item["name"] == database)); + + let tables = agent.call( + "list_tables", + json!({ + "agentSessionId": session_id, + "schema": database, + "filter": "", + "limit": 100, + "offset": 0, + "object_types": [] + }), + ); + let tables = tables.as_array().unwrap(); + assert!(tables.iter().any(|table| table["name"] == "meters" && table["table_type"] == "STABLE")); + assert!(tables.iter().any(|table| table["name"] == "d1001" && table["parent_name"] == "meters")); + + let columns = + agent.call("get_columns", json!({"agentSessionId": session_id, "schema": database, "table": "meters"})); + let columns = columns.as_array().unwrap(); + assert!(columns.iter().any(|column| column["name"] == "ts" && column["is_primary_key"] == true)); + assert!(columns.iter().any(|column| column["name"] == "location" && column["comment"] == "TAG")); + + let query = agent.call( + "execute_query", + json!({ + "agentSessionId": session_id, + "schema": database, + "sql": "SELECT * FROM d1001 ORDER BY ts", + "maxRows": 2, + "fetchSize": 100, + "timeoutSecs": 10 + }), + ); + assert_eq!(query["rows"].as_array().unwrap().len(), 2); + assert_eq!(query["truncated"], true); + assert_eq!(query["rows"][0][0], "2026-08-04 10:00:00.001"); + let connection_info = agent.call("connection_info", json!({"agentSessionId": session_id})); + assert_eq!(connection_info["databaseInfo"]["currentDatabase"], database); + + let first_page = agent.call( + "execute_query_page", + json!({ + "agentSessionId": session_id, + "schema": database, + "sql": "SELECT * FROM d1001 ORDER BY ts", + "maxRows": 2, + "pageSize": 1, + "timeoutSecs": 10 + }), + ); + assert_eq!(first_page["has_more"], true); + assert_eq!(first_page["truncated"], false); + let query_session_id = first_page["session_id"].as_str().expect("paged query session"); + let final_page = agent.call( + "fetch_query_page", + json!({ + "agentSessionId": session_id, + "sessionId": query_session_id, + "pageSize": 1, + "timeoutSecs": 10 + }), + ); + assert_eq!(final_page["rows"].as_array().unwrap().len(), 1); + assert_eq!(final_page["has_more"], false); + assert_eq!(final_page["truncated"], true); + + let exact_limit = agent.call( + "execute_query", + json!({ + "agentSessionId": session_id, + "schema": database, + "sql": "SELECT * FROM d1001 WHERE voltage <= 221 ORDER BY ts", + "maxRows": 2, + "timeoutSecs": 10 + }), + ); + assert_eq!(exact_limit["rows"].as_array().unwrap().len(), 2); + assert_eq!(exact_limit["truncated"], false); + + let completion = agent.call( + "completion_assistant_search_v1", + json!({ + "agentSessionId": session_id, + "schema": database, + "object_kinds": ["table"], + "mask": "met", + "match_mode": "prefix", + "max_results": 10 + }), + ); + assert!(completion["candidates"].as_array().unwrap().iter().any(|candidate| candidate["name"] == "meters")); + + let ddl = agent.call("get_table_ddl", json!({"agentSessionId": session_id, "schema": database, "table": "meters"})); + assert!(ddl.as_str().unwrap().to_ascii_uppercase().contains("CREATE")); + + let source = agent.call( + "get_object_source", + json!({ + "agentSessionId": session_id, + "schema": database, + "name": "meters", + "object_type": "STABLE" + }), + ); + assert!(source["source"].as_str().unwrap().to_ascii_uppercase().contains("CREATE")); + + agent.call( + "execute_batch", + json!({"agentSessionId": session_id, "statements": [format!("DROP DATABASE IF EXISTS {database}")]}), + ); + agent.shutdown(); +} diff --git a/agents/drivers/tdengine/tests/protocol.rs b/agents/drivers/tdengine/tests/protocol.rs new file mode 100644 index 0000000000..692d7c64f2 --- /dev/null +++ b/agents/drivers/tdengine/tests/protocol.rs @@ -0,0 +1,91 @@ +use std::io::{BufRead, BufReader, Write}; +use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio}; + +use serde_json::{json, Value}; + +struct AgentProcess { + child: Child, + stdin: ChildStdin, + stdout: BufReader, +} + +impl AgentProcess { + fn spawn() -> Self { + let mut child = Command::new(env!("CARGO_BIN_EXE_dbx-tdengine-driver")) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + .expect("start TDengine agent"); + let stdin = child.stdin.take().expect("agent stdin"); + let mut stdout = BufReader::new(child.stdout.take().expect("agent stdout")); + let mut ready = String::new(); + stdout.read_line(&mut ready).expect("read ready line"); + assert_eq!(serde_json::from_str::(&ready).unwrap(), json!({"ready": true})); + Self { child, stdin, stdout } + } + + fn send(&mut self, request: Value) -> Value { + writeln!(self.stdin, "{request}").expect("write request"); + self.stdin.flush().expect("flush request"); + self.read_response() + } + + fn send_raw(&mut self, request: &str) -> Value { + writeln!(self.stdin, "{request}").expect("write raw request"); + self.stdin.flush().expect("flush raw request"); + self.read_response() + } + + fn read_response(&mut self) -> Value { + let mut line = String::new(); + self.stdout.read_line(&mut line).expect("read response"); + assert!(!line.is_empty(), "agent exited before responding"); + serde_json::from_str(&line).expect("valid JSON response") + } + + fn shutdown(mut self) { + let response = self.send(json!({"jsonrpc": "2.0", "id": 99, "method": "shutdown", "params": {}})); + assert_eq!(response["result"], json!({"ok": true})); + drop(self.stdin); + assert!(self.child.wait().expect("wait for agent").success()); + } +} + +#[test] +fn stdio_protocol_reports_ready_handshake_and_structured_errors() { + let mut agent = AgentProcess::spawn(); + + let handshake = agent.send(json!({"jsonrpc": "2.0", "id": 7, "method": "handshake", "params": {}})); + assert_eq!(handshake["jsonrpc"], "2.0"); + assert_eq!(handshake["id"], 7); + assert_eq!(handshake["result"]["protocolVersion"], 2); + assert!(handshake["result"]["capabilities"] + .as_array() + .unwrap() + .iter() + .any(|capability| capability == "structured_error_v1")); + + let unknown = agent.send(json!({"jsonrpc": "2.0", "id": 8, "method": "not_a_method", "params": {}})); + assert!(unknown["error"]["message"].as_str().unwrap().contains("unknown method")); + assert_eq!(unknown["error"]["data"]["category"], "protocol"); + assert_eq!(unknown["error"]["data"]["stage"], "execute"); + + let malformed = agent.send_raw("{"); + assert!(malformed["id"].is_null()); + assert_eq!(malformed["error"]["data"]["category"], "protocol"); + assert_eq!(malformed["error"]["data"]["stage"], "request"); + assert_eq!(malformed["error"]["data"]["operationOutcome"], "not_started"); + + let invalid_connect = agent.send(json!({ + "jsonrpc": "2.0", + "id": 9, + "method": "test_connection", + "params": {"connection_string": "postgres://localhost/example"} + })); + assert_eq!(invalid_connect["error"]["data"]["category"], "connection"); + assert_eq!(invalid_connect["error"]["data"]["stage"], "connect"); + assert_eq!(invalid_connect["error"]["data"]["operationOutcome"], "not_started"); + + agent.shutdown(); +} diff --git a/agents/metadata-constraint-coverage.tsv b/agents/metadata-constraint-coverage.tsv index 55ee6f127a..b157fb3674 100644 --- a/agents/metadata-constraint-coverage.tsv +++ b/agents/metadata-constraint-coverage.tsv @@ -20,6 +20,6 @@ oracle-go native-pushdown native-go Uses Oracle-compatible metadata SQL with typ snowflake native-pushdown java-sql Uses INFORMATION_SCHEMA with type, filter, stable order, and literal LIMIT/OFFSET. spark intentional-fallback java-show-metadata Uses SHOW TABLES/SHOW DATABASES; stable server-side fuzzy paging is not portable, common constraints filter locally. sundb native-pushdown java-sql Uses information_schema with type, filter, stable order, and LIMIT/OFFSET. -tdengine intentional-fallback java-show-metadata Uses SHOW STABLES/TABLES; stable server-side fuzzy paging is not portable, common constraints filter locally. +tdengine shared-fallback native-rust Uses SHOW STABLES/TABLES with information_schema parent enrichment, then applies stable filtering and paging in the native agent. trino native-pushdown java-sql Uses information_schema.tables with type, filter, stable order, and literal LIMIT/OFFSET. xugu native-pushdown native-go Uses ALL_TABLES/ALL_VIEWS metadata with type, filter, stable order, and ROWNUM paging. diff --git a/agents/scripts/driver_release_packages_test.py b/agents/scripts/driver_release_packages_test.py index 51d5185d9a..dd95f48e90 100644 --- a/agents/scripts/driver_release_packages_test.py +++ b/agents/scripts/driver_release_packages_test.py @@ -26,6 +26,8 @@ class DriverReleasePackagesTest(unittest.TestCase): rabbitmq_source.write_bytes(b"\x7fELFtest-rabbitmq-agent") cassandra_source = release_dir / "dbx-agent-cassandra-linux-x64" cassandra_source.write_bytes(b"\x7fELFtest-cassandra-agent") + tdengine_source = release_dir / "dbx-agent-tdengine-windows-aarch64.exe" + tdengine_source.write_bytes(b"MZtest-tdengine-agent") java_source = release_dir / "dbx-agent-h2.jar" java_source.write_bytes(b"test-jar") versions = { @@ -37,6 +39,7 @@ class DriverReleasePackagesTest(unittest.TestCase): "duckdb": "0.1.0", "rabbitmq": "0.1.0", "cassandra": "0.1.37", + "tdengine": "0.1.0", } renamed = version_agent_artifacts(release_dir, versions) @@ -46,7 +49,19 @@ class DriverReleasePackagesTest(unittest.TestCase): versioned_duckdb = release_dir / "dbx-agent-duckdb-0.1.0-macos-aarch64" versioned_rabbitmq = release_dir / "dbx-agent-rabbitmq-0.1.0-linux-x64" versioned_cassandra = release_dir / "dbx-agent-cassandra-0.1.37-linux-x64" - self.assertEqual(renamed, [versioned_java, versioned_cassandra, versioned_native, versioned_vastbase, versioned_duckdb, versioned_rabbitmq]) + versioned_tdengine = release_dir / "dbx-agent-tdengine-0.1.0-windows-aarch64.exe" + self.assertEqual( + renamed, + [ + versioned_java, + versioned_cassandra, + versioned_native, + versioned_vastbase, + versioned_duckdb, + versioned_rabbitmq, + versioned_tdengine, + ], + ) registry = { "jres": {"21": {"version": "21", "platforms": {}}}, @@ -123,6 +138,19 @@ class DriverReleasePackagesTest(unittest.TestCase): } }, }, + "tdengine": { + "version": "0.1.0", + "label": "TDengine", + "min_app_version": "0.6.0", + "jre": "21", + "jar": {"url": "https://example.com/legacy-placeholder.jar", "size": 0}, + "native": { + "windows-aarch64": { + "url": f"https://example.com/{versioned_tdengine.name}", + "size": versioned_tdengine.stat().st_size, + } + }, + }, }, } (release_dir / "agent-registry.json").write_text(json.dumps(registry), encoding="utf-8") @@ -138,6 +166,7 @@ class DriverReleasePackagesTest(unittest.TestCase): release_dir / "dbx-agent-vastbase-0.1.37-linux-x64.tar.zst", release_dir / "dbx-agent-duckdb-0.1.0-macos-aarch64.tar.zst", release_dir / "dbx-agent-rabbitmq-0.1.0-linux-x64.tar.zst", + release_dir / "dbx-agent-tdengine-0.1.0-windows-aarch64.tar.zst", ], ) package_cases = [ @@ -147,6 +176,7 @@ class DriverReleasePackagesTest(unittest.TestCase): (outputs[3], "vastbase", versioned_vastbase, "native", "linux-x64"), (outputs[4], "duckdb", versioned_duckdb, "native", "macos-aarch64"), (outputs[5], "rabbitmq", versioned_rabbitmq, "native", "linux-x64"), + (outputs[6], "tdengine", versioned_tdengine, "native", "windows-aarch64"), ] for output, driver_name, source, artifact_type, platform in package_cases: tar_bytes = subprocess.run( @@ -176,6 +206,7 @@ class DriverReleasePackagesTest(unittest.TestCase): (final_registry["drivers"]["vastbase"]["native"]["linux-x64"], outputs[3]), (final_registry["drivers"]["duckdb"]["native"]["macos-aarch64"], outputs[4]), (final_registry["drivers"]["rabbitmq"]["native"]["linux-x64"], outputs[5]), + (final_registry["drivers"]["tdengine"]["native"]["windows-aarch64"], outputs[6]), ] for artifact, output in release_artifacts: self.assertEqual(artifact["url"], f"https://example.com/{output.name}") @@ -184,7 +215,18 @@ class DriverReleasePackagesTest(unittest.TestCase): self.assertEqual(len(artifact["sha256"]), 64) removed = remove_raw_driver_artifacts(release_dir) - self.assertEqual(removed, [versioned_cassandra, versioned_duckdb, versioned_java, versioned_native, versioned_rabbitmq, versioned_vastbase]) + self.assertEqual( + removed, + [ + versioned_cassandra, + versioned_duckdb, + versioned_java, + versioned_native, + versioned_rabbitmq, + versioned_tdengine, + versioned_vastbase, + ], + ) self.assertTrue(all(output.is_file() for output in outputs)) def test_full_offline_bundle_includes_supported_windows_artifacts(self) -> None: diff --git a/agents/scripts/validate_agents.py b/agents/scripts/validate_agents.py index 4d713a43fc..4e31c093b1 100644 --- a/agents/scripts/validate_agents.py +++ b/agents/scripts/validate_agents.py @@ -18,6 +18,7 @@ NATIVE_ONLY_AGENT_MODULES = { "oracle": "drivers/oracle-go", "kingbase": "drivers/kingbase-go", "vastbase": "drivers/vastbase-go", + "tdengine": "drivers/tdengine", "xugu": "drivers/xugu", "rabbitmq": "drivers/rabbitmq", } @@ -27,7 +28,6 @@ JDBC_ARCHITECTURE_ALLOWLIST = { "access": "shared lifecycle with a test-only non-creating Access URL", "dameng": "shared lifecycle with protocol-safe driver loading and native explain access", "informix": "shared lifecycle with contextual connection error reporting", - "tdengine": "shared lifecycle with WebSocket-to-REST transport fallback", } APPROVED_JDBC_BASES = { "AbstractJdbcAgent", diff --git a/agents/scripts/validate_agents_test.py b/agents/scripts/validate_agents_test.py index 318287fc68..9599911739 100644 --- a/agents/scripts/validate_agents_test.py +++ b/agents/scripts/validate_agents_test.py @@ -136,10 +136,19 @@ class ValidateAgentsTest(unittest.TestCase): "include(*(infrastructureModules + driverModules))\n", encoding="utf-8", ) - for driver in ("cassandra-go", "oracle-go", "kingbase-go", "vastbase-go", "xugu", "duckdb", "rabbitmq"): + for driver in ( + "cassandra-go", + "oracle-go", + "kingbase-go", + "vastbase-go", + "xugu", + "duckdb", + "rabbitmq", + "tdengine", + ): (root / "drivers" / driver).mkdir(parents=True) (root / "versions.json").write_text( - json.dumps({"h2": "0.1.0", "cassandra": "0.1.0", "oracle": "0.1.0", "kingbase": "0.1.0", "vastbase": "0.1.0", "xugu": "0.1.0", "rabbitmq": "0.1.0"}), + json.dumps({"h2": "0.1.0", "cassandra": "0.1.0", "oracle": "0.1.0", "kingbase": "0.1.0", "vastbase": "0.1.0", "xugu": "0.1.0", "rabbitmq": "0.1.0", "tdengine": "0.1.0"}), encoding="utf-8", ) diff --git a/agents/scripts/version_agent_artifacts.py b/agents/scripts/version_agent_artifacts.py index 6831a66a33..b15e45470e 100644 --- a/agents/scripts/version_agent_artifacts.py +++ b/agents/scripts/version_agent_artifacts.py @@ -4,7 +4,7 @@ import json from pathlib import Path -NATIVE_DRIVERS = ("cassandra", "oracle", "xugu", "kingbase", "vastbase", "duckdb", "rabbitmq") +NATIVE_DRIVERS = ("cassandra", "oracle", "xugu", "kingbase", "vastbase", "duckdb", "rabbitmq", "tdengine") PLATFORMS = ( "macos-aarch64", "macos-x64", diff --git a/agents/settings.gradle b/agents/settings.gradle index 6b55cb6748..bcd9ef8437 100644 --- a/agents/settings.gradle +++ b/agents/settings.gradle @@ -5,7 +5,7 @@ def driverModules = [ 'access', 'dameng', 'goldendb', 'databend', 'databricks', 'saphana', 'teradata', 'vertica', 'firebird', 'exasol', 'oceanbase-oracle', 'gbase8a', 'gbase8s', 'bigquery', 'kylin', 'sundb', 'h2', 'h2-legacy', 'snowflake', 'trino', 'hive', 'spark', - 'db2', 'informix', 'neo4j', 'mongodb', 'highgo', 'uxdb', 'tdengine', 'yashandb', 'oscar', + 'db2', 'informix', 'neo4j', 'mongodb', 'highgo', 'uxdb', 'yashandb', 'oscar', 'iris', 'iotdb', 'etcd', 'zookeeper', 'kafka', 'rocketmq', 'sqlserver-legacy' ]