This folder holds the prime (canonical) reference for each database provider in LibreDB Studio.
There is exactly one document per provider, named by the provider's canonical type-id and kept
in lockstep with the code (see the tri-sync rule in ../../CLAUDE.md).
| Provider | type-id | Family | Driver | Query language | Reference |
|---|---|---|---|---|---|
| PostgreSQL | postgres |
SQL | pg |
SQL | postgres.md |
| MySQL | mysql |
SQL | mysql2 |
SQL | mysql.md |
| Oracle | oracle |
SQL | oracledb (Thin) |
SQL | oracle.md |
| Db2 LUW | db2 |
SQL | db2-node (native N-API addon, Rust DRDA client) |
SQL (Db2) | db2.md |
| Microsoft SQL Server | mssql |
SQL | mssql |
SQL (T-SQL) | mssql.md |
| SQLite | sqlite |
SQL (embedded) | bun:sqlite (Bun) / node:sqlite (Node) |
SQL | sqlite.md |
| libSQL | libsql |
SQL (SQLite over a network) | none (HTTP: the Hrana protocol, POST /v2/pipeline) |
SQL (SQLite) | libsql.md |
| DuckDB | duckdb |
SQL (embedded, analytical) | @duckdb/node-api (native N-API addon) |
SQL (DuckDB) | duckdb.md |
| Redis | redis |
Key-Value | ioredis |
JSON | redis.md |
| MongoDB | mongodb |
Document | mongodb |
JSON (MQL) | mongodb.md |
| Couchbase | couchbase |
Document | none (HTTP: Query + management REST) | SQL (SQL++) | couchbase.md |
| ClickHouse | clickhouse |
SQL | none (HTTP interface) | SQL | clickhouse.md |
| Apache Druid | druid |
SQL (analytics) | none (HTTP: SQL endpoint) | SQL (Calcite) | druid.md |
| Elasticsearch | elasticsearch |
SQL (search) | none (HTTP: _sql + REST) |
SQL (Elasticsearch SQL) | elasticsearch.md |
| OpenSearch | opensearch |
SQL (search) | none (HTTP: _plugins/_sql + REST) |
SQL (OpenSearch SQL plugin) | opensearch.md |
| Trino | trino |
SQL (federated query engine) | none (HTTP: the client protocol, POST /v1/statement) |
SQL (Trino) | trino.md |
| Apache Cassandra | cassandra |
SQL (wide-column) | cassandra-driver (pure JS) |
SQL-shaped (CQL) | cassandra.md |
| Prometheus | prometheus |
Time series | none (HTTP: the Prometheus HTTP API, /api/v1/*) |
PromQL | prometheus.md |
| Apache Kafka | kafka |
Stream | @platformatic/kafka (pure TypeScript) |
JSON (a read request) | kafka.md |
| etcd | etcd |
Key-Value | @grpc/grpc-js (pure JavaScript, gRPC) |
etcdctl commands (a subset) | etcd.md |
| Oxia | oxia |
Key-Value | @grpc/grpc-js (pure JavaScript, gRPC, the shared gRPC transport) |
oxia client read commands (read-only) |
oxia.md |
| Neo4j | neo4j |
Graph | neo4j-driver-lite (pure JavaScript, Bolt) |
Cypher (read-only) | neo4j.md |
| Milvus | milvus |
Vector | @grpc/grpc-js (pure JavaScript, gRPC) |
Milvus REST v2 requests (read routes) | milvus.md |
| Qdrant | qdrant |
Vector | none, REST over the shared node:http(s) transport |
Qdrant REST requests (read routes) | qdrant.md |
| InfluxDB (InfluxQL) | influxdb |
Time series | none, HTTP over the shared node:http(s) transport |
InfluxQL (read-only) | influxdb.md |
| InfluxDB 3 (SQL) | influxdb3 |
Time series | none, HTTP over the shared node:http(s) transport |
SQL (Apache DataFusion, read-only) | influxdb3.md |
| LibreDB | libredb |
Embedded (Key-Value) | @libredb/libredb |
JSON (command grammar) | libredb.md |
- Filename = canonical type-id (
postgres.md,mssql.md, …), mirroring the source file (src/lib/db/providers/<family>/<type-id>.ts, or a<type-id>/directory when a provider is split across modules, as Couchbase, ClickHouse, Druid, Trino, Cassandra, libSQL, DuckDB, Db2, Prometheus, Kafka, etcd, Neo4j, Milvus, Qdrant, InfluxDB and Oxia are). The official product name (e.g. "SQL Server") is used only in each doc's title and prose. One directory may serve two type-ids —providers/sql/search/iselasticsearchandopensearch, andproviders/timeseries/influxdb/isinfluxdbandinfluxdb3, two classes on two base classes sharing one connection layer — and each type-id still gets its own document, because the tri-sync invariant is per type-id and each doc is the prime reference for its own product's measured behaviour. - Each doc mirrors the code. Every
file:linecitation is verified, and the per-provider triad — code, this doc, andtests/integration/db/<type-id>-provider.test.ts— must stay in sync in the same PR (the provider tri-sync invariant). - Each doc follows the same ~15-section shape: Overview → Architecture → Design decisions → Connection → Query interface → Schema → Monitoring → Maintenance → Capabilities & labels → Error handling → Testing → Usage → Known limitations → References.
These engines have no provider of their own. Each one speaks the wire protocol of a driver
above, so it connects through that driver unchanged — pick the driver's button in the connection
dialog and the engine works. The connection dialog says so too, from the same data
(src/lib/db/compatibility.ts).
A name appears here only after a live probe ran against a real instance of it through the real provider (issue #424, Phase 0). The version column is what that server reported; it is not a supported range. "Connects" is not "supported", so the support column records how much of the product actually worked:
- Full — every introspection surface answered. Caveats may still note data that is present but inaccurate.
- Partial — the editor works; parts of the object browser or the monitoring dashboard are blank.
- Query editor only — SQL runs and nothing else does. Usable, but not as a managed database.
| Engine | Connect as | Support | Probed version | What to expect |
|---|---|---|---|---|
| Percona Server for MySQL | mysql |
Full | Percona Server for MySQL 8.4.11-11 | Probed 2026-08-26 against percona/percona-server:8.4. Behaves as MySQL throughout: 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 (Table does not support optimize, doing recreate + analyze instead; OK, InnoDB's own answer) and Check. version() does not say Percona: it answers a bare 8.4.11-11 and the product name is only in @@version_comment (Percona Server (GPL), Release 11, Revision 57878ff8). The overview reads that comment since #1444 and shows Percona Server 8.4.11-11; before that it could not be told apart from a stock MySQL 8.4 - the same shape as Garnet's row above. One reading that looked like a Percona defect and was NOT: the health panel's slow-query line said Performance schema not available while @@performance_schema was 1 and the slow-query panel itself returned rows. Measured identically on a stock MySQL 26.7.0 in the same pass, so it was ours, not Percona's - since fixed in #512: the health line now carries the same digest rows the panel does (both read one statement), and where the digest table cannot be read at all the line is empty and the panel names the server's reason. See providers/mysql.md. |
| MariaDB | mysql |
Full | 12.3.2-MariaDB-ubu2404 | Behaves as MySQL throughout. The version shown is MariaDB's full build string, passed through as the server gave it. performance_schema ships OFF (@@performance_schema = 0), so the cache-hit, queries-per-second and buffer-pool figures are absent and the slow-query list is empty until the server is started with performance_schema=ON; the tables exist either way, so nothing fails, it simply measures nothing. The schema tree, sizes, row counts, sessions and EXPLAIN FORMAT=JSON come from information_schema and are unaffected. Only 12.3 was probed; the 10.x information_schema surface was not. |
| TiDB | mysql |
Full | 8.0.11-TiDB-v8.5.1 | All surfaces answer. A freshly loaded table reads 0 rows and 0 B until TiDB's background statistics catch up — they correct themselves, with no ANALYZE. Max connections reads 0 (TiDB's default, meaning unlimited) and the connection count beside it reads not published, because TiDB answers SHOW STATUS with 13 rows and none of them is Threads_connected (wire, 2026-09-06). The slow-query panel is always empty (TiDB's slow log lives in information_schema.SLOW_QUERY), and storage stats list a phantom InnoDB entry. Analyze Table answers an OK packet rather than MySQL's report rows, so it reports "ANALYZE completed; the server returned no report (1 warning, see SHOW WARNINGS)" (v8.5.8, 2026-10-04; it crashed with rows.filter is not a function before #1382). The Explain panel renders TiDB's own operator tree since 2026-09-06: TiDB rejects EXPLAIN FORMAT='json', so the provider sends a plain EXPLAIN instead, and the panel drew Projection_3 over TableDual_4 with estRows as the row estimate for a constant SELECT, measured in the browser on 2026-09-06. Probed on a standalone --store=unistore server only; PD + TiKV was not probed. Only Analyze is offered among the maintenance actions since #1387: TiDB v8.5.8 answers OPTIMIZE TABLE with 8200 OPTIMIZE TABLE is not supported and does not parse CHECK TABLE, which the provider reads at connect (2026-10-04). |
| Vitess | mysql |
Full | 8.0.43-Vitess (Vitess 24.0.2); 8.4.6-Vitess (Vitess 24.0.4) | All fifteen surfaces answer. Re-measured on 24.0.4 on 2026-10-04, the object browser was not usable until the fix that day: through vtgate information_schema.SCHEMATA lists the sidecar _vt and the physical shard vt_e2e_0 instead of the keyspace, so the tree showed those two, and every folder failed with VT13001: [BUG] could not find the column 'TABLE_TYPE' on the UNION, vtgate's planner refusing the object count's outer filter. After the fix (vitess/vttestserver:v24.0.4-mysql84) the tree lists the keyspace e2e with its 3 tables and 2 views, counts included, and Analyze Table from Monitoring > Tables addresses the keyspace instead of sending the shard name vtgate refuses (VT05003). On the 25.0.0-SNAPSHOT that the floating mysql84 tag pointed at on 2026-10-04 the Explain panel is unavailable, because the connect-time probe EXPLAIN SELECT 1 names no table and that build refuses it (VT03031); 24.0.4 accepts it. Row counts and total size are exact, checked against the engine (2000 rows read as 2000; 163840 bytes as 163840), and a foreign key is both read back and enforced. Two things do not work. A running query cannot be cancelled: vtgate refuses KILL QUERY with VT07001 and the statement runs to completion (a 5 second SLEEP took its full 5003 ms). And vtgate does not parse Check Table: CHECK TABLE customers is syntax error at position 6 near 'CHECK' on 24.0.4, while Analyze and Optimize are answered, so since #1387 the provider asks the server at connect which maintenance verbs it has and Check Table is no longer offered. Per-index sizes read 0 bytes on the first 24.0.2 probe, because Vitess names the InnoDB table after the physical shard database (vt_probe_0); the size lookup reads that name since 2026-08-23 and on 24.0.4 every index reads its real size (16 KB each for the probe tables), while the table and index statistics name the keyspace you connected to. Setting a session variable can fail where reading it works: SET @@cte_max_recursion_depth is rejected as an unknown system variable, while SELECT @@cte_max_recursion_depth answers 1000. Probed on an unsharded single-shard keyspace only; a sharded keyspace was not probed, and no permission error could be measured because vttestserver grants every login full rights. |
| ParadeDB | postgres |
Full | ParadeDB 0.25.4 on PostgreSQL 18.6 | Probed 2026-08-27 against paradedb/paradedb:0.25.4. All fifteen surfaces answer and the numbers are correct (2000 rows read as 2000, 131072 bytes as 131072), with a foreign key and its indexes read back. What it costs is width, not accuracy: the object browser lists 41 objects for 2 user tables and the index panel 74 rows, because ParadeDB ships nine extensions - pg_search, vector, postgis with its tiger geocoder, pg_ivm, pg_stat_statements and paradedb's own tables. spatial_ref_sys (8500 rows) sits in public beside your tables. Agent plan mode WORKS here, and that result refutes what the width predicted - worth recording, because the expectation was that a wide catalog would refuse the grounding read: the grounding capture reads what the ROLE can see, and a superuser sees 539 non-system columns where an unprivileged role sees 168, the tiger geocoder's 425 not being readable. Under the 200-row ceiling, so a plan run grounded on 21 tables (ctx_c35a) and drafted a LEFT JOIN over the real objects. Connect as a least-privilege role, not as a superuser: as postgres the run fails with the agent cannot run on this database engine: it offers no read-only execution profile, which is the profile refusing a superuser on any PostgreSQL rather than anything about ParadeDB. So the real trigger is a role that CAN read a wide catalog, not an extension that installs one. Slow queries do work here, unlike most PostgreSQL relatives, because pg_stat_statements ships enabled. version() names PostgreSQL only, so the version panel cannot tell this apart from a stock PostgreSQL 18.6 - the release is in the image tag and nowhere the provider reads. |
| Percona Distribution for PostgreSQL | postgres |
Full | Percona Server for PostgreSQL 18.6.1 on PostgreSQL 18.6 | Probed 2026-08-26 against percona/percona-distribution-postgresql:18.6. Behaves as PostgreSQL throughout: all fifteen surfaces answer, the numbers are correct (2000 rows read as 2000 and 131072 bytes as 131072, index 65536), a foreign key is read back with its indexes, and Analyze and Vacuum both work. 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 worth knowing when you are trying to tell a fork from stock. Slow queries are empty until pg_stat_statements is enabled, which is stock PostgreSQL behaviour rather than a Percona property. Nothing else deviates from the PostgreSQL baseline. |
| Citus | postgres |
Full | citus 14.1-1 on PostgreSQL 18.4 | All surfaces answer. Row counts and sizes for a distributed table are wrong, not missing — PostgreSQL statistics describe the empty coordinator parent, not the shards. citus_tables and citus_schemas show up in the browser. |
| OrioleDB | postgres |
Full | OrioleDB beta 16 on PostgreSQL 18.4 (nightly of 2026-08-24) | Probed 2026-08-27 against orioledb/orioledb:pg18-nightly-20260824-cc35a80-ubuntu. Read this row 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 (2000 read as 2000, 122880 bytes), 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 in the browser and the table statistics, because pg_indexes_size() returns 0 for an OrioleDB table (measured directly: 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. That second one is an absence honestly rendered, not a wrong number. Gate 7 passes under a least-privilege role: 27 visible columns, a plan run grounded on 6 tables (ctx_6b5e) and drafted a LEFT JOIN over the real objects - and as a superuser it fails on the same profile refusal ParadeDB's row describes. Verified before trusting the row that the fixture actually measures OrioleDB: default_table_access_method is orioledb and both probe tables report amname = orioledb, so a heap-table probe measuring plain PostgreSQL was ruled out. version() does name OrioleDB - the one thing ParadeDB's does not - and carries the build hash and date, which is also the caveat: the project publishes nightly images only, so there is no release tag to pin and this row describes one dated build. |
| TimescaleDB | postgres |
Full | TimescaleDB 2.29.2 on PostgreSQL 17.11 | All surfaces answer. A hypertable's row count and size are wrong, not missing — the statistics describe the empty parent table, not the chunks. The object browser now lists only user tables — TimescaleDB's own seven schemas are excluded, so a database whose sole hypertable had 31 chunks went from 61 objects to the 2 the user created (34 from _timescaledb_internal, 22 from _timescaledb_catalog, 3 from _timescaledb_cache). Measured against the extension's own hypertable_size(), the parent-table size reads 24576 bytes where the true figure is 2498560. The overview shows PostgreSQL's version, not the extension's. The agent grounds a stock install — the grounding read aggregates columns per table instead of refusing past the 200-row budget, and every chunk still appears as its own table in the inventory. |
| YugabyteDB | postgres |
Full | YugabyteDB 2.25.2.0-b0 (advertises PostgreSQL 15.12) | All surfaces answer, foreign keys included. Row counts and sizes read 0 until you run ANALYZE — nothing collects statistics automatically, so a full database looks empty. Index sizes always read 0 bytes (index storage lives in DocDB) and the overview's database size reads 0 bytes. Index types read lsm, which is the real storage rather than a misreading. Reindex is not offered since #1387: YugabyteDB 2026.1.2.0 refuses both REINDEX forms with 0A000 REINDEX not supported yet, which the provider reads at connect; Vacuum stays offered and its result quotes the server's NOTICE that VACUUM is a no-op there (2026-10-04). |
| AlloyDB Omni | postgres |
Full | PostgreSQL 17.9 (AlloyDB Omni 17.9.0) | All fifteen surfaces answer, and the numbers are exact: 2000 rows read as 2000 and 270336 bytes as 270336 (180224 table plus 90112 index), with a foreign key both read back and enforced by the engine. The version panel cannot be told apart from a stock PostgreSQL 17 - version() reports PostgreSQL 17.9 on x86_64-pc-linux-gnu and names AlloyDB nowhere; the product is identifiable only from the alloydb.* settings and the image tag. The object browser now lists only user tables, excluded by ownership rather than by name: pg_depend is asked which schemas an extension created, which catches google_ml and ai where a name list had caught only the first, and can never hide a schema a user made themselves. The count went from 10 objects to 2. That count still understates what is installed: outside the system schemas there are 70 objects, because 49 extension views live in public itself (g_columnar_*, google_db_advisor_*, hypopg_list_indexes and more), which the browser hides only because its schema query filters table_type = 'BASE TABLE'. A role with no grants at all - LOGIN plus CONNECT, ALL revoked on public - still lists those google_ml tables and reads them (SELECT count(*) FROM google_ml.supported_vertex_models answered 15 to it). The slow-query panel is empty because pg_stat_statements ships with the image but is not installed in it, which health reports honestly. The columnar engine is off by default and needs ALTER SYSTEM plus a restart; with it on the on-disk sizes stay exact but do not count the columnar copy. The agent needs a least-privilege role to ground a run — the image's own postgres superuser is refused as too broad; with a least-privilege role the capture succeeds (the grounding read aggregates columns per table), and the inventory includes the 49 extension views installed into public. Probed on the 17.9.0 image only; the 15.x and 16.x lines were not. |
| Valkey | redis |
Full | Valkey 9.1.1 | Behaves as Redis. The overview shows Valkey 9.1.1 (Redis 7.2.4), its own version labeled ahead of the Redis compat level. |
| DragonflyDB | redis |
Full | DragonflyDB df-v1.40.1 | Overview shows Dragonfly df-v1.40.1 (Redis 7.4.0). Max connections reads the limit Dragonfly publishes under the underscored max_clients; every active session's user reads default, whatever ACL user authenticated (CLIENT LIST publishes no user= field), and every session reads idle/N (no cmd= or flags= field). |
| KeyDB | redis |
Full | KeyDB 6.3.4 | Publishes no version field of its own, so the overview is indistinguishable from a Redis 6 server. A session's command can appear without its subcommand. |
| Garnet | redis |
Full | Garnet 2.1.5 (advertises Redis 7.4.3) | Probed 2026-08-26 against ghcr.io/microsoft/garnet:2.1.5. Every Redis surface answers - the key browser grouped 71 keys into user:*, session:* and queue:*, sessions list the connection, and Key Info (the Analyze action) returned 129 metrics. The overview shows Garnet 2.1.5 (Redis 7.4.3): INFO carries garnet_version:2.1.5 and server_name:garnet beside redis_version:7.4.3, and the provider labels the version with the former, the same as it does for Valkey and DragonflyDB. Two numbers are absences wearing a value, both from INFO fields Garnet does not publish: there is no used_memory, so the database size and the memory storage panel read 0 B; and there are no keyspace_hits / keyspace_misses, so the cache hit ratio reads 100% - the : 100 fallback recorded as D14. Measured side by side with Redis 8.10.0, which publishes all three. Max connections reads 0: unlike Dragonfly, Garnet's INFO carries no client-limit field under either name at all. connected_clients stays 0 even with a client attached, though the session list itself is right. Browser-verified 2026-08-26 on the built app: the tree read user:* 50, session:* 20, queue:* 1, a {"command":"GET","args":["user:1"]} run returned the value, and Monitoring showed 7.4.3, Connections 0 / no limit published, Tables 71 / 0 indexes, DB Size 0B and Cache Hit 100.0% rated Excellent - the D14 fallback being graded, which is the sharpest form of this row's warning. Fixture note: Garnet keeps everything in memory and publishes no persistence, so a docker stop empties it - measured, dbsize 71 before and 0 after a restart. |
| FerretDB | mongodb |
Full | FerretDB 2.7.0 (MongoDB 7.0.77 wire) | Every monitoring surface answers. Sign in with the backend PostgreSQL credentials — authMechanism=PLAIN is rejected. The version shown is the advertised MongoDB wire version. Needs its own backend, so it is two containers. listDatabases is retried without authorizedDatabases, a field FerretDB refuses as unknown. |
| StarRocks | mysql |
Partial | StarRocks 3.3.22-753696f | The editor, the table list, column metadata, table and storage stats, performance metrics and slow queries all work. The version reads MySQL 5.1 — version() returns a fictitious 5.1.0 and the real build is only in current_version(). Re-probed 2026-08-24 after every parameterless statement moved to MySQL's text protocol: getOverview() and getPerformanceMetrics() now succeed through the provider, and until 2026-09-06 the user still saw none of it (the re-measurement below). The active-session read is an engine gap of its own - Unknown table information_schema.PROCESSLIST - and it used to discard the whole dashboard with it. Since 2026-08-24 each panel is read independently: re-measured 2026-08-24 through the provider, getMonitoringData() returns overview, performance, slowQueries, tables, indexes and storage, with errors.activeSessions carrying StarRocks' own sentence, and the sessions panel alone shows it. Health fails on the same missing table and the badge reads Slow, measured in the browser. What the protocol change recovered on 2026-08-24 was the provider layer, not a panel; the Overview panel followed on 2026-09-06 (below). Row counts read 0 for a freshly loaded table until StarRocks' own background statistics collector catches up - re-measured 2026-09-16, a 5-row table read 0 for 45s on 4.1.4 and for roughly 4.5 minutes on 3.3.22 before both answered 5, so this is a delay rather than a gap. Sizes used to read 0 regardless of that wait: StarRocks answers INDEX_LENGTH NULL for every BASE TABLE, never a real number the way Doris's 0 is, and DATA_LENGTH + INDEX_LENGTH in SQL is NULL-poisoned by that alone even once DATA_LENGTH itself is real - fixed 2026-09-16 to DATA_LENGTH + COALESCE(INDEX_LENGTH, 0) everywhere this provider sums the two, so a table's size now tracks its row count on the same collector delay instead of staying absent. No index is reported at all, which is a real gap: information_schema.STATISTICS stays empty even for a table with a BITMAP index created and confirmed live. Re-measured in the browser on 2026-09-06: the Overview panel no longer fabricates a reading, it shows Connections N/A, not published where it used to show 0/151, because StarRocks answers a bare SHOW STATUS and SHOW VARIABLES LIKE 'max_connections' with zero rows each; health still fails on the missing PROCESSLIST, which is the engine's own. The Explain panel renders StarRocks's own text plan on the same date: EXPLAIN FORMAT='json' still does not parse, so the provider sends a plain EXPLAIN, which answered 14 rows and drew a 13-node tree for a constant SELECT. Transactions, measured 2026-10-04: BEGIN, COMMIT and ROLLBACK work and a ROLLBACK discards the rows, but StarRocks reports no transaction state in the MySQL status flags, so the editor says it cannot verify the transaction and SANDBOX is refused (mysql.md section 6.0.1). |
| Apache Doris | mysql |
Full | Apache Doris 4.1.3-rc02-7126cf65d96 (version() reports 5.7.99) |
Probed 2026-08-26, re-measured 2026-09-06. All fifteen surfaces answer since 2026-09-06, which is what moved this row from Partial to Full. The two that used to fail shared one cause that was ours: the overview and health panels sent SHOW STATUS LIKE '...', a parse error in the Doris grammar (mismatched input 'LIKE') while a bare SHOW STATUS is accepted and answers zero rows. The panels read the bare form since 2026-09-06, so both render, and the Overview publishes an absence rather than a number: uptime N/A and Connections N/A, not published, because Doris publishes no Uptime and no Threads_connected row and no max_connections variable (browser, 2026-09-06). Read this row against StarRocks above, which is a fork of Doris, because the numbers are where they part company. Doris reports the truth: the object surface read 3 rows / 2.65 KB and 2000 rows / 9.95 KB against a ground truth of 3 and 2000 (SELECT count(*)), and getTableStats() read 10187 bytes against SHOW DATA's 9.948 KB, where StarRocks reports hard zeros. The lag is the catch: immediately after the 2000-row insert every surface read 0 rows and 0 B, an ANALYZE TABLE ... WITH SYNC in between changed nothing, and the true numbers appeared about a minute later on their own - the TiDB shape, self-correcting, so a freshly loaded table looks empty for a while. What it shares with the fork: version() itself still answers the fictitious 5.7.99, and unlike StarRocks there is no current_version() to fall back to either - the real build lives only in @@version_comment (doris version doris-4.1.3-rc02-7126cf65d96), which the provider now reads for the overview's version field, so the panel correctly shows Apache Doris 4.1.3-rc02-7126cf65d96 instead of the fictitious number; no index is ever reported, information_schema.statistics being empty; and EXPLAIN FORMAT='json' does not parse, so the provider sends a plain EXPLAIN here instead and the Explain panel renders Doris's own Nereids plan: 18 rows and a 13-node tree for a constant SELECT, with the raw tab showing the plan text verbatim (browser, 2026-09-06). A foreign key is invisible AND unenforced: ADD CONSTRAINT ... FOREIGN KEY is accepted and SHOW CONSTRAINTS lists it, but information_schema.KEY_COLUMN_USAGE is empty (0 rows), so the ER diagram draws nothing - and an order row referencing customer 424242 inserted successfully, because the constraint is a planner hint there. A UNIQUE KEY table also declares no primary key to the product: COLUMN_KEY reads UNI, not PRI. What works that a reader would not assume: cancellation genuinely cancels (an 8-second SELECT sleep(8) died at 1513 ms with Doris's own cancel query by user message, where Vitess cannot cancel at all), active sessions and the monitoring dashboard answer through information_schema.processlist, storage stats answer, permission errors arrive as the right class with the server's sentence intact and a SELECT_PRIV role saw only its granted table, and Analyze works - Optimize and Check do not exist in the grammar at all. getPerformanceMetrics() answers {}: the object is empty rather than absent, because Doris's SHOW STATUS publishes no counters. What the panels actually show, verified in Chrome against the built app on 2026-08-26 (the provider's boundary is not the product's, which is what U14 cost us on Cassandra): the header badge read Slow and the Monitoring Overview tab carried This database could not answer this panel with Doris's own mismatched input 'LIKE' sentence beneath it - one panel, not the dashboard. Re-measured in the browser on 2026-09-06 the badge reads Online and that tab renders, showing Apache Doris 4.1.3-rc02-7126cf65d96, uptime N/A and Connections N/A, not published. Every other tab answered on 2026-08-26 already: Tables reads 2 tables / 2.0K rows / 12.60 KB with probe_orders at 2.0K / 9.95 KB and probe_customers at 3 / 2.65 KB, Storage shows DB Size N/A above a populated tablespace and largest-table list, Sessions lists 7, and Performance, Queries and Pool read N/A with Not measured / not available rather than a fabricated zero. The object browser shows the row count beside the table (probe_customers 3). One reading to treat with suspicion: the Tables card summarises Vacuum 0 / OK on an engine that has no vacuum-like operation at all - the numbers below it are correct, but that card is the U14 shape and is recorded here rather than claimed as healthy. Creating the connection took two clicks of Establish Connection, by design: Test Connection answered Connected, but this server answered no health data: ..., and the first Establish reported that degraded reading and stored nothing, the second stored it (the D9 contract - before that fix an engine whose health failed could not be saved at all). Since health answers, one click is enough: Test Connection reads Connection successful and the first Establish stores the connection (browser, 2026-09-06). Gate 7 passes: a plan run captured 2 tables (ctx_adf7) and drafted a LEFT JOIN over the real probe_customers / probe_orders, which then ran unmodified and answered TR 667, DE 667, US 666. Transactions, measured 2026-10-04: BEGIN, COMMIT and ROLLBACK work and a ROLLBACK discards the rows, but Doris reports no transaction state in the MySQL status flags, so the editor says it cannot verify the transaction and SANDBOX is refused (mysql.md section 6.0.1). |
| OceanBase | mysql |
Partial | 5.7.25-OceanBase_CE-v4.4.2.1 | Fourteen of the fifteen surfaces return without throwing, but only twelve of them do their job, and that gap is what makes this partial rather than full: performance metrics and storage stats are answered-but-useless, and the overview, table statistics and index statistics carry useless zeros in their size fields while their row and structure data is real. The slow-query panel read 0 rows here, an honest empty rather than a fabricated number; since #512 it shows the tenant's own ERROR 1049 instead, because getSlowQueries() no longer swallows an unreadable source. Health is the one hard failure - the tenant has no performance_schema database at all (ERROR 1049 Unknown database 'performance_schema'), and health passes on the MySQL 26.7.0 baseline probed in the same pass, so this is the engine's. One consequence is visible before any panel is opened: the header badge reads Slow rather than Online, which is not latency - the badge is set from whether the health request succeeded, and health is exactly what this engine refuses. Every size reads 0 B - table, index, database and storage - because information_schema.TABLES reports DATA_LENGTH 0 and INDEX_LENGTH 0; storage stats also list a phantom InnoDB row at ibdata1:12M:autoextend whose size reads N/A, OceanBase faking innodb_data_file_path for compatibility with no such file behind it. Row counts are correct once ANALYZE TABLE has run (2000 for 2000), and it must be the MySQL-mode statement - the Oracle-mode ANALYZE TABLE t COMPUTE STATISTICS is rejected with ERROR 1235. Analyze and Optimize Table answer an OK packet rather than MySQL's report rows, so both report "completed; the server returned no report" (4.4.2.1, 2026-10-04; both crashed with rows.filter is not a function before #1382); Check Table answers rows. The object browser is clean, 2 objects for 2 user tables: the schema query scopes to TABLE_SCHEMA and to TABLE_TYPE = 'BASE TABLE', and OceanBase's own 860 + 70 + 18 catalog objects are all SYSTEM TABLE or SYSTEM VIEW, so neither filter admits them. A foreign key is both read back and enforced (an orphan insert is refused with ERROR 1452), but no backing index is created for one, so index counts will not match an equivalent MySQL schema. Cancelling genuinely cancels, and Explain genuinely describes without executing (an 11-row ASCII plan with an EST.ROWS column). Sign in to the business tenant, not sys: MODE=mini creates a user tenant named test, so the login is root@test; the sys tenant shows nine databases including Oracle-mode artifacts (LBACSYS, ORAAUDITOR, SYS, ocs, sys_external_tbs) that a user tenant does not have. Plan mode grounds a run here, through the provider-inventory path. Probed on the 4.4.2-lts image with MODE=mini only. |
| SingleStore | mysql |
Partial | SingleStoreDB 9.1.1 (advertises MySQL 5.7.32) | Ten of the fifteen surfaces answered when this engine was first probed, and every one of them answers since 2026-09-06 (the Explain panel was the last, below); the row stays Partial because the numbers the statistics panels show are zeros rather than measurements. Four of the five failures were ours, not SingleStore's, and were fixed first - Test Connection, health, the overview and the monitoring dashboard all failed with the same engine message, This command is not supported in the prepared statement protocol yet, because the provider sent every statement through mysql2's binary prepared protocol. Every parameterless statement moved to the text protocol on 2026-08-24, and all four were re-probed in the browser on the built app: the header badge reads Online, the fleet card reads 152ms / 4 conn, and Monitoring renders MySQL 5.7.32, an uptime, Connections 7/100000 and Tables 2 / 2 indexes. The Storage panel renders too, all zeros, which is this engine's own zeroed information_schema.TABLES below and not a failure of ours. The Explain panel was not among them, and is fixed since 2026-09-06: EXPLAIN FORMAT=JSON is ER_PARSE_ERROR here on BOTH protocols, because SingleStore's grammar is EXPLAIN JSON - a statement problem wearing a protocol problem's message. The provider now measures which EXPLAIN grammar the server accepts when it connects and sends a plain EXPLAIN here, so the panel renders SingleStore's own text plan; measured in the browser on 2026-09-06, one row for a constant SELECT. Row counts and sizes are missing rather than wrong: a 2000-row table reads rowCount 0 and 0 B in the object browser, the table statistics and the storage panel, against a ground truth of 2000 rows and 77046 bytes measured four independent ways (SELECT count(*), SHOW TABLE STATUS, information_schema.OPTIMIZER_STATISTICS.ROW_COUNT, and the EXPLAIN plan's est_table_rows). SingleStore leaves information_schema.TABLES zeroed and keeps the real numbers elsewhere, and running ANALYZE does not change what the panels read, so a populated table looks empty. The index panel lists 4 rows for 2 tables against the baseline's 2, because SingleStore auto-creates a shard key on every table and reports it as an index named __SHARDKEY with index type SHARD, beside PRIMARY. Foreign keys do not exist: ALTER TABLE ... ADD FOREIGN KEY fails with ERROR 2752, and with SET GLOBAL ignore_foreign_keys = ON a CREATE TABLE carrying an inline foreign key is accepted and the constraint silently stripped - the one shape where you could believe you have a key you do not. Of the maintenance actions Analyze always worked; Optimize and Check failed with the same prepared-statement error and both succeed on the text protocol, measured through the provider (the app offers only Analyze as a global action here, so that pair was not re-probed from the UI). Permission errors are identical to MySQL, with the server's own text intact, and under a SELECT-only role the table list correctly showed only the granted table. The version the overview displays reads MySQL 5.7.32, the wire version SingleStore advertises, not SingleStoreDB 9.1.1, which is what that panel showed once it rendered (browser, 2026-08-24). No licence key is needed - the dev image self-licenses with ROOT_PASSWORD as the only variable set, which is what issue #424 recorded as the reason this engine had gone unprobed. Plan mode grounds a run here, through the provider-inventory path. Probed on the 0.2.82 dev image only. |
| CockroachDB | postgres |
Partial | CockroachDB CCL v26.2.6 | Editor, error handling, performance metrics, slow queries and sessions all work. The object browser now lists real tables (re-probed after #38680; previously blank): the schema query recovers from the missing pg_total_relation_size() builtin by falling back to an unmeasured (0-byte) size rather than failing outright, and foreign keys/indexes were never affected by that gap. The overview/health size fields stay unavailable rather than blank-crashing: pg_size_pretty(), pg_postmaster_start_time() and pg_tablespace_location() do not exist there. Its table and index counts are the user's own — crdb_internal objects reach pg_tables although information_schema's BASE TABLE filter never shows them, so before those two schemas were excluded the overview counted 98 tables (93 crdb_internal, 3 pg_extension) for the 2 the browser listed. Maintenance offers Analyze on one table and nothing else since #1387: CockroachDB v26.3.2 has no VACUUM, no REINDEX and no bare ANALYZE (each 42601), and the provider now asks the server at connect which statements it has instead of offering PostgreSQL's whole set, whose clicks answered HTTP 500 (2026-10-04). |
| Apache Cloudberry (incubating) | postgres |
Partial | PostgreSQL 14.4 (Apache Cloudberry 2.1.0-incubating) | Thirteen of the fifteen surfaces answer. Table statistics and index statistics both fail with one engine error, query plan with multiple segworker groups is not supported, which is Cloudberry's MPP planner restriction rather than a version gap. The monitoring dashboard itself is not one of them: its overview and performance tabs read connections, database size, table count, deadlocks and checkpoint stats normally, and it is specifically the Tables tab's per-table breakdown and the Storage tab's largest-tables list that fail. Row counts and sizes after ANALYZE are correct (2000 rows for 2000; 576 KB for 589824 bytes), so this is not the kind of engine whose statistics mislead; what they read before ANALYZE was not probed. The object browser now lists only user tables — pg_ext_aux (the PAX auxiliary tables), gp_toolkit, pg_aoseg and pg_bitmapindex are excluded, so the count went from 4 objects to 2; Cloudberry's schema documentation lists the last three but not pg_ext_aux, which is excluded on measurement rather than on the doc's authority. The overview's database size reads 62 MB against roughly 900 KB of user tables. A foreign key is read back as if enforced and is not: Cloudberry accepts the constraint with a warning that it will not enforce it, and an orphan insert then succeeds. The agent needs a least-privilege role to ground a run — the usual gpadmin login is refused because the execution profile reads a superuser as too broad; with a least-privilege role the capture succeeds (the grounding read aggregates columns per table). What the run reports for the first of those is that the engine offers no read-only execution profile, which describes the role rather than the engine (B47). The two failing panels each say "This database could not answer this panel" and print the engine's error under it, so the planner restriction is attributed to the statement rather than to the connection. Apache publishes build images only, so the probe ran on a third-party image. |
| ScyllaDB | cassandra |
Partial | ScyllaDB 2026.2.4-0.20260810.e54224b8cebb (advertises Cassandra 3.0.8) | All thirteen surfaces this provider offers now answer - thirteen rather than the fifteen this column counts elsewhere, because the Cassandra provider offers neither cancellation nor EXPLAIN on either engine - and the row is still Partial because five of them answer empty. Re-probed 2026-08-24 through createDatabaseProvider({type:"cassandra"}), surface by surface, after the 2026-08-24 change: the overview, health, performance metrics, active sessions and the monitoring dashboard read Cassandra's system_views virtual tables, ScyllaDB has no system_views keyspace at all, and all five used to come back with the same verbatim Keyspace system_views does not exist. They now degrade to empty the way a denied grant does, so Test Connection passes and the connection dialog can create a ScyllaDB connection - before that change it could not, because Establish Connection is gated on that same health request, and one had to arrive seeded or admin-managed. What the degradation costs: no cache hit ratio and no active-session list (both N/A). Connections now reads N/A with "not published" beneath it, not a fabricated 0 - DatabaseOverview.activeConnections and HealthInfo.activeConnections are both optional as of 2026-08-24, the provider omits the key rather than send a zero nobody measured, and the same absence reaches the fleet card (which drops the connection chip entirely) and the agent's curated health reading (null). Verified in the browser on the built app: Connections N/A / not published on ScyllaDB against 1 / no limit published on Apache Cassandra 5.0.9 in the same pass. The version, uptime, table count and index count are real: measured Apache Cassandra 3.0.8, 23.76m, 3 tables, 1 index. Apache Cassandra 5.0.9, probed in the same pass, answers all thirteen with data in every one. The SQL editor and the object browser work in full: statements run, and every one of 18 CQL types read back byte-identically to that Cassandra baseline, bigint 9007199254740993, decimal 1.25, duration 3h20m, varint, blob, inet, date, time and the collections included. The version panel reads Apache Cassandra 3.0.8 - the compatibility number system.local publishes - and not ScyllaDB 2026.2.4, which lives in system.versions where the provider does not look. The object browser lists one extra object per secondary index, and the tree and the overview disagree about it: ScyllaDB backs an index with a materialized view, so customers_country_idx_index is in system_schema.views - which the tree reads - and not in system_schema.tables, which the Tables count reads (measured: 4 objects in the tree against tableCount 3). Cassandra lists neither. Error classes are identical to Cassandra even though the server's wording is not - a missing table is unconfigured table no_such_table rather than table no_such_table does not exist, a missing column Unrecognized name nope rather than Undefined column name nope in table probe.customers - because the provider classifies on the driver's error code and not on the message text. Row counts and sizes are blank for the same reason as on Cassandra, and the panels read N/A rather than a fabricated zero. Creating a keyspace needs NetworkTopologyStrategy on the 2026.2 line: SimpleStrategy is refused outright with SimpleStrategy doesn't support tablet replication, so the setup recipe in cassandra.md §10 does not run unchanged; 2025.1 still accepts it, with a warning. ScyllaDB 2025.1.14-0.20260612.103b84070f3b was probed in the 2026-08-21/22 pass and behaved identically on every surface, including the same verbatim refusal the fix keys on, so this row describes both the 2025.1 and the 2026.2 line - but only those two builds, only a single-node container, and only 2026.2.4 was re-probed after the fix. |
| VictoriaMetrics | prometheus |
Partial | VictoriaMetrics v1.152.0 (advertises Prometheus 2.24.0) | Probed 2026-09-23 against victoriametrics/victoria-metrics:v1.152.0, a single node scraping the compose fixture's targets with -promscrape.config, through createDatabaseProvider({ type: "prometheus" }), with Prometheus 3.13.3 probed in the same pass as the baseline. Every surface the provider offers answers as it does on Prometheus except the ones named here, and a surface counts as answering only where it passed and held data wherever Prometheus did, so an empty folder does not count, and neither does a monitoring read or an object count with a failed panel or count: 20 of the 36 surfaces outside the editor answer here, where Prometheus answers all 36. The editor runs PromQL: instant vectors, range selectors, subqueries and scalars answer, the three refusals end as they do on Prometheus, and Cancel ends a running query. The Overview and Storage tabs and the Scrape pools folder fail: /api/v1/status/runtimeinfo, /api/v1/status/flags and /api/v1/scrape_pools answer 400 unsupported path requested, and the message shown names the path and the status, with a server that does not serve the path among the causes it offers. Three documents lack a member Prometheus sends, and are read without it: the TSDB status has no headStats and cuts its lists at topN rather than limit, which the provider sends as well, so the Tables tab lists up to fifty metrics as it does on Prometheus; a metadata entry has no unit, so a metric's Source tab shows its type and help alone; and a target has no scrapeInterval or scrapeTimeout, so a target's Source tab leaves both out, while its discovered labels carry them as __scrape_interval__ and __scrape_timeout__. A target not yet scraped reads as down: VictoriaMetrics reports it with health down and a last scrape of 1970-01-01T00:00:00Z, where Prometheus reports unknown. The rule folders are empty: a single-node server evaluates no rules and answers /api/v1/rules from vmalert only when it is started with -vmalert.proxyURL. A string expression returns no rows: "libredb" answers an empty vector. A subquery's points are counted back from its evaluation time: avg_over_time(up[5m])[30m:1m] answered 31 points ending at that time, where Prometheus answers 30 on whole minutes. No PromQL info or warning appears: none came with any answer measured. /api/v1/status/buildinfo answers 2.24.0, the Prometheus version VictoriaMetrics advertises for Grafana, and its own build is only in its /metrics, as vm_app_version; no panel shows either while the overview fails. Claimed for PromQL only: nothing written in MetricsQL, its own query language, was measured. Details in prometheus.md. |
| Redpanda | kafka |
Full | Redpanda v26.2.2 | Probed 2026-09-25 against redpandadata/redpanda:v26.2.2, a single node seeded by the Kafka fixture's own docker/kafka/seed.sh and docker/kafka/seed-binary.ts, through a real KafkaProvider run by tests/live/kafka-read-only.ts --redpanda, with Apache Kafka 4.3.1 probed in the same pass as the baseline. Every surface answers, with data wherever Kafka held data: the counts, the Topics, Consumer Groups and Brokers folders and their sources, a read in every from form, the four codecs (each stored codec checked from the fetched batches), the transactional topic with no marker row, the refusals of a missing topic, an out-of-range offset and an internal topic, the three monitoring panels, the group listing and each group's lag against rpk's own, and three reads at once; the broker's state, read with rpk before and after the run, was unchanged. Four things read differently from Kafka. Max connections reads 0, no limit published, because a broker's DescribeConfigs answer holds no max.connections; that answer holds nine entries where Kafka's holds 340, so a broker's Source tab is short; the Storage tab has no usage percentage, because DescribeLogDirs reports no total or usable bytes; and every group reads as classic, which is right, since Redpanda answers ListGroups up to v4, which carries no group type, and has no KIP-848 consumer protocol. |
| Databend | mysql |
Partial | Databend v1.2.925-patch-11 (advertises MySQL 8.0.90) | Probed 2026-08-27 against datafuselabs/databend:v1.2.925-patch-11, re-measured 2026-10-04 on that image, on v1.2.925-patch-13 and on :latest (1.2.881), through the provider and in a browser against the built app. SQL runs - a 2000-row count(*), a GROUP BY and a plain EXPLAIN all answer, the last with Databend's own TableScan plan. The object browser works since 2026-10-04, which is what moved this row from Query editor only to Partial. Databend implements no prepared statement at all (Prepare is not support in Databend., errno 1105, even for a statement with no parameter), and every read the provider issued with a placeholder used mysql2's prepared protocol, so the table list, the schema and the table, index and storage statistics all failed over catalogs that answer in full when asked with literal SQL. The provider now measures at connect that the server refuses to prepare and reads back exactly a literal holding every character an escaper would touch, and on such a server writes a statement's values into the text instead (mysql.md §3.4). Measured after that: the tree lists default and system (49 tables) with their tables and views, a table or a view expands to its columns and opens, table and storage statistics answer with the true 3 rows and 748 bytes, and an inline edit of a VARCHAR to it's \ edited was saved and read back exactly. What stays unavailable is Databend's own: there is no information_schema.ROUTINES, TRIGGERS or EVENTS, so the Stored Procedures, Functions, Triggers and Events folders show its UnknownTable sentence while Tables and Views count; there is no SHOW STATUS statement at all (unexpected STATUS. Did you mean SHOW STAGES...), so the overview and health panels have no source and Establish Connection asks for a second click to save; there is no information_schema.processlist and no performance_schema, so sessions and slow queries have no source either; the index statistics read is a parse error there (GROUP_CONCAT(... ORDER BY ...) is not in its grammar) and statistics is empty anyway, so no index is ever reported; foreign keys are absent (key_column_usage is empty); and Databend reports no transaction state in the MySQL status flags, so BEGIN, COMMIT and ROLLBACK work and a ROLLBACK discards the rows, but the editor says it cannot verify the transaction and SANDBOX is refused (mysql.md section 6.0.1, measured 2026-10-04). Also its own: EXPLAIN FORMAT='json' does not parse, and OPTIMIZE and CHECK do not exist. ANALYZE answers an OK packet, not MySQL's report rows, so Analyze Table reports "ANALYZE completed; the server returned no report" (measured on v1.2.925-patch-13 2026-10-04; it crashed with rows.filter is not a function before #1382). Re-measured in the browser on 2026-09-06: the Explain panel renders Databend's own text plan, 5 rows, because the provider asks the server which EXPLAIN grammar it accepts and falls back to a plain EXPLAIN here. One authoring trap worth knowing: Databend follows the SQL standard on quoting, so "TR" is an identifier and only 'TR' is a string. Browser-verified 2026-08-27: the editor really does run - SELECT country, count(*) ... GROUP BY country returned DE 1, TR 1, US 1 from the grid - which is the check QuestDB failed. Agent plan mode was not re-measured: on 2026-08-27 it drafted nothing because the schema read failed, and that read now answers. |
| Materialize | postgres |
Partial | Materialize 26.40.0 | Re-probed after #38680: the object browser now lists tables and columns (previously nothing but the editor worked). The object reads carry none of the constructs Materialize refuses or lacks - no MATERIALIZED CTE hint, no pg_total_relation_size(), and jsonb_agg()/jsonb_build_object() rather than the json_ forms it does not have (#1075) - and retry once without information_schema.constraint_column_usage, recovering real data instead of failing outright. Foreign keys and primary keys are then empty because Materialize has neither - CREATE TABLE refuses both, and its table_constraints, key_column_usage and referential_constraints shims all answer zero rows. The fallback exists because constraint_column_usage is not implemented at all (fourteen information_schema views ship, and that is not one of them, at HEAD as well as at the probed release), so the query would fail before reaching that empty answer. Materialized views are listed beside tables, with their columns: Materialize reports them as table_type = 'MATERIALIZED VIEW' and they are its central object, so listing only BASE TABLE hid the product (no other engine emits that type - measured on all six). All seven monitoring tabs render: three panels are absent rather than empty and show Materialize's own sentence under "This engine does not publish this" instead of an error, because the message names a pg_ object that is simply not there. Row counts are blank rather than zero - pg_class.reltuples answers -1 there for every relation, so there is no estimate to show; it used to read 0, which claimed a measurement nobody made. The monitoring dashboard now loads too, every statistic reading unavailable rather than the whole page erroring: no pg statistics catalog, no size functions. Functions and Procedures are the two object-tree folders that stay closed (object search, the inventory and the agent's grounding still fail on them, D226), each showing Materialize's own column "p.prokind" does not exist: its pg_proc has no prokind, and it refuses with XX000 rather than 42703, which until 2026-10-04 cost every folder in every schema because the fallback was keyed on 42703 (measured on v26.44.1; postgres.md §3.1.4). |
| RisingWave | postgres |
Partial | RisingWave 3.0.4 | Re-probed after #1075: expanding a table, a view or a materialized view shows its columns, with their types and in the table's own order, through describeObject(), describeObjects() and a bounded describeObjects() alike, measured 2026-09-24 against a live 3.0.4. This row used to say the json gap was the engine's, and it was ours: RisingWave has no json type and refused the json_agg(), json_build_object() and '[]'::json the reads were built with, while it answers every jsonb form, which the reads use now. Two more of the statement's own constructs sat behind it and are repaired too: an index's column list built by a subquery inside an aggregate, which RisingWave refuses as subquery inside aggregation calls, and a default read through pg_get_expr(), which RisingWave answers with an empty string where PostgreSQL answers NULL. Nullability and defaults are what its catalog says, and it says neither: pg_attribute.attnotnull is false for every column, so a NOT NULL column and a primary key both read nullable, and pg_attrdef is empty, so no column shows a default; the nullability half is D119. Foreign keys are empty because RisingWave has none (REFERENCES is refused in both forms). The primary key is listed as an index named after its table, and an index lists every column pg_index.indkey names, which is the key columns followed by every column the index carries: an index on (amount, customer_id) of a four-column table reads as amount, customer_id, order_id, note. The object browser lists tables and materialized views. It was unavailable until the listing learned to drop pg_class.reltuples, which RisingWave's pg_class does not have - measured on 3.0.4, where the columns are oid, relname, relnamespace, relowner, relpersistence, relkind, relpages, relam, reltablespace, reloptions, relispartition and relpartbound. The earlier entry here blamed the schema query's LEFT JOIN pg_class ON (...)::regclass, which was wrong: the engine reports an unbindable column as missing FROM-clause entry for table "c", so a column defect reads as a join defect. Row counts and sizes are blank rather than zero, since neither reltuples nor pg_total_relation_size() exists to answer. The monitoring dashboard loads with every statistic marked unavailable rather than erroring the whole page (it has no pg statistics catalog at all, and a parameterised LIMIT is rejected, so slow-query/session panels stay empty specifically). The bulk column read writes its bound into the statement rather than binding it for the same reason. No maintenance operation is offered since #1387: RisingWave 3.1.0 has none of PostgreSQL's maintenance statements, and because its BEGIN opens no transaction the connect-time probe asks nothing rather than run them (2026-10-04). |
ScyllaDB was the standing illustration of the rule above, and its probe has now run, which is why
it has a row rather than a paragraph. It speaks the CQL wire protocol and cassandra-driver connects
to it, which is exactly the kind of "connects, therefore supported" claim this table exists to refuse
— and what the probe measured is that the parts which differ are the parts that are not the wire.
Of the three doubts cassandra.md §11 raised,
two held and one was refuted: system_views is absent, which is still the whole of what makes the
row above Partial - the five surfaces that read it answered an error until 2026-08-24 and answer empty since
(re-probed 2026-08-24) - and the version string is not release_version-shaped, while gossip_generation does
exist on ScyllaDB and answers. Nothing about how a name gets in has changed: a probe against a real
instance through the real provider is still the only way, and until one runs the honest state is
untested, not unsupported.
Reproduce any row with the compat profile of the container fixture, then connect as the driver in
the second column:
docker compose -f database-compose.yml --profile compat up -dScyllaDB needs one connection field the other rows here do not, the same one Apache Cassandra
needs: Local Data Center must be datacenter1, and the driver refuses to open a session while it is
empty. The fixture's service is scylla, reachable on localhost port 9142 with no user and no
password, and the probe keyspace is probe.
Absent from the table for two different reasons, which are worth keeping apart: an engine we could not reach is not the same as an engine we reached and refused.
Measured, and refused a row. The probe ran and the result did not earn an entry, so there is no tier and no version column for it - the number is the finding:
- QuestDB 10.0.1 - it fails the one surface a query-editor-only row exists to claim, probed
on 2026-08-26 through the
postgresdriver againstquestdb/questdb:10.0.1. QuestDB does speak the PostgreSQL wire protocol on 8812, and through the provider a statement answers:SELECT country, count() c FROM probe_ordersreturned three rows. In the product it does not. The editor always attaches aqueryId, the provider then issuesSELECT pg_backend_pid()first, and QuestDB has no such function, so pressing Run answers500 unknown function name: pg_backend_pid()- measured in Chrome, and measured both ways at the provider boundary to be sure of the cause: the identical call with aqueryIdfails and without one returns the three rows. This is the Cloud Spanner shape below, and it is the reason a provider-level probe cannot award a tier. Two statements do not rescue it either: the multi-statement path drops thequeryId, but the run still failed in the browser. The rest, for whoever revisits this: the object browser fails on TWO independent causes of ours - the schema query'sAS MATERIALIZEDCTEs are a syntax error there, and with the keyword removed the same query dies onpg_total_relation_size(), which QuestDB does not have - so it is not the Materialize/RisingWave case of an absent catalog.information_schema.tableslists the table andpg_catalog.pg_classanswers with 36 columns. There is nopg_stat_activity,pg_statio_user_tables,pg_stat_user_tablesorpg_tablespace, so health, performance metrics, table statistics and storage cannot run; the slow-query and session reads fail earlier still, onNULLS LAST. A plainEXPLAINworks through the provider and shows QuestDB's own plan (PageFrame,Row forward scan) while PostgreSQL'sEXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)is rejected outright. NeitherANALYZEnorVACUUMis a QuestDB statement. Errors are classified correctly with QuestDB's own text. The version panel would read PostgreSQL 12.3 -version()names QuestDB only at the end of its string, and the real build is inbuild(), which the provider does not call. The compose service is kept under thecompatprofile deliberately, so the refusal can be reproduced rather than taken on trust. - Google Cloud Spanner, PostgreSQL dialect - 1 of 15 surfaces, probed on 2026-08-20 through
the
postgresdriver againstgcr.io/cloud-spanner-pg-adapter/pgadapter-emulator:v0.55.2(the Cloud Spanner emulator behind PGAdapter 0.55.2; the managed service was not probed and inherits nothing from this). It does not reach even query-editor-only: pressing Run fails, because the editor always attaches aqueryIdand the provider then issuesSELECT pg_backend_pid()first, which Spanner rejects as an unsupported function. Test Connection fails on a connection that works, since it readspg_stat_activity. The object browser is empty (regclassis an unsupported type), no monitoring panel answers at all, no row count or size is obtainable anywhere - the emulatedpg_classreports 0 rows for a 2000-row table and even ground truth is unreadable - Explain fails onBUFFERS, and Cancel can never work because the backend PID it needs is unreadable. Agent and plan mode fail closed, which is correct: the role-privilege verification inconnect()cannot run (current_setting(text)is unsupported), so acquisition is refused and the user is told the connection failed. Foreign keys are enforced by the engine, but the app can never display them because the schema read fails. One result is worse than a failure: Vacuum reports "VACUUM completed successfully" when Spanner has no vacuum and nothing happened - PGAdapter swallows the statement. Analyze, by contrast, correctly reports itself unsupported. Fixture notes for anyone repeating this:numerictakes no precision or scale (sodecimal(10,2)is rejected), there is noANALYZE, the emulator authenticates nobody and has no role vocabulary at all (CREATE ROLEanswers "Statement is not supported."), and nothing survives a restart because the database is in memory.
No instance was reachable, so these are deliberately absent rather than assumed to work: every managed-only service - Amazon Redshift, Aurora, AlloyDB (the managed service; the downloadable AlloyDB Omni is in the table above), Neon, Supabase, Cloud SQL, Azure SQL Database, Microsoft Fabric, Azure Synapse, Azure SQL Managed Instance, Amazon ElastiCache, Upstash, PlanetScale, Azure Cosmos DB and Amazon DocumentDB. Their status is tracked in #424.
- Provider architecture:
../DATABASE_PROVIDERS.md— the Strategy-Pattern architecture, the provider hierarchy, and the shared interface/base classes. - Adding a new provider:
../ADDING_A_PROVIDER.md— the step-by-step guide, plus the rubric for deciding whether a database needs a driver at all, the transport seam, and the traps of talking to one over HTTP. Couchbase is the worked example. - HTTP API contract (request/response for
/api/db/query, schema, maintenance, …):../API_DOCS.md.
Any connection but a Kafka one may reach its database through an SSH tunnel (sshTunnel on the connection,
src/lib/ssh/tunnel.ts). This is the only layer that can verify who
terminated the SSH hop: past it the database driver is handed 127.0.0.1 and a local port, so the
connection's own SSL settings say nothing about the bastion.
A Kafka connection refuses a tunnel: a Kafka client reaches every broker at the address that broker advertises, which a tunnel forwarding one address does not carry, so the dialog offers no SSH panel for it and the provider refuses a tunnelled connection (kafka.md §4.5).
The policy is trust-on-first-use (TOFU), pinned per connection. Not "refuse anything unknown":
requiring a pasted fingerprint before the first connection makes tunnelling unusable for someone
reaching their own bastion, TOFU is what every SSH client does on first contact, and it is strictly
better than what it replaced - ssh2 has no default host verifier, so with the callback absent the
library accepted whatever key it was offered and the tunnel completed against whatever answered on
that address.
What happens, in order:
- A connection carrying a pin (
sshTunnel.hostKeyFingerprint) is verified against it. Durable, per connection, and it always wins. - No pin, first contact with that bastion - the key is accepted and remembered for the bastion's
address (
host:port), the same thingknown_hostskeys on. The accepted fingerprint is reported back on the tunnel. - No pin, a later contact with that bastion - verified against what was remembered.
A key that does not match fails the connection outright, naming both fingerprints. Measured
2026-08-26 against a real openssh-server container, all three arms:
no pin -> accepted, reported SHA256:JWLQciyX8RYEeuK+bwRLW/YSpStz0/L7BlbVcejL1/U
(byte-identical to `ssh-keyscan -p 2226 127.0.0.1 | ssh-keygen -lf -`)
correct pin -> connects
wrong pin -> SSH host key verification failed for 127.0.0.1:2226: offered SHA256:JWLQ..., expected
SHA256:AAAA... Confirm the bastion's key with `ssh-keyscan <host> | ssh-keygen -lf -`;
if it changed legitimately, clear the pinned fingerprint for this connection.
Fingerprints are printed the way OpenSSH prints them - SHA256: followed by unpadded base64 of the
SHA-256 of the server's public key blob - so what an error shows can be compared directly against
ssh-keyscan <bastion> | ssh-keygen -lf -, against ssh-keygen -lf on a .pub file, or against an
existing known_hosts entry.
Three deliberate limits, so nobody reads more into this than it does:
- There is no "accept the new key?" prompt. A mismatch fails; the remedy is to clear the pin once you have confirmed out of band that the key legitimately changed (a rebuilt bastion). Whether to offer an in-product accept flow is a separate decision.
- First-contact memory lasts as long as the server process. A restart re-enters first contact.
- Nothing writes the durable pin yet. The verifier honours
sshTunnel.hostKeyFingerprint, but no surface sets it: there is no input for it, seed configs do not modelsshTunnel, and an accepted fingerprint is not written back. So today's protection is the process-scoped memory above - a key that changes within a server's lifetime is refused, and a restart trusts afresh. Tracked as D34.
The fingerprint is stored and displayed in the clear, on purpose. It is public key material: it
authenticates the bastion to Studio, grants no access, unlocks nothing, and it has to be readable for
the comparison above to be possible at all. src/lib/storage/connection-secrets.ts classifies it
public alongside the certificates in ssl; the tunnel's password, private key and passphrase stay
encrypted at rest.
What to type into the connection dialog for every shipped provider, against
database-compose.yml. One row per type-id, so the table covers the
same set as the table at the top of this file and a new provider is a new row rather than a re-drawn
grid. Each row was verified against the running container on 2026-08-19, and the Trino row on
2026-08-20 — the credentials are the ones the fixture actually accepts, not the ones its environment
block asks for (twice those differ; see the notes).
The Prometheus row and the prometheus-auth note below were verified against the running containers on 2026-09-23, by the capture tests/fixtures/prometheus/README.md records.
The Apache Kafka row and the kafka-auth and kafka-cluster notes below were verified against the running containers on 2026-09-24, by the capture tests/fixtures/kafka/README.md records.
The etcd row and the etcd fixtures note below were verified against the running containers on 2026-09-30, by the capture tests/fixtures/etcd/README.md records.
The Db2 LUW row is read off database-compose.yml: its image, capabilities and fixture were measured on 2026-10-03 on a container started the same way, not on the compose service itself, whose first boot creates the instance and the database and takes several minutes.
The Neo4j row was verified against the running container on 2026-10-03, by the capture tests/fixtures/neo4j/5.26.31/README.md records.
The InfluxDB rows were verified against the running containers on 2026-10-04, by the capture tests/fixtures/influxdb/README.md records.
InfluxDB (InfluxQL) has a row per server line it reads, because each line is a service of its own with its own principal.
The Oxia row was verified against the running container on 2026-10-04, by the capture tests/fixtures/oxia/README.md records.
Start the thirty-two always-on services with a plain docker compose -f database-compose.yml up -d:
twenty-four engine containers plus the one-shot couchbase-init, trino-init, kafka-init, etcd-seed, oxia-seed, milvus-seed, qdrant-seed and influxdb-seed seed sidecars; the Profile column names
the ones that need asking for. The count is derived, not written: a service in this file carries no
profiles: key precisely when it backs a SHIPPED provider, so a plain up -d can reproduce that
provider's integration pass.
| Provider | Compose service | Host | Port | User | Password | Database / service | Profile |
|---|---|---|---|---|---|---|---|
| PostgreSQL | postgres |
localhost | 5432 | postgres |
postgres |
postgres |
— |
| MySQL | mysql |
localhost | 3306 | root |
root |
mysql |
— |
| Oracle | oracle |
localhost | 1521 | system |
Password123! |
XEPDB1 (service name) |
— |
| Db2 LUW | db2 |
localhost | 50000 | db2inst1 |
Password123 |
TESTDB |
— |
| SQL Server | mssql |
localhost | 1433 | sa |
Password123! |
master |
— |
| MongoDB | mongodb |
localhost | 27017 | admin |
admin |
any; auth source admin |
— |
| Redis | redis |
localhost | 6379 | none | none | none (db index 0) | — |
| Couchbase | couchbase |
localhost | 8091 | Administrator |
password123 |
travel (bucket) |
— |
| ClickHouse | clickhouse |
localhost | 8123 | libredb |
password123 |
demo |
— |
| Apache Druid | druid-router |
localhost | 8888 | none | none | none | druid |
| Elasticsearch | elasticsearch |
localhost | 9200 | none | none | none | — |
| OpenSearch | opensearch |
localhost | 9201 | none | none | none | — |
| Trino | trino |
localhost | 8080 | none | none | tpch (catalog) |
— |
| Apache Cassandra | cassandra |
localhost | 9042 | none | none | probe (keyspace) |
— |
| Prometheus | prometheus |
localhost | 9090 | none | none | none | none |
| InfluxDB (InfluxQL), 1.x | influxdb1 |
localhost | 8087 | reader |
readonly123 |
home |
none |
| InfluxDB (InfluxQL), 2.x | influxdb2 |
localhost | 8086 | none | the read-only token for bucket home (docker/influxdb/README.md says how to read it out) |
home |
none |
| InfluxDB 3 (SQL) | influxdb3 |
localhost | 8181 | none | apiv3_libredb-influxdb3-admin-token |
home |
none |
| InfluxDB 3 (SQL), file-limit fixture | influxdb3-filelimit |
localhost | 8182 | none | apiv3_libredb-influxdb3-admin-token |
home |
influxdb-filelimit |
| Apache Kafka | kafka |
localhost | 9092 | none | none | none | none |
| etcd | etcd |
localhost | 2379 | none | none | none (one connection is one cluster) | none |
| Oxia | oxia |
localhost | 6648 | none | none | default (namespace) |
none |
| Neo4j | neo4j |
localhost | 7687 | neo4j |
password123 |
neo4j, or empty for the home database |
none |
| Milvus | milvus |
localhost | 19530 | root |
the documented default (milvus.md, section 4.2) | default |
none |
| Qdrant | qdrant |
localhost | 6333 | none | none | none | none |
| SQLite | no service | — | — | — | — | a file path on the Studio host | — |
| LibreDB | no service | — | — | — | — | a directory on the Studio host | — |
none means leave the field empty. It is never a default that happens to be blank: Druid loads no
security extension in a default install, both search services run with their security plugin off, the
trino service runs with authentication disabled, and the redis service sets no requirepass
(verified: CONFIG GET requirepass answers empty), and the etcd service runs with RBAC off, and the oxia service runs with no authentication. Never put a password on a plain-HTTP Trino
connection: the coordinator answers 401 Password not allowed for insecure authentication even
with authentication off, so a password breaks a connection that works without one
(trino.md §4.3).
Prometheus with basic auth is a second service, prometheus-auth, behind a profile of its own.
Start it with docker compose -f database-compose.yml --profile prometheus-auth up -d prometheus-auth and connect to port 9091 as user studio with password studio-probe, the throwaway credential its web config, docker/prometheus/web-auth.yml, holds as a bcrypt hash.
The plain prometheus service on 9090 takes no credential at all (prometheus.md §4.2).
Apache Kafka has two more fixtures behind profiles of their own, and the plain kafka service takes no credential and no TLS.
kafka is seeded by its kafka-init sidecar, and its binary and transactional records by bun docker/kafka/seed-binary.ts localhost:9092 from the host, which the sidecar cannot run.
kafka-cluster is three nodes: start it with docker compose -f database-compose.yml --profile kafka-cluster up -d and connect to localhost:9192, from which the client learns the other two at 9193 and 9194.
kafka-auth is TLS with SCRAM-SHA-512 and an authorizer: start it with docker compose -f database-compose.yml --profile kafka-auth up -d kafka-auth, create the principal reader with a password of your choice as the service's comment in database-compose.yml shows, copy its CA out of the container, and connect to port 19094 with SASL mechanism SCRAM-SHA-512, TLS verify-full and that CA.
reader holds no ACL, so it sees an empty cluster and is refused the cluster reads, which is what the fixture is for (kafka.md §6.1).
etcd has three more fixtures behind two profiles, and the plain etcd service takes no credential and no TLS.
etcd is seeded by its etcd-seed one-shot; etcd-cluster is three members on 127.0.0.2, 127.0.0.3 and 127.0.0.4, port 2379 (profile etcd-cluster), and etcd-auth (port 12379, TLS with client certificates) and etcd-auth-password (port 12479, TLS with a password) run with RBAC on (profile etcd-auth).
Their certificates and passwords are generated into a volume at first start and never committed; docker/etcd/README.md says how to start and seed each, how to copy the certificates out, and what every seeded key is for.
The user reader may read the prefix /app/ and the key /config/a only, which is what the auth fixtures are for (etcd.md, section 4.7).
Oxia has four more fixtures behind profiles of their own, and the plain oxia service takes no token and no TLS.
oxia is seeded by its oxia-seed one-shot; oxia-natural (ports 6658 and 6659), oxia-017 (port 6668, Oxia 0.17.1), oxia-auth (port 6678, TLS and an OIDC token) and oxia-cluster (three data servers on ports 6671 to 6673) each sit behind the profile of the same name.
The auth fixture's certificates and tokens are generated into a volume at first start and never committed; docker/oxia/README.md says how to start and seed each and what every seeded key is for.
The neo4j service starts empty, and its graph is loaded by hand.
Once it is healthy, load the seed with docker exec -i libredb-neo4j cypher-shell -u neo4j -p password123 < docker/neo4j/seed.cypher; docker/neo4j/README.md says what the graph holds and why.
Neo4j refuses a password shorter than 8 characters, so the credential is neo4j / password123, and the service runs plaintext Bolt with no TLS (neo4j.md, section 11.3).
Milvus has two more fixtures behind one profile, and the plain milvus service takes no TLS.
milvus is seeded by its milvus-seed one-shot with the users reader and nobody beside root, and publishes its management port 9091 on loopback as 19091 for the live check alone; milvus-tls (port 19531, one-way TLS) and milvus-mtls (port 19532, TLS that verifies a client certificate) sit behind the profile milvus-tls.
Their certificates are generated into a volume by the milvus-certs one-shot and the users' passwords by milvus-seed, never committed; docker/milvus/README.md says how to start and seed each, how to copy the passwords and certificates out, and what every seeded collection is for.
Qdrant has three more fixtures behind two profiles, and the plain qdrant service takes no key and no TLS.
qdrant is seeded by its qdrant-seed one-shot; qdrant-auth (port 6343, an admin key, a read-only key and JWT RBAC) sits behind the profile qdrant-auth, and qdrant-tls (port 6353, one-way TLS) and qdrant-mtls (port 6363, TLS that verifies a client certificate) behind qdrant-tls.
Their keys and certificates are generated into a volume by the qdrant-keys one-shot and never committed; docker/qdrant/README.md says how to start and seed each, how to copy the keys out, and what every seeded collection is for.
InfluxDB has one more fixture behind a profile of its own, and the three plain services take no TLS.
influxdb1, influxdb2 and influxdb3 are seeded by the influxdb-seed one-shot, which writes the home sample with timestamps in the last 25 hours, the fixed edge measurements, the 1.x user reader and the 2.x read-only token; influxdb3-filelimit (port 8182, --query-file-limit 1) sits behind the profile influxdb-filelimit and is seeded from the host.
The 1.x admin is admin / password123 and the 2.x operator token is libredb-influxdb2-operator-token; connect with the read principals in the table, because on 1.x and 2.x the server keeps a read-only mode only for a READ user or a read bucket token.
An InfluxDB (InfluxQL) connection reads the 3.x server too, on port 8181 with the same token; docker/influxdb/README.md says how to start and reseed each, and what every seeded measurement is for.
Cassandra needs one field this table has no column for, and will not connect without it. Local Data Center must be datacenter1 - a stock single node names its own data centre that, and the
driver refuses to open a session when the field is empty rather than defaulting to the only data
centre it can see (cassandra.md §3.4).
The port needs nothing: the compose service publishes the native protocol as 9042:9042, which is
also what the connection dialog prefills, so the field can be left untouched. docker port libredb-cassandra prints the mapping.
The three embedded providers have no container, and that is the whole point of them. SQLite and DuckDB each take a path resolved in the Studio process and LibreDB a directory; none of them reaches a network. SQLite and LibreDB also ship a ready-made sample connection, "Sample (Employees)" and "Sample (LibreDB)", which appear in the sidebar with no configuration at all, so the fastest way to exercise those two is to click one rather than to fill this dialog in. DuckDB ships no sample: build a local file instead, as duckdb.md describes. See sqlite.md, duckdb.md and libredb.md.
Two rows differ from what the compose file's environment asks for, which is why they are stated from the running container instead:
- Oracle sets
ORACLE_PDB: ORCLPDB1, andgvenzl/oracle-xedoes not read that variable — it takesORACLE_DATABASEfor an application PDB. So the only PDB on the node is the image default,XEPDB1(verified:SELECT name FROM v$pdbs). Connect toXEPDB1, or the listener refuses the service name. - SQL Server sets
MSSQL_DATABASE: mssql, which the official image ignores; it creates no database.masteris what the fixture guarantees (verified:SELECT name FROM sys.databases), and ashopdatabase appears only once an E2E seed has run.
The two search services share a port inside the container. OpenSearch publishes 9201 on the
host because both products ship on 9200 and the elasticsearch service claims it; the provider's own
default port stays 9200, so this is a collision on this machine and not a fact about the product.
Security is off on both, which is what makes their fixtures reproducible and the limit of what they
can prove: a bogus Basic header is ignored there (HTTP 200 on both, measured), so no 401/403 body
can ever be captured from these containers. Neither offers a Database field at all — an index has no
namespace above it. Details in elasticsearch.md and
opensearch.md.
Trino's Database field is a CATALOG, not a database. The fixture ships tpch, tpcds,
memory, system and jmx configured, and tpch is the one to type: it generates its rows on
read, so tpch.tiny.nation (25 rows) answers immediately with no seed step. The tree shows the
schemas of that one catalog; every other catalog stays reachable from the editor by qualifying names
in full. Leave jmx configured — it is the only SQL-reachable source for the overview's uptime.
Details in trino.md.
Druid is seven containers, not one. It has no single-container mode, so the whole block carries
profiles: ["druid"] and a plain up -d leaves it out. The dialog only ever needs the Router:
docker compose -f database-compose.yml --profile druid up -dFor the engines that have no provider of their own, use the compat profile and connect as the driver
named in the Wire-compatible engines table above.
Garnet needs no setup at all - docker compose -f database-compose.yml --profile compat up -d garnet
and it answers RESP on 16380 with no password.
VictoriaMetrics needs no setup either - docker compose -f database-compose.yml --profile compat up -d victoriametrics and it answers the Prometheus HTTP API on 8428 with no user and no password; connect as prometheus.
Redpanda needs its seed - docker compose -f database-compose.yml --profile compat up -d redpanda answers the Kafka protocol on 29092 with no user and no password; connect as kafka.
Seed it as the kafka service is seeded, from the host: docker run --rm --network host -v "$PWD/docker/kafka/seed.sh:/seed.sh:ro" --entrypoint bash apache/kafka:4.3.1 /seed.sh localhost:29092, then bun docker/kafka/seed-binary.ts localhost:29092.
The seed's KIP-848 group fails there, since Redpanda has no such protocol, so it holds the two classic groups.
ParadeDB (15441), OrioleDB (15442) and Databend (13308) need no setup either, beyond the usual
probe fixture. Two notes that decide whether a probe there measures anything: on OrioleDB,
check default_table_access_method is orioledb and that your tables report amname = orioledb
(SELECT relname, amname FROM pg_class c JOIN pg_am a ON a.oid = c.relam) - a heap table there
measures PostgreSQL, not OrioleDB. On ParadeDB and OrioleDB both, create a least-privilege role
before testing agent plan mode, because the execution profile refuses a superuser on any PostgreSQL:
CREATE ROLE probe_ro LOGIN PASSWORD 'Probe123pass!';
GRANT CONNECT ON DATABASE probe TO probe_ro;
GRANT USAGE ON SCHEMA public TO probe_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO probe_ro;Databend answers on 3307, not 3306, and its strings must be single-quoted ('TR', never
"TR" - that is an identifier there).
The questdb service exists to reproduce a refusal, not a connection. It answers the PostgreSQL wire on
18812 (user admin, password quest, database qdb) and publishes its own HTTP API on 19000, which is
how the probe seeded 2000 rows (INSERT INTO ... FROM long_sequence(2000) over GET /exec) - there is no
psql-friendly bulk insert there. A connection to it can be created and the tree stays empty, and pressing
Run fails; see QuestDB under Measured, and refused a row above for what that costs and why.
Two other services need a step before the dialog will work. ScyllaDB needs Local Data Center
filled in, the same field Cassandra needs. Apache Doris starts with no password on root, and an
empty password is not a credential the dialog treats as filled, so set one and create the fixture
objects before connecting - this is the recipe the 2026-08-26 probe ran, and the sleep is not
optional: information_schema reports 0 rows and 0 B until Doris's background statistics catch up.
docker compose -f database-compose.yml --profile compat up -d doris
docker exec libredb-doris mysql -uroot -P9030 -h127.0.0.1 -e "SET PASSWORD FOR 'root' = PASSWORD('Probe#2026');"
docker exec libredb-doris mysql -uroot -pProbe#2026 -P9030 -h127.0.0.1 -e "
CREATE DATABASE IF NOT EXISTS probe;
CREATE TABLE probe.probe_orders (id INT NOT NULL, customer_id INT, amount DECIMAL(10,2), created DATETIME)
DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 3 PROPERTIES('replication_num'='1');
INSERT INTO probe.probe_orders SELECT number, number % 3 + 1, number * 1.25, '2026-08-26 10:00:00'
FROM numbers('number'='2000');"
sleep 60 # information_schema reads 0 rows / 0 B until the background statistics landThen connect on port 19035 as root / Probe#2026, database probe.
Every Host in the table above is written for a Studio that runs on the host — bun dev,
bun run start, or the npm package. Inside a container, localhost is that container's own loopback
and nothing is listening on it: a plain docker run -p 3000:3000 libredb/libredb-studio reaches
none of these services (measured — curl localhost:9200 from an unrelated container answers no HTTP
status at all, not a refusal you could mistake for a credential problem).
Pick one of three, in this order of preference.
1. Join the fixture's network and address services by name. The best answer, and the only one that
needs no host ports at all: compose puts every service on libredb-studio_default with its service
name as a DNS name.
docker run -p 3000:3000 --network libredb-studio_default libredb/libredb-studioThen use the service name as the host and the in-container port — which is not always the port in the table above:
| Provider | Host inside the network | Port inside the network |
|---|---|---|
| PostgreSQL | postgres |
5432 |
| MySQL | mysql |
3306 |
| Oracle | oracle |
1521 |
| SQL Server | mssql |
1433 |
| MongoDB | mongodb |
27017 |
| Redis | redis |
6379 |
| Couchbase | couchbase |
8091 |
| ClickHouse | clickhouse |
8123 |
| Apache Druid | druid-router |
8888 |
| Elasticsearch | elasticsearch |
9200 |
| OpenSearch | opensearch |
9200 |
| Trino | trino |
8080 |
OpenSearch is 9200 here, not 9201. The 9201 in the table above is a host port published to dodge a collision with the
elasticsearchservice. Inside the network there is no collision, so the port is the product's own. Verified:http://opensearch:9200/reports distributionopensearch, number3.8.0.Credentials and database names do not change — only host and port do. And the network exists only once compose has created it, so start the fixture before Studio; the name is
<project>_default, which islibredb-studio_defaultwhen compose is run from this repository and<your-directory>_defaultotherwise (docker network lssays which).
2. Reach the published host ports through the host gateway. Use this when Studio must stay off the
fixture's network — a container you did not start, or services split across several compose projects.
On Docker Desktop host.docker.internal already resolves; on Linux it does not, and the flag below is
what creates it:
docker run -p 3000:3000 --add-host=host.docker.internal:host-gateway libredb/libredb-studioNow the table at the top of this section is correct as written, with host.docker.internal in place of
localhost — including OpenSearch on 9201, because these are the published host ports. Verified on
Linux: 9200 and 9201 both answer HTTP 200 through that name.
3. --network host. Makes localhost mean the host's loopback, so the table applies unchanged:
docker run --network host ghcr.io/libredb/libredb-studioLast because of what it costs: Linux only (on Docker Desktop the "host" is the VM, not your
machine), no -p mapping (the app binds the host's port 3000 directly), and the container shares the
host's whole network namespace, which is a far wider grant than reaching one database.
Whichever you pick, docker-compose.yml in the repository root is the
shape to copy for a real deployment: Studio and its Postgres sit on one compose network and address
each other by service name, so nothing depends on a published host port existing.
One collision to know about if you run both files: that root compose file and the fixture both name a
container libredb-postgres, so the second one to start fails with "container name is already in
use". Rename one, or run only the fixture and point Studio's STORAGE_* variables at it.