This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/doris-cli.git

commit a0c659621f6f554551a70a1403ed6f98a156701d
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Fri May 29 16:44:27 2026 +0800

    test: add end-to-end test harness for doriscli
    
    Black-box harness that drives the built `doriscli` binary against a real,
    already-deployed Doris cluster and reports pass/fail/skip per command.
    
    - start-testing.sh: builds the binary, probes connectivity, seeds a 
throwaway
      doriscli_selftest database, runs the suites, drops the database, and 
prints a
      summary; exits non-zero only when a test fails.
    - Suites: cli (offline argument contract), auth (config-file mode + 
stateless
      mode), sql, tablet, profile — plus the offline `cargo test` unit suite.
    - SKIP state for cluster-side preconditions (FE HTTP profile API, 
audit_log) so
      optional features are not reported as failures.
    - Connection from tests/e2e/cluster.env or CLI flags; assertions are 
jq-based.
    
    Also ignore /target/ (was untracked) plus the harness's local cluster.env 
and
    results/ logs.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---
 .gitignore                    |   7 ++
 start-testing.sh              | 260 ++++++++++++++++++++++++++++++++++++++++++
 tests/e2e/README.md           | 128 +++++++++++++++++++++
 tests/e2e/cluster.env.example |  24 ++++
 tests/e2e/data.sh             | 128 +++++++++++++++++++++
 tests/e2e/lib.sh              | 207 +++++++++++++++++++++++++++++++++
 tests/e2e/suite_auth.sh       |  96 ++++++++++++++++
 tests/e2e/suite_cli.sh        |  36 ++++++
 tests/e2e/suite_profile.sh    |  96 ++++++++++++++++
 tests/e2e/suite_sql.sh        |  68 +++++++++++
 tests/e2e/suite_tablet.sh     |  53 +++++++++
 11 files changed, 1103 insertions(+)

diff --git a/.gitignore b/.gitignore
index e43b0f9..f967642 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,8 @@
 .DS_Store
+
+# Rust build artifacts
+/target/
+
+# e2e test harness: local connection config + run logs
+/tests/e2e/cluster.env
+/tests/e2e/results/
diff --git a/start-testing.sh b/start-testing.sh
new file mode 100755
index 0000000..f2bfcb8
--- /dev/null
+++ b/start-testing.sh
@@ -0,0 +1,260 @@
+#!/usr/bin/env bash
+# start-testing.sh — end-to-end test runner for the doriscli binary.
+#
+# Point it at a deployed Doris cluster and it builds doriscli, exercises every
+# command against the cluster, and prints which tests passed / failed / 
skipped.
+#
+#   ./start-testing.sh --host fe.example.com --port 9030 --http-port 8030 \
+#                      --user root --password 'secret'
+#
+# or put the connection in tests/e2e/cluster.env (see cluster.env.example) and 
run:
+#
+#   ./start-testing.sh
+#
+# Exit code is 0 only when nothing FAILED (skips do not fail the run).
+# Run `./start-testing.sh --help` for all options.
+
+# Re-exec under bash if invoked as `sh start-testing.sh` on a system where 
/bin/sh
+# is not bash (the harness uses bash arrays/locals).
+if [ -z "${BASH_VERSION:-}" ]; then exec bash "$0" "$@"; fi
+
+set -o pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+REPO_ROOT="$SCRIPT_DIR"
+E2E_DIR="$SCRIPT_DIR/tests/e2e"
+
+# ---- defaults ------------------------------------------------------------
+RUN_UNIT=1
+KEEP_DB=0
+NO_BUILD=0
+BUILD_PROFILE="debug"
+LIST_ONLY=0
+ALL_SUITES="cli auth sql tablet profile"
+ONLY_SUITES=""
+CONFIG_FILE="$E2E_DIR/cluster.env"
+CONFIG_FILE_EXPLICIT=0
+
+usage() {
+  cat <<'EOF'
+Usage: ./start-testing.sh [connection options] [runner options]
+
+Connection (overrides tests/e2e/cluster.env; can also come from that file):
+  --host <h>          FE host (required, unless set in cluster.env)
+  --port <n>          MySQL/query port            (default 9030)
+  --http-port <n>     FE HTTP port                (default 8030; cloud often 
8080)
+  --user <u>          MySQL user                  (default root)
+  --password <p>      MySQL password              (default empty)
+  --init-sql <sql>    Run after connect, e.g. 'USE @<compute_group>' for cloud
+  --socks5 <u:p@h:p>  Route through a SOCKS5 proxy (BYOC)
+  --db <name>         Self-test database name     (default doriscli_selftest)
+  --rows <n>          Rows to load into events    (default 2000)
+
+Runner:
+  --config <file>     Connection file to source   (default 
tests/e2e/cluster.env)
+  --bin <path>        Use a prebuilt doriscli instead of building
+  --release           Build the release binary instead of debug
+  --no-build          Do not build; use an existing target/ binary or --bin
+  --no-unit           Skip the offline `cargo test` unit suite
+  --only "<suites>"   Run only these suites (space/comma list): cli auth sql 
tablet profile
+  --keep              Do not drop the self-test database afterwards
+  --list              List the suites and exit
+  -h, --help          Show this help
+
+Examples:
+  ./start-testing.sh --host 127.0.0.1 --user root --password ''
+  ./start-testing.sh --only "cli sql" --no-unit
+  ./start-testing.sh --host fe --http-port 8080 --init-sql 'USE @my_cg'   # 
cloud
+EOF
+}
+
+# ---- parse args ----------------------------------------------------------
+while [ $# -gt 0 ]; do
+  case "$1" in
+    --host)       FLAG_HOST="$2"; shift 2;;
+    --port)       FLAG_PORT="$2"; shift 2;;
+    --http-port)  FLAG_HTTP_PORT="$2"; shift 2;;
+    --user)       FLAG_USER="$2"; shift 2;;
+    --password)   FLAG_PASSWORD="$2"; shift 2;;
+    --init-sql)   FLAG_INIT_SQL="$2"; shift 2;;
+    --socks5)     FLAG_SOCKS5="$2"; shift 2;;
+    --db)         FLAG_DB="$2"; shift 2;;
+    --rows)       FLAG_ROWS="$2"; shift 2;;
+    --config)     CONFIG_FILE="$2"; CONFIG_FILE_EXPLICIT=1; shift 2;;
+    --bin)        BIN_OVERRIDE="$2"; shift 2;;
+    --release)    BUILD_PROFILE="release"; shift;;
+    --no-build)   NO_BUILD=1; shift;;
+    --no-unit)    RUN_UNIT=0; shift;;
+    --only)       ONLY_SUITES="$2"; shift 2;;
+    --keep)       KEEP_DB=1; shift;;
+    --list)       LIST_ONLY=1; shift;;
+    -h|--help)    usage; exit 0;;
+    *) echo "Unknown option: $1" >&2; echo; usage; exit 2;;
+  esac
+done
+
+if [ "$LIST_ONLY" = 1 ]; then
+  echo "Available suites: $ALL_SUITES"
+  echo "  cli      offline CLI contract (version/help/argument errors)"
+  echo "  auth     auth add/list/status/remove, use, stateless mode (needs 
cluster)"
+  echo "  sql      query execution, -f, --set, --no-cache, --profile, formats, 
errors"
+  echo "  tablet   model/bucket/health/detail (needs cluster + seeded data)"
+  echo "  profile  list/get/full/raw/diff/history (needs cluster; some 
auto-SKIP)"
+  echo "Plus an offline 'cargo test' unit suite unless --no-unit."
+  exit 0
+fi
+
+# ---- resolve connection (CLI flag > cluster.env > default) ---------------
+if [ -f "$CONFIG_FILE" ]; then
+  # shellcheck disable=SC1090
+  . "$CONFIG_FILE"
+elif [ "$CONFIG_FILE_EXPLICIT" = 1 ]; then
+  echo "Config file not found: $CONFIG_FILE" >&2; exit 2
+fi
+
+CFG_HOST="${FLAG_HOST:-${DORIS_TEST_HOST:-}}"
+CFG_PORT="${FLAG_PORT:-${DORIS_TEST_PORT:-9030}}"
+CFG_HTTP_PORT="${FLAG_HTTP_PORT:-${DORIS_TEST_HTTP_PORT:-8030}}"
+CFG_USER="${FLAG_USER:-${DORIS_TEST_USER:-root}}"
+CFG_PASSWORD="${FLAG_PASSWORD-${DORIS_TEST_PASSWORD-}}"
+CFG_INIT_SQL="${FLAG_INIT_SQL-${DORIS_TEST_INIT_SQL-}}"
+CFG_SOCKS5="${FLAG_SOCKS5-${DORIS_TEST_SOCKS5-}}"
+CFG_DB="${FLAG_DB:-${DORIS_TEST_DB:-doriscli_selftest}}"
+ROWS="${FLAG_ROWS:-${DORIS_TEST_ROWS:-2000}}"
+
+# Which suites, and do any of them require the cluster?
+SUITES="${ONLY_SUITES:-$ALL_SUITES}"
+SUITES="$(printf '%s' "$SUITES" | tr ',' ' ')"
+wants() { case " $SUITES " in *" $1 "*) return 0;; *) return 1;; esac; }
+
+needs_cluster=0
+for s in auth sql tablet profile; do wants "$s" && needs_cluster=1; done
+
+if [ "$needs_cluster" = 1 ] && [ -z "$CFG_HOST" ]; then
+  echo "No cluster connection provided." >&2
+  echo "Pass --host ... (see --help) or create $E2E_DIR/cluster.env" >&2
+  echo "from cluster.env.example. (Or run only the offline suite: --only cli)" 
>&2
+  exit 2
+fi
+
+# ---- preflight: dependencies + binary ------------------------------------
+command -v jq >/dev/null 2>&1 || { echo "Missing dependency: jq (install jq 
and retry)." >&2; exit 2; }
+
+if [ -n "${BIN_OVERRIDE:-}" ]; then
+  BIN="$BIN_OVERRIDE"
+else
+  REL="$REPO_ROOT/target/release/doriscli"
+  DBG="$REPO_ROOT/target/debug/doriscli"
+  if [ "$NO_BUILD" = 1 ]; then
+    if   [ -x "$REL" ]; then BIN="$REL"
+    elif [ -x "$DBG" ]; then BIN="$DBG"
+    else echo "--no-build set but no binary in target/. Build it or pass 
--bin." >&2; exit 2; fi
+  else
+    command -v cargo >/dev/null 2>&1 || { echo "cargo not found; pass --bin 
<path> or install Rust." >&2; exit 2; }
+    echo "Building doriscli ($BUILD_PROFILE) — first build may take a few 
minutes ..."
+    if [ "$BUILD_PROFILE" = "release" ]; then
+      cargo build --release --manifest-path "$REPO_ROOT/Cargo.toml" || { echo 
"build failed" >&2; exit 2; }
+      BIN="$REL"
+    else
+      cargo build --manifest-path "$REPO_ROOT/Cargo.toml" || { echo "build 
failed" >&2; exit 2; }
+      BIN="$DBG"
+    fi
+  fi
+fi
+[ -x "$BIN" ] || { echo "doriscli binary not found/executable: $BIN" >&2; exit 
2; }
+
+# ---- working dirs + logging ----------------------------------------------
+RESULTS_DIR="$E2E_DIR/results"
+mkdir -p "$RESULTS_DIR"
+TS="$(date +%Y%m%d-%H%M%S)"
+LOG_FILE="$RESULTS_DIR/run-$TS.log"
+WORKDIR="$(mktemp -d "${TMPDIR:-/tmp}/doriscli-e2e.XXXXXX")"
+ISOLATED_HOME="$WORKDIR/home"
+mkdir -p "$ISOLATED_HOME"
+
+# Export for the helpers/suites.
+export BIN CFG_HOST CFG_PORT CFG_HTTP_PORT CFG_USER CFG_PASSWORD CFG_INIT_SQL 
CFG_SOCKS5 CFG_DB ROWS
+export LOG_FILE WORKDIR ISOLATED_HOME REPO_ROOT KEEP_DB HTTP_OK
+
+# ---- source helpers + suites ---------------------------------------------
+# shellcheck source=tests/e2e/lib.sh
+. "$E2E_DIR/lib.sh"
+# shellcheck source=tests/e2e/data.sh
+. "$E2E_DIR/data.sh"
+for f in cli auth sql tablet profile; do
+  # shellcheck disable=SC1090
+  . "$E2E_DIR/suite_$f.sh"
+done
+
+# Offline unit suite (defined here; needs cargo + source tree).
+suite_unit() {
+  suite_banner "cargo unit tests (offline)"
+  if ! command -v cargo >/dev/null 2>&1; then
+    record_skip "cargo test (unit)" "cargo not found"
+    return
+  fi
+  log "  running 'cargo test' (output in the run log) ..."
+  printf '\n===== cargo test =====\n' >>"$LOG_FILE"
+  if cargo test --manifest-path "$REPO_ROOT/Cargo.toml" >>"$LOG_FILE" 2>&1; 
then
+    record_pass "cargo test (unit)"
+  else
+    record_fail "cargo test (unit)" "see the 'cargo test' section in $LOG_FILE"
+  fi
+}
+
+# ---- teardown trap (drops the self-test DB, cleans temp) -----------------
+cleanup() {
+  type teardown_data >/dev/null 2>&1 && teardown_data
+  rm -rf "$WORKDIR" 2>/dev/null || true
+}
+trap cleanup EXIT
+trap 'exit 130' INT TERM
+
+# ---- header --------------------------------------------------------------
+HTTP_OK=0
+log "${C_BOLD}doriscli end-to-end test run${C_RESET}  ($TS)"
+log "  binary : $BIN"
+log "  version: $("$BIN" --version 2>/dev/null)"
+unit_note=""; [ "$RUN_UNIT" = 1 ] && unit_note="  (+cargo unit)"
+log "  suites : $SUITES$unit_note"
+log "  log    : $LOG_FILE"
+if [ "$needs_cluster" = 1 ]; then
+  log "  cluster: $CFG_USER@$CFG_HOST:$CFG_PORT (http :$CFG_HTTP_PORT)  
db=$CFG_DB"
+  [ -n "$CFG_INIT_SQL" ] && log "  init-sql: $CFG_INIT_SQL"
+fi
+
+# ---- offline suites first (fast feedback, no cluster) --------------------
+[ "$RUN_UNIT" = 1 ] && suite_unit
+wants cli && suite_cli
+
+# ---- connectivity probe (gates the cluster suites; sets HTTP_OK) ---------
+if [ "$needs_cluster" = 1 ]; then
+  suite_banner "Connectivity probe"
+  DCLI_STATELESS=1 _run_dcli --format json auth status
+  MS="$(jget '.mysql_status')"
+  if [ "$MS" != "connected" ]; then
+    log "${C_RED}${C_BOLD}Cannot connect to the cluster — aborting cluster 
suites.${C_RESET}"
+    log "  mysql_status: $MS"
+    log "  stderr: $(_oneline "$ERR")"
+    log "  Verify host/port/user/password. For a cloud/storage-compute 
cluster, set"
+    log "  DORIS_TEST_INIT_SQL='USE @<compute_group>' and --http-port (often 
8080)."
+    print_summary
+    exit 1
+  fi
+  [ "$(jget '.http_status')" = "connected" ] && HTTP_OK=1
+  log "  ${C_GREEN}connected${C_RESET}  version=$(jget '.doris_version')  
http_status=$(jget '.http_status')  backends=$(jget '.backends|length')  
(HTTP_OK=$HTTP_OK)"
+fi
+
+# ---- cluster suites ------------------------------------------------------
+wants auth && suite_auth
+
+if wants sql || wants tablet || wants profile; then
+  setup_data
+fi
+wants sql     && suite_sql
+wants tablet  && suite_tablet
+wants profile && suite_profile
+
+# ---- summary + exit code -------------------------------------------------
+print_summary
+[ "$N_FAIL" -eq 0 ]
diff --git a/tests/e2e/README.md b/tests/e2e/README.md
new file mode 100644
index 0000000..96e065b
--- /dev/null
+++ b/tests/e2e/README.md
@@ -0,0 +1,128 @@
+# doriscli end-to-end test harness
+
+Black-box tests that drive the **built `doriscli` binary** against a **real,
+already-deployed Doris cluster** and report which commands pass, fail, or are
+skipped. Everything goes through `doriscli` itself — there is no separate MySQL
+client and no mocking — so a green run means the CLI genuinely works end to end
+against that cluster.
+
+## TL;DR
+
+```bash
+# from the repo root (doris-cli/)
+cp tests/e2e/cluster.env.example tests/e2e/cluster.env
+$EDITOR tests/e2e/cluster.env          # fill in host/port/user/password
+./start-testing.sh
+```
+
+or pass the connection on the command line:
+
+```bash
+./start-testing.sh --host fe.example.com --port 9030 --http-port 8030 \
+                   --user root --password 'secret'
+```
+
+The runner builds `doriscli`, probes connectivity, creates a throwaway
+`doriscli_selftest` database, runs every suite, **drops the database**, and
+prints a summary. Exit code is `0` only if nothing **failed** (skips don't fail
+the run). Full per-command output for the run is saved under
+`tests/e2e/results/run-<timestamp>.log`.
+
+Requirements: `bash`, `jq`, and either a Rust toolchain (to build) or a 
prebuilt
+binary via `--bin`.
+
+## Result semantics
+
+| Status | Meaning |
+|---|---|
+| **PASS** | The command behaved exactly as the contract requires. |
+| **FAIL** | The command misbehaved: wrong exit code, malformed JSON, or a 
missing/!= expected field. **These are the ones to look at.** |
+| **SKIP** | A *precondition of the cluster* (not a bug in the CLI) was 
absent, so the test couldn't run — e.g. the FE HTTP API isn't reachable, or 
`audit_log` isn't enabled. Reported separately; never fails the run. |
+
+The SKIP state exists because several `doriscli` features depend on optional
+cluster capabilities. Treating "audit_log disabled" as a CLI failure would cry
+wolf; treating it as a silent pass would hide untested surface. So it's its own
+bucket, and the summary lists exactly what was skipped and why.
+
+## What each suite covers
+
+Run a subset with `--only "<suites>"`; list them with `--list`.
+
+### `cargo test` (offline, unless `--no-unit`)
+The crate's in-tree unit tests — primarily the profile-text parsers
+(`section_parser`, `fragment_parser`, `operator_parser`, `value_parser`). No
+cluster needed.
+
+### `cli` — argument contract (offline)
+`--version` / `-V`, `--help` (usage + subcommand listing), and the error paths:
+`sql` with no query, unknown subcommand, `tablet` with no table, `profile` with
+no action, and an unknown flag. Verifies non-zero exit on misuse.
+
+### `auth` — connection management + stateless mode (needs cluster)
+Uses an **isolated `$HOME`**, so your real `~/.doris` is never touched.
+- `auth list` on an empty config → empty-list shape.
+- `auth add` → saves an env (first one becomes default); `auth list` reflects 
it.
+- `auth status` → **connects over MySQL**; asserts `.mysql_status == 
"connected"`
+  (the command always exits 0, so the field is the real connectivity check).
+- `use` / `use <name>` → show and switch the default env.
+- `auth add --mysql mysql://…` → URI parsing (skipped if the password isn't
+  URI-safe).
+- `auth remove` → deletes an env.
+- **Stateless mode** (`DORIS_HOST`+`DORIS_USER`): `auth add` is *refused*,
+  `auth status` still connects from env vars, and **no files are written to
+  disk** (verified against a pristine HOME).
+
+### `sql` — execution (needs cluster)
+The JSON envelope (`query_id`, `exec_time_ms`, `rows_returned`, `columns[]`,
+`rows[]`), type mapping (string vs number), `-f <file>`, `--set` (single and
+repeated), `--no-cache`, `--profile` (yields a `query_id`), `--format
+table`/`csv`, empty result sets, a `COUNT(*)` over the loaded data, and the
+error path (`SQL error:` on a bad reference, non-zero exit).
+
+### `tablet` — bucket / tablet analysis (needs cluster + seeded data)
+Overview: `model`, `bucket_type`, `bucket_key`, `bucket_count`, `sort_key`,
+`total_rows`; the `health.tablet_skew` summary; `columns[].ndv` (SKIP if column
+stats weren't collected). `UNIQUE` model detection on a second table.
+`--detail` (per-tablet + per-backend) and `--detail --partition` (narrowed to
+one partition). Negative: a missing table exits non-zero.
+
+### `profile` — query profiles (needs cluster; HTTP-gated parts auto-SKIP)
+`profile list` and `list --active` (arrays); `profile get <id>` summary (works
+via the SQL fallback even without HTTP); `get --full`, `get --raw`, and `diff`
+(**require the FE HTTP profile API → SKIP if `http_status != connected`**);
+`profile history` (**requires `__internal_schema.audit_log` → SKIP if 
absent**);
+and a negative case (unknown query id exits non-zero).
+
+## The seed data
+
+`setup_data` creates two tables in `doriscli_selftest` (DDL is mode-agnostic;
+`replication_num=1` works on self-hosted and is required in cloud mode):
+
+- `events` — `DUPLICATE` model, range-partitioned by `event_date` into two
+  partitions (Jan/Feb 2024), `DISTRIBUTED BY HASH(user_id) BUCKETS 4` → 8
+  tablets. Loaded with `--rows` (default 2000) rows via a generated `INSERT`
+  fed through `sql -f` (dates computed server-side by `DATE_ADD`, so no
+  dependency on any particular Doris version). `ANALYZE TABLE … WITH SYNC` runs
+  best-effort so `tablet`'s `columns[].ndv` is populated.
+- `dim_users` — `UNIQUE` model, 2 buckets, a handful of rows.
+
+The database is dropped on exit (even on Ctrl-C) unless you pass `--keep`.
+
+## Options
+
+See `./start-testing.sh --help`. Highlights: `--only`, `--no-unit`, `--keep`,
+`--release`, `--bin <path>`, `--no-build`, `--rows <n>`, `--config <file>`.
+
+## Notes on cluster prerequisites
+
+- **MySQL/query port** (default 9030) is required for everything.
+- **FE HTTP port** (default 8030; cloud often **8080**) enables `auth status`'s
+  HTTP probe and the `profile get --full/--raw` and `profile diff` paths.
+  Without it those tests SKIP rather than fail.
+- **Cloud / storage-compute**: set `DORIS_TEST_INIT_SQL='USE @<compute_group>'`
+  so queries run against a live compute group; otherwise setup fails early with
+  a hint.
+- The test user needs `CREATE`/`DROP`/`INSERT` on the self-test database (and
+  `SELECT` on `information_schema`). For full `profile` coverage the cluster
+  should retain profiles (`enable_profile` is set per-query by `--profile`) 
and,
+  for `profile history`, have the audit log enabled.
diff --git a/tests/e2e/cluster.env.example b/tests/e2e/cluster.env.example
new file mode 100644
index 0000000..4926be0
--- /dev/null
+++ b/tests/e2e/cluster.env.example
@@ -0,0 +1,24 @@
+# Connection for ./start-testing.sh
+#
+# Copy this file to  tests/e2e/cluster.env  and fill in your cluster.
+# (cluster.env is gitignored so your credentials never get committed.)
+# Any value here can be overridden by a command-line flag of the same name.
+
+DORIS_TEST_HOST=127.0.0.1
+DORIS_TEST_PORT=9030
+DORIS_TEST_HTTP_PORT=8030
+DORIS_TEST_USER=root
+DORIS_TEST_PASSWORD=
+
+# Self-test database (created and dropped by the run) + rows loaded into 
events.
+DORIS_TEST_DB=doriscli_selftest
+DORIS_TEST_ROWS=2000
+
+# --- Cloud / storage-compute clusters ---
+# Most cloud clusters require selecting a compute group before any query runs,
+# and expose the FE HTTP API on 8080 rather than 8030:
+# DORIS_TEST_INIT_SQL=USE @my_compute_group
+# DORIS_TEST_HTTP_PORT=8080
+
+# --- Reaching a bastioned / BYOC cluster through a SOCKS5 proxy ---
+# DORIS_TEST_SOCKS5=user:pass@proxy-host:1080
diff --git a/tests/e2e/data.sh b/tests/e2e/data.sh
new file mode 100644
index 0000000..66e4682
--- /dev/null
+++ b/tests/e2e/data.sh
@@ -0,0 +1,128 @@
+# shellcheck shell=bash
+# tests/e2e/data.sh — create / load / drop the self-test database.
+#
+# Everything runs THROUGH doriscli itself (no external mysql client), so 
loading
+# data also exercises `sql` (incl. the -f file path) against the real cluster.
+# All DDL is mode-agnostic (replication_num=1 works on self-hosted 
single/multi-BE
+# and is required in cloud / storage-compute mode).
+
+SETUP_DB_CREATED=0
+
+# Abort the whole run with an actionable hint. Teardown still fires via the 
trap.
+die_setup() {
+  log ""
+  log "${C_RED}${C_BOLD}SETUP FAILED${C_RESET} — ${1}"
+  log "  ${C_DIM}stderr: $(_oneline "$ERR")${C_RESET}"
+  log ""
+  log "Likely causes:"
+  log "  • Wrong host/port/user/password — check your cluster.env / flags."
+  log "  • Cloud (storage-compute) cluster needs a compute group selected 
first:"
+  log "      set  DORIS_TEST_INIT_SQL='USE @<your_compute_group>'  in 
cluster.env"
+  log "  • The user lacks CREATE / DROP / LOAD / INSERT privileges."
+  log "  • FE query port unreachable from this host."
+  exit 1
+}
+
+# Emit a single multi-row INSERT for the events table (no `numbers` TVF 
dependency;
+# dates are computed server-side by DATE_ADD so it works on any Doris version).
+_gen_events_sql() {
+  local db="$1" n="${2:-2000}"
+  awk -v db="$db" -v n="$n" 'BEGIN {
+    split("click,view,purchase,signup", t, ",");
+    printf "INSERT INTO `%s`.`events` 
(event_date,user_id,event_type,amount,detail) VALUES\n", db;
+    for (i = 0; i < n; i++) {
+      day = i % 59;            # 0..58 days -> spans Jan + Feb 2024 (both 
partitions)
+      uid = i % 1000;          # cardinality ~1000 for user_id
+      typ = t[(i % 4) + 1];
+      amt = (i % 100) + 0.5;
+      sep = (i == n - 1) ? ";" : ",";
+      printf "(DATE_ADD(\x272024-01-01\x27, INTERVAL %d 
DAY),%d,\x27%s\x27,%.2f,\x27detail_%d\x27)%s\n",
+             day, uid, typ, amt, i, sep;
+    }
+  }'
+}
+
+setup_data() {
+  suite_banner "SETUP  (database '$CFG_DB' on the target cluster)"
+  local db="$CFG_DB"
+
+  # Fresh start: drop a leftover db from a previous interrupted run.
+  _run_dcli --format json sql "DROP DATABASE IF EXISTS \`$db\`"
+  [ "$RC" -eq 0 ] || die_setup "could not DROP a pre-existing '$db' 
(connectivity / privileges?)"
+
+  _run_dcli --format json sql "CREATE DATABASE \`$db\`"
+  [ "$RC" -eq 0 ] || die_setup "CREATE DATABASE \`$db\` failed"
+  SETUP_DB_CREATED=1
+  log "  created database $db"
+
+  # --- events: DUPLICATE, range-partitioned, HASH bucketed (drives `tablet`) 
---
+  _run_dcli --format json sql "CREATE TABLE \`$db\`.\`events\` (
+      event_date DATE NOT NULL,
+      user_id    BIGINT NOT NULL,
+      event_type VARCHAR(32) NOT NULL,
+      amount     DECIMAL(10,2),
+      detail     STRING
+    )
+    DUPLICATE KEY(event_date, user_id)
+    PARTITION BY RANGE(event_date) (
+      PARTITION p20240101 VALUES [('2024-01-01'), ('2024-02-01')),
+      PARTITION p20240201 VALUES [('2024-02-01'), ('2024-03-01'))
+    )
+    DISTRIBUTED BY HASH(user_id) BUCKETS 4
+    PROPERTIES ('replication_num' = '1')"
+  [ "$RC" -eq 0 ] || die_setup "CREATE TABLE events failed"
+  log "  created table events (DUPLICATE, 2 partitions x 4 buckets)"
+
+  # --- dim_users: UNIQUE model (so `tablet` model detection is exercised 
twice) ---
+  _run_dcli --format json sql "CREATE TABLE \`$db\`.\`dim_users\` (
+      user_id BIGINT NOT NULL,
+      name    VARCHAR(64),
+      level   INT
+    )
+    UNIQUE KEY(user_id)
+    DISTRIBUTED BY HASH(user_id) BUCKETS 2
+    PROPERTIES ('replication_num' = '1')"
+  [ "$RC" -eq 0 ] || die_setup "CREATE TABLE dim_users failed"
+  log "  created table dim_users (UNIQUE, 2 buckets)"
+
+  # --- load events via a generated .sql file (exercises `sql -f`) ---
+  local events_sql="$WORKDIR/events_insert.sql"
+  _gen_events_sql "$db" "${ROWS:-2000}" >"$events_sql"
+  _run_dcli --format json sql -f "$events_sql"
+  [ "$RC" -eq 0 ] || die_setup "loading events via 'sql -f' failed"
+  log "  loaded ${ROWS:-2000} rows into events (via sql -f)"
+
+  # --- load dim_users (small inline INSERT) ---
+  _run_dcli --format json sql "INSERT INTO \`$db\`.\`dim_users\` 
(user_id,name,level) VALUES
+      (1,'alice',3),(2,'bob',1),(3,'carol',2),(4,'dave',5),(5,'erin',4),
+      (6,'frank',2),(7,'grace',1),(8,'heidi',3),(9,'ivan',2),(10,'judy',4)"
+  [ "$RC" -eq 0 ] || die_setup "loading dim_users failed"
+  log "  loaded 10 rows into dim_users"
+
+  # --- column stats so tablet's columns[].ndv is populated (best effort) ---
+  # ANALYZE may be async/unavailable on some versions; failure is not fatal —
+  # the tablet suite degrades the column-stats check to SKIP if stats are 
absent.
+  _run_dcli --format json sql "ANALYZE TABLE \`$db\`.\`events\` WITH SYNC"
+  if [ "$RC" -eq 0 ]; then
+    log "  ran ANALYZE TABLE events WITH SYNC (column stats collected)"
+  else
+    log "  ${C_YELLOW}note${C_RESET}: ANALYZE TABLE events did not succeed; 
columns[].ndv may be empty"
+  fi
+
+  log "${C_GREEN}  setup complete${C_RESET}"
+}
+
+teardown_data() {
+  # Called from the EXIT trap, so guard everything and never abort.
+  [ "${SETUP_DB_CREATED:-0}" = "1" ] || return 0
+  if [ "${KEEP_DB:-0}" = "1" ]; then
+    log ""
+    log "${C_YELLOW}--keep set: leaving database '$CFG_DB' in place.${C_RESET}"
+    return 0
+  fi
+  log ""
+  log "Teardown: dropping database '$CFG_DB' ..."
+  _run_dcli --format json sql "DROP DATABASE IF EXISTS \`$CFG_DB\`"
+  if [ "$RC" -eq 0 ]; then log "  dropped $CFG_DB"
+  else log "  ${C_YELLOW}warning${C_RESET}: could not drop $CFG_DB — drop it 
manually. ($(_oneline "$ERR"))"; fi
+}
diff --git a/tests/e2e/lib.sh b/tests/e2e/lib.sh
new file mode 100644
index 0000000..7043a64
--- /dev/null
+++ b/tests/e2e/lib.sh
@@ -0,0 +1,207 @@
+# shellcheck shell=bash
+# tests/e2e/lib.sh — shared helpers for the doriscli end-to-end test harness.
+#
+# Sourced by start-testing.sh (which runs under bash). Provides:
+#   - result tracking (PASS / FAIL / SKIP) and the final summary
+#   - _run_dcli: the single place that invokes the doriscli binary, in either
+#       * stateless mode  (DORIS_HOST/USER/... exported — no config files 
touched), or
+#       * file mode       (DORIS_* unset, isolated $HOME — exercises ~/.doris 
config)
+#   - assertion helpers built on jq: expect_json / expect_ok / expect_err / 
skip
+#
+# Design notes that the assertions depend on (verified against doriscli 
source):
+#   - Every command prints a bare JSON value to stdout and exits 0 on success,
+#     non-zero on error (error text on stderr).
+#   - `auth status` ALWAYS exits 0; real connectivity is in the .mysql_status 
field.
+#   - profile get --full/--raw and profile diff REQUIRE the FE HTTP API; the 
default
+#     `profile get` summary has a SQL fallback; `profile history` needs 
audit_log.
+#     Those are gated on HTTP_OK / detected at runtime and recorded as SKIP, 
not FAIL.
+
+# ---- result state --------------------------------------------------------
+N_PASS=0
+N_FAIL=0
+N_SKIP=0
+FAILED_NAMES=()
+SKIPPED_NAMES=()
+
+# Populated by _run_dcli on every call:
+OUT=""   # captured stdout
+ERR=""   # captured stderr
+RC=0     # exit code
+
+# ---- colors (disabled when not a tty or NO_COLOR set) --------------------
+if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
+  C_GREEN=$'\033[32m'; C_RED=$'\033[31m'; C_YELLOW=$'\033[33m'
+  C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'; C_RESET=$'\033[0m'
+else
+  C_GREEN=""; C_RED=""; C_YELLOW=""; C_BOLD=""; C_DIM=""; C_RESET=""
+fi
+
+# ---- logging -------------------------------------------------------------
+# log: to console AND the run log file. logfile_only: just the file.
+log()          { printf '%s\n' "$*"; printf '%s\n' "$*" >>"$LOG_FILE" 
2>/dev/null || true; }
+logfile_only() { printf '%s\n' "$*" >>"$LOG_FILE" 2>/dev/null || true; }
+
+# Collapse a (possibly multi-line) string to a single, truncated line for the
+# console summary. Full untruncated output always lives in $LOG_FILE.
+_oneline() {
+  printf '%s' "$1" | tr '\n\t' '  ' | cut -c1-240
+}
+
+# ---- result recorders ----------------------------------------------------
+record_pass() {
+  N_PASS=$((N_PASS + 1))
+  log "  ${C_GREEN}PASS${C_RESET} $1"
+}
+record_fail() {
+  N_FAIL=$((N_FAIL + 1))
+  FAILED_NAMES+=("$1")
+  log "  ${C_RED}FAIL${C_RESET} $1"
+  [ -n "$2" ] && log "       ${C_DIM}↳ $(_oneline "$2")${C_RESET}"
+}
+record_skip() {
+  N_SKIP=$((N_SKIP + 1))
+  SKIPPED_NAMES+=("$1")
+  log "  ${C_YELLOW}SKIP${C_RESET} $1"
+  [ -n "$2" ] && log "       ${C_DIM}↳ $(_oneline "$2")${C_RESET}"
+}
+
+# Print a section banner for a suite.
+suite_banner() {
+  log ""
+  log "${C_BOLD}━━ $1 ━━${C_RESET}"
+}
+
+# ---- the doriscli runner -------------------------------------------------
+# _run_dcli <args...>
+#   Runs $BIN with the configured global flags (--init-sql / --socks5) 
prepended,
+#   in stateless mode by default. Set DCLI_STATELESS=0 before calling to 
exercise
+#   the file-based config path (DORIS_* unset, $HOME isolated to 
$ISOLATED_HOME).
+#   Captures stdout->OUT, stderr->ERR, exit code->RC. Logs the invocation.
+_run_dcli() {
+  local globals=()
+  [ -n "${CFG_INIT_SQL:-}" ] && globals+=(--init-sql "$CFG_INIT_SQL")
+  [ -n "${CFG_SOCKS5:-}" ]   && globals+=(--socks5 "$CFG_SOCKS5")
+
+  local errf; errf="$(mktemp "${TMPDIR:-/tmp}/dcli-err.XXXXXX")"
+
+  if [ "${DCLI_STATELESS:-1}" = "1" ]; then
+    OUT=$(HOME="$ISOLATED_HOME" \
+          DORIS_HOST="$CFG_HOST" \
+          DORIS_PORT="$CFG_PORT" \
+          DORIS_HTTP_PORT="$CFG_HTTP_PORT" \
+          DORIS_USER="$CFG_USER" \
+          DORIS_PASSWORD="$CFG_PASSWORD" \
+          "$BIN" "${globals[@]}" "$@" 2>"$errf")
+    RC=$?
+  else
+    # File mode: scrub any DORIS_* the caller's shell may carry, so stateless
+    # mode is NOT triggered, and point HOME at the isolated config dir.
+    OUT=$(HOME="$ISOLATED_HOME" \
+          env -u DORIS_HOST -u DORIS_USER -u DORIS_PASSWORD \
+              -u DORIS_PORT -u DORIS_HTTP_PORT -u DORIS_ENV -u DORIS_INIT_SQL \
+          "$BIN" "${globals[@]}" "$@" 2>"$errf")
+    RC=$?
+  fi
+
+  ERR=$(cat "$errf" 2>/dev/null); rm -f "$errf"
+
+  {
+    printf '\n$ doriscli'
+    printf ' %q' "${globals[@]}" "$@"
+    printf '   [stateless=%s]\n' "${DCLI_STATELESS:-1}"
+    printf 'rc=%s\n--- stdout ---\n%s\n--- stderr ---\n%s\n' "$RC" "$OUT" 
"$ERR"
+  } >>"$LOG_FILE" 2>/dev/null || true
+}
+
+# ---- assertion helpers ---------------------------------------------------
+# All of these record exactly one PASS or FAIL (or SKIP) result.
+
+# expect_json "<name>" '<jq filter>' <doriscli args...>
+#   Appends --format json, expects exit 0 and a jq filter that evaluates 
truthy.
+#   Pass '' as the filter to only require valid JSON + exit 0.
+expect_json() {
+  local name="$1" filter="$2"; shift 2
+  _run_dcli --format json "$@"
+  if [ "$RC" -ne 0 ]; then
+    record_fail "$name" "exit=$RC; stderr: $ERR"
+    return
+  fi
+  if ! printf '%s' "$OUT" | jq -e . >/dev/null 2>&1; then
+    record_fail "$name" "stdout is not valid JSON: $OUT"
+    return
+  fi
+  if [ -n "$filter" ] && ! printf '%s' "$OUT" | jq -e "$filter" >/dev/null 
2>&1; then
+    record_fail "$name" "assertion failed [ $filter ]; got: $OUT"
+    return
+  fi
+  record_pass "$name"
+}
+
+# expect_ok "<name>" <doriscli args...>  — just requires exit 0 (raw, no 
--format).
+expect_ok() {
+  local name="$1"; shift
+  _run_dcli "$@"
+  if [ "$RC" -eq 0 ]; then record_pass "$name"
+  else record_fail "$name" "exit=$RC; stderr: $ERR"; fi
+}
+
+# expect_err "<name>" <doriscli args...>  — requires NON-zero exit.
+#   Optional: set EXPECT_ERR_MATCH to a substring required in stderr.
+expect_err() {
+  local name="$1"; shift
+  _run_dcli "$@"
+  if [ "$RC" -eq 0 ]; then
+    record_fail "$name" "expected non-zero exit, got 0; stdout: $OUT"
+    return
+  fi
+  if [ -n "${EXPECT_ERR_MATCH:-}" ] && ! printf '%s' "$ERR" | grep -qiF 
"$EXPECT_ERR_MATCH"; then
+    record_fail "$name" "exit ok but stderr missing '$EXPECT_ERR_MATCH': $ERR"
+    return
+  fi
+  record_pass "$name"
+}
+
+# expect_stdout_contains "<name>" "<substr>" <doriscli args...>
+#   Raw run (no --format), exit 0, and stdout contains substr (for --help, 
table/csv).
+expect_stdout_contains() {
+  local name="$1" needle="$2"; shift 2
+  _run_dcli "$@"
+  if [ "$RC" -ne 0 ]; then
+    record_fail "$name" "exit=$RC; stderr: $ERR"
+    return
+  fi
+  if printf '%s' "$OUT" | grep -qF "$needle"; then record_pass "$name"
+  else record_fail "$name" "stdout missing '$needle': $(_oneline "$OUT")"; fi
+}
+
+# skip "<name>" "<reason>" — record a SKIP (precondition absent, not a bug).
+skip() { record_skip "$1" "$2"; }
+
+# jget '<jq filter>' — echo a scalar pulled from the last OUT (raw -r). Empty 
on error.
+jget() { printf '%s' "$OUT" | jq -r "$1" 2>/dev/null; }
+
+# ---- final summary -------------------------------------------------------
+print_summary() {
+  local total=$((N_PASS + N_FAIL + N_SKIP))
+  log ""
+  log "${C_BOLD}══════════════════════ SUMMARY 
══════════════════════${C_RESET}"
+  log "  total: $total    ${C_GREEN}pass: $N_PASS${C_RESET}    ${C_RED}fail: 
$N_FAIL${C_RESET}    ${C_YELLOW}skip: $N_SKIP${C_RESET}"
+  if [ "$N_SKIP" -gt 0 ]; then
+    log ""
+    log "${C_YELLOW}Skipped (precondition not met on this cluster — not a 
failure):${C_RESET}"
+    local s; for s in "${SKIPPED_NAMES[@]}"; do log "  - $s"; done
+  fi
+  if [ "$N_FAIL" -gt 0 ]; then
+    log ""
+    log "${C_RED}${C_BOLD}Failed tests:${C_RESET}"
+    local f; for f in "${FAILED_NAMES[@]}"; do log "  ${C_RED}✗${C_RESET} $f"; 
done
+    log ""
+    log "Full command output for every test is in:"
+    log "  $LOG_FILE"
+    log "${C_RED}${C_BOLD}RESULT: FAIL ($N_FAIL failing)${C_RESET}"
+  else
+    log ""
+    log "Full run log: $LOG_FILE"
+    log "${C_GREEN}${C_BOLD}RESULT: PASS${C_RESET}"
+  fi
+}
diff --git a/tests/e2e/suite_auth.sh b/tests/e2e/suite_auth.sh
new file mode 100644
index 0000000..86c8c75
--- /dev/null
+++ b/tests/e2e/suite_auth.sh
@@ -0,0 +1,96 @@
+# shellcheck shell=bash
+# suite_auth — connection management via the ~/.doris config files (file mode),
+# plus the stateless (env-var only) mode and its "never touches disk" 
guarantee.
+#
+# File-mode runs use an ISOLATED $HOME, so the real ~/.doris is never touched.
+# `auth status` ALWAYS exits 0 in doriscli — so the real connectivity assertion
+# is `.mysql_status == "connected"`, not the exit code.
+
+suite_auth() {
+  suite_banner "auth / use  (config-file mode + stateless mode)"
+
+  # Start from an empty config so the empty-list assertion is meaningful.
+  rm -rf "$ISOLATED_HOME/.doris"
+  DCLI_STATELESS=0
+
+  # Empty config: object with an empty environments array + a hint message.
+  expect_json "auth: list is empty on a fresh config" \
+    '.environments == [] and (.message|type=="string")' \
+    auth list
+
+  # Add the cluster under test as 'selftest' (becomes the default env).
+  expect_json "auth: add saves an environment" \
+    '.status=="added" and .name=="selftest" and (.http_probe|type=="object")' \
+    auth add selftest --host "$CFG_HOST" --port "$CFG_PORT" \
+      --http-port "$CFG_HTTP_PORT" --user "$CFG_USER" --password 
"$CFG_PASSWORD"
+
+  # List now shows it, marked default.
+  expect_json "auth: list shows the added env as default" \
+    '(type=="array") and (.[0].name=="selftest") and (.[0].default==true)' \
+    auth list
+
+  # status actually connects over MySQL — this is the live connectivity gate.
+  expect_json "auth: status connects to the cluster (mysql_status=connected)" \
+    '.environment=="selftest" and .mysql_status=="connected"' \
+    auth status
+  # Surface the version we saw, for the log.
+  DCLI_STATELESS=0 _run_dcli --format json auth status
+  log "  ${C_DIM}cluster: version=$(jget '.doris_version')  http=$(jget 
'.http_status')  backends=$(jget '.backends|length')${C_RESET}"
+
+  # `use` with no arg reports the current default.
+  expect_json "use: shows current environment" \
+    '.current=="selftest"' \
+    use
+
+  # mysql:// URI form — only when the password is URI-safe (parser splits on : 
/ @).
+  case "$CFG_PASSWORD" in
+    *:*|*@*|*/*)
+      skip "auth: add via mysql:// URI" "password contains : / or @ (not 
URI-safe to test)";;
+    *)
+      expect_json "auth: add via mysql:// URI parses host+port" \
+        '.host=="'"$CFG_HOST"'" and .mysql_port=='"$CFG_PORT" \
+        auth add uritest --mysql 
"mysql://$CFG_USER:$CFG_PASSWORD@$CFG_HOST:$CFG_PORT"
+
+      # Switch default to it, then remove it, and confirm it's gone.
+      expect_json "use: switch to another environment" \
+        '.status=="switched" and .environment=="uritest"' \
+        use uritest
+      expect_json "auth: remove deletes an environment" \
+        '.status=="removed" and .name=="uritest"' \
+        auth remove uritest
+      expect_json "auth: removed env no longer listed" \
+        'any(.[]?; .name=="uritest") | not' \
+        auth list
+      # restore default
+      DCLI_STATELESS=0 _run_dcli --format json use selftest
+      ;;
+  esac
+
+  # ---- stateless mode (DORIS_HOST + DORIS_USER drive everything) ----
+  DCLI_STATELESS=1
+
+  # Mutating config is refused in stateless mode (multi-tenant bastion safety).
+  EXPECT_ERR_MATCH="stateless"
+  expect_err "stateless: 'auth add' is refused" \
+    auth add nope --host "$CFG_HOST" --user "$CFG_USER"
+  EXPECT_ERR_MATCH=""
+
+  # ...but read/connect still works, purely from env vars.
+  expect_json "stateless: status connects from env vars only" \
+    '.mysql_status=="connected"' \
+    auth status
+
+  # And it must NOT create any config files. Prove it with a pristine HOME.
+  local clean_home; clean_home="$WORKDIR/clean_home_$$"
+  local saved_home="$ISOLATED_HOME"
+  mkdir -p "$clean_home"
+  ISOLATED_HOME="$clean_home"
+  DCLI_STATELESS=1 _run_dcli --format json auth status
+  if [ -e "$clean_home/.doris" ]; then
+    record_fail "stateless: writes no files to disk" "created 
$clean_home/.doris"
+  else
+    record_pass "stateless: writes no files to disk"
+  fi
+  ISOLATED_HOME="$saved_home"
+  rm -rf "$clean_home"
+}
diff --git a/tests/e2e/suite_cli.sh b/tests/e2e/suite_cli.sh
new file mode 100644
index 0000000..157443c
--- /dev/null
+++ b/tests/e2e/suite_cli.sh
@@ -0,0 +1,36 @@
+# shellcheck shell=bash
+# suite_cli — CLI-level contract. Pure argument parsing / help / error paths.
+# These need no live cluster (clap handles --version/--help/bad-args before any
+# connection). The one runtime path here — `sql` with no query — bails before
+# connecting, so we run it in stateless mode to skip the auth lookup.
+
+suite_cli() {
+  suite_banner "CLI contract (offline: version / help / argument errors)"
+  DCLI_STATELESS=1
+
+  # --version / -V : clap prints "doriscli <ver>" and exits 0.
+  expect_stdout_contains "cli: --version prints the binary name" "doriscli" 
--version
+  expect_stdout_contains "cli: -V short flag works"             "doriscli" -V
+
+  # --help lists usage and the subcommands.
+  expect_stdout_contains "cli: --help shows Usage"     "Usage"  --help
+  expect_stdout_contains "cli: --help lists 'sql'"     "sql"    --help
+  expect_stdout_contains "cli: --help lists 'profile'" "profile" --help
+
+  # `sql` with neither a query nor -f bails at runtime (before connecting).
+  EXPECT_ERR_MATCH="Provide a SQL query"
+  expect_err "cli: 'sql' with no query is rejected" sql
+  EXPECT_ERR_MATCH=""
+
+  # Unknown subcommand → clap usage error, non-zero exit.
+  expect_err "cli: unknown subcommand is rejected" frobnicate-xyz
+
+  # `tablet` requires a positional table name.
+  expect_err "cli: 'tablet' with no table is rejected" tablet
+
+  # `profile` requires a subcommand (list/get/diff/history).
+  expect_err "cli: 'profile' with no action is rejected" profile
+
+  # Unknown flag on a real subcommand → clap error.
+  expect_err "cli: unknown flag is rejected" sql "SELECT 1" 
--definitely-not-a-flag
+}
diff --git a/tests/e2e/suite_profile.sh b/tests/e2e/suite_profile.sh
new file mode 100644
index 0000000..a9cd716
--- /dev/null
+++ b/tests/e2e/suite_profile.sh
@@ -0,0 +1,96 @@
+# shellcheck shell=bash
+# suite_profile — query profile analysis.
+#
+# Dependency map (from the source):
+#   profile list / list --active   MySQL only            (always testable)
+#   profile get <id>  (summary)    HTTP, with SQL fallback (testable if 
--profile worked)
+#   profile get --full / --raw     HTTP profile fetch ONLY -> SKIP if HTTP_OK=0
+#   profile diff                   HTTP profile fetch ONLY -> SKIP if HTTP_OK=0
+#   profile history                __internal_schema.audit_log -> SKIP if 
absent
+#
+# HTTP_OK is set in preflight from `auth status`.http_status.
+
+suite_profile() {
+  suite_banner "profile  (list / get / full / raw / diff / history)"
+  DCLI_STATELESS=1
+
+  # Generate two profiled queries and capture their ids.
+  DCLI_STATELESS=1 _run_dcli --format json sql \
+    "SELECT event_type, COUNT(*) AS c FROM \`$CFG_DB\`.events GROUP BY 
event_type ORDER BY c DESC" --profile
+  local qid_a; qid_a="$(jget '.query_id')"
+  DCLI_STATELESS=1 _run_dcli --format json sql \
+    "SELECT user_id, SUM(amount) AS s FROM \`$CFG_DB\`.events GROUP BY user_id 
ORDER BY s DESC LIMIT 10" --profile
+  local qid_b; qid_b="$(jget '.query_id')"
+  log "  ${C_DIM}profiled query ids: A=$qid_a B=$qid_b  
(HTTP_OK=$HTTP_OK)${C_RESET}"
+
+  # profile list -> JSON array.
+  DCLI_STATELESS=1 _run_dcli --format json profile list
+  if [ "$RC" -ne 0 ]; then
+    record_fail "profile: list returns an array" "exit=$RC; $ERR"
+  elif printf '%s' "$OUT" | jq -e 'type=="array"' >/dev/null 2>&1; then
+    record_pass "profile: list returns an array"
+    if [ -n "$qid_a" ] && printf '%s' "$OUT" | jq -e --arg q "$qid_a" 
'any(.[]?; .query_id==$q)' >/dev/null 2>&1; then
+      record_pass "profile: list includes the just-profiled query"
+    else
+      record_skip "profile: list includes the just-profiled query" "not 
retained in SHOW QUERY PROFILE (eviction/timing)"
+    fi
+  else
+    record_fail "profile: list returns an array" "not a JSON array: $(_oneline 
"$OUT")"
+  fi
+
+  # profile list --active -> JSON array (usually empty).
+  expect_json "profile: list --active returns an array" 'type=="array"' \
+    profile list --active
+
+  if [ -z "$qid_a" ]; then
+    skip "profile: get <id> summary"        "no query_id captured (--profile 
produced none)"
+    skip "profile: get --full"              "no query_id captured"
+    skip "profile: get --raw"               "no query_id captured"
+    skip "profile: diff slow vs fast"       "no query_id captured"
+  else
+    # Default get: summary object. Works even without HTTP via the SQL 
fallback.
+    expect_json "profile: get <id> returns a summary" \
+      '(.summary|type=="object") and (.summary|has("query_id"))' \
+      profile get "$qid_a"
+
+    # --full and --raw require the FE HTTP profile API.
+    if [ "$HTTP_OK" = "1" ]; then
+      expect_json "profile: get --full returns the parsed tree" \
+        '(.profile|type=="object") and (.operators|type=="array")' \
+        profile get "$qid_a" --full
+      expect_json "profile: get --raw returns the raw profile text" \
+        'type=="string" and (contains("Summary"))' \
+        profile get "$qid_a" --raw
+    else
+      skip "profile: get --full" "FE HTTP API not reachable (http_status != 
connected)"
+      skip "profile: get --raw"  "FE HTTP API not reachable (http_status != 
connected)"
+    fi
+
+    # diff needs both ids + HTTP.
+    if [ "$HTTP_OK" = "1" ] && [ -n "$qid_b" ]; then
+      expect_json "profile: diff compares two runs" \
+        '(.slow|type=="object") and (.fast|type=="object") and 
(.time_ratio|type=="number")' \
+        profile diff "$qid_a" "$qid_b"
+    else
+      skip "profile: diff slow vs fast" "needs FE HTTP API and two query ids"
+    fi
+  fi
+
+  # profile history: depends on __internal_schema.audit_log being enabled.
+  DCLI_STATELESS=1 _run_dcli --format json profile history "events" --days 1
+  if [ "$RC" -ne 0 ]; then
+    if printf '%s' "$ERR" | grep -qi "audit_log"; then
+      record_skip "profile: history reads audit_log" "audit_log not 
enabled/accessible on this cluster"
+    else
+      record_fail "profile: history reads audit_log" "exit=$RC; $ERR"
+    fi
+  elif printf '%s' "$OUT" | jq -e 'has("executions") and has("pattern")' 
>/dev/null 2>&1; then
+    record_pass "profile: history reads audit_log"
+  else
+    record_fail "profile: history reads audit_log" "unexpected shape: 
$(_oneline "$OUT")"
+  fi
+
+  # Negative: an unknown query id cannot be fetched anywhere -> non-zero exit.
+  expect_err "profile: get on an unknown id errors" \
+    profile get "00000000-0000-0000-0000-000000000000"
+}
diff --git a/tests/e2e/suite_sql.sh b/tests/e2e/suite_sql.sh
new file mode 100644
index 0000000..8ee73a4
--- /dev/null
+++ b/tests/e2e/suite_sql.sh
@@ -0,0 +1,68 @@
+# shellcheck shell=bash
+# suite_sql — SQL execution surface: the JSON envelope, -f files, --set session
+# vars, --no-cache, --profile, the three output formats, and the error path.
+# Runs in stateless mode (env-var connection; no config files).
+
+suite_sql() {
+  suite_banner "sql  (execution, session vars, formats, errors)"
+  DCLI_STATELESS=1
+
+  # Envelope shape: query_id, exec_time_ms, rows_returned, columns[], rows[].
+  expect_json "sql: simple select returns the full JSON envelope" \
+    '.rows[0].one==1 and (.columns|index("one")) and 
(.query_id|type=="string") and (.rows_returned==1) and (.exec_time_ms>=0)' \
+    sql "SELECT 1 AS one"
+
+  # Mixed types: string stays a string, integer becomes a JSON number.
+  expect_json "sql: string and integer columns map correctly" \
+    '.rows[0].g=="hello" and .rows[0].n==42' \
+    sql "SELECT 'hello' AS g, 42 AS n"
+
+  # -f <file>: read the query from a file.
+  printf 'SELECT 7 AS seven' > "$WORKDIR/seven.sql"
+  expect_json "sql: -f reads the query from a file" \
+    '.rows[0].seven==7' \
+    sql -f "$WORKDIR/seven.sql"
+
+  # --set applies a session var before the query (integer var avoids 
normalization).
+  expect_json "sql: --set applies a session variable" \
+    '.rows[0].p==5' \
+    sql "SELECT @@parallel_pipeline_task_num AS p" --set 
"parallel_pipeline_task_num=5"
+
+  # --set is repeatable.
+  expect_json "sql: multiple --set flags all apply" \
+    '.rows[0].p==6 and .rows[0].w==2000' \
+    sql "SELECT @@parallel_pipeline_task_num AS p, 
@@runtime_filter_wait_time_ms AS w" \
+      --set "parallel_pipeline_task_num=6" --set 
"runtime_filter_wait_time_ms=2000"
+
+  # --no-cache: just confirm it executes cleanly.
+  expect_json "sql: --no-cache executes" \
+    '.rows_returned==1' \
+    sql "SELECT 1 AS one" --no-cache
+
+  # --profile: produces a non-empty query_id (reused by the profile suite).
+  expect_json "sql: --profile yields a query_id" \
+    '(.query_id|type=="string") and ((.query_id|length)>0)' \
+    sql "SELECT 2 AS two" --profile
+
+  # Output formats: a TTY-style table and CSV both carry the column header.
+  expect_stdout_contains "sql: --format table renders the column" "one" \
+    sql "SELECT 1 AS one" --format table
+  expect_stdout_contains "sql: --format csv renders the header" "one" \
+    sql "SELECT 1 AS one" --format csv
+
+  # Empty result set: zero rows, empty rows array, still exit 0.
+  expect_json "sql: empty result set is handled" \
+    '.rows_returned==0 and (.rows==[])' \
+    sql "SELECT table_name FROM information_schema.tables WHERE 1=0"
+
+  # End-to-end against loaded data: row count matches what setup loaded.
+  expect_json "sql: count over loaded table matches the load" \
+    '.rows[0].c=='"${ROWS:-2000}" \
+    sql "SELECT COUNT(*) AS c FROM \`$CFG_DB\`.events"
+
+  # Error path: a bad reference exits non-zero with an "SQL error:" message.
+  EXPECT_ERR_MATCH="SQL error"
+  expect_err "sql: invalid query exits non-zero" \
+    sql "SELECT * FROM ${CFG_DB}.no_such_table_xyz"
+  EXPECT_ERR_MATCH=""
+}
diff --git a/tests/e2e/suite_tablet.sh b/tests/e2e/suite_tablet.sh
new file mode 100644
index 0000000..bf4fc62
--- /dev/null
+++ b/tests/e2e/suite_tablet.sh
@@ -0,0 +1,53 @@
+# shellcheck shell=bash
+# suite_tablet — bucket/tablet analysis against the seeded tables.
+# Overview keys verified against src/commands/tablet/overview.rs;
+# --detail keys against src/commands/tablet/detail.rs (note: without 
--partition,
+# detail attributes every tablet to the first partition name — asserted as 
such).
+
+suite_tablet() {
+  suite_banner "tablet  (model / bucket / health / detail)"
+  DCLI_STATELESS=1
+  local events="$CFG_DB.events"
+  local dim="$CFG_DB.dim_users"
+
+  # Overview: model, distribution, sort key, row count.
+  expect_json "tablet: events overview (model/bucket/sortkey/rows)" \
+    '.model=="DUPLICATE" and .bucket_type=="HASH" and 
(.bucket_key|index("user_id")) and (.bucket_count==4) and (.partitions>=2) and 
(.total_rows>0) and (.sort_key|index("event_date"))' \
+    tablet "$events"
+
+  # Health summary is computed from SHOW DATA SKEW (object with a numeric 
skew).
+  expect_json "tablet: events health has a numeric tablet_skew" \
+    '(.health|type=="object") and (.health.tablet_skew|type=="number")' \
+    tablet "$events"
+
+  # Column stats (ndv) require ANALYZE to have populated them — SKIP if absent.
+  DCLI_STATELESS=1 _run_dcli --format json tablet "$events"
+  if [ "$RC" -ne 0 ]; then
+    record_fail "tablet: events columns[].ndv populated" "exit=$RC; $ERR"
+  elif [ "$(jget '.columns|length')" = "0" ] || [ -z "$(jget 
'.columns|length')" ]; then
+    record_skip "tablet: events columns[].ndv populated" "no column stats 
collected (ANALYZE async/unsupported)"
+  elif printf '%s' "$OUT" | jq -e '.columns[0]|has("ndv")' >/dev/null 2>&1; 
then
+    record_pass "tablet: events columns[].ndv populated"
+  else
+    record_fail "tablet: events columns[].ndv populated" "columns present but 
missing ndv: $(jget '.columns[0]')"
+  fi
+
+  # Model detection on the UNIQUE table.
+  expect_json "tablet: dim_users is detected as UNIQUE" \
+    '.model=="UNIQUE"' \
+    tablet "$dim"
+
+  # --detail: per-tablet + per-backend distribution. 2 partitions x 4 buckets 
= 8 tablets.
+  expect_json "tablet: --detail lists tablets and backends" \
+    '(.partitions|type=="array") and (.tablets|type=="array") and 
((.tablets|length)>=8) and (.backends|type=="array")' \
+    tablet "$events" --detail
+
+  # --partition narrows to one partition's 4 tablets.
+  expect_json "tablet: --detail --partition filters to one partition" \
+    '((.partitions|length)==1) and (.partitions[0].name=="p20240101") and 
((.tablets|length)==4)' \
+    tablet "$events" --detail --partition p20240101
+
+  # Negative: a missing table errors (SHOW CREATE TABLE fails).
+  expect_err "tablet: missing table exits non-zero" \
+    tablet "$CFG_DB.no_such_table_xyz"
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to