Skip to content

Tracking: database provider coverage - shipped, queued, and declined #424

Description

@cevheri

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 @ bda1cf8 on 2026-08-26, by:

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 (Strategy
Pattern, provider hierarchy) and
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/<family>/<id>.ts

  • docs/providers/<id>.md + tests/integration/db/<id>-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:

Document:

  • mongodb : MongoDB
  • couchbase : Couchbase (HTTP only, zero runtime dep)

Key-value:

  • redis : Redis

Embedded:


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/<family>/<type-id>.ts +
docs/providers/<type-id>.md + tests/integration/db/<type-id>-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 feat(providers): Apache Cassandra over CQL, its numbers left blank rather than faked (#424) #448)" step was measured on that retired denominator.
    On the published one the history is: 27 -> 29 with Vitess and Apache Cloudberry (PR feat(compat): Vitess and Apache Cloudberry, measured on a pinned image (#424) #446,
    fd5e101), 29 -> 32 with AlloyDB Omni, OceanBase and SingleStore (PR feat(compat): four more Phase 0 engines measured, three registered and one refused (#424) #447, 2465c92),
    32 -> 33 with ScyllaDB (PR feat(providers): ScyllaDB is a partial relative, and one absent keyspace costs six surfaces (#424) #455), which moved the relatives half 18 -> 19. PR feat(providers): Apache Cassandra over CQL, its numbers left blank rather than faked (#424) #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 feat(compat): four more Phase 0 engines measured, three registered and one refused (#424) #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.

  • 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/<name>.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.
  • 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
  • CockroachDB - partial, v26.2.5. Editor, errors, performance metrics, slow queries and sessions work; object browser and all size/health panels blank.
  • YugabyteDB - full, 2.25.2.0-b0 (advertises PostgreSQL 15.12). PR feat(compat): four more wire-compatible engines, measured rather than assumed (#424) #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.
  • 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.
  • 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 feat(compat): Vitess and Apache Cloudberry, measured on a pinned image (#424) #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.
  • 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)
  • TimescaleDB - full, 2.29.2 on PostgreSQL 17.11. PR feat(compat): four more wire-compatible engines, measured rather than assumed (#424) #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.
  • Citus - full, citus 14.1-1 on PostgreSQL 18.4. All 15 surfaces answer, but distributed-table row counts and sizes are silently WRONG.
  • 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.
  • Supabase - connects and is usable, same provenance and same limits as Neon above.
  • Materialize - query editor only, 26.37.0. No pg statistics catalog, no size functions, MATERIALIZED is reserved.
  • 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
  • QuestDB - MEASURED AND REFUSED A ROW (2026-08-26, PR feat(compat): Garnet is a full Redis relative, QuestDB is refused a row, and a failed schema read shows the wrong tables (#424) #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.
  • Percona Distribution for PostgreSQL - full, Percona Server for PostgreSQL 18.6.1 on
    PostgreSQL 18.6. Registered 2026-08-26, PR feat(compat): both Percona distributions are full relatives, measured rather than assumed (#424) #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.
  • ParadeDB - full, ParadeDB 0.25.4 on PostgreSQL 18.6. Registered 2026-08-27, PR feat(compat): ParadeDB and OrioleDB are full relatives with opposite costs, Databend is query-only for a reason of ours (#424) #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.
  • OrioleDB - full, OrioleDB beta 16 on PostgreSQL 18.4 (nightly of 2026-08-24). Registered
    2026-08-27, PR feat(compat): ParadeDB and OrioleDB are full relatives with opposite costs, Databend is query-only for a reason of ours (#424) #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):

  • MariaDB - full, 12.3.2. Zero deviation from the MySQL baseline. Only 12.3 probed.
  • TiDB - full, 8.0.11-TiDB-v8.5.1. PR feat(compat): four more wire-compatible engines, measured rather than assumed (#424) #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.
  • 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 fix(providers): every MySQL read went through the prepared protocol, and four more #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.
  • 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.
  • StarRocks - partial, 3.3.22-753696f. PR feat(compat): four more wire-compatible engines, measured rather than assumed (#424) #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 fix(providers): every MySQL read went through the prepared protocol, and four more #476 and the per-panel degradation in PR fix(monitoring): one refused read cost the whole dashboard, and three more #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)
  • 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 '<database>/%' 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 feat(compat): Vitess and Apache Cloudberry, measured on a pinned image (#424) #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
  • Apache Doris - partial, doris version doris-4.1.3-rc02-7126cf65d96 (version() reports
    a fictitious 5.7.99). Registered 2026-08-26, PR feat(compat): Apache Doris is a partial MySQL relative, and the fork we shipped was the wrong half (#424) #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.
  • Databend - query editor only, Databend v1.2.925-patch-11 (advertises MySQL 8.0.90).
    Registered 2026-08-27, PR feat(compat): ParadeDB and OrioleDB are full relatives with opposite costs, Databend is query-only for a reason of ours (#424) #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.
  • Percona Server for MySQL - full, Percona Server for MySQL 8.4.11-11. Registered
    2026-08-26, PR feat(compat): both Percona distributions are full relatives, measured rather than assumed (#424) #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):

  • Valkey - full, 9.1.1. Reports Redis emulation level 7.2.4 in the overview.
  • DragonflyDB - full, df-v1.40.1. Max connections reads 0; sessions show a numeric id instead of a username.
  • 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.
  • Microsoft Garnet - full, Garnet 2.1.5 (advertises Redis 7.4.3). Registered 2026-08-26,
    PR feat(compat): Garnet is a full Redis relative, QuestDB is refused a row, and a failed schema read shows the wrong tables (#424) #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
  • 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:

  • Decide whether Turso / libSQL is worth a type-id (HTTP + its own auth). Decided yes and BUILT - see Phase 5 below (PR feat(providers): libSQL over the Hrana protocol, one type-id for sqld and Turso Cloud (#424) #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:

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:

  • Propagate the verified list to the per-channel LISTING COPY, keeping the number
    identical to the README (see claim discipline above). Done in docs(claims): make every engine count state what it counts, and stop two thirteens colliding (#424) #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.

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • All seven gates, for both type-ids. Gates 1-3 and 5-6 landed with feat(providers): Elasticsearch and OpenSearch, two type-ids over one SQL transport (#424) #429. Gate 7 was the
    one genuinely outstanding
    : feat(providers): Elasticsearch and OpenSearch, two type-ids over one SQL transport (#424) #429 recorded it as "asserted rather than assumed" on the
    grounds that both ids ground through feat(agent): ground plan mode on every engine, from the provider's own schema #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).
  • 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).

  • Provider over the Trino HTTP protocol (nextUri polling). PR feat(providers): Apache Trino over its own client protocol, zero runtime dependency (#424) #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.
  • 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.
  • 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 feat(providers): Apache Trino over its own client protocol, zero runtime dependency (#424) #438 and
    is no longer the cheap next item; it is unscheduled pending that scope.
  • All seven gates. PR feat(providers): Apache Trino over its own client protocol, zero runtime dependency (#424) #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.
  • 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.

  • Apache Cassandra - shipped in PR feat(providers): Apache Cassandra over CQL, its numbers left blank rather than faked (#424) #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 fix(monitoring): five panels answered an absent measurement with a confident zero (#424) #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.
  • ScyllaDB as a wire-compatible label on the Cassandra provider - shipped in PR feat(providers): ScyllaDB is a partial relative, and one absent keyspace costs six surfaces (#424) #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 fix(providers): a ScyllaDB connection could not be created at all, and an Oracle LOB failed the whole query #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.

  • libSQL - shipped in PR feat(providers): libSQL over the Hrana protocol, one type-id for sqld and Turso Cloud (#424) #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 (fix(monitoring): one refused read cost the whole dashboard, and three more #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, Firebird. Native clients and licensing cost more than the persona
    they bring, without JDBC to amortise it. Revisit only on a concrete customer ask.
  • 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.
  • 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

dependenciesDependency version updatesenhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions