libredb / libredb/libredb-studio
Tracking: database provider coverage - shipped, queued, and declined
- Dominant language
- TypeScript
- Stars
- 726
- Forks
- 119
- Avg merge
- 7h 47m
- Merged PRs (30d)
- 265
Description
Living coverage map for database providers. This item is the source of truth for
what we ship, what is queued, and what we have decided not to build.
**Keep it current:** when a provider lands, tick it in the phase list AND move it
up into "Shipped" with its type-id. When a decision changes, edit the reasoning
here rather than re-arguing it in a new issue.
This stays a **single epic**: no child issues. The checkboxes below are the only
tracking surface, and a PR that lands a provider ticks them in the same PR.
Last verified against `origin/main` @ bda1cf88 on 2026-08-26, by:
```bash
ls src/lib/db/providers/*/ docs/providers/ tests/integration/db/
grep -n '^ [a-z][a-z0-9_-]*:$' database-compose.yml
grep -rln read-only-budget src
```
Architecture and authoring companions this list depends on:
[`docs/DATABASE_PROVIDERS.md`](https://github.com/libredb/libredb-studio/blob/main/docs/DATABASE_PROVIDERS.md) (Strategy
Pattern, provider hierarchy) and
[`docs/ADDING_A_PROVIDER.md`](https://github.com/libredb/libredb-studio/blob/main/docs/ADDING_A_PROVIDER.md) (the full
touch-list, 700+ lines). Neither is optional reading before starting a phase.
---
## Shipped - 16 providers
All sixteen satisfy the tri-sync invariant: `src/lib/db/providers//.ts`
+ `docs/providers/.md` + `tests/integration/db/-provider.test.ts`.
**What the number counts.** Sixteen is every type-id the factory can build, the embedded
`libredb` included. The published surfaces say **fifteen**, which is the same set without
`libredb` - an engine a user connects *to* rather than one we ship *inside*. Both are true and
they count different things, so each surface states its own denominator instead of leaving the
reader to infer it. The login page's engine figure is the internal number and lists `libredb`;
the README's "fifteen engines" is the external one and does not. Neither is written by hand any
more: the login page derives its figure from `listShowcaseDatabases()`, and the prose counts are
read from `SHIPPED` in `src/lib/db/compatibility.ts`, which is an exhaustive record over
`DatabaseType` so the compiler refuses an omission.
SQL:
- [x] `postgres` : PostgreSQL
- [x] `mysql` : MySQL
- [x] `sqlite` : SQLite
- [x] `oracle` : Oracle
- [x] `mssql` : Microsoft SQL Server
- [x] `clickhouse` : ClickHouse (HTTP transport, zero runtime dep)
- [x] `druid` : Apache Druid (HTTP transport, zero runtime dep)
- [x] `elasticsearch` : Elasticsearch (HTTP `_sql`, zero runtime dep) - PR #429
- [x] `opensearch` : OpenSearch (HTTP `_plugins/_sql`, zero runtime dep, same module) - PR #429
- [x] `trino` : Apache Trino (client protocol over HTTP, zero runtime dep) - PR #438
- [x] `cassandra` : Apache Cassandra (CQL over the DataStax pure-JS driver) - PR #448
Document:
- [x] `mongodb` : MongoDB
- [x] `couchbase` : Couchbase (HTTP only, zero runtime dep)
Key-value:
- [x] `redis` : Redis
Embedded:
- [x] `libredb` : LibreDB
- [x] ~~Add a `redis` service to `database-compose.yml`.~~ Closed by #426: `redis`
was the one shipped engine with no compose entry, so its own gate-4 probe could
not be reproduced on a clean machine. Every shipped engine now has a service.
---
## Definition of Done - per provider
One provider is "added" when all seven gates pass. Every phase below refers to
this list instead of restating it. A phase checkbox is ticked only when its
provider has cleared all seven; "the code is written" is not one of the gates.
**Gate 1 - Tri-sync.** `src/lib/db/providers//.ts` +
`docs/providers/.md` + `tests/integration/db/-provider.test.ts`,
all three landing in the same PR. This is a CLAUDE.md invariant, not a preference.
**Gate 2 - Registration surfaces.** A provider is not reachable from the UI until
every surface knows its type-id. `docs/ADDING_A_PROVIDER.md` holds the authoritative
list; at minimum `src/lib/db/factory.ts`, `src/lib/types.ts` + `src/lib/db/types.ts`
(`DatabaseType`, capabilities, `ProviderLabels`), `src/lib/db-ui-config.ts`,
`src/hooks/use-connection-form.ts`, `src/lib/connection-string-parser.ts`,
`src/components/icons/db-icons.tsx`, `src/lib/query-generators.ts`,
`src/lib/explain/`, `src/lib/seed/types.ts`, plus `docs/providers/README.md` and
the root README table. Read that doc; do not work from this paragraph.
**Gate 3 - Unit + integration tests at 100% line coverage.** `bun run test:coverage
&& bun run coverage:check` stays green. TDD is the working style here, not a
retrofit; the coverage gate makes untested lines unmergeable.
**Gate 4 - Live probe (the gate that separates "coded" from "works").** Our
integration tests run the **real provider over a fake transport**, which is only
meaningful because the fake speaks what a real server said. So:
- a service in `database-compose.yml` (or a documented hosted instance where a
container is not possible - warehouses, managed-only services),
- every payload in the integration test captured from that live instance,
- the **exact server version** recorded in the test file header, the way
`clickhouse-provider.test.ts` records ClickHouse 26.7.1.1315,
- connect / list schema / run a query / cancel / hit a permission error, all
against the live instance, at least once by hand.
Mocks cannot catch what this catches. Couchbase produced 8 such traps, Druid
overruled three decisions already written into its issue, and the Node 26 IP-SNI
break in `mssql` was invisible until the built image ran.
**Gate 5 - E2E (Playwright, `e2e/`).** Connect, browse the schema, run a query,
export - through the browser, not through a unit harness. CI has no live external
databases, so if a provider cannot be exercised in CI, the exemption is recorded
**in this issue, by name, with the reason**, and the manual run is done before the
tick. A silent exemption is a failed gate.
**Gate 6 - Functional and regression.** `e2e/functional-smoke.spec.ts` passes, and
all six local gates pass before the PR is called done: `bun run format` ·
`bun run lint` · `bun run typecheck` · `bun run knip` · `bun run test` ·
`bun run build`. Providers touch shared surfaces (`sql-base.ts`, the connection
form, the seed types), so the regression risk is to the other ten, not to the new one.
**Gate 7 - Agent plan mode.** Plan mode is expected to work for every provider
with no per-provider cost, because its grounding is driven through capabilities
rather than type checks. Expected is not verified: run one grounded plan against
the live instance from gate 4 and confirm the plan is grounded in the real schema.
### Agent auto (operate) mode is out of scope for this epic
Agent auto mode today works for `postgres` and `sqlite` only. The reason is
concrete and greppable: `src/lib/db/providers/sql/read-only-budget.ts` is imported
by `postgres.ts` and `sqlite.ts` and by nothing else. Extending it per provider
means a new read-only enforcement implementation plus a live adversarial security
probe - the class of work that found `VACUUM INTO` on SQLite and `COPY TO PROGRAM`
on PostgreSQL, both of which escaped a read-only claim that looked correct on paper.
That work is deliberately **not** part of adding a provider here, and no phase
below is blocked on it. When it is scheduled it gets its own issue.
---
## How a name is counted (claim discipline)
- A name counts as **supported** only after it clears gate 4 above. "Connects" is
not "supported".
- The root README and `docs/providers/README.md` carry the **same** number, and that
number is changed only from this issue. `distribution/channels.yaml` does NOT: it is a
machine-read inventory (identity, `kind`, `platforms`, `runtime`, `update.*`, `links.*`,
`pin.*`) with no description field, so no count can live in it. The per-channel LISTING
COPY is what carries engine names - see the wrap-up item below.
- Today's published count is **33**, and its denominator is now stated by the code that
computes it: `connectableProductCount()` = **14 external drivers + 19 relatives**. Read it
from `src/lib/db/compatibility.ts`, never from this paragraph.
**Amended 2026-08-26.** The three sentences this bullet used to carry were each true of a
different denominator, which is the failure the counting rule exists to prevent:
`verifiedEngineCount()` no longer exists (it counted `SHIPPED_DATABASE_TYPES`, so it included
the embedded store and answered 33 where README.md published 32 for the same claim), and the
"32 -> 33 with the `cassandra` DRIVER (PR #448)" step was measured on that retired denominator.
On the published one the history is: 27 -> 29 with Vitess and Apache Cloudberry (PR #446,
`fd5e101`), 29 -> 32 with AlloyDB Omni, OceanBase and SingleStore (PR #447, `2465c92`),
32 -> 33 with **ScyllaDB** (PR #455), which moved the relatives half 18 -> 19. PR #448 moved
the SHIPPED half and therefore left the published figure where it was. The two halves must
never both be incremented for one engine.
- Cloud Spanner was measured in the #447 pass and deliberately earns no entry, so a probe run
does not imply a count increase. A tick records a measurement; the published count moves only
when the registry, docs and tests land together.
- **The ceiling is a count of this list, not a digit to quote.** If every remaining Phase 0
candidate cleared gate 4 the published figure would reach **58**: 33 today + 2 ticked but
unregistered (Neon, Supabase) + 15 unticked candidates already listed + the 8 added to this
inventory on 2026-08-26 (see the audit note at the end of Phase 0). The earlier figure of 47
was computed on the retired denominator and against a shorter list, so it is withdrawn rather
than corrected. Publishing a ceiling before probing it is the overclaim we are avoiding:
a buyer's technical diligence checks a claim like that by connecting.
## Prioritisation rubric
Phases are ordered by four axes, scored 1 (poor) to 5 (strong). Demand = how often
the name is actually asked for. Transport = how cheap the implementation is (5 =
HTTP, zero runtime dependency). Packaging = freedom from native-dependency risk (5
= no native dep). Differentiation = whether shipping it distinguishes us from the
JDBC-era tools.
| Phase | Demand | Transport | Packaging | Diff. | Note |
|---|---|---|---|---|---|
| 0 - wire-compatible labelling | 5 | 5 | 5 | 3 | No driver work at all; only verification and honest publishing |
| 1 - Elasticsearch + OpenSearch | 5 | 5 | 5 | 4 | Largest single gap; one implementation, two type-ids |
| 2 - Trino | 4 | 4 | 5 | 4 | Opens the lake query layer behind one endpoint |
| 3 - Cloud warehouses | 5 | 3 | 5 | 2 | Highest demand, weakest fit with "deploys next to the data" |
| 4 - Wide-column / serverless KV | 3 | 2 | 3 | 3 | Cassandra needs a driver; DynamoDB needs SigV4 |
## Why this list needs to grow
We are Node/TypeScript with no **JDBC** universe, so every provider is a hand-written
class. We cannot win on count and should not target it. Our asymmetry is the
HTTP-native / cloud-native / AI side, where both competitors are late - and where
the ClickHouse/Druid/Couchbase pattern already gives us a proven zero-dependency
template.
Note: `druid` appears in neither competitor's list. Either differentiation or a
low-demand investment; see Open questions.
---
## Phase 0 - Wire-compatible labelling (no new drivers)
These already speak the wire protocol of a driver we ship. Each needs a live probe
(gate 4) and then a docs/README entry. The highest ROI item on this list, because
the driver work is zero.
There is **no labelling mechanism in the code today**: `ProviderLabels` exists but
carries no compatibility name. So the first decision below is a design decision,
not a wording one.
Blocked on the first two decisions; do not start ticking names before they are settled.
- [x] **Decided (2026-08-18): a compatibility registry in code, with two consumers.** `src/lib/db/compatibility.ts` is the single source of truth; the connection dialog renders a hint under the driver selector (`WireCompatibilityHint`), and the docs table is generated from the same data. Caveats are announced in the dialog and detailed in the docs, never listed inline. Rejected: a `docs/providers/.md` page per name (tri-sync illusion), and docs-only (a MariaDB user still cannot tell which of eleven buttons to press). Original wording kept below for the record:
- [ ] ~~Decide what a verified name earns: a `docs/providers/` page, a README table
row, a compatibility list only, or a UI-visible hint in the connection dialog
("MariaDB - connect using the MySQL driver"). "Connects" is not "supported" -
Redshift answers `pg` but its `pg_stat_*` surface differs, CockroachDB's system
catalogs are partial. Whatever we publish must not imply introspection parity
we did not test. If the answer is UI-visible, it needs a code change and
therefore gates 2, 3, 5 and 6 in its own right.~~
- [x] **Done (2026-08-18).** Inventory is the probe run below: 9 names reached a live instance and were measured; 6 dockerable names could not be brought up (image budget / cluster / licence); every managed-only service has no credentials on this machine and therefore cannot clear gate 4 at all. Recorded per name above. **Amended 2026-08-20:** "no credentials on this machine" is a fact about the machine, not about the engines - the maintainer holds accounts on some of these and is working through them himself, ticking each as he goes. Neon and Supabase are ticked on that basis already. A name ticked from owner use says so, carries no tier, and does not enter `WIRE_COMPATIBLE_ENGINES` until its surfaces are measured one at a time. This gates the
Postgres wire (`pg`):
- [ ] Amazon Redshift
- [x] CockroachDB - **partial**, v26.2.5. Editor, errors, performance metrics, slow queries and sessions work; object browser and all size/health panels blank.
- [x] YugabyteDB - **full**, 2.25.2.0-b0 (advertises PostgreSQL 15.12). PR #442. All 15 surfaces answer, foreign keys included, and a plan run grounds. Row counts and sizes read 0 until someone runs `ANALYZE` - measured on a 20000-row table: 0 rows / 0 bytes before, 20000 rows / 7878 kB after - so a full database looks empty. Index sizes always read 0 bytes (DocDB storage is invisible to `pg_relation_size()`), the overview's database size reads 0 bytes, and index types read `lsm`.
- [x] ~~Greenplum~~ - **dropped, not labelled: the open-source project is dead.** Every repository in `github.com/greenplum-db` is archived (`gh api orgs/greenplum-db/repos --paginate` returns zero non-archived); Broadcom closed the source in May 2024 and Tanzu Greenplum is now a closed commercial product. Probed anyway on 2026-08-20 via `datagrip/greenplum:6.8` and it scored 8/15 - but that image is from 2020 (Greenplum 6.8.1 on **PostgreSQL 9.4**) and every single failure was a kernel-version gap, not a Greenplum property: `AS MATERIALIZED` (needs PG 12), `wait_event_type` (9.6), `array_position` (9.5), `to_regrole` (9.5, which also blocked the agent profile). Measuring a corpse. Replaced by Apache Cloudberry below.
- [x] Apache Cloudberry (incubating) - **partial**, 2.1.0-incubating on PostgreSQL 14.4. The community successor to Greenplum, forked by its original developers. 12 of 15 surfaces answer: object browser, schema, relations, indexes, overview, health, sessions, slow queries and storage all work, and **row counts are correct** (20000 estimated against 20000 real), so not the Citus silently-wrong class. Three fail with one distributed-planner error - *"query plan with multiple segworker groups is not supported"* - taking the monitoring dashboard, table stats and index stats. Two `pg_ext_aux.pg_pax_*` internal tables leak into the object browser (4 objects for 2 user tables), and foreign-key syntax is accepted with a warning and never enforced. **Gate 7 FAILS, and the earlier note that it passed is withdrawn** (re-probed 2026-08-20, PR #446): as the cluster's own `gpadmin` the execution profile refuses the role as unverified or too broad, and with a least-privilege role the grounding capture is refused at 289 rows against the 200 allowed, 282 of those rows belonging to Cloudberry's own `gp_toolkit`. That is B52's shape on a second engine, and since Cloudberry is a fork rather than an extension it narrows B52 to any PostgreSQL-wire server whose own catalogs are wide before the user creates anything. Probed via `woblerr/cloudberry:2.1.0-incubating` - Apache publishes only BUILD images, no runnable database, so the image provenance is third-party and belongs in the caveat.
- [x] AlloyDB Omni - **full**, PostgreSQL 17.9 (AlloyDB Omni 17.9.0). Registered 2026-08-20. All 15 surfaces answer and the numbers are EXACT (2000 rows read as 2000, 270336 bytes as 270336), with a foreign key read back and enforced. `version()` names AlloyDB nowhere, so the version panel is indistinguishable from stock PostgreSQL 17. The browser lists 10 objects for 2 user tables (8 `google_ml` tables), and that understates it: 49 extension views live in `public` itself and are hidden only by the schema query's BASE TABLE filter. A role with no grants at all still lists and reads those `google_ml` tables. **Gate 7 FAILS, and this is the instance that decides B52's fix**: refused at 536 rows against 200, only 7 of them the user's, and 341 of the 348 attributed to `public` are those extension views - so narrowing to a schema still refuses at 348, which refutes the exclude-internal-schemas fix that would have rescued TimescaleDB and Cloudberry. Only per-object aggregation survives. The MANAGED AlloyDB service remains unreachable from here and is not claimed.
- [ ] Amazon Aurora (PostgreSQL)
- [x] TimescaleDB - **full**, 2.29.2 on PostgreSQL 17.11. PR #442. **The hypertable/chunk question is now answered, and the caution was justified.** A hypertable's row count and size are WRONG rather than missing: `metrics` reports 0 rows / 24 KB where it holds 5000 rows and `hypertable_size()` is 2.4 MB, because the statistics describe the empty parent and the rows live in 31 chunks - the Citus class exactly. The object browser shows **61 objects for 2 user tables** (31 `_hyper_*` chunks plus the `_timescaledb_catalog` and `_timescaledb_cache` schemas), confirmed in the UI with `metrics` at 0 beside `plain_rows` at 100. Gate 7 FAILS here and the reason is measured, not guessed - see B52 below.
- [x] Citus - **full**, citus 14.1-1 on PostgreSQL 18.4. All 15 surfaces answer, but distributed-table row counts and sizes are silently WRONG.
- [x] Neon - **connects and is usable**, reported by the maintainer from his own prior use (2026-08-20). Evidence is OWNER USE, not a gate-4 probe run: no per-surface measurement, no server version recorded, so it earns no tier and is deliberately NOT in `WIRE_COMPATIBLE_ENGINES` yet. Both are hosted PostgreSQL rather than forks, so the expectation is `full`; that stays an expectation until the 15 surfaces are called one at a time.
- [x] Supabase - **connects and is usable**, same provenance and same limits as Neon above.
- [x] Materialize - **query editor only**, 26.37.0. No pg statistics catalog, no size functions, `MATERIALIZED` is reserved.
- [x] RisingWave - **query editor only**, 3.0.3. Same shape as Materialize, plus it rejects a parameterised LIMIT.
- [ ] EDB Postgres Advanced Server - **blocked on credentials (measured 2026-08-20)**: the supported registry answers 401 (`docker.enterprisedb.com/v2/`), and the still-public quay mirror carries an old build.
- [ ] CloudNativePG - **nothing distinct to measure (measured 2026-08-20)**: `ghcr.io/cloudnative-pg/postgresql` is an OPERAND image for the operator (`Entrypoint=[]`, `Cmd=[bash]`, no `docker-entrypoint.sh`, so it needs a hand-run `initdb`), and underneath it is stock PostgreSQL, which we already ship and probe. Probing it would measure PostgreSQL, not CloudNativePG. Left unticked deliberately: it is a deployment of a driver we ship rather than a relative of one.
- [ ] Google Cloud SQL for PostgreSQL
- [x] QuestDB - **MEASURED AND REFUSED A ROW (2026-08-26, PR #506): it fails the one surface a
query-editor-only tier exists to claim.** It does speak the PostgreSQL wire protocol on 8812,
and through the provider a statement answers three rows - which is why every signal short of a
browser said "register it as query-only". In the product **pressing Run answers `500 unknown
function name: pg_backend_pid()`**: the editor always attaches a `queryId` and the provider
then issues `SELECT pg_backend_pid()` first. Established rather than guessed - the identical
provider call with a `queryId` fails and without one returns the rows. Same shape as Cloud
Spanner above, handled the same way: no registry entry, the number is the finding in
`docs/providers/README.md`, and the `questdb` compose service is kept so the refusal can be
reproduced. Two further notes for anyone revisiting it: the object browser fails on TWO
independent causes of ours (`AS MATERIALIZED`, then `pg_total_relation_size()`), so this is
NOT the Materialize/RisingWave case of an absent catalog - `information_schema.tables` and
`pg_catalog.pg_class` both answer - and that narrows the MATERIALIZED portability debt, since
removing the keyword alone would still leave the row where it is.
- [x] Percona Distribution for PostgreSQL - **full**, Percona Server for PostgreSQL 18.6.1 on
PostgreSQL 18.6. Registered 2026-08-26, PR #508, probed against
`percona/percona-distribution-postgresql:18.6`. All fifteen surfaces answer, the numbers are
correct (2000 rows read as 2000, 131072 bytes as 131072), a foreign key and its indexes are
read back, `Analyze` and `Vacuum` both work, and the browser pass produced **zero failed API
requests**. **The version panel names Percona** - `version()` answers *PostgreSQL 18.6 -
Percona Server for PostgreSQL 18.6.1* - which is the OPPOSITE of the MySQL distribution below
and the only thing separating the pair. Slow queries stay empty until `pg_stat_statements` is
enabled, which is stock PostgreSQL behaviour and not a Percona property.
- [x] ParadeDB - **full**, ParadeDB 0.25.4 on PostgreSQL 18.6. Registered 2026-08-27, PR #510,
probed against `paradedb/paradedb:0.25.4`. All fifteen surfaces answer with correct numbers
(2000 rows read as 2000, 131072 bytes as 131072), and slow queries work where most PostgreSQL
relatives have nothing, because `pg_stat_statements` ships enabled. **Its cost is width**: nine
extensions come with it, so the object browser lists **41 objects for 2 user tables** and the
index panel 74 rows, with PostGIS's `spatial_ref_sys` (8500 rows) in `public` beside the user's
tables. `version()` names PostgreSQL only, so the panel cannot tell it apart from a stock 18.6.
**The width predicted B52 and the measurement REFUTED it, which is the finding worth carrying
out of this row**: the grounding capture reads what the ROLE can see - a superuser sees 539
non-system columns, an unprivileged role 168, the tiger geocoder's 425 not being readable - so
the capture stays under its 200-row ceiling and **gate 7 passes** (21 tables read, `ctx_c35a`,
a `LEFT JOIN` drafted over the real objects). As a superuser the run fails on the execution
profile refusing a superuser, which is true on any PostgreSQL. **So B52's trigger is a role
that CAN read a wide catalog, not an extension that installs one.**
- [x] OrioleDB - **full**, OrioleDB beta 16 on PostgreSQL 18.4 (nightly of 2026-08-24). Registered
2026-08-27, PR #510. **Read it against ParadeDB above: both are full and their costs are
opposites.** Here the object browser is clean (2 objects for 2 user tables), row counts are
exact and a foreign key and its indexes are read back. What is missing is what PostgreSQL
cannot see of OrioleDB's own storage: **every index reads 0 bytes**, `pg_indexes_size()`
returning 0 for an OrioleDB table (measured: 114688 bytes of table, 0 of indexes) - the
YugabyteDB DocDB shape - and **the cache hit ratio reads N/A**, because OrioleDB has its own
buffer manager and `pg_statio_user_tables` stays at 0 hits / 0 reads, which is an absence
honestly rendered rather than a wrong number. Gate 7 passes under a least-privilege role (6
tables, `ctx_6b5e`). Checked before trusting any of it that the fixture measures OrioleDB at
all - `default_table_access_method` is `orioledb` and both probe tables report
`amname = orioledb`, so a heap-table probe measuring plain PostgreSQL was ruled out. Caveat on
the version: `version()` DOES name OrioleDB, and the project publishes **nightly images only**,
so the compose tag is a dated nightly and this row describes one build.
MySQL wire (`mysql2`):
- [x] MariaDB - **full**, 12.3.2. Zero deviation from the MySQL baseline. Only 12.3 probed.
- [x] TiDB - **full**, 8.0.11-TiDB-v8.5.1. PR #442. **This bullet's premise was wrong: there IS a single-container path.** `pingcap/tidb:v8.5.1 --store=unistore --path= --host=0.0.0.0` runs standalone with no PD and no TiKV, and it is now a compose service. All 15 surfaces answer and gate 7 passes (grounded on the real `orders.country`, ran unmodified, answered `TR`). Caveats: a freshly loaded table reads 0 rows / 0 B until TiDB's own background statistics catch up (self-correcting, no `ANALYZE`); max connections reads 0 (its default, meaning unlimited); the slow-query panel is always empty because TiDB keeps its slow log in `information_schema.SLOW_QUERY` rather than the `performance_schema` view the provider reads; storage stats list a phantom `InnoDB` entry on a server with no InnoDB; and the Explain panel fails outright, TiDB rejecting `EXPLAIN FORMAT='json'`. Probed on unistore only - a PD + TiKV deployment was not probed and no claim is made about it.
- [x] SingleStore - **partial**, SingleStoreDB 9.1.1 (advertises MySQL 5.7.32). Registered 2026-08-20. **The recorded reason was STALE: no licence key is needed** - the dev image self-licenses (`SetLicenseV4: Expiration = unlimited`, `Options = 72 (Developer Image edition)`) with only `ROOT_PASSWORD` set. 10 of 15 surfaces. Row counts and sizes read 0 EVERYWHERE against 2000 rows and 77046 bytes measured four ways, so a full table looks empty and `ANALYZE` does not help. Foreign keys do not exist at all (`ERROR 2752`), and with `ignore_foreign_keys = ON` an inline FK is accepted and silently stripped. Every table carries an extra `__SHARDKEY` index. Gate 7 passes. Five of its six defects share ONE cause that is OURS, filed as D8: the mysql provider sends everything through mysql2's prepared protocol, and SHOW / EXPLAIN / OPTIMIZE / CHECK all fail there while ALL succeeding on the text protocol - the same cause already recorded for StarRocks' missing panels. **D8 was fixed in PR #476 (2026-08-24) and four of those five surfaces are recovered** - Test Connection, health, the overview and the monitoring dashboard, all re-probed in a browser; the Explain panel is not among them, because SingleStore's grammar is `EXPLAIN JSON` and the statement fails on both protocols (X14). The row stays `partial`: its row counts and sizes still read 0.
- [x] OceanBase - **partial**, 5.7.25-OceanBase_CE-v4.4.2.1. Registered 2026-08-20. The reason above was half true and not blocking: 483 MB compressed with a documented 2-core / 8 GB floor. **The documented run command fails** - the container's default soft `nofile` is 1024 and the deployer needs 20000, so boot dies in 12 seconds with OBD-1007; `MODE=mini` plus `--ulimit nofile=655350:655350` starts it. 14 of 15 surfaces answer but only 12 do their job, which is why it is partial rather than full. Zero object-browser leakage (2 objects for 2 tables), row counts CORRECT after MySQL-mode `ANALYZE TABLE`, foreign keys read back and enforced, `cancelQuery` genuinely cancels and `explain` genuinely estimates without executing. Every size reads 0 B, `getHealth` fails outright (the tenant has no `performance_schema`), and the storage panel shows a fake InnoDB `ibdata1` row. Connect to the business tenant (`root@test`), not `sys`. Gate 7 passes.
- [x] StarRocks - **partial**, 3.3.22-753696f. PR #442. The image pulled once the link recovered. 11 of 15 surfaces answer: the editor, table list, column metadata, table and storage stats, performance metrics and slow queries work, and gate 7 passes. Unavailable: the overview and health panels (StarRocks refuses their statements through the prepared-statement protocol) and active sessions plus the monitoring dashboard (no `information_schema.PROCESSLIST`). **Its sharpest trap is that `version()` returns a fictitious `5.1.0`**, so the product displays "MySQL 5.1" for a 3.3.22 server; the real build is only in `current_version()`, which the provider does not call. Row counts and sizes are hard zeros checked directly (not delayed statistics), no index is reported at all, and `EXPLAIN FORMAT='json'` does not parse. **Amended 2026-08-26:** the prepared-protocol cause was fixed in PR #476 and the per-panel degradation in PR #477, so `getOverview()` and `getPerformanceMetrics()` answer and the monitoring dashboard now renders six panels instead of one error. The row is unchanged - the fictitious `version()`, the hard zeros, the absent indexes, no Explain, and health still failing on the missing `information_schema.PROCESSLIST` - because none of those is what D8 caused.
- [ ] Amazon Aurora (MySQL)
- [x] Vitess - **full**, 8.0.43-Vitess, 15 of 15 surfaces. Row counts and sizes correct, foreign keys read correctly, `EXPLAIN FORMAT=JSON` works, and the `agent-operations` acquisition grounding needs succeeds. Caveats: table and index stats report the physical shard database (`vt_probe_0`) rather than the keyspace the user connected to; setting a session variable can fail where reading it works (`SET @@cte_max_recursion_depth` is rejected with `VT05006`, while `SELECT` of the same answers 1000); **cancelling a running query does nothing** - vtgate refuses `KILL QUERY` with `VT07001`, a 5 second `SLEEP` ran its full 5003 ms, and the same call cancels on a MySQL 9 control; **per-index sizes always read 0 bytes, and that defect is ours** (D6) - the size query matches `INNODB_TABLES.NAME` against `'/%'` and Vitess names the InnoDB table after the shard database, so the rows are `vt_probe_0/orders`; and **no permission-error class is measurable on this fixture at all**, because `vttestserver` grants any credential full rights and `CREATE USER` is a vtgate parse error - a property of the fixture, not of Vitess. Re-probed on the PINNED release `vitess/vttestserver:v24.0.2-mysql80` (PR #446); the first run had used the rolling `:mysql80` tag, which serves a 25.0.0-SNAPSHOT built from main and is therefore not reproducible. Unsharded single-shard keyspace only (`show vitess_shards` returns `probe/0`), so the earlier "a sharded keyspace refuses DML without a primary vindex" caveat is DROPPED as untestable here rather than carried as measured. **PlanetScale itself is NOT covered by this** - it is a managed service with its own auth and no credentials here, so it stays unticked below.
- [ ] PlanetScale (the managed service - Vitess above is the engine, not this listing)
- [ ] Google Cloud SQL for MySQL
- [x] Apache Doris - **partial**, `doris version doris-4.1.3-rc02-7126cf65d96` (`version()` reports
a fictitious 5.7.99). Registered 2026-08-26, PR #503, probed against
`apache/doris:all-in-one-4.1.3`. **Thirteen of fifteen surfaces answer, and the fork's row was
a prior rather than an inheritance**: Doris reports the TRUE numbers where StarRocks reports
hard zeros (3 rows / 2.65 KB and 2000 rows / 9.95 KB against a ground truth of 3 and 2000;
`getTableStats()` 10187 bytes against `SHOW DATA`'s 9.948 KB), and **cancellation genuinely
cancels** - an 8-second `SELECT sleep(8)` died at 1513 ms with Doris's own message, where
Vitess cannot cancel at all. Sessions, storage and slow queries answer, permission errors
arrive as the right class with the server's sentence intact, a `SELECT_PRIV` role saw only its
granted table, and gate 7 passes (2 tables captured, a `LEFT JOIN` drafted over the real
objects, run unmodified: `TR 667`, `DE 667`, `US 666`). **Both failures are ours, filed as
D30**: the overview and health reads send `SHOW STATUS LIKE '...'`, a parse error in the Doris
grammar, while a bare `SHOW STATUS` is accepted and answers zero rows - so a panel that could
render absence renders an error. Caveats: `Analyze` works while `Optimize` and `Check` do not
exist in the grammar; `EXPLAIN FORMAT='json'` does not parse; no index is ever reported
(`information_schema.statistics` is empty); a declared foreign key is **invisible AND
unenforced** (`SHOW CONSTRAINTS` lists it, `KEY_COLUMN_USAGE` is empty, an orphan row inserts);
a freshly loaded table reads 0 rows / 0 B for about a minute until the background statistics
land; a `UNIQUE KEY` table declares no primary key to the product (`COLUMN_KEY` is `UNI`).
**What only the browser found**: the badge reads Slow, the Monitoring Overview tab carries the
engine's own sentence while the other six tabs answer, and **creating the connection takes two
clicks of Establish Connection** - Test Connection answers *Connected, but this server answered
no health data*, the first click reports that and stores nothing, the second stores it, which
is D9's contract working on an engine it was never tested against. One reading is recorded as
suspect rather than claimed healthy: the Tables card summarises *Vacuum 0 / OK* on an engine
with no vacuum-like operation.
- [x] Databend - **query editor only**, Databend v1.2.925-patch-11 (advertises MySQL 8.0.90).
Registered 2026-08-27, PR #510. SQL runs and the editor was verified in a browser -
`SELECT country, count(*) ... GROUP BY country` returned `DE 1`, `TR 1`, `US 1` from the grid,
which is the check QuestDB failed - and a plain `EXPLAIN` shows Databend's own plan. **Nothing
else does, and the cause is OURS**: every parameterised read goes through mysql2's prepared
protocol and Databend implements no PREPARE (`Prepare is not support in Databend`), so the
table list, schema, sessions and the table, index and storage statistics all fail. The catalogs
are fine - asked with literal SQL, `information_schema.tables` reports the true 3 and 2000 rows
with `data_length` 124 and 49000. D8 moved the PARAMETERLESS statements to the text protocol;
these carry placeholders and still prepare. Filed as **D33**, together with a crash rather than
a failure: `runMaintenance('analyze')` throws `rows.filter is not a function`, Databend
answering with an object where the reader expects an array. Two gaps are the engine's own and
would survive that fix: **no `SHOW STATUS` statement at all** and no
`information_schema.processlist`. Plan mode's behaviour is the honest downstream - the run
succeeds, drafts NOTHING, says it was given no inventory and asks for the table and column
names rather than inventing a schema. One authoring trap: Databend follows the SQL standard on
quoting, so a double-quoted value is an identifier and only `'TR'` is a string.
- [x] Percona Server for MySQL - **full**, Percona Server for MySQL 8.4.11-11. Registered
2026-08-26, PR #508, probed against `percona/percona-server:8.4`. All fifteen surfaces answer,
the numbers are correct (2000 rows read as 2000, 114688 bytes as 114688, index 65536), indexes
and a foreign key are read back, `EXPLAIN FORMAT=JSON` parses, and **all three maintenance
actions succeed** - `Analyze`, `Optimize` (InnoDB's own *doing recreate + analyze instead; OK*)
and `Check`. Browser pass: zero failed API requests, and Monitoring rendered version, uptime,
`Connections 9/151`, `DB Size 192 KB`, `Cache Hit 97.5%`, `Tables 2 / 2 indexes`. **Nothing on
screen says Percona**: `version()` answers a bare `8.4.11-11` and the product name is only in
`@@version_comment` (*Percona Server (GPL), Release 11, Revision 57878ff8*), which the provider
does not read - the Garnet shape, and the reason the compose tag is the only durable record of
which build was measured. **One reading found here belongs to the BASELINE, not to Percona**:
`getHealth()` reports *Performance schema not available* on a server whose
`@@performance_schema` is 1 and whose slow-query panel returns rows - reproduced identically on
a stock MySQL 26.7.0 in the same pass, so it is ours and pre-existing, filed as **D32**.
Redis wire (`ioredis`):
- [x] Valkey - **full**, 9.1.1. Reports Redis emulation level 7.2.4 in the overview.
- [x] DragonflyDB - **full**, df-v1.40.1. Max connections reads 0; sessions show a numeric id instead of a username.
- [x] KeyDB - **full**, 6.3.4. No version field of its own, so indistinguishable from Redis 6.
- [ ] Amazon ElastiCache
- [ ] Upstash - the one managed name on this list with a free tier, so it is the only Phase 0
entry outside the container path that does not need a purchased account.
- [x] Microsoft Garnet - **full**, Garnet 2.1.5 (advertises Redis 7.4.3). Registered 2026-08-26,
PR #506, probed against `ghcr.io/microsoft/garnet:2.1.5`. Every Redis surface answers: the key
browser grouped 71 keys into `user:*` (50), `session:*` (20) and `queue:*` (1), a `GET user:1`
run returned the value in the browser, and Monitoring rendered version, uptime, sessions and
table counts. **It is the first relative here whose own version was available and went
unread** - `INFO` carries `garnet_version:2.1.5` and `server_name:garnet` while the product
shows Redis 7.4.3; Valkey and DragonflyDB publish only an emulation level and KeyDB nothing of
its own, so this is the first row where the displayed version is not the best reading
available. **Two numbers are absences wearing a value**, both checked against Redis 8.10.0 in
the same pass: no `used_memory`, so the database size and the memory storage panel read 0 B;
and no `keyspace_hits` / `keyspace_misses`, so the cache hit ratio reads 100% **and the
dashboard rates it Excellent** - D14's fallback being graded. Max connections reads 0 (no
`maxclients`, as on DragonflyDB) and `connected_clients` stays 0 even with a client attached,
though the session list itself is correct. Fixture note: in-memory with no persistence,
measured - `dbsize` 71 before a `docker stop`, 0 after.
MongoDB wire (`mongodb`):
- [ ] Azure Cosmos DB (MongoDB API) - **blocked (measured 2026-08-20)**: a `mongodb` tag of the emulator exists, but the image was built 2024-04 and refuses to run at all, so no instance can be reached.
- [ ] Amazon DocumentDB
- [x] FerretDB - **full**, 2.7.0 (MongoDB 7.0.77 wire). All 11 introspection surfaces answer. Backend PostgreSQL credentials; two containers.
TDS wire (`mssql`):
- [ ] Azure SQL Database - **no emulator exists (measured 2026-08-20)**: every candidate MCR path 404s and the only remaining local artifact is `azure-sql-edge`, a retired product. Managed-only in practice.
- [ ] Microsoft Fabric (SQL analytics endpoint)
- [ ] Azure Synapse Analytics (dedicated SQL pool)
- [ ] Azure SQL Managed Instance
SQLite-family - **not** wire-compatible, listed here only so they stop being
rediscovered as free wins. SQLite has no wire protocol; each of these is an HTTP
API and therefore real provider work under all seven gates, not a label:
- [x] Decide whether Turso / libSQL is worth a type-id (HTTP + its own auth). **Decided yes and BUILT - see Phase 5 below (PR #511).** The 2026-08-20 reading held: `ghcr.io/tursodatabase/libsql-server` answers `sqld 0.24.33` over its Hrana HTTP API, so this was provider work under all seven gates rather than a label.
- [ ] Decide whether Cloudflare D1 is worth a type-id (HTTP, account-scoped)
- [ ] rqlite - recorded, unlikely, revisit only on a concrete ask. **Confirmed HTTP-only (measured 2026-08-20)**: `rqlite/rqlite` runs locally at 23 MB and answers `v10.2.7` over `GET /db/query`, so the same applies - a provider, never a label.
### Audit 2026-08-26: eight names this inventory never had, and two costs that dropped
Two findings, both from re-reading this list against the code rather than from a new probe.
**The list was missing eight candidates**, now added above and marked with that date. Six of
them are named nowhere in this repository at all (Apache Doris, Percona Server for MySQL,
Percona Distribution for PostgreSQL, ParadeDB, OrioleDB, Microsoft Garnet - verified with
`grep -rli` over the tree; Databend appears only inside a competitor's driver list in
`docs/BRAND_MESSAGING.md`), and QuestDB was carried as
an unscored open question rather than as the Phase 0 name its own parenthesis said it probably
was. This issue calls itself the source of truth for coverage, so a name it never considered is a
gap in the tracking surface and not merely unfinished work. Each new bullet carries the reason it
is cheap and the trap its nearest registered relative already measured; none is ticked, because
nothing has been probed.
**Two of the reasons this list gives for `partial` rows are ours and have since been fixed**,
which lowers the expected cost of every remaining MySQL-wire name:
- **D8** (merged in PR #476, 2026-08-24): the `mysql` provider sent every statement through
mysql2's binary prepared protocol, where `SHOW`, `EXPLAIN`, `OPTIMIZE` and `CHECK` are refused.
Every parameterless statement now goes over the text protocol. This is the single cause behind
five of SingleStore's six recorded defects and four of StarRocks' missing panels.
- **D9** (merged in PR #472, 2026-08-24): `handleConnect` gated the SAVE on `getHealth()`, so an
engine whose health surface failed could not be stored through the connection dialog at all -
recorded in the ScyllaDB bullet of Phase 4, and true by inspection of StarRocks and SingleStore.
A failed health read after a successful connect is now a degraded success, and the connection
saves.
Neither changes a tier or a count on its own - a quality change inside a `partial` row moves no
denominator - but both change what a first probe of Apache Doris or Databend should be expected
to find, so a new probe must be run rather than inherit StarRocks' row. **Confirmed 2026-08-26:**
the Doris probe reproduced none of StarRocks' zeroed statistics, and its own two failures turned out
to be a THIRD cause of ours (D30, a statement form rather than a protocol).
**Gate 7 exemption, recorded by name as this issue requires.** Three of the four engines added by
PR #442 clear gate 7 (TiDB, YugabyteDB, StarRocks - each captured its schema and drafted a
statement over real objects). **TimescaleDB does not, and the failure is the agent's rather than
the engine's.** The composed PostgreSQL grounding capture projects one row per COLUMN against
`maxResultRows: 200` and refuses rather than truncates; a stock TimescaleDB answers 478 rows from
`information_schema.columns`, of which 473 belong to the extension's own schemas and 5 to the
user, so the run answers ungrounded before the user has created anything. Verified
extension-caused and not path-caused by three controls: plain PostgreSQL 18 and YugabyteDB both
captured their schemas under the same least-privilege role, and granting that role the internal
schemas changed nothing. Tracked as **B52** in `docs/BACKLOG.md` and cited in `docs/AGENT.md`.
The same shape will appear on any PostgreSQL carrying a catalog-heavy extension, so this is not a
TimescaleDB-only concern.
Wrap-up:
- [x] Propagate the verified list to the per-channel LISTING COPY, keeping the number
identical to the README (see claim discipline above). Done in #444: the chart
READMEs (`charts/` and the operator copy), the operator CSV keywords, the Flatpak
metainfo, the winget locale template and the Azure listing. Not `channels.yaml` -
that file has no field a count could go in. A wire-compatible relative is named in
listing copy only where its caveats are COSMETIC (TiDB, FerretDB); an engine that
reports row counts which are wrong rather than missing (TimescaleDB, YugabyteDB,
Citus) is not, because a listing is the wrong place for that footnote.
`deploy/rancher/CATALOG_LISTING.md` deliberately stays at ten: it describes a
released version, per its own accuracy gate.
## Phase 1 - Elasticsearch + OpenSearch
The single largest gap - HTTP-only, zero runtime dependency, one implementation
serves two products.
- [x] Provider: HTTP transport over `fetch`, zero runtime dependency. Auth is Basic only, and the limit is stated rather than glossed: both probe clusters ran with security DISABLED and a bogus `Basic` header was measured to be IGNORED there (HTTP 200), so no 401/403 body could be captured. `auth` is therefore decided on HTTP status alone and no unmeasured fault name appears in the code. API key and cloud-id are NOT implemented.
- [x] Schema mapping: indices as tables, mapping fields as columns, read from `_mapping` and `_cat/indices` rather than from SQL - `SELECT *` describes the statement, not the index (measured: an index mapping `flattened`/`nested` answers `SELECT *` with zero columns). Containers and multi-fields are excluded; see the findings comment.
- [x] **Decided: SQL, not the Query DSL.** Measured 2026-08-19 - the SQL endpoint works on BOTH products with no licence gate (`_sql?format=json` on Elasticsearch 9.1.4 basic, `_plugins/_sql` on OpenSearch 3.8.0). ES|QL works too but exists only on Elasticsearch (OpenSearch answers 405), so it cannot be the shared language. Choosing SQL puts the provider on `SQLBaseProvider`, which is what makes the explorer, ER diagrams, schema diff, the query limiter and agent plan mode work with no per-provider cost. The subset limitation is recorded in both docs, not hidden.
- [x] `opensearch` as a separate type-id sharing one implementation (`providers/sql/search/`). Their divergences are data in a dialect table, never a branch on the type-id.
- [x] Distribution and licence recorded, and this box's own premise was wrong. It said
"Elasticsearch is SSPL / Elastic License"; the pinned image
`docker.elastic.co/elasticsearch/elasticsearch:9.1.4` ships **Elastic License 2.0** and says
so in its own `/usr/share/elasticsearch/LICENSE.txt` - read out of the running container
rather than recalled. OpenSearch's `opensearchproject/opensearch:3.8.0` ships
**Apache-2.0**, likewise read from the image. The blur worth removing turned out to be a
different one: both docs said "basic licence", where **basic** is the self-generated
*subscription tier* `GET /_license` reports (`type: basic`, `issuer: elasticsearch`) and not
the software licence at all. Each doc now separates the two and names the image
explicitly, as `database-compose.yml` already did.
- [x] All seven gates, for both type-ids. Gates 1-3 and 5-6 landed with #429. **Gate 7 was the
one genuinely outstanding**: #429 recorded it as "asserted rather than assumed" on the
grounds that both ids ground through #414's `provider-inventory` path - but this epic's own
wording is "Expected is not verified", so an argument was standing in for a run. Both are
now run, against the live containers, 2026-08-20: plan mode captured **3 indices** (its
words, not "tables") on each, and drafted
`SELECT shipping.country, COUNT(*) AS order_count FROM orders GROUP BY shipping.country
ORDER BY order_count DESC LIMIT 1` - grounded on the real index *and* on `shipping.country`,
a nested field it could only know from the mapping capture. Applied unmodified, it answered
`TR | 40` on both, columns typed `keyword` and `long`, 66 ms on OpenSearch and 131 ms on
Elasticsearch. Gate 2 was re-walked in the browser on the way: the dialog offers
Elasticsearch, defaults to port 9200 and reported `Connected successfully (51ms)`.
- [x] Move both into Shipped above
## Phase 2 - Trino
Same shape as ClickHouse/Druid: HTTP protocol, zero dependency. Opens the
data-lake query layer (Iceberg, Delta, Hive behind one endpoint).
- [x] Provider over the Trino HTTP protocol (nextUri polling). PR #438. Zero runtime
dependency, `fetch` only. The loop ends on the absence of `nextUri` and never on
`stats.state`: measured, `state: "FINISHED"` arrives with a link still attached on the
page that carries the rows, and a page with no `data` is mid-flight rather than the end.
- [x] **Decided: the connection's `database` field pins ONE catalog**, rather than a
three-level tree. Trino is `catalog.schema.table` and is the first three-level engine
here, but there is no cluster-wide `information_schema` - it is per-catalog - so any
"list everything" operation is a fan-out of one query per catalog, and `jmx` and
`system` are always present, which makes it unbounded. So a connection pins one catalog
exactly the way a PostgreSQL connection pins one database, the tree stays two-level, and
the field is labelled **Catalog Name** in the dialog. Cross-catalog queries - Trino's
headline feature - still work in the editor by fully qualifying names, and they are not
visible in the tree. A catalog level in the tree is a possible follow-up and is not
blocked by anything here.
- [x] **Decided: PrestoDB is a SEPARATE type-id**, sharing one transport parameterised by
header prefix - the `elasticsearch`/`opensearch` shape. Every protocol header is
generated from a product prefix (`X-Trino-User` vs `X-Presto-User`), a stock Trino
coordinator ignores `X-Presto-*` entirely, and the only switch that would accept it is
`@Deprecated` and off by default. One type-id would mean `if (isPresto)` branches through
introspection, and one doc and one test file covering two wire protocols - the drift the
tri-sync invariant exists to prevent. **The separate type-id is confirmed by measurement:**
a live PrestoDB 0.295 silently IGNORES `X-Trino-*`, so one id could never have served both.
**The cost estimate in this bullet was wrong and is withdrawn.** It said `presto` "is a
descriptor plus its doc and test pair"; a gate-4 probe against `prestodb/presto:0.295`,
run side by side with a live Trino 476, refutes that - see the findings comment. The
descriptor holds for the wire mechanics only. Beyond them: all four missing-object faults
collapse to `SYNTAX_ERROR`, which makes `unknown-object` unreachable and turns the
introspection degradation contract fail-hard; `OFFSET` parses but is disabled and needs a
session header the transport has no concept of; `EXPLAIN (FORMAT JSON)` is a different
document, so `toFragments()` returns `null` and a second strategy is required; `array` /
`map` / `row` arrive as pretty-printed strings rather than JSON; the JMX domain differs; a
password over plain HTTP is accepted rather than refused; and a missing user header is 400
rather than 401. It also needs a rename, since `providers/sql/trino/` hosting two type-ids
conflicts with tri-sync. One correction goes the other way: this bullet claimed `version()`
"returns a bare `476` on Trino and a `0.2xx` string on PrestoDB" - on PrestoDB the function
is **not registered at all**, and `versionQuery` should be deleted from the dialect rather
than given a value, because nothing reads it (`getOverview()` uses
`system.runtime.nodes.node_version`, correct on both). `presto` is not part of PR #438 and
is no longer the cheap next item; it is unscheduled pending that scope.
- [x] All seven gates. PR #438, every check green.
**Gate 1** tri-sync: `src/lib/db/providers/sql/trino/` + `docs/providers/trino.md` +
`tests/integration/db/trino-provider.test.ts`, one PR.
**Gate 2** every registration surface, confirmed in a browser rather than by grep: the
driver is offered in the connection dialog with port 8080, a field labelled **Catalog
Name**, and the TLS rule stated under the password field before a user can hit it.
**Gate 3** 100% line coverage - `39141/39141` on the merged lcov.
**Gate 4** `trinodb/trino:476`, official image, authentication disabled, catalogs
`tpch` / `tpcds` / `memory` / `system` / `jmx`; version recorded in the test header.
Connect, list schema (all 72 `tpch` tables), typed rows, cancellation and three refusal
classes, through the provider and again through the browser. 33 scenarios captured page
by page before any provider code was written.
**Gate 5** `e2e/trino-provider.spec.ts`, green in CI. Recorded rather than glossed: the
spec could not be run on the development machine, where every spec using the shared
login helper - including the shipped Phase 1 `search-providers.spec.ts`, untouched by
this PR - fails with a strict-mode violation, the login page serving one
`input[type="email"]` in its SSR body and having two in the DOM after hydration. A local
timing artifact, not a regression; CI is the authority here, and everything the spec
asserts was additionally verified by hand in a browser.
**Gate 6** all six local gates plus the full CI matrix.
**Gate 7** a plan run captured the schema (72 tables) and drafted a statement over the
real `tiny.region` / `tiny.nation`, which then ran unchanged and answered `ASIA, 5`.
- [x] Move into Shipped above
## Phase 3 - Cloud warehouses
The data-analyst persona's actual home. Both reachable over REST, so no native
dependency. Tension with the "deploys next to the data" positioning - the counter
is that a buyer's diligence will ask for these by name.
Gate 4 has no container option here: these are managed-only, so each needs a real
account, and the gate-5 exemption (if any) must be recorded by name.
- [ ] Snowflake via the SQL API v2 (key-pair JWT auth)
- [ ] Google BigQuery via the REST API (service-account OAuth)
- [ ] Decide whether Databricks SQL Warehouse joins this phase or waits
- [ ] All seven gates, per warehouse
- [ ] Move each into Shipped above
## Phase 4 - Wide-column and serverless KV
The first phase since `mysql` to need a real driver dependency, which is why it is
last: packaging risk is the axis we score worst on.
- [x] Apache Cassandra - **shipped in PR #448**, merged `4c7278c`, probed against Apache Cassandra 5.0.9. **This bullet's own premise was wrong on the base class**: it says `BaseDatabaseProvider` path not `SQLBaseProvider`, and the measurement says the opposite - `SELECT "id"` returns the column, a backtick is `no viable alternative`, a double-quoted string is a syntax error, and `LIMIT n` is correct CQL, so CQL is SQL-shaped enough that `SQLBaseProvider` is the right parent and the id stays OUT of `NON_SQL_DIALECTS`. The driver risk that made this phase last is also settled: `cassandra-driver` 4.9.0 is pure JavaScript (no `binding.gyp`, no `.node`, no postinstall), so the better-sqlite3 packaging class does not apply, and the three Bun segfault issues that name this driver do not reproduce - measured on bun 1.3.14 with three clean connects, 2500 concurrent prepared inserts, a 400-statement batch, and paged `eachRow`/`stream()` over 500 and 2000 rows, with node as a control. **Row counts and sizes are deliberately BLANK**: `size_estimates` counts partitions rather than rows (143 for a 500-row clustered table) and `disk_usage` is whole mebibytes (1 MiB for 19,476 bytes), so publishing either would be the Citus/TimescaleDB defect committed on purpose. No cancellation exists, so `cancelQuery` is not implemented rather than returning false; no explain, no maintenance, no foreign keys, and `supportsCreateTable` is false because every type the modal emits is refused. `localDataCenter` is a new first-class required connection field, the first in this repo. Gate 7 passes with the capture recorded in the ledger (`tableCount 4`, `ctx_1ee1d2c7`) and a drafted statement that ran unmodified. Three `prepareQuery` traps are measured, and one of them is a shared-reader limitation filed as U17: CQL has a third comment form (`//`) and a line comment of either form is a syntax error at EOF, so the shared limiter would turn a valid statement into one. **One claim in this bullet needed correcting after the merge, and the correction is recorded in the comment of 2026-08-21:** "row counts and sizes are deliberately BLANK" was true of the PROVIDER and false of the PRODUCT. Measured in Chrome against the same node, the monitoring Tables tab still summarised the empty `getTableStats()` as *Tables* `0` over *0 rows*, *Size* `0 B` over *Total* and *Vacuum* `0` over **OK**, in the very frame the Overview tab read *Tables 6 / 2 indexes* - the sentinel §3.2 of `docs/providers/cassandra.md` had just deleted, refilled one component downstream, plus a clean bill of health for an operation Cassandra does not have. Gate 4 passed on the provider's own boundary and does not read the panels a provider feeds. Fixed across five tabs in PR #452 (`408a1e9`), which lands backlog item U14; `SessionsTab` was examined and refuted. D7 (three providers FABRICATE a cache hit ratio, so there is no absence for a panel to render) and U12 are NOT covered by it and stay open.
- [x] ScyllaDB as a wire-compatible label on the Cassandra provider - **shipped in PR #455**, probed 2026-08-21/22 against `scylladb/scylla:2026.2.4` and `scylladb/scylla:2025.1`, with `cassandra:5.0.9` measured in the same pass. Recorded as `partial`, and the whole delta is ONE cause: **ScyllaDB has no `system_views` keyspace at all**, so five of the thirteen surfaces this provider offers fail with the same verbatim `Keyspace system_views does not exist` (`getOverview`, `getPerformanceMetrics`, `getActiveSessions`, `getHealth`, `getMonitoringData`) while all thirteen pass on 5.0.9. Thirteen and not fifteen because this provider has neither cancellation nor `EXPLAIN` on either engine. Both Scylla builds behaved identically on every surface. **What only a browser pass found, and it is the U14 lesson again:** Test Connection calls `getHealth`, and `handleConnect` gates the SAVE on that same request, so **Establish Connection refuses too and a ScyllaDB connection cannot be created through the dialog at all** - the pass reached the editor through a seeded, admin-managed connection. StarRocks and SingleStore sit on the same gate by inspection and neither row records it; filed as D9 rather than fixed, because widening the degradation contract is what §3.6 argues against and would plausibly lift this row to `full`. The monitoring dashboard blames a *Connection Error* the connection did not cause, and the header badge reads *Slow*. What works: the editor and object browser in full, all 18 CQL types byte-identical to the baseline, and identical error CLASSES despite different server wording (the classifier reads the driver's error code). Two visible differences: one extra table per secondary index (4 objects for 3 user tables, Scylla backing an index with a view), and no version displayed anywhere - were the panel fixed it would read Apache Cassandra **3.0.8**, the compatibility number `system.local` publishes, not ScyllaDB 2026.2.4, which lives in `system.versions`. Of this list's own three doubts two held and **`gossip_generation` was refuted** - it exists on ScyllaDB and answers. Relatives 18 -> 19, named engines 32 -> 33; the SHIPPED half does not move. **D9 was fixed in PR #472 (2026-08-24): a ScyllaDB connection can now be created through the dialog, because a failed health read after a successful connect is a degraded success rather than a failed test.** The five surfaces still answer empty, so the tier stays `partial`.
- [ ] Amazon DynamoDB (HTTP + SigV4 signing)
- [ ] All seven gates, per provider
- [ ] Move each into Shipped above
---
## Phase 5 - libSQL
Built 2026-08-27, PR #511. **One type-id, `libsql`, for TWO deployments**: a self-hosted
libSQL server (`sqld`) and Turso Cloud. They speak the same protocol and embed the same
SQLite - 3.47.0 on both - so two ids would have meant two docs and two tests describing one
set of measurements. Zero runtime dependency: Hrana is JSON over `POST /v2/pipeline` through
the runtime's own `fetch`, the fifth driver-free provider after couchbase, clickhouse, druid
and the two search ids, and trino.
- [x] libSQL - **shipped in PR #511**, probed against `sqld 0.24.33 (f8fb14f3 2026-08-11)`
and a Turso Cloud database in `aws-eu-west-1`, both on 2026-08-27. The gate-4 harness
calls every surface separately: **17 of 19 answer on both arms**, the two that do not
being the intended refusals. Five measured findings shaped the code:
**(1) a failed statement answers HTTP 200** with the failure inside `results[]`, and a
failing step does NOT abort the rest of a batch - measured with a three-statement
pipeline whose middle statement failed while the third still answered, which is why
`executeBatch` hands back a per-statement outcome instead of throwing (#477's rule
applied before the fact);
**(2) the two deployments word one refusal differently** - `unsupported statement:
VACUUM` against `SQL not allowed statement: VACUUM`, both `SQL_PARSE_ERROR` - so
nothing keys on wording;
**(3) the server refuses `VACUUM`, `ANALYZE`, `PRAGMA optimize`, `PRAGMA wal_checkpoint`
and `PRAGMA query_only`** and accepts `REINDEX` and `PRAGMA integrity_check`, so only
two maintenance operations are offered and `runMaintenance` refuses the rest HERE rather
than relaying a server error; the `query_only` refusal is why this provider implements
no `queryReadOnly` and the agent read-only profile stays postgres+sqlite;
**(4) `notnull` is a SQLite keyword**, so projecting it bare from `pragma_table_info` is
`near NOTNULL: syntax error` - it costs the COLUMNS of every table and leaves the tree
intact, so the browser listed both tables with none while every unit test stayed green.
**Only the live probe found it**, and the statement text is now pinned by a test;
**(5) `dbstat` answers on BOTH**, which `bun:sqlite` cannot do at all, so table and index
bytes here are measured rather than absent.
`GET /version` is a sqld route Turso Cloud does not have, so the version panel reads
`sqld 0.24.33 (…) (SQLite 3.47.0)` there and `SQLite 3.47.0` on the cloud - neither is
"Unknown". **Gate 7 passes**: plan mode grounded on the real schema (2 tables,
`ctx_a49a`) and drafted a statement that ran unchanged and answered `de 1`; where it
cannot execute the rail says so in the engine's own name.
**Two corrections the browser forced**, both filed rather than fixed here: the e2e spec
had asserted the dialog renders no Username and no Database input because
`connectionFields` names neither - it renders both, for every non-file-based engine, so
Druid and the two search ids have shown a Database box they do not take since they
shipped (**U22**); and the Storage tab labels its remainder "Other (TOAST, FSM)" on every
engine, both PostgreSQL structures (**U23**). **D34** came out of the same pass: the
migration generator emits `ADD CONSTRAINT` for the `sqlite` dialect, which SQLite cannot
parse. Counts: shipped 15 -> 16 type-ids, published products 40 -> **41**.
- [ ] Turso Database (the Rust rewrite) - **NOT a row, and not for effort reasons.** It is a
different engine rather than a deployment of this one, and on 2026-08-27 it published no
server image at all: `tursodatabase/turso`, `tursodatabase/tursodb` and
`ghcr.io/tursodatabase/turso-server` were all unpullable, and it ships as an in-process
npm package (`@tursodatabase/database`). Nothing to connect to, so no name is published.
Revisit when a server image exists.
---
## Deliberately not building
Recorded so we stop re-litigating it. Tick a box here only to mark that the
decision has been revisited and still holds.
- [ ] Legacy JDBC column - IBM Db2, SAP HANA, Teradata, Vertica, Exasol, Informix,
Sybase ASE. Native clients and licensing cost more than the persona
they bring, without JDBC to amortise it. Revisit only on a concrete customer ask.
**Firebird left this bullet on 2026-09-07**: it was named here until then, and the
reason above turned out to be false of it - a pure-JavaScript driver exists and the
product is not licensed like Db2 or Sybase. The decision has not changed; the premise
had. It is now an Open question below, unscored and unscheduled.
- [ ] Java-embedded - H2, HSQLDB, Apache Derby. First-class in both competitors
because they are JVM tools. No standing in the Node ecosystem.
- [ ] Prometheus / VictoriaMetrics. Neither competitor ships it, which reads as
evidence the category belongs to Grafana rather than to a database IDE.
(Time-series stores like InfluxDB/TDengine are a different question and stay open.)
## Open questions
- [ ] DuckDB: high signal with data analysts, but a native N-API dependency - the
same class of risk that broke the AppImage channel with better-sqlite3.
Decide whether the packaging cost is acceptable before scheduling it.
- [ ] Vector databases (Qdrant, Milvus, Weaviate): absent from both competitors,
all HTTP, and aligned with the AI positioning. Genuine white space - decide
whether it is a differentiation bet or a distraction.
- [ ] Time-series over HTTP: InfluxDB (v3 / SQL). Named here because the "not building"
section rules out metrics stores but explicitly leaves this one open, and it has never
been scored. **QuestDB left this question on 2026-08-26**: its own parenthesis here said
it was probably a Phase 0 name rather than a provider, and it is - it speaks the
PostgreSQL wire protocol, so it is now a Postgres-wire candidate in Phase 0 above and
needs a probe, not a decision.
- [ ] Firebird: **the "Legacy JDBC column" premise was measured on 2026-09-07 and does not
hold here, so this is a candidate rather than a refusal.** Nothing is scheduled and no
date is implied. What was measured, from the registry and the repository rather than from
recollection: `node-firebird` is at **2.15.0, published 2026-08-22**
(https://www.npmjs.com/package/node-firebird), has **zero runtime dependencies and no
native addon** - no `gypfile`, no `binding.gyp`, no postinstall; its four devDependencies
are TypeScript, vitest, oxlint and `@types/node` - and it is written in TypeScript and
ships its own declarations. It is actively maintained: last commit
**2026-09-07**, release `v2.15.0` tagged 2026-08-22
(https://github.com/hgourvest/node-firebird). Its CI matrix is **Node 20, 22, 24, 26 x
Firebird 3, 4, 5 and 6-snapshot** against the `firebirdsql/firebird` images
(`.github/workflows/node.js.yml`), so the "2.5 through 6.0" range that circulates for
this driver is NOT what is tested - 2.5 appears only as the floor for one feature (AbortSignal
cancellation) in the README, and the tested floor is Firebird 3. Licence MPL-2.0,
~157,800 npm downloads in the 30 days to 2026-09-06. So this would be the
`cassandra` packaging class (pure JS, one dependency), not the `better-sqlite3` one -
the two axes the rubric scores worst on, Transport and Packaging, are the two the
premise above got wrong. The rival is a different thing and must not be confused with
it: `node-firebird-driver` / `node-firebird-driver-native` 3.7.1 (2026-08-09) sit on
`node-firebird-native-api` 3.3.0, which IS native (`gypfile: true`, `node-addon-api`,
`node-gyp`, `bindings`) and needs `fbclient` present - that package, and only that one,
is what the original licensing/native-client reason described.
**The concrete ask this bullet exists for** arrived as comment 1.30 ("А firebird?") on
the opennet.ru thread for 0.13.7:
https://www.opennet.ru/opennews/art.shtml?num=66216 - one question from one reader, which
is a signal and not yet demand.
**Nothing is claimed as supported**: no `DatabaseType` id, no registry entry, no count
moves, and per the claim discipline above "a driver exists" is not "connects" and
"connects" is not "supported". A Definition of Done here is the full seven gates, and
three of them carry the real cost: gate 1's triad
(`src/lib/db/providers/sql/firebird.ts` + `docs/providers/firebird.md` +
`tests/integration/db/firebird-provider.test.ts`, one PR), gate 3's 100% line coverage,
and gate 4's live probe - a `firebirdsql/firebird` service in `database-compose.yml`,
every test payload captured from it, the exact server version in the test header, and
connect / list schema / run a query / cancel / permission error done by hand. Open
questions a probe has to answer before a phase could be scored: what
`RDB$` system tables give for row counts and sizes, whether dialect 1 versus 3
databases need two paths, and whether the SQL surface is close enough to sit under
`SQLBaseProvider` (the Cassandra bullet's premise was wrong on exactly that, in the
cheaper direction).
- [ ] Apache Druid: we ship it, and it appears in neither competitor's list.
Measure whether it is differentiation or a low-demand investment before
spending anything further on that side.
- [ ] Whether the seven-gate Definition of Done should move into
`docs/ADDING_A_PROVIDER.md` once it has survived one phase, leaving this issue
to track coverage only.
Contributor guide
Assessment
This issue has not been assessed yet.