diff --git a/Cargo.lock b/Cargo.lock
index e699780f8f..38927a191a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13635,6 +13635,7 @@ dependencies = [
"local-ip-address",
"mysql",
"num_cpus",
+ "regex",
"reqwest 0.12.24",
"serde",
"serde_json",
@@ -13646,6 +13647,7 @@ dependencies = [
"tokio",
"tokio-postgres",
"tokio-stream",
+ "toml 0.8.23",
]
[[package]]
diff --git a/tests/compatibility/AGENTS.md b/tests/compatibility/AGENTS.md
new file mode 100644
index 0000000000..edce1edf1a
--- /dev/null
+++ b/tests/compatibility/AGENTS.md
@@ -0,0 +1,53 @@
+# Agent Guidance for Compatibility Framework
+
+This file is intended for AI agents editing compat cases or the compat runner. Follow these rules to avoid common pitfalls.
+
+## `verify.result` is Expected Output (Not Auto-Generated Silently)
+
+- `verify.result` contains the **expected** output of `verify.sql`.
+- If the file is **missing**, the runner generates it from actual output and **fails**.
+ The agent must review the generated file (`git diff`), hand-verify correctness,
+ and commit it before rerunning. Do **not** blindly commit generated output.
+- If actual output **differs** from the expected file, the runner **updates**
+ `verify.result` with actual output and **fails**. The agent must inspect the
+ diff and decide whether the change is intentional (accept) or a bug (fix code).
+
+## Namespace Isolation
+
+- Each case owns a **unique** namespace. No shared namespace support exists.
+- Duplicate namespaces are a **hard error** detected before version filtering.
+- The removed `isolation` field is no longer recognized; `case.toml` uses
+ `deny_unknown_fields`, so any stale `isolation = "shared"` entry causes a
+ parse error.
+
+## `case.toml` is Strict
+
+- `deny_unknown_fields` is enabled. Unknown keys cause a hard parse error.
+- Version constraint entries in `from_range` / `to_range` are validated early;
+ invalid constraints (e.g. `>=not-a-version`) are hard errors, not silent skips.
+- All required fields must be non-empty: `name`, `reason`, `introduced_by`,
+ `topologies`, `from_range`, `to_range`, `features`, `owner`.
+
+## Phase Semantics
+
+- `setup.sql` runs on the **old (from)** binary. Only success is required;
+ output is not compared to any file.
+- `verify.sql` runs on the **new (to)** binary. Output is compared against
+ `verify.result`.
+
+## PostgreSQL Protocol Cases
+
+- When using `-- SQLNESS PROTOCOL POSTGRES`, avoid unqualified table names
+ starting with `pg_`. GreptimeDB issue #8359 causes the parser to rewrite them
+ to `pg_catalog.
`. Qualify such names explicitly or rename the table.
+
+## Previewing with `--dry-run`
+
+```shell
+cargo run -p sqlness-runner -- compat --dry-run [--from-version vX.Y.Z] [--test-filter "..."]
+```
+
+The dry-run performs full discovery and filtering (name, topology, metadata
+validation, namespace dedup, version-range matching) but starts no services,
+creates no temp dirs, and mutates no files. Use it to check which cases
+would be selected before a real run.
diff --git a/tests/compatibility/README.md b/tests/compatibility/README.md
new file mode 100644
index 0000000000..1b16fdb470
--- /dev/null
+++ b/tests/compatibility/README.md
@@ -0,0 +1,148 @@
+# GreptimeDB Compatibility Test Framework
+
+Compatibility tests verify that a newer version of GreptimeDB can read data written by an older version.
+
+Tests are run via `cargo sqlness compat` and reuse the sqlness-runner infrastructure.
+
+## Quick Start
+
+```shell
+# Self-compat smoke test (current binary only):
+cargo run -p sqlness-runner -- compat
+
+# Test from a specific released version to current:
+cargo run -p sqlness-runner -- compat --from-version v0.9.5
+
+# Test between two local binary directories:
+cargo run -p sqlness-runner -- compat --from-bins-dir ./bins/old --to-bins-dir ./bins/new
+
+# Run a specific case:
+cargo run -p sqlness-runner -- compat --test-filter "basic_table"
+
+# Preview which cases would run (no services started):
+cargo run -p sqlness-runner -- compat --dry-run --from-version v0.9.5
+
+# See all options:
+cargo run -p sqlness-runner -- compat --help
+```
+
+## Prerequisites
+
+- **Docker** (for etcd): PR1 always uses Docker etcd for distributed metadata. External metadata stores are future work.
+- **From binary**: Either `--from-version ` to auto-pull a release, or `--from-bins-dir ` to use a local build. The binary `greptime` must exist directly inside the given directory.
+- **To binary**: Defaults to the current debug build (`target/debug/greptime`). Override with `--to-bins-dir `.
+- **Custom target-dir**: If you use a non-default `CARGO_TARGET_DIR`, the debug binary won't be at `target/debug/greptime`. Pass `--from-bins-dir` / `--to-bins-dir` explicitly pointing to your custom target directory. Alternatively, run `cargo build -p greptime` without a custom target-dir.
+
+## Case Format
+
+Each compat case is a directory under `tests/compatibility/cases/` containing three required files plus an expected output file:
+
+```
+my_case/
+ case.toml # Metadata (required)
+ setup.sql # SQL to run on old version (required)
+ verify.sql # SQL to run on new version (required)
+ verify.result # Expected output from verify.sql
+```
+
+### `case.toml` — Required Metadata
+
+```toml
+name = "my_case"
+reason = "Why this compatibility case exists"
+introduced_by = "PR #1234 or feature name"
+topologies = ["distributed"] # full distributed topology, including flownode
+from_range = ["*"]
+to_range = ["*"]
+features = ["table"]
+owner = "team-name"
+# optional:
+namespace = "my_explicit_namespace" # defaults to sanitized directory name
+```
+
+**Required fields**: `name`, `reason`, `introduced_by`, `topologies`, `from_range`, `to_range`, `features`, `owner`.
+
+### Version-Range Filtering
+
+`from_range` and `to_range` control which binary versions a case applies to:
+
+| Entry | Meaning |
+|-------|---------|
+| `"*"` | Matches any version (including unknown). |
+| `"vX.Y.Z"` or `"=vX.Y.Z"` | Matches exactly version X.Y.Z. |
+| `">=vX.Y.Z"` | Matches X.Y.Z or later. |
+| `">vX.Y.Z"` | Matches versions strictly later than X.Y.Z. |
+| `"<=vX.Y.Z"` | Matches X.Y.Z or earlier. |
+| `" --version` to infer the version.
+- When the version **cannot** be determined (e.g. binary missing or `--version` fails), non-wildcard ranges are **skipped** with a message; wildcard (`*`) ranges still match.
+
+**Example** (`legacy_jsonb`):
+```toml
+from_range = ["<=v1.1.0"]
+to_range = [">=v1.1.1"]
+```
+This case only runs when the old binary is <= v1.1.0 and the new binary is >= v1.1.1.
+
+### `setup.sql` — Setup Phase (Old Version)
+
+SQL statements executed on the **old** version cluster. These must succeed (any error fails the case). Setup output is NOT compared against any result file.
+
+Rules:
+- Statements are semicolon-terminated
+- `--` prefix for ordinary comments
+- `-- SQLNESS ...` interceptor comments follow ordinary sqlness semantics
+### `verify.sql` — Verify Phase (New Version)
+
+SQL statements executed on the **new** version cluster. Output is compared against `verify.result` in sqlness snapshot style.
+
+### `verify.result` — Expected Output
+
+Expected output in sqlness format. If this file is missing, the runner generates it from actual output and **fails** — the author must review, commit the generated file, and rerun.
+
+```
+;
+
+