diff --git a/CLAUDE.md b/CLAUDE.md index a16f10d302..ab6bd2f7f8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ Guidance for Claude Code in this repo — conventions, rules, and gotchas only. ## Project Overview -Web-based SQL IDE for cloud-native teams: nineteen external engines, plus the embedded LibreDB store (`EXTERNAL_DATABASE_TYPES` in [`src/lib/db/compatibility.ts`](src/lib/db/compatibility.ts) is the external-engine list, never a prose enumeration), plus AI query assistance. It runs **two ways**, as a standalone Next.js app and as a published npm package, and the two render different chrome, so a UI change verified in one is not verified in the other. +Web-based SQL IDE for cloud-native teams: twenty external engines, plus the embedded LibreDB store (`EXTERNAL_DATABASE_TYPES` in [`src/lib/db/compatibility.ts`](src/lib/db/compatibility.ts) is the external-engine list, never a prose enumeration), plus AI query assistance. It runs **two ways**, as a standalone Next.js app and as a published npm package, and the two render different chrome, so a UI change verified in one is not verified in the other. ## Branching & PRs @@ -81,7 +81,7 @@ A clean local pass is still not a guarantee: the same job also runs `build:lib` > - **One directory may serve two type-ids** — `sql/search/` is both `elasticsearch` and `opensearch` (#424). Docs and tests stay 1:1 anyway: the invariant is per type-id. > - Any change to one side MUST sync the others **in the same PR**. The doc mirrors the code and the code mirrors the doc — never let them drift. -- **DB abstraction:** Strategy Pattern. SQL-dialect providers extend `SQLBaseProvider`; the non-SQL ones (`mongodb`, `redis`, `couchbase`, `prometheus`, `kafka`, `etcd`, `libredb`) extend `BaseDatabaseProvider` directly, and `SQLBaseProvider` itself extends it. Inside `src/lib/db`, never branch on the type id; drive behaviour through capabilities/labels. Three `=== "mongodb"` branches survive in the UI layer as known debt (`src/hooks/use-connection-form.ts`, `src/lib/editor/tab-language.ts`, `src/components/ConnectionModal.tsx`); do not add a fourth. +- **DB abstraction:** Strategy Pattern. SQL-dialect providers extend `SQLBaseProvider`; the non-SQL ones (`mongodb`, `redis`, `couchbase`, `prometheus`, `kafka`, `etcd`, `libredb`) extend `BaseDatabaseProvider` directly, `neo4j` extends `GraphBaseProvider` (`src/lib/db/graph/`), and `SQLBaseProvider` and `GraphBaseProvider` themselves extend it. Inside `src/lib/db`, never branch on the type id; drive behaviour through capabilities/labels. Three `=== "mongodb"` branches survive in the UI layer as known debt (`src/hooks/use-connection-form.ts`, `src/lib/editor/tab-language.ts`, `src/components/ConnectionModal.tsx`); do not add a fourth. - **Auth:** `NEXT_PUBLIC_AUTH_PROVIDER` = `local` (email/password) or `oidc` (PKCE → the same JWT cookie); `src/proxy.ts` enforces RBAC (admin vs user). [`docs/OIDC.md`](docs/OIDC.md). - **Storage:** write-through cache — localStorage serves reads, `useStorageSync` pushes mutations to the server (debounced). `STORAGE_PROVIDER` (server-side only) = `local` | `sqlite` | `postgres`. [`docs/STORAGE.md`](docs/STORAGE.md). - **API routes:** all backend in `src/app/api/`, except `/health`, which is a route at the app root so the plainest liveness path exists; JWT-protected except the public set in [`src/proxy.ts`](src/proxy.ts) — `/login`, `/api/auth/*`, `/health`, `/api/health`, `/api/db/health`, `/api/storage/config`, `/_next`, `/favicon.ico` and static assets — plus two machine paths gated by bearer credentials of their own instead of the JWT: the agent drive callback and /api/mcp. `src/proxy.ts` is the authority; do not restate the list elsewhere. diff --git a/DOCKERHUB.md b/DOCKERHUB.md index f2246647f7..08d89f4fd0 100644 --- a/DOCKERHUB.md +++ b/DOCKERHUB.md @@ -20,7 +20,7 @@ > 📖 **Full documentation, source, and issues:** -Query **PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka and etcd** from your browser, with AI-powered query assistance, interactive ER diagrams, schema diff, a virtualized data grid, RBAC, OIDC SSO, and a live monitoring dashboard. A lightweight, secure bridge between heavy desktop tools (DataGrip/DBeaver) and minimal CLIs. +Query **PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd and Neo4j** from your browser, with AI-powered query assistance, interactive ER diagrams, schema diff, a virtualized data grid, RBAC, OIDC SSO, and a live monitoring dashboard. --- @@ -109,8 +109,8 @@ Every one of those tags is published on three bases, and the suffix is appended ## Supported databases -Nineteen external engines share one interface. -The twentieth row is the embedded LibreDB store, which ships inside the image rather than being a server you connect out to. +Twenty external engines share one interface. +The twenty-first row is the embedded LibreDB store, which ships inside the image rather than being a server you connect out to. | Database | Driver | Highlights | | :--- | :--- | :--- | @@ -133,6 +133,7 @@ The twentieth row is the embedded LibreDB store, which ships inside the image ra | **Prometheus** | none, HTTP | PromQL editor, metric, rule and target browser | | **Apache Kafka** | `@platformatic/kafka` | Topic, group and broker browser, reads by offset or time | | **etcd** | `@grpc/grpc-js` | etcdctl command editor, key-prefix browser, guarded value edits | +| **Neo4j** | `neo4j-driver-lite` | Read-only Cypher editor, label and relationship-type browser | | **LibreDB** | `@libredb/libredb` | The embedded key-value store, for a database with nothing to install | **Read-only where the engine is.** Druid, Elasticsearch and OpenSearch have no `UPDATE` and no `CREATE TABLE` anywhere in their grammar, so inline editing and DDL are reported as unsupported instead of failing when used. @@ -140,7 +141,7 @@ Prometheus and Apache Kafka are read-only too: Studio calls only their read APIs ### Engines with no provider of their own -Twenty-eight further engines speak the wire protocol of one of the nineteen drivers above, so they connect through it unchanged: pick that driver in the connection dialog. Every one of them was measured against a real instance rather than assumed, and how much of the product worked is recorded per engine. +Twenty-eight further engines speak the wire protocol of one of the twenty drivers above, so they connect through it unchanged: pick that driver in the connection dialog. Every one of them was measured against a real instance rather than assumed, and how much of the product worked is recorded per engine. | Engine | Connect as | Support | | :--- | :--- | :--- | diff --git a/README.md b/README.md index 83e19510d8..1f0e9b5700 100644 --- a/README.md +++ b/README.md @@ -116,23 +116,24 @@ You create a Postgres on a managed platform. It is ready in forty seconds. Then LibreDB Studio goes the other way. It deploys next to the data: a container, a Helm chart, an operator, a one-click template on your PaaS, or `npm i @libredb/studio` inside your own product. Nothing has to face outward. -Nineteen engines share one interface: PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka and etcd, with the same explorer everywhere, and ER diagrams, schema diff and monitoring wherever the engine has something to report. Three of the nineteen are read-only because their own SQL is: Druid, Elasticsearch and OpenSearch have no `UPDATE` and no `CREATE TABLE` in the grammar at all, so those controls are reported as unsupported instead of failing when used. Cassandra is the one that reports the least on purpose: it publishes no row count and no size that is true, so the object browser shows neither rather than showing a number that is wrong; the estimate it does publish counts partitions from flushed files, and it read 143 for a 500-row table. Trino is the other odd one: it is a query engine rather than a database, so it declares no keys and no indexes and reports the bytes as belonging to the systems behind its connectors. -etcd is the newest: an etcdctl subset over etcd's gRPC API reads and writes keys, the browser shows key-prefix groups and a key browser, a key's value is edited in one transaction guarded by its revision, and an admin can compact, defragment and disarm alarms, while every write under a Kubernetes prefix is refused and every Kubernetes secret withheld. -Apache Kafka is the one before it: a JSON read request reads a topic's messages over the Kafka protocol, by partition, offset or timestamp, and the browser shows topics, consumer groups with their lag, and brokers, read-only by construction, because Studio never produces, commits an offset, joins a consumer group or creates a topic. -Neither etcd, Apache Kafka nor Prometheus is SQL at all, as MongoDB and Redis are not: Prometheus speaks PromQL over the Prometheus HTTP API, browses metrics, rules and scrape targets, and is read-only because Studio calls none of the server's write or admin endpoints. +Twenty engines share one interface: PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd and Neo4j, with the same explorer everywhere, and ER diagrams, schema diff and monitoring wherever the engine has something to report. Three of the twenty are read-only because their own SQL is: Druid, Elasticsearch and OpenSearch have no `UPDATE` and no `CREATE TABLE` in the grammar at all, so those controls are reported as unsupported instead of failing when used. Cassandra is the one that reports the least on purpose: it publishes no row count and no size that is true, so the object browser shows neither rather than showing a number that is wrong; the estimate it does publish counts partitions from flushed files, and it read 143 for a 500-row table. Trino is the other odd one: it is a query engine rather than a database, so it declares no keys and no indexes and reports the bytes as belonging to the systems behind its connectors. +Neo4j is the newest: Cypher typed in the editor reads a Neo4j 5.26 database over Bolt, the browser shows node labels, relationship types, indexes and constraints, and nodes, relationships and paths reach the grid as JSON cells, read-only by construction, because every statement passes a read policy, the server's own classification (which an allowlisted SHOW form skips) and a READ session before it runs. +etcd is the one before it: an etcdctl subset over etcd's gRPC API reads and writes keys, the browser shows key-prefix groups and a key browser, a key's value is edited in one transaction guarded by its revision, and an admin can compact, defragment and disarm alarms, while every write under a Kubernetes prefix is refused and every Kubernetes secret withheld. +Apache Kafka came before etcd: a JSON read request reads a topic's messages over the Kafka protocol, by partition, offset or timestamp, and the browser shows topics, consumer groups with their lag, and brokers, read-only by construction, because Studio never produces, commits an offset, joins a consumer group or creates a topic. +None of Neo4j, etcd, Apache Kafka and Prometheus is SQL at all, as MongoDB and Redis are not: Prometheus speaks PromQL over the Prometheus HTTP API, browses metrics, rules and scrape targets, and is read-only because Studio calls none of the server's write or admin endpoints. And nothing is held back. Single sign-on, ER diagrams, the AI features and the NoSQL engines all ship in the MIT build. MIT is not generosity here, it is a requirement of the architecture: you cannot place a per-seat licensed, feature-gated tool into every environment you own. ### Why LibreDB Studio? - **Deploys next to the data**: container, Helm chart, Rancher, OpenShift operator, one-click PaaS template, or embedded via npm. -- **Nineteen engines, one interface**: PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Cassandra, Prometheus, Apache Kafka, etcd. +- **Twenty engines, one interface**: PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Cassandra, Prometheus, Apache Kafka, etcd, Neo4j. - **Runs where you are**: browser, phone, Windows, MacOS, Linux desktop. - **A read-only agent, with your own model**: state a question, and the run drafts SQL, reads the results, and writes a report whose claims cite them. Gemini, OpenAI, or a local Ollama with open-source models. - **Nothing behind a wall**: RBAC, OIDC single sign-on, query audit trail, and ER diagrams all ship under MIT.

Multi-Database Connection Manager -
Connect to PostgreSQL, MySQL, Oracle, SQL Server, MongoDB, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Cassandra, Redis, SQLite, DuckDB, libSQL, Prometheus, Apache Kafka, or etcd with SSL/TLS and SSH Tunnel support (Kafka takes TLS and no tunnel). +
Connect to PostgreSQL, MySQL, Oracle, SQL Server, MongoDB, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Cassandra, Redis, SQLite, DuckDB, libSQL, Prometheus, Apache Kafka, etcd, or Neo4j with SSL/TLS and SSH Tunnel support (Kafka takes TLS and no tunnel).

--- @@ -303,9 +304,10 @@ Standalone application only: the embedded `@libredb/studio` package carries no a | **Prometheus** | none, HTTP (the Prometheus HTTP API, port 9090) | PromQL editor whose text reaches the server unchanged, results in the grid and the chart tab (a stepped subquery such as `rate(x[5m])[1h:1m]` charts as lines over its timestamps: the tab opens on the first series, more are added from the Y-Axis menu, one line each, and at most eight are drawn at once, past which it says "Showing first 8 of N series"; but the chart draws a missing sample, and a `NaN` or `Inf` among numbers, at 0, so a raw range over targets scraped at their own offsets charts false zeros), a metric browser with label names as columns and metadata as the source, rule groups with recording and alerting rules (a firing alert is marked in the tree), scrape pools and targets (a down target is marked), and health, version, uptime and TSDB statistics. Read-only by design: no admin API, no remote write, no EXPLAIN (the parse endpoint is experimental) and no maintenance. A credential over plain HTTP is sent rather than refused, so enable TLS across a network you do not control | | **Apache Kafka** | `@platformatic/kafka` (pure TypeScript, port 9092) | A JSON read request that reads a topic by partition, offset or timestamp, from its earliest offset or its latest messages, with keys, values and headers decoded as JSON, text or base64 and a Confluent-framed value labelled with its schema id; a topic browser with partitions and non-default configs (an offline or under-replicated topic is marked), consumer groups of both protocols with lag per partition, brokers with their configs, and health, topic counts and size on disk. Read-only by construction: no produce, no commit, no group membership and no topic creation. TLS with a custom CA and client certificates, and SASL PLAIN or SCRAM over TLS only; no SSH tunnel, because the brokers are reached at the addresses they advertise | | **etcd** | `@grpc/grpc-js` (pure JavaScript, gRPC, port 2379) | An etcdctl subset in the editor (`get`, `put`, `del`, `txn`, leases, a bounded `watch`, members, alarms, users and roles); key-prefix groups in the tree and every key in the Keys panel; a key's value edited in one transaction guarded by its revision; compaction, defragmentation and alarm disarm for admins, each confirmed by typing the connection's name. Every write under a Kubernetes prefix or to `compact_rev_key` is refused, and Kubernetes secrets and protobuf or encrypted values are never shown. TLS with a custom CA, client certificates (their Common Name as the etcd user) and password sign-in over TLS only; SSH tunnel; a seed-declared read-only mode | +| **Neo4j** | `neo4j-driver-lite` (pure JavaScript, Bolt, port 7687) | Read-only Cypher in the editor, one statement per run, against Neo4j 5.26 LTS; node labels and relationship types with their properties as columns, indexes and constraints in the tree; nodes, relationships and paths as tagged JSON cells in the grid, with 64-bit integers and temporal values kept exact; the monitoring panels Community serves. Every statement passes a read policy over its tokens, then the server's own classification through `EXPLAIN`, which runs only a read or an allowlisted procedure call and which an allowlisted SHOW form skips, and runs in a READ session, so a write has to defeat all three. No EXPLAIN or PROFILE view, no maintenance, no agent execution and no MCP `run_read_query`. TLS with a custom CA and SSH tunnel; no connection string | | **Redis** | `ioredis` | Command editor, key browser, INFO-based monitoring | -> **Twenty-eight more engines have no driver of their own.** The nineteen above are the drivers this build ships. Twenty-eight further engines speak one of those wire protocols and connect through an existing driver unchanged, so nineteen drivers reach forty-seven named engines in all. They are MariaDB, Percona Server for MySQL, TiDB, Vitess, StarRocks, Apache Doris, OceanBase, SingleStore, Databend, Citus, Percona Distribution for PostgreSQL, ParadeDB, OrioleDB, TimescaleDB, YugabyteDB, AlloyDB Omni, Apache Cloudberry (incubating), CockroachDB, Materialize and RisingWave (as PostgreSQL or MySQL), Valkey, DragonflyDB, KeyDB and Garnet (as Redis), FerretDB (as MongoDB), ScyllaDB (as Cassandra), VictoriaMetrics (as Prometheus), and Redpanda (as Apache Kafka). Each was measured against a live instance, and how much of the product works differs per engine. MariaDB, both Percona distributions, TiDB, Vitess, AlloyDB Omni, Citus, TimescaleDB, YugabyteDB, ParadeDB, OrioleDB, Valkey, DragonflyDB, KeyDB and FerretDB behave as their driver's own engine, though three of them report statistics you should not trust: a Citus distributed table and a TimescaleDB hypertable report row counts and sizes that are wrong rather than missing, and YugabyteDB reports 0 until you run `ANALYZE`. Vitess is not one of those three, its row counts and sizes being exact to the byte, but a running query cannot be cancelled there: vtgate refuses `KILL QUERY` and the statement runs to completion. AlloyDB Omni is not one of them either, reporting 2000 rows for 2000 and 270336 bytes for 270336, but two things there surprise: `version()` names AlloyDB nowhere, so the version panel cannot be told apart from a stock PostgreSQL 17, and eight of AlloyDB's own `google_ml` tables list in the object browser, which any role that can connect at all may also read. StarRocks reports itself as MySQL 5.1 and loses its overview, health and session panels, its monitoring dashboard rendering six panels with the session one carrying the engine's own refusal; Apache Doris - the engine StarRocks is a fork of - loses only the overview and health panels, to one statement form its grammar rejects, and is the more trustworthy of the two where it counts: it reports 2000 rows and 10187 bytes for a table holding exactly that, where StarRocks reads zero at first too - its own background statistics collector is slower, measured 4.5 minutes against 3.3.22 where Doris's is about a minute - and, until a 2026-09-16 fix, read zero forever afterward for sizes specifically, because StarRocks' `INDEX_LENGTH` is NULL rather than Doris's real 0 and poisoned the sum the provider computed in SQL; no index is ever reported, and a foreign key is accepted, listed by `SHOW CONSTRAINTS`, invisible to the ER diagram and unenforced; Cloudberry loses the monitoring dashboard and its table and index statistics, all three to one MPP planner restriction, and reads a foreign key back as though it were enforced when it is not, though its row counts are correct; CockroachDB loses the object browser and the size panels; OceanBase answers fourteen of the fifteen surfaces but only twelve of them usefully, health failing outright because its tenant has no `performance_schema` database at all and every size reading 0 B, though its row counts are correct once `ANALYZE TABLE` has run; SingleStore lost five surfaces to a cause that was ours rather than its own - the provider sent every statement through the prepared-statement protocol, which SingleStore refuses for the `SHOW` and `EXPLAIN` statements four panels need - and four of those five are now recovered, its Explain panel being the one that is not, because there the grammar wants `EXPLAIN JSON` and the statement fails on either protocol; its numbers are still missing rather than wrong, a 2000-row table reading 0 rows and 0 B with no `ANALYZE` able to change it; ScyllaDB loses five surfaces and Test Connection with them, all six to one absent keyspace - the overview, health, performance-metrics, active-session and monitoring panels read Cassandra's `system_views` virtual tables and ScyllaDB has no `system_views` keyspace at all - those five now degrade to empty rather than throwing, so Test Connection passes and the dialog saves the connection, which it could not do at all until that change - while the editor and the object browser work in full, every one of 18 CQL types reading back byte-identically to the Cassandra 5.0.9 probed in the same pass; ParadeDB and OrioleDB are both full and their costs are opposites: ParadeDB's nine extensions put 41 objects in the object browser for 2 user tables and break agent plan mode on a stock install, while OrioleDB's browser is clean and its own storage is invisible to PostgreSQL's size functions, so every index reads 0 bytes and the cache hit ratio reads N/A. Materialize and RisingWave are partial: the object browser shows their tables, views and materialized views with their columns, while row counts, sizes and the monitoring statistics stay blank. Databend is query-editor-only, and its catalogs answer perfectly well when asked directly - the object browser is empty because our parameterised reads use a prepared protocol it does not implement. Garnet behaves as Redis and is one of three relatives here (with Valkey and DragonflyDB) whose own version `INFO` carries beside the Redis compat level and the overview now labels ahead of it - `Garnet 2.1.5 (Redis 7.4.3)` - and two of its readings are absences wearing a value, every size showing 0 B because it publishes no `used_memory` and the cache hit ratio showing 100% because it publishes no keyspace counters. +> **Twenty-eight more engines have no driver of their own.** The twenty above are the drivers this build ships. Twenty-eight further engines speak one of those wire protocols and connect through an existing driver unchanged, so twenty drivers reach forty-eight named engines in all. They are MariaDB, Percona Server for MySQL, TiDB, Vitess, StarRocks, Apache Doris, OceanBase, SingleStore, Databend, Citus, Percona Distribution for PostgreSQL, ParadeDB, OrioleDB, TimescaleDB, YugabyteDB, AlloyDB Omni, Apache Cloudberry (incubating), CockroachDB, Materialize and RisingWave (as PostgreSQL or MySQL), Valkey, DragonflyDB, KeyDB and Garnet (as Redis), FerretDB (as MongoDB), ScyllaDB (as Cassandra), VictoriaMetrics (as Prometheus), and Redpanda (as Apache Kafka). Each was measured against a live instance, and how much of the product works differs per engine. MariaDB, both Percona distributions, TiDB, Vitess, AlloyDB Omni, Citus, TimescaleDB, YugabyteDB, ParadeDB, OrioleDB, Valkey, DragonflyDB, KeyDB and FerretDB behave as their driver's own engine, though three of them report statistics you should not trust: a Citus distributed table and a TimescaleDB hypertable report row counts and sizes that are wrong rather than missing, and YugabyteDB reports 0 until you run `ANALYZE`. Vitess is not one of those three, its row counts and sizes being exact to the byte, but a running query cannot be cancelled there: vtgate refuses `KILL QUERY` and the statement runs to completion. AlloyDB Omni is not one of them either, reporting 2000 rows for 2000 and 270336 bytes for 270336, but two things there surprise: `version()` names AlloyDB nowhere, so the version panel cannot be told apart from a stock PostgreSQL 17, and eight of AlloyDB's own `google_ml` tables list in the object browser, which any role that can connect at all may also read. StarRocks reports itself as MySQL 5.1 and loses its overview, health and session panels, its monitoring dashboard rendering six panels with the session one carrying the engine's own refusal; Apache Doris - the engine StarRocks is a fork of - loses only the overview and health panels, to one statement form its grammar rejects, and is the more trustworthy of the two where it counts: it reports 2000 rows and 10187 bytes for a table holding exactly that, where StarRocks reads zero at first too - its own background statistics collector is slower, measured 4.5 minutes against 3.3.22 where Doris's is about a minute - and, until a 2026-09-16 fix, read zero forever afterward for sizes specifically, because StarRocks' `INDEX_LENGTH` is NULL rather than Doris's real 0 and poisoned the sum the provider computed in SQL; no index is ever reported, and a foreign key is accepted, listed by `SHOW CONSTRAINTS`, invisible to the ER diagram and unenforced; Cloudberry loses the monitoring dashboard and its table and index statistics, all three to one MPP planner restriction, and reads a foreign key back as though it were enforced when it is not, though its row counts are correct; CockroachDB loses the object browser and the size panels; OceanBase answers fourteen of the fifteen surfaces but only twelve of them usefully, health failing outright because its tenant has no `performance_schema` database at all and every size reading 0 B, though its row counts are correct once `ANALYZE TABLE` has run; SingleStore lost five surfaces to a cause that was ours rather than its own - the provider sent every statement through the prepared-statement protocol, which SingleStore refuses for the `SHOW` and `EXPLAIN` statements four panels need - and four of those five are now recovered, its Explain panel being the one that is not, because there the grammar wants `EXPLAIN JSON` and the statement fails on either protocol; its numbers are still missing rather than wrong, a 2000-row table reading 0 rows and 0 B with no `ANALYZE` able to change it; ScyllaDB loses five surfaces and Test Connection with them, all six to one absent keyspace - the overview, health, performance-metrics, active-session and monitoring panels read Cassandra's `system_views` virtual tables and ScyllaDB has no `system_views` keyspace at all - those five now degrade to empty rather than throwing, so Test Connection passes and the dialog saves the connection, which it could not do at all until that change - while the editor and the object browser work in full, every one of 18 CQL types reading back byte-identically to the Cassandra 5.0.9 probed in the same pass; ParadeDB and OrioleDB are both full and their costs are opposites: ParadeDB's nine extensions put 41 objects in the object browser for 2 user tables and break agent plan mode on a stock install, while OrioleDB's browser is clean and its own storage is invisible to PostgreSQL's size functions, so every index reads 0 bytes and the cache hit ratio reads N/A. Materialize and RisingWave are partial: the object browser shows their tables, views and materialized views with their columns, while row counts, sizes and the monitoring statistics stay blank. Databend is query-editor-only, and its catalogs answer perfectly well when asked directly - the object browser is empty because our parameterised reads use a prepared protocol it does not implement. Garnet behaves as Redis and is one of three relatives here (with Valkey and DragonflyDB) whose own version `INFO` carries beside the Redis compat level and the overview now labels ahead of it - `Garnet 2.1.5 (Redis 7.4.3)` - and two of its readings are absences wearing a value, every size showing 0 B because it publishes no `used_memory` and the cache hit ratio showing 100% because it publishes no keyspace counters. > VictoriaMetrics runs PromQL through the Prometheus driver and lists its metrics with their label columns, its targets, and up to fifty of its metrics with the most series, but its rule folders are empty, because a single-node server evaluates no rules; the overview, the storage statistics and the scrape pool folder fail, because it does not serve three of the Prometheus endpoints they read; a metric's source shows no unit and a target's no scrape interval or timeout, because it sends none, and a target it has not scraped yet reads as down; and a string expression returns no rows, a subquery's points are counted back from its evaluation time, and no PromQL info or warning appears beside any result. > Redpanda answers every Kafka surface through the Apache Kafka driver, its topics, consumer groups with their lag, brokers and reads alike, but its overview reads max connections as 0 and its broker source lists nine configs, because a broker's config answer holds only those, its storage statistics carry no usage percentage, and every group reads as classic, which is right there, since Redpanda has no other group protocol. > The per-engine detail, with the exact version probed, is in [`docs/providers/README.md`](docs/providers/README.md#wire-compatible-engines): we publish a name only after connecting to it, so a name absent there is untested rather than unsupported. @@ -328,7 +330,7 @@ Standalone application only: the embedded `@libredb/studio` package carries no a | **Editor** | Monaco Editor (VS Code Engine) | Web | | **AI** | Multi-Model (Gemini, OpenAI, Ollama, Custom) | Web, Mobile | | **Auth** | JWT (`jose`) + OIDC (`openid-client`), PKCE, Role Mapping | Web, Mobile | -| **Database** | PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Redis, Prometheus, Apache Kafka, etcd | Web, Mobile | +| **Database** | PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Redis, Prometheus, Apache Kafka, etcd, Neo4j | Web, Mobile | | **Charts** | Recharts (Bar, Line, Pie, Area, Scatter, Histogram, Stacked) | Web, Mobile | | **ERD** | React Flow, ELK.js (auto-layout) | Web | | **State/Grid** | TanStack Table & Virtual | Web, Mobile | @@ -421,7 +423,7 @@ journalctl -u libredb-studio ### Prerequisites - [Bun](https://bun.sh/) (Recommended) or Node.js 24+ - - A target database to query (PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Redis, Prometheus, Apache Kafka, or etcd) + - A target database to query (PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Redis, Prometheus, Apache Kafka, etcd, or Neo4j) ### Quick Start (Local) 1. **Clone & Install** @@ -556,6 +558,7 @@ docker compose -f database-compose.yml --profile druid down -v | **etcd**, three members (profile `etcd-cluster`) | 127.0.0.2, 127.0.0.3 or 127.0.0.4 | 2379 | none | none | none | | **etcd** with TLS and client certificates (profile `etcd-auth`) | localhost | 12379 | none | none | none; TLS `verify-full` with the fixture's CA and a client certificate from `docker/etcd/README.md` | | **etcd** with TLS and a password (profile `etcd-auth`) | localhost | 12479 | reader | the password in `reader.password` | none; TLS `verify-full` with the fixture's CA | +| **Neo4j** | localhost | 7687 | neo4j | password123 | `neo4j` (the default database); load `docker/neo4j/seed.cypher` as `docker/neo4j/README.md` says | ### PostgreSQL Sample Data @@ -919,7 +922,7 @@ extraEnvFrom: | `defaults` | No | Default values merged into all connections | | `connections[].id` | Yes | Unique slug (`[a-z0-9-]+`, max 64 chars) | | `connections[].name` | Yes | Display name in UI | -| `connections[].type` | Yes | `postgres`, `mysql`, `sqlite`, `libsql`, `duckdb`, `oracle`, `mssql`, `clickhouse`, `druid`, `trino`, `cassandra`, `elasticsearch`, `opensearch`, `mongodb`, `couchbase`, `redis`, `prometheus`, `kafka`, `etcd`, `libredb` | +| `connections[].type` | Yes | `postgres`, `mysql`, `sqlite`, `libsql`, `duckdb`, `oracle`, `mssql`, `clickhouse`, `druid`, `trino`, `cassandra`, `elasticsearch`, `opensearch`, `mongodb`, `couchbase`, `redis`, `prometheus`, `kafka`, `etcd`, `neo4j`, `libredb` | | `connections[].roles` | Yes | `["*"]` (everyone), `["admin"]`, `["user"]`, or `["admin", "user"]` | | `connections[].managed` | No | `true` = admin-controlled, not editable in the UI (default), `false` = editable copy for user | | `connections[].readOnly` | No | `true` refuses every write on an engine that enforces it (etcd); refused on an unmanaged connection | diff --git a/README_es.md b/README_es.md index abebeb754a..daf2a2d51b 100644 --- a/README_es.md +++ b/README_es.md @@ -102,9 +102,9 @@ Tomada en serio, esa frase deja de ser una preferencia y se vuelve una especific ## Capacidades principales -### Diecinueve motores, una sola interfaz +### Veinte motores, una sola interfaz -PostgreSQL · MySQL · Oracle · SQL Server · SQLite · libSQL · DuckDB · MongoDB · Redis · Couchbase · ClickHouse · Apache Druid · Elasticsearch · OpenSearch · Trino · Apache Cassandra · Prometheus · Apache Kafka · etcd +PostgreSQL · MySQL · Oracle · SQL Server · SQLite · libSQL · DuckDB · MongoDB · Redis · Couchbase · ClickHouse · Apache Druid · Elasticsearch · OpenSearch · Trino · Apache Cassandra · Prometheus · Apache Kafka · etcd · Neo4j Todos los motores SQL comparten el mismo explorador de esquemas, los diagramas ER, la comparación de esquemas y los paneles de monitoreo. MongoDB y Redis no son motores SQL: no tienen diagrama ER ni comparación de esquemas. Druid, Elasticsearch, OpenSearch y Trino son doblemente excepcionales: sus interfaces SQL sobre HTTP no tienen una forma de URI que este build sepa interpretar, así que se configuran por host y puerto, y las migraciones que se generan explican la limitación en lugar de inventar DDL para un motor cuyo SQL no tiene sentencias de cambio de columna. Lo mismo pasa con las colecciones sin esquema de Couchbase. El diagrama ER de los clústeres de búsqueda tiene cajas pero no líneas: los índices no declaran claves foráneas, y en el modelo del motor no hay ninguna que declarar. @@ -128,6 +128,7 @@ Todos los motores SQL comparten el mismo explorador de esquemas, los diagramas E | **Prometheus** | Sin driver, HTTP puro (la API HTTP de Prometheus, puerto 9090) | Editor de PromQL que envía el texto al servidor sin cambios, resultados en la grilla y en la pestaña de gráficos (una subconsulta con paso, como `rate(x[5m])[1h:1m]`, se grafica como líneas sobre sus marcas de tiempo: la pestaña abre con la primera serie, se agregan más desde el menú Y-Axis, una línea cada una, y se dibujan como máximo ocho a la vez, más allá de lo cual dice "Showing first 8 of N series"; pero el gráfico dibuja en 0 una muestra que falta y un `NaN` o `Inf` entre números, así que un rango crudo sobre targets con desfases de scrape distintos muestra ceros falsos), explorador de métricas con los nombres de etiqueta como columnas y los metadatos como fuente, grupos de reglas con reglas de registro y de alerta (una alerta disparada se marca en el árbol), pools y targets de scrape (un target caído se marca en el árbol), y salud, versión, tiempo de actividad y estadísticas del TSDB. Solo lectura por diseño: no llama a la API de administración, no hace remote write, no tiene EXPLAIN (el endpoint de parseo es experimental) ni operaciones de mantenimiento. Una credencial sobre HTTP en texto plano se envía en vez de rechazarse, así que activá TLS si la conexión cruza una red que no controlás | | **Apache Kafka** | `@platformatic/kafka` (TypeScript puro, puerto 9092) | Una solicitud de lectura en JSON que lee un topic por partición, offset o marca de tiempo, desde su offset más antiguo o sus mensajes más recientes, con claves, valores y cabeceras decodificados como JSON, texto o base64 y un valor con el formato de Confluent etiquetado con su id de esquema; explorador de topics con sus particiones y configuraciones no predeterminadas (un topic offline o con réplicas insuficientes se marca), grupos de consumidores de ambos protocolos con su retraso por partición, brokers con su configuración, y salud, cantidad de topics y tamaño en disco. Solo lectura por construcción: no produce, no confirma offsets, no se une a grupos y no crea topics. TLS con CA propia y certificados de cliente, y SASL PLAIN o SCRAM solo sobre TLS; sin túnel SSH, porque a los brokers se llega por las direcciones que anuncian | | **etcd** | `@grpc/grpc-js` (JavaScript puro, gRPC, puerto 2379) | Un subconjunto de etcdctl en el editor (`get`, `put`, `del`, `txn`, leases, un `watch` acotado, miembros, alarmas, usuarios y roles); grupos por prefijo de clave en el árbol y cada clave en el panel de claves; el valor de una clave editado en una transacción condicionada a su revisión; compactación, desfragmentación y desactivación de alarmas para administradores, cada una confirmada escribiendo el nombre de la conexión. Se rechaza toda escritura bajo un prefijo de Kubernetes o en `compact_rev_key`, y nunca se muestran los Secrets de Kubernetes ni los valores protobuf o cifrados. TLS con CA propia, certificados de cliente (su Common Name como usuario de etcd) e inicio de sesión con contraseña solo sobre TLS; túnel SSH; un modo de solo lectura declarado en la semilla | +| **Neo4j** | `neo4j-driver-lite` (JavaScript puro, Bolt, puerto 7687) | Cypher de solo lectura en el editor, una sentencia por ejecución, contra Neo4j 5.26 LTS; etiquetas de nodo y tipos de relación con sus propiedades como columnas, índices y restricciones en el árbol; nodos, relaciones y caminos como celdas JSON etiquetadas en la grilla, con los enteros de 64 bits y los valores temporales exactos; los paneles de monitoreo que ofrece Community. Cada sentencia pasa una política de lectura sobre sus tokens, luego la clasificación del propio servidor mediante `EXPLAIN` (que una forma SHOW permitida omite), y corre en una sesión READ, así que una escritura tendría que vencer las tres. Sin vista de EXPLAIN ni PROFILE, sin mantenimiento, sin ejecución del agente y sin `run_read_query` de MCP. TLS con CA propia y túnel SSH; sin cadena de conexión | | **Redis** | `ioredis` | Editor de comandos, explorador de claves, monitoreo basado en INFO | > **La seguridad del transporte es transversal, no depende del motor.** El túnel SSH se establece antes de que el proveedor abra la conexión, y la conexión se reescribe hacia el extremo local: por eso no depende del motor, y aplica a cualquier conexión configurada con host y puerto, salvo una conexión de Kafka, que lo rechaza porque un cliente de Kafka llega a cada broker por la dirección que ese broker anuncia, y un túnel que reenvía una sola dirección no la transporta. Las conexiones que se llenan con una cadena de conexión (MongoDB, Couchbase y ClickHouse lo permiten) no tienen host ni puerto, así que no pasan por el túnel; SQLite y DuckDB tampoco tienen ninguno de los dos. El panel de SSL/TLS hoy tiene efecto en PostgreSQL, MySQL, SQL Server, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Prometheus y Apache Kafka; en Trino no es opcional, porque el coordinador rechaza contraseñas sobre HTTP en texto plano. Oracle, MongoDB y Redis ignoran esta opción, así que si el tráfico de esos tres va cifrado depende de cómo esté escrita la cadena de conexión, no de lo que se elija en el diálogo. diff --git a/README_hi.md b/README_hi.md index a48a7c7d47..3d65a1845b 100644 --- a/README_hi.md +++ b/README_hi.md @@ -103,9 +103,9 @@ LibreDB Studio दूसरा रास्ता चुनता है: **tool ## मुख्य क्षमताएँ -### उन्नीस engines, एक interface +### बीस engines, एक interface -PostgreSQL · MySQL · Oracle · SQL Server · SQLite · libSQL · DuckDB · MongoDB · Redis · Couchbase · ClickHouse · Apache Druid · Elasticsearch · OpenSearch · Trino · Apache Cassandra · Prometheus · Apache Kafka · etcd +PostgreSQL · MySQL · Oracle · SQL Server · SQLite · libSQL · DuckDB · MongoDB · Redis · Couchbase · ClickHouse · Apache Druid · Elasticsearch · OpenSearch · Trino · Apache Cassandra · Prometheus · Apache Kafka · etcd · Neo4j सभी SQL engines एक ही schema browser, ER diagram, schema diff और monitoring dashboard इस्तेमाल करते हैं। MongoDB और Redis SQL engines नहीं हैं, इसलिए उनमें ER diagram और schema diff नहीं है। Druid, Elasticsearch, OpenSearch और Trino दोहरे अपवाद हैं: उनके HTTP SQL interface का कोई ऐसा URI रूप नहीं है जिसे यह build पढ़ सके, इसलिए उन्हें सिर्फ़ host/port से configure किया जाता है। साथ ही, generated migration सीधे अपनी सीमा बताता है, बजाय ऐसे engine के लिए DDL बनाने के जिसकी SQL में column बदलने का कोई statement ही नहीं है। Couchbase के schemaless collections पर भी यही लागू है। Search clusters के ER diagram में सिर्फ़ boxes होते हैं, कोई line नहीं: indexes foreign keys declare नहीं करते, और engine के model में declare करने के लिए foreign key होती ही नहीं। @@ -129,6 +129,7 @@ PostgreSQL · MySQL · Oracle · SQL Server · SQLite · libSQL · DuckDB · Mon | **Prometheus** | कोई driver नहीं, सिर्फ़ HTTP (Prometheus HTTP API, port 9090) | PromQL editor जो text को बिना बदले server तक भेजता है, नतीजे grid और chart tab में (`rate(x[5m])[1h:1m]` जैसी step वाली subquery timestamps पर lines के रूप में chart होती है: tab पहली series से खुलता है, Y-Axis menu से और series जोड़ी जा सकती हैं, हर series की एक line, और एक साथ अधिकतम आठ lines बनती हैं, उससे आगे chart "Showing first 8 of N series" दिखाता है; लेकिन chart किसी missing sample को, और numbers के बीच `NaN` या `Inf` को, 0 पर दिखाता है, इसलिए अलग-अलग समय पर scrape होने वाले targets की raw range query झूठे शून्य दिखाती है), metrics browser जिसमें label names columns हैं और metadata source है, rule groups और recording व alerting rules (firing alert tree में चिह्नित), scrape pools और targets (down target tree में चिह्नित), और health, version, uptime व TSDB statistics। Design से read-only: न admin API, न remote write, न EXPLAIN (parse endpoint अभी experimental है), न maintenance। Plain HTTP पर credential मना नहीं होता बल्कि भेज दिया जाता है, इसलिए जिस network पर आपका नियंत्रण न हो वहाँ TLS चालू करें | | **Apache Kafka** | `@platformatic/kafka` (pure TypeScript, port 9092) | JSON read request जो topic को partition, offset या timestamp से, सबसे पुराने offset से या नवीनतम messages पढ़ता है; keys, values और headers JSON, text या base64 में decode होते हैं और Confluent format वाला value अपने schema id से चिह्नित होता है; topic browser में partitions और non-default configs (offline या under-replicated topic चिह्नित), दोनों protocols के consumer groups और हर partition का lag, brokers और उनके configs, और health, topic count व disk size। Construction से read-only: न produce, न offset commit, न consumer group में शामिल होना, न topic बनाना। Custom CA और client certificates के साथ TLS, और SASL PLAIN या SCRAM सिर्फ़ TLS पर; SSH tunnel नहीं, क्योंकि brokers तक उनके advertised addresses पर पहुँचा जाता है | | **etcd** | `@grpc/grpc-js` (pure JavaScript, gRPC, port 2379) | Editor में etcdctl का subset (`get`, `put`, `del`, `txn`, leases, सीमित `watch`, members, alarms, users और roles); tree में key-prefix groups और Keys panel में हर key; एक key की value उसके revision से guarded एक transaction में edit होती है; admins के लिए compaction, defragmentation और alarm disarm, हर एक connection का नाम type करके confirm होता है। Kubernetes prefix या `compact_rev_key` पर हर write refuse होता है, और Kubernetes secrets तथा protobuf या encrypted values कभी नहीं दिखते। Custom CA के साथ TLS, client certificates (उनका Common Name etcd user) और password sign-in सिर्फ़ TLS पर; SSH tunnel; seed में घोषित read-only mode | +| **Neo4j** | `neo4j-driver-lite` (pure JavaScript, Bolt, port 7687) | Neo4j 5.26 LTS के लिए editor में read-only Cypher, हर run में एक statement; tree में node labels और relationship types (उनकी properties columns के रूप में), indexes और constraints; grid में nodes, relationships और paths tagged JSON cells के रूप में, 64-bit integers और temporal values बिना precision खोए; Community जो monitoring panels देता है। हर statement पहले उसके tokens पर read policy से, फिर `EXPLAIN` के ज़रिए server के अपने classification से (जिसे allowlisted SHOW form छोड़ देता है) गुज़रता है और READ session में चलता है, इसलिए किसी write को तीनों layers तोड़नी होंगी। EXPLAIN या PROFILE view, maintenance, agent execution और MCP `run_read_query` नहीं; Custom CA के साथ TLS और SSH tunnel; connection string नहीं | | **Redis** | `ioredis` | Command editor, key browser, INFO पर आधारित monitoring | > **Transport security हर engine की अलग सुविधा नहीं, सब पर लागू होने वाली क्षमता है।** SSH tunnel provider के connect होने से पहले ही बन जाता है और connection को local endpoint पर मोड़ देता है, इसलिए यह engine पर निर्भर नहीं: जिस connection में host और port हो, उस पर यह लागू होता है, Kafka connection को छोड़कर: Kafka client हर broker तक उसके advertised address पर पहुँचता है, जिसे एक address forward करने वाला tunnel नहीं ले जा सकता, इसलिए Kafka connection tunnel मना कर देता है। Connection string से भरे गए connections (MongoDB, Couchbase और ClickHouse में यह तरीका है) में host/port नहीं होता, इसलिए वे tunnel से नहीं जाते। SQLite और DuckDB में भी दोनों नहीं होते। SSL/TLS panel अभी PostgreSQL, MySQL, SQL Server, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Prometheus और Apache Kafka पर काम करता है। Trino पर यह वैकल्पिक नहीं है, क्योंकि coordinator plain HTTP पर password मना कर देता है। Oracle, MongoDB और Redis इस setting को नज़रअंदाज़ करते हैं, इसलिए इन तीनों में encryption इस पर निर्भर है कि connection string में क्या लिखा है, dialog में क्या चुना है इस पर नहीं। diff --git a/README_ja.md b/README_ja.md index bfa6de90e3..f30c845807 100644 --- a/README_ja.md +++ b/README_ja.md @@ -101,9 +101,9 @@ LibreDB Studioは逆向きです。**データをツールのところへ持っ ## 主な機能 -### 19のエンジン、1つのインターフェース +### 20のエンジン、1つのインターフェース -PostgreSQL · MySQL · Oracle · SQL Server · SQLite · libSQL · DuckDB · MongoDB · Redis · Couchbase · ClickHouse · Apache Druid · Elasticsearch · OpenSearch · Trino · Apache Cassandra · Prometheus · Apache Kafka · etcd +PostgreSQL · MySQL · Oracle · SQL Server · SQLite · libSQL · DuckDB · MongoDB · Redis · Couchbase · ClickHouse · Apache Druid · Elasticsearch · OpenSearch · Trino · Apache Cassandra · Prometheus · Apache Kafka · etcd · Neo4j スキーマエクスプローラ、ER図、スキーマ差分、モニタリングは全SQLエンジンで共通です。MongoDBとRedisはSQLエンジンではないため、ER図とスキーマ差分はありません。Druid、Elasticsearch、OpenSearch、TrinoはこのビルドがパースできるURI形式を持たないためhostとportで設定する二重の例外で、生成されるマイグレーションもDDLを出力せず制約を明示します(Couchbaseのスキーマレスなコレクションも同様)。検索クラスタのER図は箱だけで線がありません。インデックスは外部キーを宣言せず、エンジンのモデルにも宣言できる外部キーが存在しないためです。 @@ -127,6 +127,7 @@ PostgreSQL · MySQL · Oracle · SQL Server · SQLite · libSQL · DuckDB · Mon | **Prometheus** | ドライバなし、HTTPのみ(Prometheus HTTP API、9090) | テキストをそのままサーバーに送るPromQLエディタ、結果はグリッドとチャートタブに表示(`rate(x[5m])[1h:1m]` のようなステップ付きサブクエリはタイムスタンプを横軸とする線グラフになります。タブは最初の系列だけを表示し、Y-Axisメニューから系列を追加すると1系列につき1本の線が描かれ、同時に描かれるのは最大8本で、それを超えると「Showing first 8 of N series」と表示されます。ただし、チャートは欠けたサンプルと数値の中の `NaN`・`Inf` を0として描くため、スクレイプのタイミングがターゲットごとに異なる生の範囲クエリでは偽のゼロが描かれます)、ラベル名をカラム・メタデータをソースとするメトリクスブラウザ、ルールグループと記録ルール・アラートルール(発火中のアラートはツリーに表示)、スクレイププールとターゲット(ダウンしたターゲットはツリーに表示)、ヘルス・バージョン・稼働時間・TSDB統計。設計上読み取り専用です:管理APIもremote writeも呼ばず、EXPLAINもなく(パース用エンドポイントは実験的なため)、メンテナンス操作もありません。平文HTTP上の認証情報は拒否されずに送信されるため、管理下にないネットワークを越える場合はTLSを有効にしてください | | **Apache Kafka** | `@platformatic/kafka`(純粋なTypeScript、9092) | トピックをパーティション、オフセット、タイムスタンプ、最古のオフセット、または最新のメッセージから読むJSONの読み取りリクエスト。キー、値、ヘッダーはJSON、テキスト、base64としてデコードされ、Confluent形式の値はスキーマIDで表示されます。パーティションとデフォルト以外の設定を持つトピックブラウザ(オフラインまたはレプリカ不足のトピックはツリーに表示)、両プロトコルのコンシューマーグループとパーティションごとのラグ、brokerとその設定、ヘルス・トピック数・ディスク上のサイズ。構造上読み取り専用です:producer送信、オフセットのコミット、コンシューマーグループへの参加、トピックの作成は行いません。独自CAとクライアント証明書によるTLS、SASL PLAINとSCRAMはTLS上のみ。brokerには各brokerが広告するアドレスで接続するため、SSHトンネルは使えません | | **etcd** | `@grpc/grpc-js`(純粋なJavaScript、gRPC、2379) | エディタでetcdctlのサブセット(`get`、`put`、`del`、`txn`、リース、上限付きの`watch`、メンバー、アラーム、ユーザーとロール)。ツリーにはキープレフィックスのグループ、キーパネルにはすべてのキー。キーの値はリビジョンを条件とする1つのトランザクションで編集されます。管理者向けにコンパクション、デフラグ、アラーム解除があり、いずれも接続名の入力で確認します。Kubernetesのプレフィックスや`compact_rev_key`への書き込みはすべて拒否され、KubernetesのSecretとprotobufまたは暗号化された値は表示されません。独自CAによるTLS、クライアント証明書(Common Nameがetcdユーザー)、TLS上のみのパスワード認証。SSHトンネル。シードで宣言する読み取り専用モード | +| **Neo4j** | `neo4j-driver-lite`(純粋なJavaScript、Bolt、7687) | Neo4j 5.26 LTSに対する読み取り専用のCypherエディタ(1回の実行で1ステートメント)。ツリーにはノードラベルとリレーションシップタイプ(プロパティを列として)、インデックスと制約。ノード、リレーションシップ、パスはタグ付きJSONセルとしてグリッドに表示され、64ビット整数と日時の値は正確に保たれます。Communityが提供する監視パネル。すべてのステートメントはトークン単位の読み取りポリシー、`EXPLAIN`によるサーバー自身の分類(許可されたSHOW形式はこれを省略)、READセッションを通るため、書き込みは3つの層をすべて破る必要があります。EXPLAIN・PROFILEビュー、メンテナンス、エージェント実行、MCPの`run_read_query`はありません。独自CAによるTLS、SSHトンネル。接続文字列なし | | **Redis** | `ioredis` | コマンドエディタ、キーブラウザ、INFOベースの監視 | > **トランスポート層のセキュリティはエンジンごとではなく横断的な機能です。** SSHトンネルはproviderが接続する前に張られ、接続先はローカルのエンドポイントに書き換えられます。つまりエンジンに依存せず、hostとportが設定された接続であれば適用されます。ただしKafkaの接続はトンネルを拒否します。Kafkaクライアントは各brokerにそのbrokerが広告するアドレスで接続し、1つのアドレスだけを転送するトンネルはそのアドレスを運べないためです。接続文字列で入力した接続(MongoDB、Couchbase、ClickHouseで選択できます)はhostもportも持たないためトンネルされません。SQLiteとDuckDBも同様です。SSL/TLSパネルが実際に効くのはPostgreSQL、MySQL、SQL Server、Couchbase、ClickHouse、Druid、Elasticsearch、OpenSearch、Trino、Prometheus、Apache Kafkaです。Trinoでは任意ではなく必須に近い意味を持ちます。コーディネータが平文HTTP上のパスワードを拒否するためです。Oracle、MongoDB、Redisはこの設定を無視するため、この3つで暗号化されるかどうかはダイアログの選択ではなく接続文字列の内容次第になります。 diff --git a/README_pt.md b/README_pt.md index 52b7cf2390..7386a2dc20 100644 --- a/README_pt.md +++ b/README_pt.md @@ -102,9 +102,9 @@ Levada a sério, essa frase deixa de ser preferência e vira especificação. ## Capacidades principais -### Dezenove motores, uma única interface +### Vinte motores, uma única interface -PostgreSQL · MySQL · Oracle · SQL Server · SQLite · libSQL · DuckDB · MongoDB · Redis · Couchbase · ClickHouse · Apache Druid · Elasticsearch · OpenSearch · Trino · Apache Cassandra · Prometheus · Apache Kafka · etcd +PostgreSQL · MySQL · Oracle · SQL Server · SQLite · libSQL · DuckDB · MongoDB · Redis · Couchbase · ClickHouse · Apache Druid · Elasticsearch · OpenSearch · Trino · Apache Cassandra · Prometheus · Apache Kafka · etcd · Neo4j Todos os motores SQL compartilham o mesmo explorador de esquema, diagramas ER, comparação de esquema e painéis de monitoramento. MongoDB e Redis não são motores SQL: não têm diagrama ER nem comparação de esquema. Druid, Elasticsearch, OpenSearch e Trino são exceções duplas: suas interfaces SQL sobre HTTP não têm uma forma de URI que este build saiba interpretar, então se configuram por host e porta, e as migrações geradas explicam a limitação em vez de inventar DDL para um motor cujo SQL não tem comandos de alteração de coluna. O mesmo vale para coleções sem esquema no Couchbase. O diagrama ER dos clusters de busca tem caixas, mas não linhas: os índices não declaram chaves estrangeiras — e no modelo do motor não há nenhuma para declarar. @@ -128,6 +128,7 @@ Todos os motores SQL compartilham o mesmo explorador de esquema, diagramas ER, c | **Prometheus** | Sem driver, HTTP puro (a API HTTP do Prometheus, porta 9090) | Editor de PromQL que envia o texto ao servidor sem alterações, resultados na grade e na aba de gráficos (uma subconsulta com passo, como `rate(x[5m])[1h:1m]`, é plotada como linhas sobre seus timestamps: a aba abre com a primeira série, mais séries são adicionadas pelo menu Y-Axis, uma linha cada, e no máximo oito são desenhadas por vez, além disso aparece "Showing first 8 of N series"; mas o gráfico desenha em 0 uma amostra ausente e um `NaN` ou `Inf` entre números, então um range bruto sobre targets com offsets de scrape diferentes mostra zeros falsos), explorador de métricas com os nomes de label como colunas e os metadados como fonte, grupos de regras com regras de gravação e de alerta (um alerta disparado é marcado na árvore), pools e targets de scrape (um target fora do ar é marcado na árvore), e saúde, versão, uptime e estatísticas do TSDB. Somente leitura por design: não chama a API de administração, não faz remote write, não tem EXPLAIN (o endpoint de parse é experimental) nem operações de manutenção. Uma credencial sobre HTTP em texto plano é enviada em vez de recusada, então ative TLS se a conexão atravessa uma rede que você não controla | | **Apache Kafka** | `@platformatic/kafka` (TypeScript puro, porta 9092) | Uma requisição de leitura em JSON que lê um tópico por partição, offset ou timestamp, a partir do offset mais antigo ou das mensagens mais recentes, com chaves, valores e cabeçalhos decodificados como JSON, texto ou base64, e um valor no formato Confluent rotulado com o ID do schema; explorador de tópicos com partições e configurações não padrão (um tópico offline ou com réplicas insuficientes é sinalizado), grupos de consumidores de ambos os protocolos com lag por partição, brokers com suas configurações, além de informações de saúde, quantidade de tópicos e tamanho em disco. Somente leitura por design: não produz mensagens, não faz commit de offsets, não participa de grupos e não cria tópicos. TLS com CA personalizada e certificados de cliente, e SASL PLAIN ou SCRAM somente sobre TLS; sem túnel SSH, porque os brokers são acessados pelos endereços que anunciam | | **etcd** | `@grpc/grpc-js` (JavaScript puro, gRPC, porta 2379) | Um subconjunto do etcdctl no editor (`get`, `put`, `del`, `txn`, leases, um `watch` limitado, membros, alarmes, usuários e papéis); grupos por prefixo de chave na árvore e cada chave no painel de chaves; o valor de uma chave editado em uma transação condicionada à sua revisão; compactação, desfragmentação e desarme de alarmes para administradores, cada um confirmado digitando o nome da conexão. Toda escrita sob um prefixo do Kubernetes ou em `compact_rev_key` é recusada, e Secrets do Kubernetes e valores protobuf ou criptografados nunca são exibidos. TLS com CA personalizada, certificados de cliente (o Common Name como usuário do etcd) e login com senha somente sobre TLS; túnel SSH; um modo somente leitura declarado no seed | +| **Neo4j** | `neo4j-driver-lite` (JavaScript puro, Bolt, porta 7687) | Cypher somente leitura no editor, uma instrução por execução, contra o Neo4j 5.26 LTS; rótulos de nó e tipos de relacionamento com suas propriedades como colunas, índices e restrições na árvore; nós, relacionamentos e caminhos como células JSON marcadas na grade, com inteiros de 64 bits e valores temporais exatos; os painéis de monitoramento que o Community oferece. Toda instrução passa por uma política de leitura sobre seus tokens, depois pela classificação do próprio servidor via `EXPLAIN` (que uma forma SHOW permitida pula), e roda em uma sessão READ, então uma escrita teria de vencer as três camadas. Sem visão de EXPLAIN ou PROFILE, sem manutenção, sem execução pelo agente e sem `run_read_query` do MCP. TLS com CA personalizada e túnel SSH; sem string de conexão | | **Redis** | `ioredis` | Editor de comandos, explorador de chaves, monitoramento baseado em INFO | > **A segurança do transporte é transversal — não depende do motor.** O túnel SSH é estabelecido antes do provider abrir a conexão, e a conexão é reescrita para o endpoint local: por isso não depende do motor e vale para qualquer conexão configurada com host e porta, exceto uma conexão Kafka, que recusa o túnel porque um cliente Kafka chega a cada broker pelo endereço que esse broker anuncia, e um túnel para um único endereço não cobre isso. Conexões preenchidas com connection string (MongoDB, Couchbase e ClickHouse permitem) não têm host nem porta, então não passam pelo túnel; SQLite e DuckDB também não têm nenhum dos dois. O painel SSL/TLS hoje tem efeito em PostgreSQL, MySQL, SQL Server, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Prometheus e Apache Kafka; no Trino não é opcional, porque o coordinator rejeita senhas sobre HTTP em texto plano. Oracle, MongoDB e Redis ignoram essa opção — se o tráfego desses três vai cifrado depende de como a connection string está escrita, não do que você escolhe no diálogo. diff --git a/README_ru.md b/README_ru.md index 646c7a9649..bbbbd25709 100644 --- a/README_ru.md +++ b/README_ru.md @@ -102,9 +102,9 @@ LibreDB Studio устроен наоборот: **инструмент прих ## Основные возможности -### Девятнадцать СУБД, один интерфейс +### Двадцать СУБД, один интерфейс -PostgreSQL · MySQL · Oracle · SQL Server · SQLite · libSQL · DuckDB · MongoDB · Redis · Couchbase · ClickHouse · Apache Druid · Elasticsearch · OpenSearch · Trino · Apache Cassandra · Prometheus · Apache Kafka · etcd +PostgreSQL · MySQL · Oracle · SQL Server · SQLite · libSQL · DuckDB · MongoDB · Redis · Couchbase · ClickHouse · Apache Druid · Elasticsearch · OpenSearch · Trino · Apache Cassandra · Prometheus · Apache Kafka · etcd · Neo4j Для всех SQL-СУБД доступен одинаковый набор инструментов: дерево объектов базы (схемы, таблицы, столбцы), ER-диаграммы, сравнение схем и панели мониторинга. MongoDB и Redis работают не на SQL, поэтому ER-диаграмм и сравнения схем для них нет. @@ -132,6 +132,7 @@ ER-диаграмма для Elasticsearch и OpenSearch показывает и | **Prometheus** | Без драйвера, по HTTP (HTTP API Prometheus, порт 9090) | Редактор PromQL: запрос уходит на сервер без изменений, результат выводится таблицей и графиком. На графике одновременно рисуется не больше восьми рядов. Учтите, что пропущенные значения, `NaN` и `Inf` график рисует как 0. Есть просмотр метрик с их метками, групп правил записи и оповещения (сработавшие оповещения помечаются), целей опроса (недоступные помечаются), а также состояние, версия, время работы и статистика TSDB. Только чтение: Studio не вызывает административный API и remote write. EXPLAIN и операций обслуживания нет. Учётные данные отправляются и по незашифрованному HTTP, поэтому в недоверенной сети включайте TLS | | **Apache Kafka** | `@platformatic/kafka` (чистый TypeScript, порт 9092) | Чтение сообщений топика (topic): можно выбрать партицию (partition) и начать с нужного смещения (offset) или момента времени, с самого начала или с последних сообщений. Ключи, значения и заголовки (headers) показываются как JSON, текст или base64. Есть просмотр топиков с партициями и настройками (проблемные топики помечаются), групп потребителей (consumer groups) с отставанием (lag) по каждой партиции, брокеров (brokers), а также состояние, число топиков и объём на диске. Только чтение: Studio не отправляет сообщения, не фиксирует смещения, не вступает в группы и не создаёт топики. Поддерживаются TLS с собственным CA и клиентскими сертификатами, SASL PLAIN и SCRAM только поверх TLS. SSH-туннель не поддерживается, потому что клиент Kafka подключается к каждому брокеру по адресу, который тот сам сообщает | | **etcd** | `@grpc/grpc-js` (чистый JavaScript, gRPC, порт 2379) | Подмножество etcdctl в редакторе (`get`, `put`, `del`, `txn`, аренды (leases), ограниченный `watch`, участники кластера, тревоги (alarms), пользователи и роли); группы по префиксу ключа в дереве и каждый ключ в панели ключей; значение ключа меняется одной транзакцией с проверкой его ревизии; сжатие истории, дефрагментация и снятие тревог для администраторов, каждое с подтверждением вводом имени подключения. Любая запись под префиксом Kubernetes или в `compact_rev_key` отклоняется, а секреты Kubernetes и значения в protobuf или зашифрованные никогда не показываются. TLS с собственным CA, клиентские сертификаты (их Common Name как пользователь etcd) и вход по паролю только поверх TLS; SSH-туннель; режим только для чтения, заданный в seed-файле | +| **Neo4j** | `neo4j-driver-lite` (чистый JavaScript, Bolt, порт 7687) | Cypher только для чтения в редакторе, один оператор за запуск, для Neo4j 5.26 LTS; метки узлов и типы связей с их свойствами в качестве столбцов, индексы и ограничения в дереве; узлы, связи и пути в таблице как помеченные JSON-ячейки, 64-битные целые и значения даты и времени без потери точности; панели мониторинга, доступные в Community. Каждый оператор проходит политику чтения по его токенам, затем собственную классификацию сервера через `EXPLAIN` (разрешённая форма SHOW её пропускает) и выполняется в сеансе READ, так что запись должна преодолеть все три уровня. Нет просмотра EXPLAIN и PROFILE, нет обслуживания, нет выполнения агентом и нет `run_read_query` в MCP. TLS с собственным CA и SSH-туннель; без строки подключения | | **Redis** | `ioredis` | Редактор команд, просмотр ключей, мониторинг на основе INFO | > **Защита соединения настраивается одинаково для всех СУБД.** SSH-туннель открывается до подключения к базе, поэтому он работает с любой СУБД, если подключение задано хостом и портом. Исключение — Kafka: её клиент подключается к каждому брокеру по отдельному адресу, и туннель до одного адреса здесь не поможет. Если подключение задано строкой подключения (так можно для MongoDB, Couchbase, ClickHouse и libSQL), туннель не используется. У SQLite и DuckDB сетевого соединения нет вообще. Настройки SSL/TLS доступны для всех СУБД, кроме файловых. Для Trino TLS обязателен: без него координатор не принимает пароль. diff --git a/README_ur.md b/README_ur.md index 4825f6d81f..3e2da356b0 100644 --- a/README_ur.md +++ b/README_ur.md @@ -111,11 +111,11 @@ npx @libredb/studio ## بنیادی صلاحیتیں -### انیس engines، ایک interface +### بیس engines، ایک interface -PostgreSQL · MySQL · Oracle · SQL Server · SQLite · libSQL · DuckDB · MongoDB · Redis · Couchbase · ClickHouse · Apache Druid · Elasticsearch · OpenSearch · Trino · Apache Cassandra · Prometheus · Apache Kafka · etcd +PostgreSQL · MySQL · Oracle · SQL Server · SQLite · libSQL · DuckDB · MongoDB · Redis · Couchbase · ClickHouse · Apache Druid · Elasticsearch · OpenSearch · Trino · Apache Cassandra · Prometheus · Apache Kafka · etcd · Neo4j
@@ -143,6 +143,7 @@ PostgreSQL · MySQL · Oracle · SQL Server · SQLite · libSQL · DuckDB · Mon | **Prometheus** | کوئی مخصوص driver نہیں؛ براہِ راست HTTP (Prometheus HTTP API، port 9090) | PromQL editor جو text کو بغیر تبدیلی server تک بھیجتا ہے، نتائج grid اور chart tab میں (`rate(x[5m])[1h:1m]` جیسی step والی subquery timestamps پر lines کی صورت میں chart ہوتی ہے: tab پہلی series سے کھلتا ہے، Y-Axis menu سے مزید series شامل کی جا سکتی ہیں، ہر series کی ایک line، اور ایک وقت میں زیادہ سے زیادہ آٹھ lines بنتی ہیں، اس سے آگے chart "Showing first 8 of N series" دکھاتا ہے؛ لیکن chart کسی missing sample کو، اور numbers کے درمیان `NaN` یا `Inf` کو، 0 پر دکھاتا ہے، اس لیے الگ الگ وقت پر scrape ہونے والے targets کی raw range query جھوٹے صفر دکھاتی ہے)، metrics explorer جس میں label names columns اور metadata source ہیں، rule groups اور recording و alerting rules (firing alert tree میں نشان زد)، scrape pools اور targets (down target tree میں نشان زد)، اور health، version، uptime اور TSDB statistics۔ Design کے لحاظ سے صرف پڑھنے کے لیے: admin API یا remote write استعمال نہیں ہوتے، EXPLAIN نہیں (parse endpoint ابھی experimental ہے) اور maintenance operations نہیں۔ Plain HTTP پر credential رد نہیں ہوتا بلکہ بھیج دیا جاتا ہے، اس لیے جس network پر آپ کا اختیار نہ ہو وہاں TLS فعال کریں | | **Apache Kafka** | `@platformatic/kafka` (pure TypeScript، port 9092) | JSON read request جو topic کو partition، offset یا timestamp سے، سب سے پرانے offset سے یا تازہ ترین messages پڑھتی ہے؛ keys، values اور headers JSON، text یا base64 میں decode ہوتے ہیں اور Confluent format والی value اپنے schema id سے نشان زد ہوتی ہے؛ topic browser میں partitions اور non-default configs (offline یا under-replicated topic نشان زد)، دونوں protocols کے consumer groups اور ہر partition کا lag، brokers اور ان کے configs، اور health، topic count اور disk size۔ ساخت کے لحاظ سے صرف پڑھنے کے لیے: نہ produce، نہ offset commit، نہ consumer group میں شمولیت، نہ topic بنانا۔ Custom CA اور client certificates کے ساتھ TLS، اور SASL PLAIN یا SCRAM صرف TLS پر؛ SSH tunnel نہیں، کیونکہ brokers تک ان کے advertised addresses پر پہنچا جاتا ہے | | **etcd** | `@grpc/grpc-js` (pure JavaScript، gRPC، port 2379) | Editor میں etcdctl کا subset (`get`، `put`، `del`، `txn`، leases، محدود `watch`، members، alarms، users اور roles)؛ tree میں key-prefix groups اور Keys panel میں ہر key؛ کسی key کی value اس کی revision سے guarded ایک transaction میں edit ہوتی ہے؛ admins کے لیے compaction، defragmentation اور alarm disarm، ہر ایک connection کا نام type کر کے confirm ہوتا ہے۔ Kubernetes prefix یا `compact_rev_key` پر ہر write refuse ہوتا ہے، اور Kubernetes secrets اور protobuf یا encrypted values کبھی نہیں دکھتیں۔ Custom CA کے ساتھ TLS، client certificates (ان کا Common Name etcd user) اور password sign-in صرف TLS پر؛ SSH tunnel؛ seed میں اعلان کردہ read-only mode | +| **Neo4j** | `neo4j-driver-lite` (pure JavaScript، Bolt، port 7687) | Neo4j 5.26 LTS کے لیے editor میں read-only Cypher، ہر run میں ایک statement؛ tree میں node labels اور relationship types (ان کی properties بطور columns)، indexes اور constraints؛ grid میں nodes، relationships اور paths بطور tagged JSON cells، 64-bit integers اور temporal values بغیر precision کھوئے؛ Community کے فراہم کردہ monitoring panels۔ ہر statement پہلے اپنے tokens پر read policy سے، پھر `EXPLAIN` کے ذریعے server کی اپنی classification سے (جسے allowlisted SHOW form چھوڑ دیتا ہے) گزرتا ہے اور READ session میں چلتا ہے، اس لیے کسی write کو تینوں layers توڑنی ہوں گی۔ EXPLAIN یا PROFILE view، maintenance، agent execution اور MCP `run_read_query` نہیں؛ Custom CA کے ساتھ TLS اور SSH tunnel؛ connection string نہیں | | **Redis** | `ioredis` | command editor، keys explorer، INFO پر مبنی monitoring |
diff --git a/README_zh.md b/README_zh.md index b78a15c57b..9ee889aa7a 100644 --- a/README_zh.md +++ b/README_zh.md @@ -132,24 +132,25 @@ LibreDB Studio 走另一条路:**工具去找数据,而不是把数据搬来 LibreDB Studio 走的是另一条路。它部署在数据旁边:一个容器、一个 Helm chart、一个 operator、一份 PaaS 一键模板,或者用 `npm i @libredb/studio` 嵌进你自己的产品。没有任何东西需要朝外暴露。 -十九种引擎共用一个界面,PostgreSQL、MySQL、Oracle、SQL Server、SQLite、libSQL、DuckDB、MongoDB、Redis、Couchbase、ClickHouse、Druid、Elasticsearch、OpenSearch、Trino、Apache Cassandra、Prometheus、Apache Kafka 和 etcd,处处是同一套浏览器,凡是引擎有东西可报的地方都有 ER 图、schema 对比和监控。十九种里有三种是只读的,因为它们自己的 SQL 就是只读的:Druid、Elasticsearch 和 OpenSearch 的文法里根本没有 `UPDATE`,也没有 `CREATE TABLE`,所以那些控件被如实报告为不支持,而不是等到用的时候才失败。Cassandra 是其中刻意报告得最少的一个:它给出的任何行数和容量都不真实,所以对象浏览器索性两者都不显示,而不是显示一个错的数字;它确实会发布的分区估算来自已刷盘的文件,实测一张 500 行的表被读作 143。Trino 是另一个异类:它是查询引擎而不是数据库,所以不声明任何主键和索引,报告的字节数属于它背后那些连接器所在的系统。 -etcd 是最新的一个:通过 etcd 的 gRPC API 以 etcdctl 子集读写键,浏览器显示键前缀分组和键浏览器,键值在一个以修订号为条件的事务中编辑,管理员可以压缩、碎片整理和解除告警;任何写入 Kubernetes 前缀的操作都会被拒绝,Kubernetes Secret 从不显示。 -Apache Kafka 是在它之前的一个:JSON 读取请求通过 Kafka 协议按分区、偏移量或时间戳读取主题中的消息,浏览器显示主题、带延迟的消费者组和 broker,并且按构造只读,因为 Studio 从不生产消息、提交偏移量、加入消费者组或创建主题。 -etcd、Apache Kafka 和 Prometheus 都和 MongoDB、Redis 一样完全不是 SQL:Prometheus 通过 Prometheus HTTP API 使用 PromQL,浏览指标、规则和抓取目标,并且是只读的,因为 Studio 不调用服务器的任何写入或管理端点。 +二十种引擎共用一个界面,PostgreSQL、MySQL、Oracle、SQL Server、SQLite、libSQL、DuckDB、MongoDB、Redis、Couchbase、ClickHouse、Druid、Elasticsearch、OpenSearch、Trino、Apache Cassandra、Prometheus、Apache Kafka、etcd 和 Neo4j,处处是同一套浏览器,凡是引擎有东西可报的地方都有 ER 图、schema 对比和监控。二十种里有三种是只读的,因为它们自己的 SQL 就是只读的:Druid、Elasticsearch 和 OpenSearch 的文法里根本没有 `UPDATE`,也没有 `CREATE TABLE`,所以那些控件被如实报告为不支持,而不是等到用的时候才失败。Cassandra 是其中刻意报告得最少的一个:它给出的任何行数和容量都不真实,所以对象浏览器索性两者都不显示,而不是显示一个错的数字;它确实会发布的分区估算来自已刷盘的文件,实测一张 500 行的表被读作 143。Trino 是另一个异类:它是查询引擎而不是数据库,所以不声明任何主键和索引,报告的字节数属于它背后那些连接器所在的系统。 +Neo4j 是最新的一个:在编辑器中输入的 Cypher 通过 Bolt 读取 Neo4j 5.26 数据库,浏览器显示节点标签、关系类型、索引和约束,节点、关系和路径以 JSON 单元格进入结果表格,并且按构造只读,因为每条语句在运行前都要经过读取策略、服务器自身的分类和 READ 会话。 +etcd 是在它之前的一个:通过 etcd 的 gRPC API 以 etcdctl 子集读写键,浏览器显示键前缀分组和键浏览器,键值在一个以修订号为条件的事务中编辑,管理员可以压缩、碎片整理和解除告警;任何写入 Kubernetes 前缀的操作都会被拒绝,Kubernetes Secret 从不显示。 +Apache Kafka 在 etcd 之前:JSON 读取请求通过 Kafka 协议按分区、偏移量或时间戳读取主题中的消息,浏览器显示主题、带延迟的消费者组和 broker,并且按构造只读,因为 Studio 从不生产消息、提交偏移量、加入消费者组或创建主题。 +Neo4j、etcd、Apache Kafka 和 Prometheus 都和 MongoDB、Redis 一样完全不是 SQL:Prometheus 通过 Prometheus HTTP API 使用 PromQL,浏览指标、规则和抓取目标,并且是只读的,因为 Studio 不调用服务器的任何写入或管理端点。 而且没有任何东西被留在门后。单点登录、ER 图、AI 功能和全部 NoSQL 引擎都在 MIT 构建里。MIT 在这里不是慷慨,而是这套架构的要求:你没法把一个按席位授权、功能分级的工具,放进你拥有的每一个环境。 ### 为什么选择 LibreDB Studio? - **部署在数据旁边**:容器、Helm chart、Rancher、OpenShift operator、PaaS 一键模板,或用 npm 嵌入。 -- **十九种引擎,一个界面**:PostgreSQL、MySQL、Oracle、SQL Server、SQLite、libSQL、DuckDB、MongoDB、Redis、Couchbase、ClickHouse、Druid、Elasticsearch、OpenSearch、Trino、Cassandra、Prometheus、Apache Kafka、etcd。 +- **二十种引擎,一个界面**:PostgreSQL、MySQL、Oracle、SQL Server、SQLite、libSQL、DuckDB、MongoDB、Redis、Couchbase、ClickHouse、Druid、Elasticsearch、OpenSearch、Trino、Cassandra、Prometheus、Apache Kafka、etcd、Neo4j。 - **你人在哪它就在哪跑**:浏览器、手机、Windows、MacOS、Linux 桌面。 - **一个只读的 Agent,配你自己的模型**:说一个问题,这次运行就会起草 SQL、读取结果,并写出一份每条结论都引用来源的报告。Gemini、OpenAI,或者跑开源模型的本地 Ollama。 - **没有东西被锁在墙后**:RBAC、OIDC 单点登录、查询审计日志和 ER 图,全部以 MIT 发布。

多数据库连接管理器 -
连接 PostgreSQL、MySQL、Oracle、SQL Server、MongoDB、Couchbase、ClickHouse、Druid、Elasticsearch、OpenSearch、Trino、Cassandra、Redis、SQLite、DuckDB、libSQL、Prometheus、Apache Kafka 或 etcd,支持 SSL/TLS 与 SSH 隧道(Kafka 支持 TLS,不支持隧道)。 +
连接 PostgreSQL、MySQL、Oracle、SQL Server、MongoDB、Couchbase、ClickHouse、Druid、Elasticsearch、OpenSearch、Trino、Cassandra、Redis、SQLite、DuckDB、libSQL、Prometheus、Apache Kafka、etcd 或 Neo4j,支持 SSL/TLS 与 SSH 隧道(Kafka 支持 TLS,不支持隧道)。

[![Ask DeepWiki](.github/assets/deepwiki-badge.svg)](https://deepwiki.com/libredb/libredb-studio) @@ -275,9 +276,10 @@ Studio 最主要的 AI 界面是编辑器旁边的 **Agent 侧栏**,下面列 | **Prometheus** | 无驱动,纯 HTTP(Prometheus HTTP API,9090 端口) | PromQL 编辑器,文本原样发送到服务器;结果进入表格和图表页(像 `rate(x[5m])[1h:1m]` 这样带步长的子查询以时间戳为横轴画成折线:图表页起初只画第一个序列,其余序列可在 Y-Axis 菜单中勾选添加,每个序列一条线,同时最多画八条,超出时显示“Showing first 8 of N series”;但图表会把缺失的样本以及数值中的 `NaN` 或 `Inf` 画成 0,所以在各目标抓取时间错开的原始范围查询上会画出虚假的零值);指标浏览器以标签名为列、以元数据为源;规则组及其记录规则和告警规则,触发中的告警在树中标出;抓取池和抓取目标,宕机的目标在树中标出;以及健康、版本、运行时长和 TSDB 统计。按设计只读:不调用管理 API,不做 remote write,没有 EXPLAIN(解析端点仍是实验性的),也没有维护操作。凭据在明文 HTTP 上会照常发送而不会被拒绝,跨越你无法控制的网络时请启用 TLS | | **Apache Kafka** | `@platformatic/kafka`(纯 TypeScript,9092 端口) | JSON 读取请求,按分区、偏移量或时间戳读取主题,或从最早偏移量读取,或读取最新消息;键、值和消息头按 JSON、文本或 base64 解码,Confluent 格式的值标注其 schema id;主题浏览器显示分区和非默认配置(离线或副本不足的主题会被标出),两种协议的消费者组及每个分区的延迟,broker 及其配置,以及健康状态、主题数量和磁盘占用。按构造只读:不生产消息、不提交偏移量、不加入消费者组、不创建主题。支持自定义 CA 与客户端证书的 TLS,SASL PLAIN 或 SCRAM 只能走 TLS;不支持 SSH 隧道,因为 broker 通过它们通告的地址访问 | | **etcd** | `@grpc/grpc-js`(纯 JavaScript,gRPC,2379 端口) | 编辑器中的 etcdctl 子集(`get`、`put`、`del`、`txn`、租约、有界 `watch`、成员、告警、用户和角色);树中按键前缀分组,键浏览器列出每个键;在一个以修订号为条件的事务中编辑键值;管理员可执行压缩、碎片整理和解除告警,每项都需输入连接名确认。拒绝任何写入 Kubernetes 前缀或 `compact_rev_key` 的操作,从不显示 Kubernetes Secret 以及 protobuf 或加密的值。支持自定义 CA 的 TLS、客户端证书(其 Common Name 作为 etcd 用户)以及只能走 TLS 的密码登录;支持 SSH 隧道;支持由种子配置声明的只读模式 | +| **Neo4j** | `neo4j-driver-lite`(纯 JavaScript,Bolt,7687 端口) | 针对 Neo4j 5.26 LTS 的只读 Cypher 编辑器,每次运行一条语句;树中显示节点标签和关系类型(属性作为列)、索引和约束;节点、关系和路径在结果表格中显示为带标签的 JSON 单元格,64 位整数和时间值保持精确;Community 版提供的监控面板。每条语句先经过基于词法单元的读取策略,再经过服务器通过 `EXPLAIN` 做出的分类(允许的 SHOW 形式跳过此分类),并在 READ 会话中运行,因此一次写入必须同时突破这三层。没有 EXPLAIN 或 PROFILE 视图,没有维护操作,没有智能体执行,也没有 MCP `run_read_query`。支持自定义 CA 的 TLS 和 SSH 隧道;不支持连接字符串 | | **Redis** | `ioredis` | 命令编辑器、键浏览器、基于 INFO 的监控 | -> **另有二十八种引擎没有自己的驱动。** 上面十九种是这个构建自带的驱动。另有二十八种引擎使用其中某一种线协议,通过已有驱动原样接入,所以十九个驱动一共覆盖四十七个具名引擎。它们是 MariaDB、Percona Server for MySQL、TiDB、Vitess、StarRocks、Apache Doris、OceanBase、SingleStore、Databend、Citus、Percona Distribution for PostgreSQL、ParadeDB、OrioleDB、TimescaleDB、YugabyteDB、AlloyDB Omni、Apache Cloudberry(孵化中)、CockroachDB、Materialize 和 RisingWave(按 PostgreSQL 或 MySQL 接入),Valkey、DragonflyDB、KeyDB 和 Garnet(按 Redis 接入),FerretDB(按 MongoDB 接入),ScyllaDB(按 Cassandra 接入),VictoriaMetrics(按 Prometheus 接入),以及 Redpanda(按 Apache Kafka 接入)。每一种都对着一个真实实例测过,而产品能用的部分因引擎而异。MariaDB、两个 Percona 发行版、TiDB、Vitess、AlloyDB Omni、Citus、TimescaleDB、YugabyteDB、ParadeDB、OrioleDB、Valkey、DragonflyDB、KeyDB 和 FerretDB 的表现与它们所借驱动自身的引擎一致,不过其中三种会报告不该信任的统计值:Citus 的分布式表和 TimescaleDB 的 hypertable 报告的行数与容量是错的而不是缺的,YugabyteDB 则在你跑 `ANALYZE` 之前一直报 0。Vitess 不属于那三种,它的行数与容量精确到字节,但在那里无法取消正在运行的查询:vtgate 拒绝 `KILL QUERY`,语句会一直跑到结束。AlloyDB Omni 也不属于那三种,2000 行就报 2000 行、270336 字节就报 270336 字节,但那里有两件事出人意料:`version()` 在任何地方都不提 AlloyDB,所以版本面板与一个原生 PostgreSQL 17 无法区分;而 AlloyDB 自己的八个 `google_ml` 表会出现在对象浏览器里,任何能连上的角色也都能读它们。StarRocks 自称 MySQL 5.1,并失去了概览、健康和会话面板,它的监控面板只渲染出六个,其中会话面板带着引擎自己的拒绝理由;Apache Doris 是 StarRocks 所 fork 的引擎,只失去概览和健康面板,原因是一种它的文法拒绝的语句形式,而在真正要紧的地方它比 StarRocks 更可信:一张确实有那么多数据的表,它报 2000 行和 10187 字节,而 StarRocks 起初也读作零(它自己的后台统计收集器更慢,实测 4.5 分钟对 3.3.22,而 Doris 大约一分钟),并且在 2026-09-16 的一次修复之前,单就容量而言此后会永远读作零,因为 StarRocks 的 `INDEX_LENGTH` 是 NULL 而不是 Doris 那样的真实 0,把 provider 在 SQL 里算的那个和值毒化了;索引从不被报告,外键会被接受、被 `SHOW CONSTRAINTS` 列出、对 ER 图不可见且不被强制执行;Cloudberry 失去监控面板及其表和索引统计,三者都源于同一条 MPP 计划器限制,并且会把外键读成似乎被强制执行的样子,实际并没有,尽管它的行数是正确的;CockroachDB 失去对象浏览器和容量面板;OceanBase 能应答十五个界面中的十四个,但只有十二个有用,健康面板直接失败,因为它的租户根本没有 `performance_schema` 库,而每一处容量都读作 0 B,不过一旦跑过 `ANALYZE TABLE`,它的行数就是正确的;SingleStore 失去五个界面,原因在我们这边而不在它那边,provider 把每条语句都走预处理语句协议,而 SingleStore 对四个面板所需的 `SHOW` 和 `EXPLAIN` 语句拒绝该协议,这五个里现在已有四个恢复,未恢复的是它的 Explain 面板,因为那里的文法要 `EXPLAIN JSON`,而该语句在两种协议下都失败;它的数字仍然是缺的而不是错的,一张 2000 行的表读作 0 行和 0 B,且任何 `ANALYZE` 都改不了;ScyllaDB 失去五个界面,连 Test Connection 一起,六个全都源于同一个缺失的 keyspace,概览、健康、性能指标、活动会话和监控面板都读 Cassandra 的 `system_views` 虚拟表,而 ScyllaDB 根本没有 `system_views` keyspace,这五个现在降级为空而不是抛错,于是 Test Connection 能通过、对话框能保存连接,而在那次改动之前它完全做不到,同时编辑器与对象浏览器完好可用,18 种 CQL 类型每一种读回来的字节都与同一轮里探测的 Cassandra 5.0.9 一致;ParadeDB 和 OrioleDB 都是完整的,代价却正好相反:ParadeDB 的九个扩展为 2 张用户表在对象浏览器里放进 41 个对象,并让全新安装上的 Agent Plan 模式失效,而 OrioleDB 的浏览器很干净,但它自己的存储对 PostgreSQL 的容量函数不可见,所以每个索引都读作 0 字节,缓存命中率读作 N/A。Materialize 和 RisingWave 为部分支持:对象浏览器能显示它们的表、视图和物化视图及其列,但行数、大小和监控统计仍为空。Databend 只有查询编辑器可用,直接问它时 catalog 应答得很好,对象浏览器为空是因为我们的参数化读取用的是它没有实现的预处理协议。Garnet 的表现与 Redis 一致,并且是这里三个近亲之一(另外两个是 Valkey 和 DragonflyDB),它们的 `INFO` 里会在 Redis 兼容级别旁边带上自己的版本,概览现在把后者标在前面,即 `Garnet 2.1.5 (Redis 7.4.3)`,而它有两处读数是以数值面目出现的空缺:每处容量都显示 0 B,因为它不发布 `used_memory`,缓存命中率显示 100%,因为它不发布 keyspace 计数器。 +> **另有二十八种引擎没有自己的驱动。** 上面二十种是这个构建自带的驱动。另有二十八种引擎使用其中某一种线协议,通过已有驱动原样接入,所以二十个驱动一共覆盖四十八个具名引擎。它们是 MariaDB、Percona Server for MySQL、TiDB、Vitess、StarRocks、Apache Doris、OceanBase、SingleStore、Databend、Citus、Percona Distribution for PostgreSQL、ParadeDB、OrioleDB、TimescaleDB、YugabyteDB、AlloyDB Omni、Apache Cloudberry(孵化中)、CockroachDB、Materialize 和 RisingWave(按 PostgreSQL 或 MySQL 接入),Valkey、DragonflyDB、KeyDB 和 Garnet(按 Redis 接入),FerretDB(按 MongoDB 接入),ScyllaDB(按 Cassandra 接入),VictoriaMetrics(按 Prometheus 接入),以及 Redpanda(按 Apache Kafka 接入)。每一种都对着一个真实实例测过,而产品能用的部分因引擎而异。MariaDB、两个 Percona 发行版、TiDB、Vitess、AlloyDB Omni、Citus、TimescaleDB、YugabyteDB、ParadeDB、OrioleDB、Valkey、DragonflyDB、KeyDB 和 FerretDB 的表现与它们所借驱动自身的引擎一致,不过其中三种会报告不该信任的统计值:Citus 的分布式表和 TimescaleDB 的 hypertable 报告的行数与容量是错的而不是缺的,YugabyteDB 则在你跑 `ANALYZE` 之前一直报 0。Vitess 不属于那三种,它的行数与容量精确到字节,但在那里无法取消正在运行的查询:vtgate 拒绝 `KILL QUERY`,语句会一直跑到结束。AlloyDB Omni 也不属于那三种,2000 行就报 2000 行、270336 字节就报 270336 字节,但那里有两件事出人意料:`version()` 在任何地方都不提 AlloyDB,所以版本面板与一个原生 PostgreSQL 17 无法区分;而 AlloyDB 自己的八个 `google_ml` 表会出现在对象浏览器里,任何能连上的角色也都能读它们。StarRocks 自称 MySQL 5.1,并失去了概览、健康和会话面板,它的监控面板只渲染出六个,其中会话面板带着引擎自己的拒绝理由;Apache Doris 是 StarRocks 所 fork 的引擎,只失去概览和健康面板,原因是一种它的文法拒绝的语句形式,而在真正要紧的地方它比 StarRocks 更可信:一张确实有那么多数据的表,它报 2000 行和 10187 字节,而 StarRocks 起初也读作零(它自己的后台统计收集器更慢,实测 4.5 分钟对 3.3.22,而 Doris 大约一分钟),并且在 2026-09-16 的一次修复之前,单就容量而言此后会永远读作零,因为 StarRocks 的 `INDEX_LENGTH` 是 NULL 而不是 Doris 那样的真实 0,把 provider 在 SQL 里算的那个和值毒化了;索引从不被报告,外键会被接受、被 `SHOW CONSTRAINTS` 列出、对 ER 图不可见且不被强制执行;Cloudberry 失去监控面板及其表和索引统计,三者都源于同一条 MPP 计划器限制,并且会把外键读成似乎被强制执行的样子,实际并没有,尽管它的行数是正确的;CockroachDB 失去对象浏览器和容量面板;OceanBase 能应答十五个界面中的十四个,但只有十二个有用,健康面板直接失败,因为它的租户根本没有 `performance_schema` 库,而每一处容量都读作 0 B,不过一旦跑过 `ANALYZE TABLE`,它的行数就是正确的;SingleStore 失去五个界面,原因在我们这边而不在它那边,provider 把每条语句都走预处理语句协议,而 SingleStore 对四个面板所需的 `SHOW` 和 `EXPLAIN` 语句拒绝该协议,这五个里现在已有四个恢复,未恢复的是它的 Explain 面板,因为那里的文法要 `EXPLAIN JSON`,而该语句在两种协议下都失败;它的数字仍然是缺的而不是错的,一张 2000 行的表读作 0 行和 0 B,且任何 `ANALYZE` 都改不了;ScyllaDB 失去五个界面,连 Test Connection 一起,六个全都源于同一个缺失的 keyspace,概览、健康、性能指标、活动会话和监控面板都读 Cassandra 的 `system_views` 虚拟表,而 ScyllaDB 根本没有 `system_views` keyspace,这五个现在降级为空而不是抛错,于是 Test Connection 能通过、对话框能保存连接,而在那次改动之前它完全做不到,同时编辑器与对象浏览器完好可用,18 种 CQL 类型每一种读回来的字节都与同一轮里探测的 Cassandra 5.0.9 一致;ParadeDB 和 OrioleDB 都是完整的,代价却正好相反:ParadeDB 的九个扩展为 2 张用户表在对象浏览器里放进 41 个对象,并让全新安装上的 Agent Plan 模式失效,而 OrioleDB 的浏览器很干净,但它自己的存储对 PostgreSQL 的容量函数不可见,所以每个索引都读作 0 字节,缓存命中率读作 N/A。Materialize 和 RisingWave 为部分支持:对象浏览器能显示它们的表、视图和物化视图及其列,但行数、大小和监控统计仍为空。Databend 只有查询编辑器可用,直接问它时 catalog 应答得很好,对象浏览器为空是因为我们的参数化读取用的是它没有实现的预处理协议。Garnet 的表现与 Redis 一致,并且是这里三个近亲之一(另外两个是 Valkey 和 DragonflyDB),它们的 `INFO` 里会在 Redis 兼容级别旁边带上自己的版本,概览现在把后者标在前面,即 `Garnet 2.1.5 (Redis 7.4.3)`,而它有两处读数是以数值面目出现的空缺:每处容量都显示 0 B,因为它不发布 `used_memory`,缓存命中率显示 100%,因为它不发布 keyspace 计数器。 > VictoriaMetrics 通过 Prometheus 驱动运行 PromQL,并列出它的指标及其标签列、它的抓取目标,以及序列数最多的至多五十个指标,但它的规则文件夹是空的,因为单节点服务器不评估任何规则;概览、存储统计和抓取池文件夹都会失败,因为它们读取的 Prometheus 端点中有三个它根本不提供;指标的源不显示单位,目标的源不显示抓取间隔和超时,因为它不发送这些字段,它尚未抓取过的目标也会显示为 down;此外,字符串表达式不返回任何行,子查询的数据点从求值时间往回计算,任何结果旁边也不会出现 PromQL 的 info 或 warning 提示。 > Redpanda 通过 Apache Kafka 驱动应答每一个 Kafka 界面,主题、带延迟的消费者组、broker 和读取都可用,但它的概览把最大连接数读作 0,broker 的源只列出九项配置,因为它对 broker 配置的应答只有这些;它的存储统计没有使用百分比;每个消费者组都读作 classic,这在 Redpanda 上是对的,因为它没有别的组协议。 > 每个引擎的细节,连同探测所用的确切版本,都在 [`docs/providers/README.md`](docs/providers/README.md#wire-compatible-engines) 里:我们只有在连上某个引擎之后才会写出它的名字,所以那里没有的名字是尚未测试,而不是不受支持。 @@ -298,7 +300,7 @@ Studio 最主要的 AI 界面是编辑器旁边的 **Agent 侧栏**,下面列 | **编辑器** | Monaco Editor(VS Code 内核) | Web | | **AI** | 多模型(Gemini、OpenAI、Ollama、自定义) | Web、移动端 | | **认证** | JWT(`jose`)+ OIDC(`openid-client`)、PKCE、角色映射 | Web、移动端 | -| **数据库** | PostgreSQL、MySQL、Oracle、SQL Server、SQLite、libSQL、DuckDB、MongoDB、Couchbase、ClickHouse、Apache Druid、Elasticsearch、OpenSearch、Trino、Apache Cassandra、Redis、Prometheus、Apache Kafka、etcd | Web、移动端 | +| **数据库** | PostgreSQL、MySQL、Oracle、SQL Server、SQLite、libSQL、DuckDB、MongoDB、Couchbase、ClickHouse、Apache Druid、Elasticsearch、OpenSearch、Trino、Apache Cassandra、Redis、Prometheus、Apache Kafka、etcd、Neo4j | Web、移动端 | | **图表** | Recharts(柱状图、折线图、饼图、面积图、散点图、直方图、堆叠图) | Web、移动端 | | **ERD** | React Flow、ELK.js(自动布局) | Web | | **状态与表格** | TanStack Table 与 Virtual | Web、移动端 | @@ -385,7 +387,7 @@ journalctl -u libredb-studio ### 前置条件 - [Bun](https://bun.sh/)(推荐)或 Node.js 24+ -- 一个可查询的目标数据库(PostgreSQL、MySQL、Oracle、SQL Server、SQLite、libSQL、DuckDB、MongoDB、Couchbase、ClickHouse、Apache Druid、Elasticsearch、OpenSearch、Trino、Apache Cassandra、Redis、Prometheus、Apache Kafka 或 etcd) +- 一个可查询的目标数据库(PostgreSQL、MySQL、Oracle、SQL Server、SQLite、libSQL、DuckDB、MongoDB、Couchbase、ClickHouse、Apache Druid、Elasticsearch、OpenSearch、Trino、Apache Cassandra、Redis、Prometheus、Apache Kafka、etcd 或 Neo4j) ### 快速开始(本地) 1. **克隆并安装** @@ -879,7 +881,7 @@ extraEnvFrom: | `defaults` | 否 | 合并进所有连接的默认值 | | `connections[].id` | 是 | 唯一 slug(`[a-z0-9-]+`,最多 64 字符) | | `connections[].name` | 是 | UI 中显示的名称 | -| `connections[].type` | 是 | `postgres`、`mysql`、`sqlite`、`libsql`、`duckdb`、`oracle`、`mssql`、`clickhouse`、`druid`、`trino`、`cassandra`、`elasticsearch`、`opensearch`、`mongodb`、`couchbase`、`redis`、`prometheus`、`kafka`、`etcd`、`libredb` | +| `connections[].type` | 是 | `postgres`、`mysql`、`sqlite`、`libsql`、`duckdb`、`oracle`、`mssql`、`clickhouse`、`druid`、`trino`、`cassandra`、`elasticsearch`、`opensearch`、`mongodb`、`couchbase`、`redis`、`prometheus`、`kafka`、`etcd`、`neo4j`、`libredb` | | `connections[].roles` | 是 | `["*"]`(所有人)、`["admin"]`、`["user"]` 或 `["admin", "user"]` | | `connections[].managed` | 否 | `true` = 由管理员控制,界面中不可编辑(默认),`false` = 给用户一份可编辑的副本 | | `connections[].readOnly` | 否 | `true` 时拒绝该连接上的所有写入(仅在执行该模式的引擎上,即 etcd);在非托管连接上会被拒绝 | diff --git a/bun.lock b/bun.lock index 3b605469e6..c1d2606ca1 100644 --- a/bun.lock +++ b/bun.lock @@ -63,6 +63,7 @@ "mongodb": "^7.6.0", "mssql": "^12.7.0", "mysql2": "^3.24.3", + "neo4j-driver-lite": "6.2.0", "next": "^16.3.4", "next-themes": "^0.4.6", "openid-client": "^6.8.8", @@ -2166,6 +2167,12 @@ "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], + "neo4j-driver-bolt-connection": ["neo4j-driver-bolt-connection@6.2.0", "", { "dependencies": { "buffer": "^6.0.3", "neo4j-driver-core": "6.2.0", "string_decoder": "^1.3.0" } }, "sha512-SEnFSBfumleDxmDAZKkjXxZ5LgL2iZSTx08sO+qXX45ZKaxZJOcg9oxwoXACXZ1RbIq3XbDfHU8I5tExanYEOA=="], + + "neo4j-driver-core": ["neo4j-driver-core@6.2.0", "", {}, "sha512-U8PSdvDECWmCgSWyhqNNVzMONXFHWPcTd5gqi12+EDxxt0aETA0m+XCXf9H7q6qQXdisc40rUJEOcVQOu6peEA=="], + + "neo4j-driver-lite": ["neo4j-driver-lite@6.2.0", "", { "dependencies": { "neo4j-driver-bolt-connection": "6.2.0", "neo4j-driver-core": "6.2.0" } }, "sha512-0UjIO7cdysELMc20+hUxwG1kJ75YAqhTbMpgY098wjNUp1/NxIidysZd7hBmFj1blrkNFRpkpwcUkqq6AwG8jg=="], + "next": ["next@16.3.6", "", { "dependencies": { "@next/env": "16.3.6", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.3.6", "@next/swc-darwin-x64": "16.3.6", "@next/swc-linux-arm64-gnu": "16.3.6", "@next/swc-linux-arm64-musl": "16.3.6", "@next/swc-linux-x64-gnu": "16.3.6", "@next/swc-linux-x64-musl": "16.3.6", "@next/swc-win32-arm64-msvc": "16.3.6", "@next/swc-win32-x64-msvc": "16.3.6", "sharp": "^0.35.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-L+otWM/aQbYTx98aZhgEoMb4bZAXx1YVW4UMA/vuCyCoWG5HJyZUili8QAkqzrcC+5///tsz3s0M+SlyB5bLMw=="], "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], diff --git a/charts/libredb-studio/Chart.yaml b/charts/libredb-studio/Chart.yaml index 77e8b3fd6f..4ae0153e7f 100644 --- a/charts/libredb-studio/Chart.yaml +++ b/charts/libredb-studio/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 name: libredb-studio -description: Web-based SQL IDE for cloud-native teams supporting nineteen engines - PostgreSQL, MySQL, SQLite, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Apache Trino, Apache Cassandra, libSQL, Prometheus, Apache Kafka and etcd +description: Web-based SQL IDE for cloud-native teams supporting twenty engines - PostgreSQL, MySQL, SQLite, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Apache Trino, Apache Cassandra, libSQL, Prometheus, Apache Kafka, etcd and Neo4j type: application -version: 0.1.73 +version: 0.1.74 appVersion: "0.17.0" kubeVersion: ">=1.26.0-0" home: https://github.com/libredb/libredb-studio @@ -48,6 +48,10 @@ keywords: # One keyword only, like kafka above: `etcd` is the type-id and the word an evaluator looking # for an etcd client types, so there is no second spelling to catch. - etcd + # Two keywords, like the prometheus pair above: `neo4j` is the type-id and the product, and + # `cypher` is what an evaluator looking for a Cypher client types. + - neo4j + - cypher # A relative, not a provider: VictoriaMetrics answers the Prometheus HTTP API and connects as # `prometheus` at the tier its gate-4 probe measured (docs/providers/README.md). The first # wire-compatible relative given a keyword, because an evaluator looking for a VictoriaMetrics @@ -155,6 +159,8 @@ annotations: # setting the image already read, this one exposes a setting ahead of the image. # False: 0.1.73 changes chart metadata only, naming etcd in the description, the keywords and # the README; no template, value or default moves. + # False: 0.1.74 changes chart metadata only, naming Neo4j in the description, the keywords and + # the README; no template, value or default moves. artifacthub.io/containsSecurityUpdates: "false" artifacthub.io/images: | - name: libredb-studio @@ -170,7 +176,7 @@ annotations: - name: Source url: https://github.com/libredb/libredb-studio artifacthub.io/changes: | - - "The application image gains the etcd provider (#1089); no template, value or default moves, and it acts once an application release moves appVersion." + - "The application image gains the read-only Neo4j provider; no template, value or default moves, and it acts once an application release moves appVersion." dependencies: - name: postgresql version: "16.x.x" diff --git a/charts/libredb-studio/README.md b/charts/libredb-studio/README.md index f6b0726b4f..74d180398f 100644 --- a/charts/libredb-studio/README.md +++ b/charts/libredb-studio/README.md @@ -2,7 +2,7 @@ [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/libredb-studio)](https://artifacthub.io/packages/search?repo=libredb-studio) -Web-based SQL IDE for cloud-native teams supporting nineteen engines - PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Apache Trino, Apache Cassandra, Prometheus, Apache Kafka and etcd. +Web-based SQL IDE for cloud-native teams supporting twenty engines - PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Apache Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd and Neo4j. ## Prerequisites @@ -40,7 +40,7 @@ helm install libredb libredb/libredb-studio \ ```bash helm install libredb oci://ghcr.io/libredb/charts/libredb-studio \ - --version 0.1.73 \ + --version 0.1.74 \ --set secrets.jwtSecret=$(openssl rand -base64 32) \ --set secrets.adminPassword=MyAdmin123 ``` diff --git a/database-compose.yml b/database-compose.yml index 3885efab7f..0c807be848 100644 --- a/database-compose.yml +++ b/database-compose.yml @@ -1498,6 +1498,51 @@ services: entrypoint: ["sh", "-c", "sh /etcd/seed.sh https://etcd-auth-password:2379 && sh /etcd/rbac.sh https://etcd-auth-password:2379"] <<: *etcd-one-shot-limits + # --------------------------------------------------------------------------------------- + # Neo4j (the neo4j provider). One Community server, Bolt for the provider and HTTP for the + # health probe only. The seed is a graph written for this repository, loaded by hand once the + # service is healthy; the official Movies graph carries no licence and is not vendored. The + # graph and the commands are described in docker/neo4j/README.md. + # --------------------------------------------------------------------------------------- + neo4j: + # Pinned to the exact build the provider is verified against, by tag and by digest: the + # error codes, the value encodings and the captures the provider is tested on are claims + # about 5.26.31, not about Neo4j in general. + image: neo4j:5.26.31-community@sha256:d9cfe82983d27f5a75b3aaae8f316d04f9a698a3b7f6103a508f7caf8362f255 + container_name: libredb-neo4j + restart: unless-stopped + ports: + # Loopback only, as the postgres service's. 7687 is Bolt, the provider's transport; 7474 + # is HTTP, kept for the probe and the Browser a developer may open by hand. + - "127.0.0.1:7687:7687" + - "127.0.0.1:7474:7474" + environment: + # The test credential, as the clickhouse and couchbase services spell theirs. Neo4j + # refuses a password shorter than 8 characters, which rules out the postgres/postgres form. + NEO4J_AUTH: neo4j/password123 + # Small enough to sit inside the bound below: heap plus page cache plus the JVM's own. + NEO4J_server_memory_heap_max__size: 512m + NEO4J_server_memory_pagecache_size: 128m + # No usage report to Neo4j from a test fixture. + NEO4J_server_bolt_telemetry_enabled: "false" + # No data volume: removing the container resets the fixture. + healthcheck: + # The HTTP discovery document, answered once the server accepts connections. The image + # ships wget; cypher-shell would start a JVM per probe. + test: ["CMD-SHELL", "wget -q -O - http://localhost:7474/ || exit 1"] + start_period: 60s + start_interval: 2s + interval: 60s + timeout: 5s + retries: 3 + # Bounded as the etcd and Kafka fixtures are; no swap past it. + deploy: + resources: + limits: + cpus: "1" + memory: 1G + memswap_limit: 1G + # ========================================================================== # Wire-compatible engines (issue #424, Phase 0) # diff --git a/deploy/aws/listing/description.md b/deploy/aws/listing/description.md index e579cc42f9..023bd09c4e 100644 --- a/deploy/aws/listing/description.md +++ b/deploy/aws/listing/description.md @@ -1,2 +1,2 @@ -Open-source SQL IDE for cloud-native teams. One interface for 19 engines, among them PostgreSQL, MySQL, Oracle, SQL Server, MongoDB, Redis, ClickHouse and Trino. Browse schemas, run queries with schema-aware autocomplete, compare schemas, and generate SQL with AI assistance. Runs on your own instance; no data leaves your account. +Open-source SQL IDE for cloud-native teams. One interface for 20 engines, among them PostgreSQL, MySQL, Oracle, SQL Server, MongoDB, Redis, ClickHouse and Trino. Browse schemas, run queries with schema-aware autocomplete, compare schemas, and generate SQL with AI assistance. Runs on your own instance; no data leaves your account. diff --git a/deploy/aws/listing/listing-fields.md b/deploy/aws/listing/listing-fields.md index e69c3ca292..98e5699cb5 100644 --- a/deploy/aws/listing/listing-fields.md +++ b/deploy/aws/listing/listing-fields.md @@ -10,7 +10,7 @@ enforces them, along with the character set the portal accepts (ASCII 0-126 plus (R), (C), (TM) and currency symbols). The engine count is checked against the code by the repo-wide catalog gate, not by a number kept here. - + ## Product title @@ -27,7 +27,7 @@ sentence verbatim, which is what the AMI product checklist asks for. -- Nineteen engines behind one interface, among them PostgreSQL, MySQL, Oracle, SQL Server, MongoDB, Redis, ClickHouse and Trino. +- Twenty engines behind one interface, among them PostgreSQL, MySQL, Oracle, SQL Server, MongoDB, Redis, ClickHouse and Trino. - AI query assistance using your own model key, or run it with no AI at all. - Self-hosted on a single instance: connections, query history and results stay in your account. diff --git a/deploy/azure/listing/description.html b/deploy/azure/listing/description.html index a9f5828f7a..62bb626009 100644 --- a/deploy/azure/listing/description.html +++ b/deploy/azure/listing/description.html @@ -4,10 +4,10 @@

What you get

    -
  • One workspace for nineteen engines - PostgreSQL, MySQL/MariaDB/TiDB, Microsoft SQL Server +
  • One workspace for twenty engines - PostgreSQL, MySQL/MariaDB/TiDB, Microsoft SQL Server (including Azure SQL), Oracle, SQLite, libSQL/Turso, DuckDB, MongoDB/FerretDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, - Apache Kafka and etcd.
  • + Apache Kafka, etcd and Neo4j.
  • A real editor - Monaco-based SQL editing with schema-aware autocomplete, formatting, query history and virtualized result grids that stay fast on large result sets.
  • Optional, read-only AI - an agent that investigates a question you state and diff --git a/deploy/azure/listing/listing-fields.md b/deploy/azure/listing/listing-fields.md index f3a691e4f9..b7485853a9 100644 --- a/deploy/azure/listing/listing-fields.md +++ b/deploy/azure/listing/listing-fields.md @@ -31,12 +31,12 @@ LibreDB Studio **Search results summary** (limit 100): -Open-source SQL IDE for 19 engines: PostgreSQL, MySQL, SQL Server, Oracle, MongoDB, Redis and more +Open-source SQL IDE for 20 engines: PostgreSQL, MySQL, SQL Server, Oracle, MongoDB, Redis and more **Short description** (limit 256): -Open-source, self-hosted SQL IDE for nineteen engines: PostgreSQL, MySQL, SQL Server, Oracle, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Cassandra, Prometheus, Kafka and etcd, with read-only AI. +Open-source, self-hosted SQL IDE for 20 engines: PostgreSQL, MySQL, SQL Server, Oracle, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Cassandra, Prometheus, Kafka, etcd and Neo4j, with read-only AI. **Description** (limit 5000, HTML): see [description.html](description.html) — the limit is asserted by the unit test. @@ -75,7 +75,7 @@ Planned captions: 1. `hero-editor` — "Write and run SQL with schema-aware autocomplete and a virtualized result grid." 2. `agent-rail` — "Ask the read-only agent a question; every claim in its answer cites the result it came from." 3. `erd-diagram` — "Explore relationships with an automatically generated ERD." -4. `connection-modal`: "Connect to nineteen engines, from PostgreSQL and SQL Server to Apache Cassandra." +4. `connection-modal`: "Connect to twenty engines, from PostgreSQL and SQL Server to Apache Cassandra." 5. `data-profiler` — "Profile table data: distributions, null ratios and outliers at a glance." ## Plan (§7.6) diff --git a/deploy/caprover/libredb-studio.yml b/deploy/caprover/libredb-studio.yml index 22143dd57a..cb574b898f 100644 --- a/deploy/caprover/libredb-studio.yml +++ b/deploy/caprover/libredb-studio.yml @@ -74,10 +74,10 @@ caproverOneClickApp: instructions: start: |- LibreDB Studio is a modern, open-source, web-based SQL IDE for the cloud era. - Query nineteen engines - PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, + Query twenty engines - PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, - Apache Kafka and etcd - from your browser, with an ERD viewer, schema diff and optional + Apache Kafka, etcd and Neo4j - from your browser, with an ERD viewer, schema diff and optional read-only AI on your own model key. What you can set below (defaults are sensible - you can usually just click Deploy): @@ -163,5 +163,5 @@ caproverOneClickApp: See https://github.com/libredb/libredb-studio displayName: LibreDB Studio isOfficial: false - description: Open-source web-based SQL IDE. Query nineteen engines - PostgreSQL, MySQL, Oracle, SQL Server, MongoDB, Redis and more - from your browser, with an ERD viewer, schema diff and optional read-only AI. + description: Open-source web-based SQL IDE. Query twenty engines - PostgreSQL, MySQL, Oracle, SQL Server, MongoDB, Redis and more - from your browser, with an ERD viewer, schema diff and optional read-only AI. documentation: Image and docs - https://github.com/libredb/libredb-studio . Uses ghcr.io/libredb/libredb-studio. Deploys with SQLite persistence; supports OIDC SSO and multiple AI providers via extra env vars. diff --git a/deploy/digitalocean/assets/description-long.md b/deploy/digitalocean/assets/description-long.md index 23b93b52ce..345166be82 100644 --- a/deploy/digitalocean/assets/description-long.md +++ b/deploy/digitalocean/assets/description-long.md @@ -2,11 +2,11 @@ **The open-source SQL IDE built for cloud-native teams.** -LibreDB Studio gives you a full-featured database workspace in your browser: connect to PostgreSQL, MySQL, MongoDB, Redis and fifteen more engines, write and run queries, have the optional AI explain them wherever the engine returns a query plan, and share results with your team. +LibreDB Studio gives you a full-featured database workspace in your browser: connect to PostgreSQL, MySQL, MongoDB, Redis and sixteen more engines, write and run queries, have the optional AI explain them wherever the engine returns a query plan, and share results with your team. ## Features -- **Nineteen engines, one interface**: PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka and etcd +- **Twenty engines, one interface**: PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd and Neo4j - **Read-only AI agent** — state a question and the agent investigates it, and every claim in its report cites the result it came from; it runs SQL on PostgreSQL, SQLite, DuckDB and SQL Server only, in a session the database enforces as read-only, so writes and DDL are refused by the engine rather than by reading the statement. On every other engine it drafts the statement and you run it, and nothing reaches your editor unless you consent to the hand-over when the run opens - **AI query explanation** — one click turns an unfamiliar query into plain English, with your own schema as context, on PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Couchbase, ClickHouse, Apache Druid and Trino: the write-up is derived from the engine's own `EXPLAIN` plan, so it is offered where an engine returns one (bring your own key: Gemini, OpenAI, Ollama or any OpenAI-compatible endpoint; off unless configured) - **Modern editor** — autocomplete, syntax highlighting, query history diff --git a/deploy/koyeb/CATALOG_SUBMISSION.md b/deploy/koyeb/CATALOG_SUBMISSION.md index d724111b16..1326fe23e8 100644 --- a/deploy/koyeb/CATALOG_SUBMISSION.md +++ b/deploy/koyeb/CATALOG_SUBMISSION.md @@ -53,7 +53,7 @@ a backup. > We maintain **LibreDB Studio**, an open-source, web-based SQL IDE for > cloud-native teams (PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, > MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, -> Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd, with AI-assisted querying). It's free and MIT licensed: +> Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd, Neo4j, with AI-assisted querying). It's free and MIT licensed: > https://github.com/libredb/libredb-studio > > We already ship a working **Deploy to Koyeb** button (prebuilt GHCR image diff --git a/deploy/railway/TEMPLATE_OVERVIEW.md b/deploy/railway/TEMPLATE_OVERVIEW.md index c8b2dfc794..f5282b9b0e 100644 --- a/deploy/railway/TEMPLATE_OVERVIEW.md +++ b/deploy/railway/TEMPLATE_OVERVIEW.md @@ -1,6 +1,6 @@ # Deploy and Host libredb-studio on Railway -LibreDB Studio is an open-source, web-based SQL IDE for cloud-native teams. Query PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka and etcd from your browser, with an ERD viewer, schema diff, a data profiler, and optional AI on your own model key: a read-only agent that investigates a question and cites the result each claim came from, plus one-click explanation of a query on the engines that return an `EXPLAIN` plan. No desktop client required. +LibreDB Studio is an open-source, web-based SQL IDE for cloud-native teams. Query PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd and Neo4j from your browser, with an ERD viewer, schema diff, a data profiler, and optional AI on your own model key: a read-only agent that investigates a question and cites the result each claim came from, plus one-click explanation of a query on the engines that return an `EXPLAIN` plan. No desktop client required. ## About Hosting libredb-studio @@ -16,7 +16,7 @@ Hosting LibreDB Studio means running a single stateless Next.js container that s ## Dependencies for libredb-studio Hosting -- A database to connect to: any of the nineteen engines above (bring your own, or add a Railway database to the project). +- A database to connect to: any of the twenty engines above (bring your own, or add a Railway database to the project). - A persistent volume mounted at `/app/data` for the SQLite-backed store of saved connections and settings (included in this template). ### Deployment Dependencies diff --git a/deploy/railway/template.json b/deploy/railway/template.json index 751a037308..f19c31eebc 100644 --- a/deploy/railway/template.json +++ b/deploy/railway/template.json @@ -1,7 +1,7 @@ { "$comment": "Reviewable serialization of the LibreDB Studio Railway template. Railway does not ingest this file directly — the marketplace template is built in Railway's template editor (Workspace -> Templates -> New Template, at https://railway.com/workspace/templates). This file is the single source of truth for what to enter there (see PUBLISH.md) and what a future API automation would consume. Mirrors deploy/caprover/libredb-studio.yml.", "name": "LibreDB Studio", - "description": "Open-source web-based SQL IDE. Query nineteen engines - PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka & etcd - from your browser, with optional read-only AI on your own model key.", + "description": "Open-source web-based SQL IDE. Query twenty engines - PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd & Neo4j - from your browser, with optional read-only AI on your own model key.", "tags": ["database", "sql", "ide", "postgres", "mysql", "mongodb", "redis"], "services": [ { diff --git a/deploy/rancher/CATALOG_LISTING.md b/deploy/rancher/CATALOG_LISTING.md index a7309cdf92..fba2b5c627 100644 --- a/deploy/rancher/CATALOG_LISTING.md +++ b/deploy/rancher/CATALOG_LISTING.md @@ -15,25 +15,25 @@ The import takes the page title from `appVersion` and the chart version from `ve Edits here do not propagate automatically — SUSE owns the page, so any change has to be mailed to the partner contact. What gets mailed is `pcsc-listing.html` beside this file, not the sections below: the page template holds at most 1494 characters including the list markup, and `tests/unit/pcsc-listing.test.ts` keeps that file under it. -> **Accuracy gate: engine count.** The wording below says nineteen engines. That is true only -> from the release that carries **etcd** ([#1089](https://github.com/libredb/libredb-studio/issues/1089)), which followed Apache Kafka; eighteen is true of a release that carries **Apache Kafka** ([#1088](https://github.com/libredb/libredb-studio/issues/1088)) and not etcd, if one is cut, which followed Prometheus; seventeen is true of a release that carries **Prometheus** ([#1085](https://github.com/libredb/libredb-studio/issues/1085)) and not Kafka, if one is cut, +> **Accuracy gate: engine count.** The wording below says twenty engines. That is true only +> from the release that carries **Neo4j**, which followed etcd; nineteen is true of a release that carries **etcd** ([#1089](https://github.com/libredb/libredb-studio/issues/1089)) and not Neo4j, if one is cut, which followed Apache Kafka; eighteen is true of a release that carries **Apache Kafka** ([#1088](https://github.com/libredb/libredb-studio/issues/1088)) and not etcd, if one is cut, which followed Prometheus; seventeen is true of a release that carries **Prometheus** ([#1085](https://github.com/libredb/libredb-studio/issues/1085)) and not Kafka, if one is cut, > which followed DuckDB; sixteen was true from the release that carried **DuckDB** > ([#424](https://github.com/libredb/libredb-studio/issues/424)), which followed libSQL, and fourteen from **0.13.0** onwards, the release that carried > Elasticsearch, OpenSearch, Trino and Apache Cassandra alongside the ten of 0.11.0. > The number is the `SHIPPED` record in > `src/lib/db/compatibility.ts` minus the embedded `libredb`, which `EXTERNAL` in the same > file already splits out; read it from there rather than from this file. The catalog entry -> is version-scoped, so do not publish the nineteen-engine wording against a version that -> predates etcd: send the eighteen-engine variant for a release with Apache Kafka and not etcd (its size-cut body is `pcsc-listing.html` at 2ddb99c9), the seventeen-engine variant for a release with Prometheus and not Kafka (its size-cut body is `pcsc-listing.html` at 8dfdfcce), the sixteen-engine variant (the DuckDB release onwards; its size-cut body is `pcsc-listing.html` at 06a4cb11), the fourteen-engine one (0.13.0 onwards), the ten-engine one +> is version-scoped, so do not publish the twenty-engine wording against a version that +> predates Neo4j: send the nineteen-engine variant for a release with etcd and not Neo4j (its size-cut body is `pcsc-listing.html` at 42050550), the eighteen-engine variant for a release with Apache Kafka and not etcd (its size-cut body is `pcsc-listing.html` at 2ddb99c9), the seventeen-engine variant for a release with Prometheus and not Kafka (its size-cut body is `pcsc-listing.html` at 8dfdfcce), the sixteen-engine variant (the DuckDB release onwards; its size-cut body is `pcsc-listing.html` at 06a4cb11), the fourteen-engine one (0.13.0 onwards), the ten-engine one > (0.11.0 onwards) or the eight-engine one instead. > -> **The scope goes with the count.** Browsing and querying reach all nineteen; editing data does +> **The scope goes with the count.** Browsing and querying reach all twenty; editing data does > not, so "manage data across …" must never be written over the whole list. Read the split from > the providers: `supportsInlineRowEdit` and `supportsCreateTable` default to `true` in > `src/lib/db/base-provider.ts` and each provider that cannot turns them off, which leaves inline > row editing on PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL and DuckDB, and table > creation on those seven plus Trino. Every other engine (Cassandra, ClickHouse, Couchbase, Druid, -> Elasticsearch, Apache Kafka, MongoDB, OpenSearch, Prometheus, Redis and etcd) reports those controls as unsupported. The +> Elasticsearch, Apache Kafka, MongoDB, Neo4j, OpenSearch, Prometheus, Redis and etcd) reports those controls as unsupported. The > reason differs per engine and the copy must not flatten it: on Elasticsearch no mutation is in > the SQL grammar at all, while OpenSearch's grammar carries exactly one — `DELETE`, off by > default on the cluster (`docs/providers/opensearch.md` §5.6, and `SEARCH_SCHEMA_REFRESH_PATTERN` @@ -88,11 +88,11 @@ What gets mailed is `pcsc-listing.html` beside this file, not the sections below > `operator/helm-charts/libredb-studio/` by hand, or the sync guard fails the required check. > > What *is* release-coupled is every marketplace description that spells the count: -> `deploy/azure`, `deploy/railway` and `deploy/caprover` said seventeen from the merge of #1085, eighteen from the merge of #1088, and say nineteen from the merge of #1089, +> `deploy/azure`, `deploy/railway` and `deploy/caprover` said seventeen from the merge of #1085, eighteen from the merge of #1088, nineteen from the merge of #1089, and say twenty from the merge of the Neo4j provider, > because `tests/unit/lib/catalog-copy-engine-count.test.ts` holds every counted numeral there to > `EXTERNAL_DATABASE_TYPES` on `main`. > CapRover and Railway both moved to 0.17.0, which carries Prometheus and Apache Kafka, so their numerals and their tags agreed again until the merge of #1089. -> From that merge they say nineteen, one ahead of the 0.17.0 they name, until the release that carries etcd moves their tags: the exception the merge of #1088 opened and 0.17.0 closed, opened once more. +> From that merge they said nineteen, one ahead of the 0.17.0 they name, and from the merge of the Neo4j provider they say twenty, two ahead, until the release that carries etcd and Neo4j moves their tags: the exception the merge of #1088 opened and 0.17.0 closed, opened once more. > At the DuckDB release they said sixteen - and all three were still on fourteen when it landed, a full engine behind, because > libSQL had moved the code and not them. `deploy/railway/template.json` and > `deploy/caprover/libredb-studio.yml` were on thirteen once for the same reason: each channel @@ -100,14 +100,14 @@ What gets mailed is `pcsc-listing.html` beside this file, not the sections below > describes an artifact a user can already download, so the number has to be true at the tag it > names. Read the number from `SHIPPED` when a tag carries it, and see BACKLOG D39 - nothing > counts these lists, which is why three PRs in a row have corrected them by hand. -> The AUR package is one ahead as CapRover and Railway are: `packaging/aur/PKGBUILD` and its `.SRCINFO` pin `pkgver=0.17.0`, whose payload has eighteen engines and no etcd, and say nineteen from the merge of #1089, because `tests/unit/lib/catalog-copy-engine-count.test.ts` counts the `pkgdesc` line on `main` too. -> Its first import is a hand push of those files (`packaging/aur/README.md`, Releases), so one made from `main` after that merge lists 0.17.0 as nineteen engines: push it before the merge, or render the `PKGBUILD` with `scripts/render-aur-pkgbuild.mjs` for the release that carries etcd, regenerate `.SRCINFO` from that render with `makepkg --printsrcinfo > .SRCINFO`, since the script writes the `PKGBUILD` alone, and push both. +> The AUR package is two ahead as CapRover and Railway are: `packaging/aur/PKGBUILD` and its `.SRCINFO` pin `pkgver=0.17.0`, whose payload has eighteen engines and no etcd or Neo4j, and say twenty from the merge of the Neo4j provider (nineteen from the merge of #1089), because `tests/unit/lib/catalog-copy-engine-count.test.ts` counts the `pkgdesc` line on `main` too. +> Its first import is a hand push of those files (`packaging/aur/README.md`, Releases), so one made from `main` after those merges lists 0.17.0 as twenty engines: push it before them, or render the `PKGBUILD` with `scripts/render-aur-pkgbuild.mjs` for the release that carries etcd and Neo4j, regenerate `.SRCINFO` from that render with `makepkg --printsrcinfo > .SRCINFO`, since the script writes the `PKGBUILD` alone, and push both. > `packaging/winget`, `packaging/chocolatey` and `packaging/homebrew` carry **no number** in > their summaries - nothing regenerates them from the registry, so any digit there is stale the > day the next engine lands (issue #445) - but their exhaustive descriptions still name every > engine, and so does `desktop/src-tauri/tauri.conf.json`. > `packaging/linux/nfpm.yaml` and the operator CSVs are consumed at release time from `main`, -> so they name nineteen now and the next tag publishes it. +> so they name twenty now and the next tag publishes it. ## Listing facts @@ -133,14 +133,14 @@ What gets mailed is `pcsc-listing.html` beside this file, not the sections below LibreDB Studio is an MIT-licensed, AI-assisted open source SQL IDE that connects to PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, -Prometheus, Apache Kafka and etcd directly from the browser. +Prometheus, Apache Kafka, etcd and Neo4j directly from the browser. ## Long description LibreDB Studio brings a full SQL IDE to Rancher-managed Kubernetes clusters: browse schemas and run queries across PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, -Apache Cassandra, Prometheus, Apache Kafka and etcd from a single web interface, with no desktop +Apache Cassandra, Prometheus, Apache Kafka, etcd and Neo4j from a single web interface, with no desktop client to install. Editing data follows the engine rather than the IDE: inline row editing on PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL and DuckDB, table creation on those seven and Trino, and @@ -165,9 +165,9 @@ versions are documented and validated for every release. ## Key features (bullet form, if the catalog template asks for them) -- Nineteen database engines in one browser-based IDE: PostgreSQL, MySQL, Oracle, +- Twenty database engines in one browser-based IDE: PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, - Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd + Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd, Neo4j - One-click install from the Rancher Apps catalog — deployable with default values, zero configuration required - Optional AI assistance (Gemini, OpenAI, or a self-hosted model; off by default): @@ -190,8 +190,8 @@ partner contact rather than sending them one at a time. | Field on the page | Published value | Should be | |---|---|---| | Version | LibreDB Studio 0.9.44 | 0.13.5 when this row was written, with the release link pointing at ; resolved by the weekly import, which carries 0.16.2 as of 2026-09-23 (the note of that date below) | -| Key features | "Seven database engines" | sixteen while the page carries a release before Prometheus, 0.16.2 as of 2026-09-23, seventeen from a release that carries Prometheus (#1085) and not Kafka, eighteen from a release that carries Apache Kafka (#1088) and not etcd, and nineteen, the wording in this file, from the release that carries etcd (#1089) | -| Short and long description | the pre-0.11.0 revision, which names seven engines and an AI that writes SQL from natural language | until the page carries the release with Prometheus, the sixteen-engine body at 06a4cb11 (`git show 06a4cb11:deploy/rancher/pcsc-listing.html`); from a release with Prometheus and not Kafka, the seventeen-engine body at 8dfdfcce; from a release with Kafka and not etcd, the eighteen-engine body at 2ddb99c9; from the release with etcd, `pcsc-listing.html` as it stands, nineteen engines. No natural-language-to-SQL claim either way: that feature was removed from the product | +| Key features | "Seven database engines" | sixteen while the page carries a release before Prometheus, 0.16.2 as of 2026-09-23, seventeen from a release that carries Prometheus (#1085) and not Kafka, eighteen from a release that carries Apache Kafka (#1088) and not etcd, nineteen from a release that carries etcd (#1089) and not Neo4j, and twenty, the wording in this file, from the release that carries Neo4j | +| Short and long description | the pre-0.11.0 revision, which names seven engines and an AI that writes SQL from natural language | until the page carries the release with Prometheus, the sixteen-engine body at 06a4cb11 (`git show 06a4cb11:deploy/rancher/pcsc-listing.html`); from a release with Prometheus and not Kafka, the seventeen-engine body at 8dfdfcce; from a release with Kafka and not etcd, the eighteen-engine body at 2ddb99c9; from a release with etcd and not Neo4j, the nineteen-engine body at 42050550; from the release with Neo4j, `pcsc-listing.html` as it stands, twenty engines. No natural-language-to-SQL claim either way: that feature was removed from the product | | Hardware Architecture | x86-64 | x86-64 and Arm64 (`ghcr.io/libredb/libredb-studio` is linux/amd64 + linux/arm64) | Two open questions for the same mail: whether the version field can track the latest @@ -249,6 +249,7 @@ The open question to SUSE is whether their import can read the listing from a ch Since #1085, `app-readme.md` and `pcsc-listing.html` name seventeen engines, one more than #1168 and the page's 0.16.2 carry, so neither goes out before a release that carries Prometheus reaches the catalog. Since #1088 they name eighteen, two more than the page's 0.16.2 carries, so neither goes out before a release that carries Apache Kafka reaches the catalog. Since #1089 they name nineteen, three more than the page's 0.16.2 carries, so neither goes out before a release that carries etcd reaches the catalog. +Since the Neo4j provider they name twenty, four more than the page's 0.16.2 carries, so neither goes out before a release that carries Neo4j reaches the catalog. Vendor naming, as settled: the page heads the partner as **Sekoya** (the legal entity, Sekoya Grup Bilisim ve Teknoloji Ltd. Sti.) with the product named **LibreDB Studio**. diff --git a/deploy/rancher/app-readme.md b/deploy/rancher/app-readme.md index b9aac2141e..84d0190687 100644 --- a/deploy/rancher/app-readme.md +++ b/deploy/rancher/app-readme.md @@ -1,11 +1,11 @@ # LibreDB Studio -LibreDB Studio is an MIT-licensed, AI-assisted open source SQL IDE that connects to nineteen database engines directly from the browser. +LibreDB Studio is an MIT-licensed, AI-assisted open source SQL IDE that connects to twenty database engines directly from the browser. Use cases: * Browser-based database management - * Browse schemas and run queries across nineteen engines: PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka and etcd. Editing data follows the engine rather than the IDE: inline row editing on PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL and DuckDB, and everywhere else the controls are reported as unsupported rather than offered and then failed. + * Browse schemas and run queries across twenty engines: PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd and Neo4j. Editing data follows the engine rather than the IDE: inline row editing on PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL and DuckDB, and everywhere else the controls are reported as unsupported rather than offered and then failed. * AI-assisted querying * An optional AI assistant (bring your own key: Gemini, OpenAI, or a local model) explains a query in plain English from the engine's own EXPLAIN plan, on PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Couchbase, ClickHouse, Apache Druid and Trino. It also runs a read-only investigation agent on PostgreSQL, SQLite, DuckDB and SQL Server that never writes: the database, not the IDE, is what refuses writes and DDL. It stays off unless you configure a provider. * Self-hosted data diff --git a/deploy/rancher/pcsc-listing.html b/deploy/rancher/pcsc-listing.html index 30304ff6d6..5cdcffddef 100644 --- a/deploy/rancher/pcsc-listing.html +++ b/deploy/rancher/pcsc-listing.html @@ -1,8 +1,8 @@ -LibreDB Studio brings a full SQL IDE to Rancher-managed Kubernetes clusters: browse schemas and run queries across nineteen database engines from one web interface, with no desktop client to install. Where an engine cannot edit data, the controls are shown as unsupported instead of failing. +LibreDB Studio brings a full SQL IDE to Rancher-managed Kubernetes clusters: browse schemas and run queries across twenty database engines from one web interface, with no desktop client to install. Where an engine cannot edit data, the controls are shown as unsupported instead of failing. The Helm chart installs from the Rancher Apps catalog with default values: first-run admin credentials are generated and printed to the pod log, while production installs can supply their own secrets. LibreDB Studio runs on your own infrastructure and is developed and commercially supported by the LibreDB team.
      -
    • Nineteen engines: PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd
    • +
    • Twenty engines: PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd, Neo4j
    • Optional AI, off by default (Gemini, OpenAI or a local model): plain-English query explanation from the EXPLAIN plan on PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Couchbase, ClickHouse, Apache Druid and Trino. A read-only agent on PostgreSQL, SQLite, DuckDB and SQL Server never writes
    • Hardened chart defaults: non-root, read-only root filesystem, NetworkPolicy, PDB, HPA, Ingress/TLS
    • Self-hosted and air-gap friendly: no external services required
    • diff --git a/desktop/src-tauri/desktop-entry.hbs b/desktop/src-tauri/desktop-entry.hbs index 388e0718e1..b3f63554c3 100644 --- a/desktop/src-tauri/desktop-entry.hbs +++ b/desktop/src-tauri/desktop-entry.hbs @@ -7,6 +7,6 @@ Exec={{exec}} Icon={{icon}} Terminal=false Categories={{categories}} -Keywords=SQL;Database;PostgreSQL;MySQL;SQLite;libSQL;Turso;DuckDB;MongoDB;Redis;Oracle;SQLServer;ClickHouse;Couchbase;Druid;Elasticsearch;OpenSearch;Trino;Cassandra;Prometheus;PromQL;Kafka;etcd;IDE;Query; +Keywords=SQL;Database;PostgreSQL;MySQL;SQLite;libSQL;Turso;DuckDB;MongoDB;Redis;Oracle;SQLServer;ClickHouse;Couchbase;Druid;Elasticsearch;OpenSearch;Trino;Cassandra;Prometheus;PromQL;Kafka;etcd;Neo4j;Cypher;IDE;Query; StartupNotify=true StartupWMClass=libredb-studio-desktop diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 6f7c0daf99..d5c7b232ce 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -30,7 +30,7 @@ "targets": ["appimage", "deb"], "category": "DeveloperTool", "shortDescription": "The open-source SQL IDE for cloud-native teams", - "longDescription": "LibreDB Studio is an open-source SQL IDE for PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka and etcd, with AI query assistance. The desktop build runs the same server LibreDB Studio ships everywhere else as a local sidecar and keeps all data on this machine.", + "longDescription": "LibreDB Studio is an open-source SQL IDE for PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd and Neo4j, with AI query assistance. The desktop build runs the same server LibreDB Studio ships everywhere else as a local sidecar and keeps all data on this machine.", "homepage": "https://libredb.org", "publisher": "LibreDB", "license": "MIT", diff --git a/docker/neo4j/README.md b/docker/neo4j/README.md new file mode 100644 index 0000000000..55e527f11a --- /dev/null +++ b/docker/neo4j/README.md @@ -0,0 +1,76 @@ +# Neo4j fixture + +The live Neo4j server of `database-compose.yml`, and the graph that seeds it. +The Neo4j provider's captures, its integration test and every hand pass run against this server. + +## The server + +| Service | Profile | From the host | Transport and authentication | +|---|---|---|---| +| `neo4j` | none | Bolt `127.0.0.1:7687`, HTTP `127.0.0.1:7474` | Plaintext, user `neo4j`, password `password123` | + +The server runs `neo4j:5.26.31-community`, pinned by the digest `sha256:d9cfe82983d27f5a75b3aaae8f316d04f9a698a3b7f6103a508f7caf8362f255` (pulled on 2026-10-03). +Bolt is the provider's transport; HTTP is published for the health probe and for the Neo4j Browser a developer may open by hand. +The password follows the clickhouse and couchbase services, since Neo4j refuses one shorter than 8 characters. +Both ports are bound to the loopback address only, and the container is bounded at 1 GB of memory with no swap, a 512 MB heap and a 128 MB page cache. +Bolt telemetry is off. +There is no data volume: removing the container resets the server. + +## Bringing it up + +```sh +docker compose -f database-compose.yml up -d --wait neo4j +docker exec -i libredb-neo4j cypher-shell -u neo4j -p password123 < docker/neo4j/seed.cypher +docker exec libredb-neo4j cypher-shell -u neo4j -p password123 'MATCH (n) RETURN count(n)' +``` + +The last command answers `30`. +The seed may run again: its first statement deletes every node, and the index and the constraint are created with `IF NOT EXISTS`, so a second load gives the same graph. + +## The graph + +The seed is a graph written for this repository: 30 nodes and 40 relationships. +The official Movies example graph is not used, because it carries no licence file. + +| Label | Nodes | What it is for | +|---|---|---| +| `Team` | 4 | Teams with a `DATE` founding day | +| `Person` | 10 | People with `STRING`, `INTEGER`, `BOOLEAN` and `LIST` properties, some names with non-ASCII letters | +| `Service` | 8 | Services; the one with `id: 'types'` holds one property of every value type below | +| `Weird Label` | 2 | A label with a space in its name | +| ``Back`tick`` | 2 | A label with a backtick in its name | +| `Shared` | 2 | A label that has the same name as a relationship type | +| `Marker` | 2 | A label whose nodes carry no property at all | + +| Relationship type | Count | Between | +|---|---|---| +| `MEMBER_OF` | 16 | `Person` to `Team` (some people in two teams, with `since` and `role`), and one ``Back`tick`` to `Weird Label` | +| `OWNS` | 9 | `Team` to `Service`, and one `Weird Label` to ``Back`tick`` | +| `DEPENDS_ON` | 14 | `Service` to `Service` with a `critical` flag and a cycle among `checkout`, `ledger` and `auth`, plus the two quoted labels to `gateway` | +| `Shared` | 1 | `Shared` to `Shared` | + +The `types` service holds: + +| Property | Type | Value | +|---|---|---| +| `aString` | `STRING` | `'plain text'` | +| `anInteger` | `INTEGER` | `42` | +| `beyondDoubles` | `INTEGER` | `9007199254740993`, which a JavaScript number cannot hold | +| `minInteger` | `INTEGER` | `-9223372036854775808`, the smallest 64-bit integer | +| `aFloat` | `FLOAT` | `0.1` | +| `notANumber` | `FLOAT` | `NaN` | +| `aBoolean` | `BOOLEAN` | `false` | +| `aList` | `LIST` | `[1, 2, 3]` | +| `aDate` | `DATE` | `2026-10-03` | +| `aLocalTime` | `LOCAL TIME` | `13:45:30.123456789` | +| `aTime` | `ZONED TIME` | `13:45:30.123456789+03:00` | +| `aLocalDateTime` | `LOCAL DATETIME` | `2026-10-03T13:45:30.123456789` | +| `aDateTime` | `ZONED DATETIME` | `2026-10-03T13:45:30.123456789+03:00[Europe/Istanbul]` | +| `aDuration` | `DURATION` | `P1Y2M3DT4H5M6.789S` | +| `aCartesianPoint` | `POINT` | cartesian, SRID 7203, `x: 1.5, y: -2.25` | +| `aWgs84Point` | `POINT` | WGS-84, SRID 4326, longitude `28.9784`, latitude `41.0082` | + +A byte array is not seeded: no core Cypher function produces one, and the APOC plugin is not installed on this image. + +The schema holds a `RANGE` index `person_name` on `Person(name)` and a uniqueness constraint `service_id` on `Service(id)`, besides the two `LOOKUP` indexes every database has. +The constraint brings its own `RANGE` index, also named `service_id`, so `SHOW INDEXES` lists four indexes. diff --git a/docker/neo4j/seed.cypher b/docker/neo4j/seed.cypher new file mode 100644 index 0000000000..7ef62813c0 --- /dev/null +++ b/docker/neo4j/seed.cypher @@ -0,0 +1,110 @@ +// Seed graph for the neo4j service in database-compose.yml, written for this repository. +// Load it once the service is healthy (docker/neo4j/README.md): +// docker exec -i libredb-neo4j cypher-shell -u neo4j -p password123 < docker/neo4j/seed.cypher +// Every statement ends with a semicolon and runs in its own transaction. The first one empties +// the graph, so a second load gives the same graph rather than a doubled one. + +MATCH (n) DETACH DELETE n; + +CREATE INDEX person_name IF NOT EXISTS FOR (p:Person) ON (p.name); + +CREATE CONSTRAINT service_id IF NOT EXISTS FOR (s:Service) REQUIRE s.id IS UNIQUE; + +// Teams. +CREATE (:Team {name: 'Platform', founded: date('2019-03-01')}), + (:Team {name: 'Payments', founded: date('2020-07-15')}), + (:Team {name: 'Search', founded: date('2021-01-04')}), + (:Team {name: 'Data', founded: date('2022-11-30')}); + +// People. Non-ASCII names on purpose: they reach the grid and the index as they are. +CREATE (:Person {name: 'Ayse Yilmaz', age: 34, active: true, skills: ['go', 'kubernetes']}), + (:Person {name: 'Mehmet Demir', age: 41, active: true, skills: ['java', 'kafka']}), + (:Person {name: 'Zeynep Kaya', age: 29, active: false, skills: ['python']}), + (:Person {name: 'Emre Çelik', age: 38, active: true, skills: ['rust', 'postgres']}), + (:Person {name: 'Elif Şahin', age: 27, active: true, skills: []}), + (:Person {name: 'Jonas Berg', age: 45, active: true, skills: ['scala', 'spark']}), + (:Person {name: 'Ana Souza', age: 31, active: true, skills: ['typescript']}), + (:Person {name: 'Kenji Sato', age: 36, active: false, skills: ['c', 'linux']}), + (:Person {name: 'Lena Fischer', age: 33, active: true, skills: ['sql', 'dbt']}), + (:Person {name: 'Omar Haddad', age: 40, active: true, skills: ['elixir']}); + +// Services. One per team at least; `types` holds one property of every value type the +// provider maps (spec 3.4). A byte array is not seeded: no core Cypher function produces one, +// and the APOC plugin is not installed on this image. +CREATE (:Service {id: 'gateway', tier: 1, uptime: 99.95}), + (:Service {id: 'auth', tier: 1, uptime: 99.99}), + (:Service {id: 'ledger', tier: 1, uptime: 99.9}), + (:Service {id: 'checkout', tier: 2, uptime: 99.5}), + (:Service {id: 'indexer', tier: 2, uptime: 98.7}), + (:Service {id: 'query', tier: 2, uptime: 99.1}), + (:Service {id: 'warehouse', tier: 3, uptime: 97.0}), + (:Service { + id: 'types', + aString: 'plain text', + anInteger: 42, + beyondDoubles: 9007199254740993, + minInteger: -9223372036854775808, + aFloat: 0.1, + notANumber: 0.0 / 0.0, + aBoolean: false, + aList: [1, 2, 3], + aDate: date('2026-10-03'), + aLocalTime: localtime('13:45:30.123456789'), + aTime: time('13:45:30.123456789+03:00'), + aLocalDateTime: localdatetime('2026-10-03T13:45:30.123456789'), + aDateTime: datetime({year: 2026, month: 10, day: 3, hour: 13, minute: 45, second: 30, nanosecond: 123456789, timezone: 'Europe/Istanbul'}), + aDuration: duration('P1Y2M3DT4H5M6.789S'), + aCartesianPoint: point({x: 1.5, y: -2.25}), + aWgs84Point: point({longitude: 28.9784, latitude: 41.0082}) + }); + +// Names that need quoting in Cypher: a space, and a backtick inside the name. +CREATE (:`Weird Label` {name: 'with a space', rank: 1}), + (:`Weird Label` {name: 'second', rank: 2}), + (:`Back``tick` {name: 'with a backtick'}), + (:`Back``tick` {name: 'another'}); + +// A label and a relationship type with the same name. +CREATE (:Shared {id: 1})-[:Shared]->(:Shared {id: 2}); + +// A label whose nodes carry no property at all. +CREATE (:Marker), (:Marker); + +// Membership: everyone in one team, a few in two. +UNWIND [ + ['Ayse Yilmaz', 'Platform'], ['Mehmet Demir', 'Payments'], ['Zeynep Kaya', 'Search'], + ['Emre Çelik', 'Platform'], ['Elif Şahin', 'Data'], ['Jonas Berg', 'Data'], + ['Ana Souza', 'Payments'], ['Kenji Sato', 'Platform'], ['Lena Fischer', 'Data'], + ['Omar Haddad', 'Search'], ['Ayse Yilmaz', 'Search'], ['Emre Çelik', 'Data'], + ['Ana Souza', 'Platform'], ['Lena Fischer', 'Search'], ['Jonas Berg', 'Payments'] +] AS pair +MATCH (p:Person {name: pair[0]}), (t:Team {name: pair[1]}) +CREATE (p)-[:MEMBER_OF {since: date('2023-01-01'), role: CASE WHEN p.age > 39 THEN 'lead' ELSE 'engineer' END}]->(t); + +// Ownership: every service has one owning team. +UNWIND [ + ['Platform', 'gateway'], ['Platform', 'auth'], ['Payments', 'ledger'], + ['Payments', 'checkout'], ['Search', 'indexer'], ['Search', 'query'], + ['Data', 'warehouse'], ['Data', 'types'] +] AS pair +MATCH (t:Team {name: pair[0]}), (s:Service {id: pair[1]}) +CREATE (t)-[:OWNS]->(s); + +// Dependencies between services, with a cycle (checkout, ledger, auth) on purpose. +UNWIND [ + ['gateway', 'auth', true], ['checkout', 'ledger', true], ['checkout', 'auth', true], + ['ledger', 'auth', true], ['auth', 'checkout', false], ['query', 'indexer', true], + ['gateway', 'query', false], ['indexer', 'warehouse', false], ['warehouse', 'ledger', false], + ['gateway', 'checkout', true], ['query', 'auth', true], ['indexer', 'auth', false] +] AS edge +MATCH (a:Service {id: edge[0]}), (b:Service {id: edge[1]}) +CREATE (a)-[:DEPENDS_ON {critical: edge[2]}]->(b); + +// The quoted labels take part in relationships too, so a tree click on them reaches a path. +MATCH (w:`Weird Label` {name: 'with a space'}), (b:`Back``tick` {name: 'with a backtick'}), (s:Service {id: 'gateway'}) +CREATE (w)-[:DEPENDS_ON {critical: false}]->(s), + (b)-[:DEPENDS_ON {critical: false}]->(s), + (w)-[:OWNS]->(b); + +MATCH (w:`Weird Label` {name: 'second'}), (b:`Back``tick` {name: 'another'}) +CREATE (b)-[:MEMBER_OF]->(w); diff --git a/docs/ADDING_A_PROVIDER.md b/docs/ADDING_A_PROVIDER.md index fefc7e1670..e34680d0c7 100644 --- a/docs/ADDING_A_PROVIDER.md +++ b/docs/ADDING_A_PROVIDER.md @@ -44,6 +44,9 @@ Three decisions. The first is the consequential one, which is why it is first. `escapeIdentifier()` and `buildLimitClause()` are inherited unchanged and `prepareQuery()` is the only override — for a single dialect trap, not for the transport. - **Non-SQL databases → extend `BaseDatabaseProvider`** directly, like MongoDB and Redis. + - **Graph databases that speak Cypher over Bolt → extend `GraphBaseProvider`**, which extends + `BaseDatabaseProvider` and owns the query path; the engine supplies a profile + ([Adding a graph engine](#adding-a-graph-engine)). - The one reason a SQL-speaking provider extends `BaseDatabaseProvider` anyway is a dialect the shared helpers cannot express. Couchbase is that case: SQL++ quotes identifiers with doubled backticks, which `escapeIdentifier()` produces for no existing type, so it owns its quoting in @@ -54,6 +57,8 @@ Three decisions. The first is the consequential one, which is why it is first. 3. **Query language?** - `'sql'` → Monaco editor uses SQL mode with autocomplete - `'json'` → Monaco editor uses JSON mode with MQL-style autocomplete + - `'cypher'` → the graph layer's Cypher language, with completion from the schema and the engine's + policy profile ### Why a driver-free provider is worth the effort @@ -169,6 +174,45 @@ directly; reshaping rows inside the transport would have broken schema loading. --- +## Adding a graph engine + +A graph engine that speaks Cypher over Bolt joins the shared graph layer, [`src/lib/db/graph/`](../src/lib/db/graph/), instead of writing a provider from `BaseDatabaseProvider` up. +Neo4j is the one engine on it today ([neo4j.md](./providers/neo4j.md)); the layer was designed from Neo4j 5.26 and a probe of Memgraph 3.13.1, and a second engine is expected to find what that probe did not. + +**The layers, and which way they may import.** + +| Layer | Files | Runs | May import | +|---|---|---|---| +| Graph core | `cypher/` (lexer, statements, quoting, read policy, generators), `objects.ts`, `values.ts`, `profile.ts` | server and browser | nothing of `bolt/`, `graph-base-provider.ts` or `src/lib/db/providers/`, and no `node:` module | +| Bolt transport | `bolt/client.ts` (the `GraphClient` seam), `bolt/uri.ts`, `bolt/bolt-client.ts`, `bolt/record-values.ts` | server | the driver, in `bolt-client.ts` and `record-values.ts` only | +| Provider base | `graph-base-provider.ts` | server | the core and the transport | +| Engine | `src/lib/db/providers/graph//` | server, except its policy profile | the layer, never another provider | + +`tests/unit/db/graph/seam-guard.test.ts` holds the direction and the driver members `bolt-client.ts` may call. + +**`GraphBaseProvider`** implements the query path, the object surface, the cancel, health and the maintenance refusal: every statement passes the read policy, then the engine's statement gate, then one `GraphClient.run` in a READ session on the connection's one database, bounded by `DEFAULT_QUERY_LIMIT` rows and the query timeout, and a cancel aborts the run, which closes its session. +It leaves abstract `getCapabilities`, `getLabels` and the seven monitoring reads, so an engine provider is declarations, a profile and monitoring, as `src/lib/db/providers/graph/neo4j/index.ts` is. + +**`GraphEngineProfile`** is what an engine supplies, and its four hooks are the four places where the Memgraph probe measured an engine differing from Neo4j: + +| Hook | What it decides | Neo4j's answer | +|---|---|---| +| `readPolicy` | The denied words and namespaces, the allowlisted procedures, qualified functions and SHOW forms, and the refused prefixes the shared policy reads | `NEO4J_POLICY_PROFILE` in `neo4j/profile.ts`, pure, so the editor reads it too | +| `dialect` | Whether a `CYPHER ` prefix exists, and the offset keyword generators write | the prefix exists; `SKIP` | +| `statementGate` | The server's own classification of a statement the policy allowed, or none | `EXPLAIN` and `summary.queryType`, in `neo4j/statement-gate.ts` | +| `catalog` | The home database and the statements that list each kind, the properties and the indexes | `neo4j/catalog.ts` | + +Beside them the profile carries `engineLabel`, `defaultPort` and `mapError`, the engine's error table. +Two seams the probe measured are deliberately not hooks: READ-mode enforcement and cancellation. +The transport has one execution strategy, an auto-commit `session.run` in a READ session that a closed session cancels, and Memgraph enforces READ mode only in an explicit transaction and stops a statement only on its timeout or `TERMINATE TRANSACTIONS `. +So an engine that needs another strategy adds it to the Bolt transport in its own PR, and the profile gains a field then; it edits nothing under `src/lib/db/providers/graph/neo4j/`. +The Memgraph provider is filed as `docs/BACKLOG.md` D141. + +**What a graph engine still registers** is the same as any engine (Steps 1 to 4): its type-id, its `DB_UI_CONFIG` entry, its factory case, and every record the checklist below names. +`queryLanguage: "cypher"` already has its readers, the editor language and the completion; a policy profile is looked up for the editor by type-id in `src/lib/db/graph-policy-profiles.ts`. + +--- + ## Step 1: Register the Database Type ### 1.1 — Add to `DatabaseType` union @@ -192,7 +236,7 @@ If your database uses a new editor mode (not `'sql'` or `'mongodb'`), add it: ```typescript export interface QueryTab { // ... - type: 'sql' | 'mongodb' | 'redis' | 'libredb' | 'promql' | 'kafka' | 'etcd'; // Add your type here if needed + type: 'sql' | 'mongodb' | 'redis' | 'libredb' | 'promql' | 'kafka' | 'etcd' | 'cypher'; // Add your type here if needed } ``` @@ -201,7 +245,7 @@ For most SQL databases, the existing `'sql'` type is sufficient. You only need a A new tab type is reached one of two ways, and both are wired in `src/lib/editor/tab-language.ts` and its neighbours. A language that is a kind of JSON declares a `queryDialect` on the provider and gets a record in `QUERY_DIALECTS` (`src/lib/db/query-dialects.ts`), whose `tabType` `resolveTabType()` reads **before** the `queryLanguage === 'json'` rung; skipping the dialect leaves the tab typed `mongodb`, which is exactly what #427 fixed. A language that is neither SQL nor JSON widens `ProviderCapabilities.queryLanguage` instead, as PromQL did (#1085), and every reader of that union then needs an explicit arm or a test pinning that its branch is right, because a reader written `=== 'json'` sends the new member into its SQL branch and one written `!== 'sql'` into its JSON branch. -Either way the type gets a record in `DIALECT_EDITORS` (`src/lib/editor/dialect-editors.ts`), its Monaco language for `editorLanguageForTabType()` and its formatter, if any, for `QueryEditor`'s Format button, and that language module is registered in `QueryEditor`'s `handleBeforeMount` alongside `registerLibreDBLanguage`, `registerRedisLanguage`, `registerPromqlLanguage` and `registerEtcdLanguage`. +Either way the type gets a record in `DIALECT_EDITORS` (`src/lib/editor/dialect-editors.ts`), its Monaco language for `editorLanguageForTabType()` and its formatter, if any, for `QueryEditor`'s Format button, and that language module is registered in `QueryEditor`'s `handleBeforeMount` alongside `registerLibreDBLanguage`, `registerRedisLanguage`, `registerPromqlLanguage`, `registerEtcdLanguage` and `registerCypherLanguage`. A JSON kind may instead render in Monaco's built-in `json` mode and register no module, as Kafka's read request does (#1088). Then the MongoDB completion provider `QueryEditor` registers for `json` must stay off it: it registers only where the declared capabilities name no JSON dialect, so its MongoDB snippets and column completions never reach a tab whose parser refuses them. @@ -224,6 +268,7 @@ is kept in sync with its per-provider doc). Don't copy a skeleton from this guid | Document store reached over HTTP/REST (no driver) | `BaseDatabaseProvider` | `document/couchbase/` | [couchbase.md](./providers/couchbase.md) | | Key-value store | `BaseDatabaseProvider` | `redis.ts` | [redis.md](./providers/redis.md) | | Embedded (in-process, no wire protocol) | `BaseDatabaseProvider` | `embedded/libredb.ts` | [libredb.md](./providers/libredb.md) | +| Graph database speaking Cypher over Bolt | `GraphBaseProvider` | `graph/neo4j/` | [neo4j.md](./providers/neo4j.md) | **Implement the abstract methods** from the `DatabaseProvider` interface: `connect`, `disconnect`, `query`, the five REQUIRED object methods (`listContainers`, `countObjects`, `listObjects`, @@ -249,10 +294,10 @@ never off the kind id. **It is OPTIONAL, and omitting it entirely is the right answer for an engine that publishes no definition text.** `readObjectSource?` is declared optional on `DatabaseProvider` in `src/lib/db/types.ts` for that reason: a provider with no source-bearing kind can never reach the -method, so requiring it would put an unreachable throw in each. Two shipped providers are exactly -that case and say so in their own docs, `druid` and `libredb`. If yours is a third, declare +method, so requiring it would put an unreachable throw in each. Three shipped providers are exactly +that case and say so in their own docs, `druid`, `libredb` and `neo4j`. If yours is a fourth, declare `hasSource` on no kind, write no method, and add your type-id to the committed ABSTAINER list in -`tests/isolated/object-source-declarations.test.ts` beside those two. Do NOT write the method +`tests/isolated/object-source-declarations.test.ts` beside those three. Do NOT write the method answering an empty document, an empty string or any other neutral value: the pairing fails by name on a method with no source-bearing kind, and an empty text is a RAISE everywhere in the table below. @@ -451,6 +496,8 @@ bun add # optional; this one is pure JS, which is the next best thing) # bun add @duckdb/node-api (DuckDB — an embedded engine, so there is no protocol at all and no # HTTP alternative; this one is a NATIVE N-API addon) +# bun add --exact neo4j-driver-lite (Neo4j: Bolt is a binary protocol; pure JS, and the graph +# layer's one transport, so a second graph engine adds no driver) ``` If your engine exposes a documented HTTP API, weigh it against the native driver before adding a @@ -699,7 +746,7 @@ Every field and what it controls: | Field | Type | Controls | |-------|------|----------| -| `queryLanguage` | `'sql' \| 'json' \| 'promql'` | Monaco editor language mode, AI prompt style, query template format. A closed union: a new member needs an arm, or a test pinning its branch, in every reader (#1085) | +| `queryLanguage` | `'sql' \| 'json' \| 'promql' \| 'cypher'` | Monaco editor language mode, AI prompt style, query template format. A closed union: a new member needs an arm, or a test pinning its branch, in every reader (#1085) | | `queryDialect` | `'libredb' \| 'redis' \| 'kafka' \| 'etcd' \| undefined` | Optional. Names the dialect's records in three registries, which the tab type, the Monaco language, the formatter, the generated statements and the Generate Code and Generate Count Query gates consult **before** `queryLanguage` (only Profile answers an SQL language first, `offersColumnProfiling` in `src/lib/db/types.ts`): `QUERY_DIALECTS` (`src/lib/db/query-dialects.ts`, the tab type and the row-menu gates), `DIALECT_EDITORS` (`src/lib/editor/dialect-editors.ts`, the Monaco language and the formatter) and `DIALECT_GENERATORS` (`query-generators.ts`, what a tree click and Generate Query write). A new dialect adds its three records, not a check in each reader: `queryLanguage: 'json'` alone means MongoDB, which is how Redis silently got MongoDB documents until #427. Left undefined by SQL and MongoDB | | `supportsExplain` | `boolean` | EXPLAIN button visibility in QueryEditor toolbar | | `explainFormat` | `ExplainFormat \| undefined` | **Required whenever `supportsExplain` is true.** Selects the strategy in `src/lib/explain/index.ts`. Setting the flag without the format leaves the control visible and dead — the UI resets out of explain mode when metadata lacks it | @@ -777,12 +824,12 @@ For the authoritative, code-verified reference for each shipped provider (extend driver, pooling, capabilities, labels, `prepareQuery` behaviour, and limitations), see the prime docs — they are the single source of truth and are kept in sync with the code: -**[docs/providers/](./providers/README.md)** → postgres · mysql · oracle · mssql · sqlite · libsql · duckdb · redis · mongodb · couchbase · clickhouse · druid · elasticsearch · opensearch · trino · cassandra · prometheus · kafka · etcd · libredb +**[docs/providers/](./providers/README.md)** → postgres · mysql · oracle · mssql · sqlite · libsql · duckdb · redis · mongodb · couchbase · clickhouse · druid · elasticsearch · opensearch · trino · cassandra · prometheus · kafka · etcd · neo4j · libredb When implementing a new provider, the closest existing analogue is the best template: a pooled SQL -provider (postgres/mysql), an embedded SQL provider (sqlite), a non-SQL provider (mongodb/redis), or -a driverless provider reached over HTTP (clickhouse, druid or trino for SQL, couchbase for a -document store). +provider (postgres/mysql), an embedded SQL provider (sqlite), a non-SQL provider (mongodb/redis), a +graph engine on the graph layer (neo4j), or a driverless provider reached over HTTP (clickhouse, +druid or trino for SQL, couchbase for a document store). ## Driver-free candidates diff --git a/docs/AGENT.md b/docs/AGENT.md index f38b17ee52..6e056b40d1 100644 --- a/docs/AGENT.md +++ b/docs/AGENT.md @@ -59,14 +59,14 @@ Three properties frame everything below, and each of them is load-bearing rather on which of its two readings it takes**: `agent-read-only` on the dialects `CATALOG_PLANS` serves, because it composes catalog statements, and `agent-operations` everywhere else, because asking a provider to describe its own schema sends nothing an engine has to plan. That is what lets grounding - reach the sixteen the read-only profile refuses, and it still cannot narrow any workflow's + reach the seventeen the read-only profile refuses, and it still cannot narrow any workflow's reach: the profile whose acquisition would be refused is never the profile that capture asks for. Everything else about the three acquisitions is identical: the same `readOnly: true` open, the same optional least-privilege `agentUser`, and the same profiled cache, so neither an operations run nor an editor replay is ever handed the editor's writable pool. **Plan mode grounds itself the same way** — the server reads the schema, and on PostgreSQL and SQLite the engine's estimated statistics beside it, before the model's first turn — so a plan run is now ordinarily grounded on every engine, - including the sixteen where an agent run cannot read anything at all. What is left of the old engine + including the seventeen where an agent run cannot read anything at all. What is left of the old engine rule is narrower and still worth stating: the engine no longer decides WHETHER a plan run is grounded, only whether it is grounded through a composed statement or through its provider, and whether it gets statistics. A run whose reading fails — a provider that cannot describe itself, a @@ -569,7 +569,7 @@ What a grounded plan run is given, and where each part comes from: which process happened to have read a catalog first. They are read from what the engine already holds — `pg_class.reltuples` and `pg_stats` on PostgreSQL, `sqlite_stat1` on SQLite — so no column is scanned and no value is read out of any row. `ESTIMATE_BUILDERS` serves those two and SQL Server (`buildMssqlEstimates`, from `sys.partitions`) and - nothing else, and #414 added no engine to it: on the other seventeen `readSchemaStatistics` answers + nothing else, and #414 added no engine to it: on the other eighteen `readSchemaStatistics` answers `DIALECT_HAS_NO_STATISTICS` — *"this engine does not hold statistics this run knows how to read"* — so **a known schema with no statistics is now the ORDINARY combination rather than a rare one**, and the two sentences the run is handed agree: the inventory is a record of what exists, and every @@ -690,9 +690,9 @@ Three consequences worth stating plainly, because each is easy to assume the oth 1. **A plan run costs statements now.** On PostgreSQL and SQLite grounding is catalog reads plus one statistics read (two on SQLite: the `sqlite_stat1` availability probe has to be its own statement, - because SQLite resolves table names at prepare time). On the other eighteen it is **one**, the + because SQLite resolves table names at prepare time). On the other nineteen it is **one**, the single `db.schema.read` call, except on SQL Server, which adds the one statistics read - `ESTIMATE_BUILDERS` serves outside `CATALOG_PLANS`; the other seventeen hold no statistics this run + `ESTIMATE_BUILDERS` serves outside `CATALOG_PLANS`; the other eighteen hold no statistics this run knows how to read. Since #789 an engine that declares object kinds costs **one more**, the object-surface reading above; an engine that declares none is charged nothing extra, because a read that cannot exist is never admitted. They come out of the same per-run statement budget every other read does, @@ -748,9 +748,9 @@ engine. The deliverable is one runnable statement, not a lecture. The rules ask for it in a single fenced block tagged with the connection's canonical type-id, rationale after the block, and no name that is not in the inventory. -Since #414 the WORDING varies with the engine's `queryLanguage` and the TAG does not: on an engine whose language is not SQL (a `json` engine, and since #1085 a `promql` one) the run is asked for one statement or command in that engine's own language, a MongoDB aggregation or a PromQL expression rather than a SELECT, and told that this engine speaks no SQL, while the tag stays the canonical type-id in both arms. +Since #414 the WORDING varies with the engine's `queryLanguage` and the TAG does not: on an engine whose language is not SQL (a `json` engine, since #1085 a `promql` one, and a `cypher` one since Neo4j) the run is asked for one statement or command in that engine's own language, a MongoDB aggregation or a PromQL expression rather than a SELECT, and told that this engine speaks no SQL, while the tag stays the canonical type-id in both arms. That is deliberate rather than an oversight: `isQueryFenceTag` -is a total record over `DatabaseType`, so all twenty ids pass it, whereas a draft the model fenced as +is a total record over `DatabaseType`, so all twenty-one ids pass it, whereas a draft the model fenced as ```` ```javascript ```` passes nothing and records no `plan-statement-drafted` event at all — the run would score as having drafted nothing while the user is looking at a statement. A run that cannot answer from the inventory takes the other legitimate ending: a line beginning `NO STATEMENT:` saying exactly what is @@ -942,7 +942,7 @@ Two consequences worth stating: composed path has and the provider path cannot: reads audited statement by statement rather than as one opaque call; foreign keys, which no provider can report on an engine that declares none; and SQLite's inventory, which is parsed out of the DDL text the engine stored and which its provider - does not expose in the same shape. Collapsing the other eighteen onto the composed one is the thing + does not expose in the same shape. Collapsing the other nineteen onto the composed one is the thing #414 exists because nobody can do: a catalog statement has to be written per dialect and verified against a live server, and until it is, refusing the dialect was the honest answer and reading the provider is a better one. @@ -2560,7 +2560,7 @@ src/lib/agent/ ├── runtime.ts # composition root: the only place that assembles a tool context ├── tools.ts # the four tools + server-side selection; the only database reach, # the model's tools and the server's own grounding reads alike -├── composed-sql.ts # the SQL the SERVER writes, per dialect: four of the twenty +├── composed-sql.ts # the SQL the SERVER writes, per dialect: four of the twenty-one ├── sqlite-ddl.ts # reading SQLite's stored DDL back into an inventory ├── execution-policy.ts # the frozen policy and the run-level ceilings ├── deadline.ts # the wall-clock deadline and the timeout clamp @@ -2691,6 +2691,7 @@ the role's own grants are the whole boundary (A3). - **B90**: the worked call a refusal attaches for a model to copy is gated by `refusalExamples`, which defaults to false and is on for 2 of the 40 shipped profiles, so it is off for every model nobody has measured yet - which is where the repeated refusals are: `qwen2.5:3b-instruct` earned 155 `compose_report` refusals in one data-analysis cell with the flag off. The refusal names the failing paths and offers what is citable either way; what the flag withholds is the shape a model can copy rather than parse. - **B91**: the MCP settings page renders the OpenCode snippet under `mcp.servers.libredb` while that client reads `mcp.libredb` and wants an `enabled` field, so a user copying it gets a configuration error rather than a server; the same route accepts a request body of any size, measured at 2 MB with HTTP 200. - **B92**: on etcd, plan mode can draft a read of a whole prefix group the connection can only partly read, because the snapshot marks the group `partlyReadable` and says so in the prompt but `plan-statement-drafted` is recorded with no check of the draft against that mark, and the model, which is not given the readable key's name, follows the note only some of the time; measured at 4 of 4 drafts of `etcdctl get /config/ --prefix` on one model, each refused by etcd when run. +- **B93**: Neo4j is served by plan mode and the MCP metadata tools only: its provider implements no `queryReadOnly`, so agent execution and MCP `run_read_query` refuse it, because both guard a statement with SQL readers and a Cypher statement needs a contract of its own, measured against the `LOAD CSV` and APOC reach its read policy refuses (`docs/providers/neo4j.md` section 3.6). **Settled as limits rather than as work.** The eight below have no entry in `docs/BACKLOG.md`, and that is the point: each is how the product behaves, stated where a reader of this document will meet diff --git a/docs/AGENT_DATA_FLOW.md b/docs/AGENT_DATA_FLOW.md index 3ba2dddf6c..c7e2daf98c 100644 --- a/docs/AGENT_DATA_FLOW.md +++ b/docs/AGENT_DATA_FLOW.md @@ -44,7 +44,7 @@ if it only lists the good news. | Surface | What it sends | Fenced? | | --- | --- | --- | | `POST /api/agent/classify`, **before any run exists** | Your objective, and nothing else. One short completion that asks the model to name one of the five workflows. It fires when you press **Start** with the workflow left on **Automatic**, which is the default; naming a workflow yourself under **Advanced** skips it entirely. See [the classification, before any run](#the-classification-before-any-run) | No — it carries no database content to fence. Your objective is sent as the user message, and the server's instructions tell the model to treat that text as data to classify and never as instructions to it | -| Any **run**, in either mode, on **any** engine | Your objective, and a schema inventory (table, column, index identifiers and column types) with its relations graph (identifiers only). **Since #414 that inventory leaves on every engine**, where before it left on PostgreSQL and SQLite alone: those two are read with catalog statements the server composes, and the other eighteen by asking the connection's own provider to describe its schema. On **MongoDB and Couchbase** the provider works out a collection's fields from a **sample of your own documents**: no value from them is in the message, but the **existence** of a field there is derived from your data rather than read from a catalog, which is a weaker claim than a catalog reading's and is why that reading has its own operation id (`db.schema.read`) an operator can deny alone. When the reading cannot be taken (a refusal, a provider that cannot describe its own schema, a description that overran the time the run granted it), nothing of the schema leaves and a server sentence saying which of those happened goes in its place. See [the schema inventory](#3-the-schema-inventory--identifiers-and-types-fenced) | Everything derived from the database is wrapped in an untrusted-content fence before it reaches a prompt | +| Any **run**, in either mode, on **any** engine | Your objective, and a schema inventory (table, column, index identifiers and column types) with its relations graph (identifiers only). **Since #414 that inventory leaves on every engine**, where before it left on PostgreSQL and SQLite alone: those two are read with catalog statements the server composes, and the other nineteen by asking the connection's own provider to describe its schema. On **MongoDB and Couchbase** the provider works out a collection's fields from a **sample of your own documents**: no value from them is in the message, but the **existence** of a field there is derived from your data rather than read from a catalog, which is a weaker claim than a catalog reading's and is why that reading has its own operation id (`db.schema.read`) an operator can deny alone. When the reading cannot be taken (a refusal, a provider that cannot describe its own schema, a description that overran the time the run granted it), nothing of the schema leaves and a server sentence saying which of those happened goes in its place. See [the schema inventory](#3-the-schema-inventory--identifiers-and-types-fenced) | Everything derived from the database is wrapped in an untrusted-content fence before it reaches a prompt | | An **agent run** | The above, plus the rows of each read the model performed, up to 200 per read; engine error text; server-written refusals; server-minted ids | Same fence | | Any **run that continues a conversation**, in either mode, on any engine | **In addition to everything above**: the earlier steps' objectives — *your own earlier questions*, capped at 200 characters each — and the most recent step's report, which is a model's claims about your data. No row of any result, and no earlier step's report but the newest. Sent only when the rail attaches a previous run's id, which it does for a follow-up on the same connection; a run that starts its own conversation sends no such message. Bounded to 4000 characters by default and switchable off with `LIBREDB_AGENT_THREAD_CONTEXT=false`. See [the conversation](#2a-the-conversation-when-a-run-continues-one--fenced) | Same fence, identified as `operation agent/thread`: it is prose a user and a model wrote, and none of it is the server's voice | | An **agent run opened as Operate** | Your objective; a **schema inventory reduced to its own names and index names** (no column names, no column types, no relations graph), read whichever of the two ways that engine is read and named with whichever noun that engine's provider declares — tables, collections, datasources, key patterns; in Plan mode the engine's **row-count estimates** for those same tables where the engine holds any — PostgreSQL and SQLite — and nothing per column; and the rows of each curated reading — which, for the `sessions` and `slow-queries` kinds, include **other database users' in-flight statement text and their database usernames**. See [the operations workflow](#5a-the-operations-workflow-what-a-curated-reading-sends) | Same fence: the inventory and every reading's rows are database content and are fenced | @@ -201,7 +201,7 @@ One field of your connection record *is*: its **engine type**, the canonical typ spends it on the fence tag the deliverable must carry, and since the Operate-engine fix a prose plan spends it twice: on the rule that binds the readings it may name to the engine it is planning against, and on the fence tag for a reading that engine happens to express as a statement. It is -a server-side enum with twenty members, so what it discloses is which of twenty engines this +a server-side enum with twenty-one members, so what it discloses is which of twenty-one engines this connection is — never its host, its database name or its credentials, none of which reach a prompt at all (see [What never leaves](#what-never-leaves)). @@ -271,18 +271,18 @@ a refusal — nothing of the schema leaves and a server-written note says so in **Two readings produce it, and which one runs is the dialect's decision** (#414). On PostgreSQL and SQLite the server composes a catalog statement per kind and executes it read-only. On the other -eighteen it invokes `db.schema.read`, which walks the connection's own OBJECT SURFACE, the same +nineteen it invokes `db.schema.read`, which walks the connection's own OBJECT SURFACE, the same `listContainers`, `countObjects`, `listObjects` and `describeObjects` the sidebar walks when it lists your objects, and composes no statement at all. -**Eighteen counts type-ids the factory can build, not engines a user would name**: `SHIPPED` holds -twenty, `CATALOG_PLANS` serves two of them, and the remainder is what this second reading covers. +**Nineteen counts type-ids the factory can build, not engines a user would name**: `SHIPPED` holds +twenty-one, `CATALOG_PLANS` serves two of them, and the remainder is what this second reading covers. Every other count said about grounding in these docs counts the same thing. libSQL is one of the -eighteen and not one of the two: it speaks SQLite's dialect, but the read-only catalog path +nineteen and not one of the two: it speaks SQLite's dialect, but the read-only catalog path `CATALOG_PLANS` serves needs a database-native read-only profile, and `PRAGMA query_only` is refused by a libSQL server (see [`providers/libsql.md`](./providers/libsql.md)). Two things it does NOT count. The wire-compatible engines of [`docs/providers/README.md`](./providers/README.md) are not extra members — TiDB is grounded because it -arrives as `mysql`, and it is that type-id that is counted. And two of the eighteen, the embedded +arrives as `mysql`, and it is that type-id that is counted. And two of the nineteen, the embedded `libredb` and `duckdb`, reach this path through a handle they do not open: the file takes an exclusive lock, so the grounding acquisition borrows the connection's own open provider rather than opening a second one that the lock would refuse (`findOpenSingleWriterProvider`, `src/lib/db/factory.ts`; see @@ -347,7 +347,7 @@ The inventory's foreign keys, rendered as a relation list and fenced beside the It carries table names, column names, and at the deepest detail level a table's primary-key and leading-index column names (`keyColumns`, `er-diagram.ts:145-157`). **Never a row value.** -On the thirteen engines that declare no foreign keys at all (MongoDB, Redis, LibreDB, Druid, ClickHouse, Couchbase, Trino, Cassandra, Elasticsearch, OpenSearch, Prometheus, Apache Kafka and etcd) this block carries no relations and says why: the engine has no such constraint to +On the fourteen engines that declare no foreign keys at all (MongoDB, Redis, LibreDB, Druid, ClickHouse, Couchbase, Trino, Cassandra, Elasticsearch, OpenSearch, Prometheus, Apache Kafka, etcd and Neo4j) this block carries no relations and says why: the engine has no such constraint to declare, so there is nothing here a reading could have missed (#414, driven from `ProviderCapabilities.declaresForeignKeys` rather than from the connection's type). It is one server sentence and no database content, which makes it the one form of this block that discloses nothing. @@ -545,7 +545,7 @@ The frozen execution policies are the ceiling on one run's egress, one row per w | Bound | Value | What it caps | | --- | --- | --- | | `maxResultRows` / `maxResultBytes` | 200 rows / 256 KiB | The most one read can return — and therefore the most one tool result can send | -| `maxStatementsPerRun` | 18-45, by workflow | Reads per run, folded across its drives (#999), grounding reads and repairs included: the composed catalog reads and, since #414, the one `db.schema.read` call that replaces them on the other eighteen. The figures did not move for it: that path is the cheapest of the three, so nothing had to be bought (`docs/AGENT.md`, the budget arithmetic) | +| `maxStatementsPerRun` | 18-45, by workflow | Reads per run, folded across its drives (#999), grounding reads and repairs included: the composed catalog reads and, since #414, the one `db.schema.read` call that replaces them on the other nineteen. The figures did not move for it: that path is the cheapest of the three, so nothing had to be bought (`docs/AGENT.md`, the budget arithmetic) | | `AGENT_CONTEXT_PACK_MAX_CHARS` | 6000 | The fenced schema inventory | | `MAX_ER_CHARS` | 2000 | The fenced relations block | | `AGENT_MAX_OBJECTIVE_LENGTH` | 4000 | Your objective | diff --git a/docs/AGENT_GUIDE.md b/docs/AGENT_GUIDE.md index b7c2706756..8f9366f5ec 100644 --- a/docs/AGENT_GUIDE.md +++ b/docs/AGENT_GUIDE.md @@ -214,8 +214,8 @@ having **no statistics**, never as empty. On SQLite the statistics exist only af **Every engine, read one of two ways.** On **PostgreSQL and SQLite** the server composes catalog statements and reads them through the same audited, read-only path an Agent run uses. On every other -connection, which is the other eighteen (MySQL, Oracle, SQL Server, libSQL, DuckDB, MongoDB, Redis, -ClickHouse, Couchbase, Druid, Elasticsearch, OpenSearch, Trino, Cassandra, Prometheus, Apache Kafka, etcd and the bundled LibreDB +connection, which is the other nineteen (MySQL, Oracle, SQL Server, libSQL, DuckDB, MongoDB, Redis, +ClickHouse, Couchbase, Druid, Elasticsearch, OpenSearch, Trino, Cassandra, Prometheus, Apache Kafka, etcd, Neo4j and the bundled LibreDB store), it asks that connection's own provider to describe its schema, which is the reading the sidebar already performs when it lists your tables, and composes no statement at all. Grounding is no longer decided by the engine, and that changed in #414; what decides it now is whether the @@ -228,7 +228,7 @@ workflow including **Operate**. now two different sentences, and the difference is the whole of what changed: - **Grounding — every engine.** What a Plan run is TOLD about your database. It needs no read-only - statement path, because the provider reading sends no statement, so it reaches all twenty engines. + statement path, because the provider reading sends no statement, so it reaches all twenty-one engines. - **Agent mode — PostgreSQL, SQLite, DuckDB and SQL Server.** What a run may DO by itself. Its tools execute statements and need a database-native read-only path, which only those four providers implement, so a schema-workflow Agent run on any other engine still ends *"The agent cannot run on this database @@ -242,7 +242,7 @@ inspection found and not proof that nothing else exists, and the plan is told so engines it works out a collection's fields from a **sample of your own documents**: no value from them is kept, but the existence of a field there is derived from your data rather than read from a catalog. The **estimated statistics** below are PostgreSQL's, SQLite's and SQL Server's alone; on the other -seventeen the plan is told that this engine holds none it knows how to read, which means it has an +eighteen the plan is told that this engine holds none it knows how to read, which means it has an inventory and no sizes — the ordinary case now rather than a rare one. On an engine that speaks no SQL, a Plan run is asked for one statement or command **in that engine's @@ -955,14 +955,14 @@ Stated plainly, because a surface that hides its edges is the one that surprises Acquiring a profiled provider for any other engine raises `PROFILE_UNSUPPORTED_BY_PROVIDER` (`src/lib/db/factory.ts`), which the runtime reports as `engine-unsupported` (`src/lib/agent/runtime.ts`) — the rail says so in as many words - (`src/components/agent/timeline.ts`). So on the other sixteen ids in the `DatabaseType` union + (`src/components/agent/timeline.ts`). So on the other seventeen ids in the `DatabaseType` union (`src/lib/types.ts`), an Agent-mode run cannot read anything: MySQL, Oracle, libSQL, MongoDB, Redis, - ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Cassandra, Couchbase, Prometheus, Apache Kafka, etcd and LibreDB. That + ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Cassandra, Couchbase, Prometheus, Apache Kafka, etcd, Neo4j and LibreDB. That last id is the bundled **LibreDB sample** connection, whose provider implements no `queryReadOnly` (`src/lib/db/providers/embedded/libredb.ts`) — the bundled **SQLite sample** is the seeded connection to try a run against (`src/lib/seed/sqlite-sample.ts:131`). **Plan** mode still opens on every connection — the model is toolless there, so no profile has to be acquired for it — and since - #414 its **grounding** no longer takes this path at all on the other eighteen: it asks the provider to + #414 its **grounding** no longer takes this path at all on the other nineteen: it asks the provider to describe its schema, which needs no read-only statement profile, so a Plan run on MongoDB or MySQL is ordinarily grounded while an Agent run on the same connection still cannot read anything. Where the reading does fail — a provider that cannot describe itself, a description that overran its diff --git a/docs/API_DOCS.md b/docs/API_DOCS.md index fe65cd578e..9d83ae113d 100644 --- a/docs/API_DOCS.md +++ b/docs/API_DOCS.md @@ -27,12 +27,12 @@ ## Overview -LibreDB Studio provides a RESTful API for database management operations. The API supports PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Redis, Prometheus, Apache Kafka and etcd. +LibreDB Studio provides a RESTful API for database management operations. The API supports PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Redis, Prometheus, Apache Kafka, etcd and Neo4j. ### Key Features - **JWT Authentication** - Secure token-based authentication stored in HTTP-only cookies -- **Multi-Database Support** - Nineteen engines: PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Redis, Prometheus, Apache Kafka, etcd +- **Multi-Database Support** - Twenty engines: PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Redis, Prometheus, Apache Kafka, etcd, Neo4j - **AI-Powered Insights** - EXPLAIN explanations, query-safety analysis and schema docs, streamed - **Real-time Health Monitoring** - Database metrics and performance insights @@ -423,7 +423,7 @@ Under that cap, a result of exactly `limit` rows still has `wasLimited: true` an A bound the provider reported sets `wasLimited` and never `hasMore`, because no `offset` can advance a bound the server did not write. A statement the server returned **untouched** — one carrying its own `LIMIT n`, or one whose end the limiter declined to cut into — runs identically at every `offset`, because the requested offset is discarded along with the rewrite. `hasMore` is `false` for those however many rows come back, and re-requesting with a higher `offset` would return the same rows again. Where `hasMore` is `true`, re-request with `offset` advanced by the number of rows you received. See [`docs/editor/query-optimization.md`](editor/query-optimization.md). -Not every engine can serve a positive `offset`. Cassandra and Elasticsearch answer one with HTTP 400 rather than silently returning page one; MongoDB, Redis, LibreDB, Prometheus, Kafka and etcd ignore it. `POST /api/db/provider-meta` reports each one's `capabilities.supportsResultPagination`, which is the same flag the app reads before offering its Load More control. +Not every engine can serve a positive `offset`. Cassandra and Elasticsearch answer one with HTTP 400 rather than silently returning page one; MongoDB, Redis, LibreDB, Prometheus, Kafka, etcd and Neo4j ignore it. `POST /api/db/provider-meta` reports each one's `capabilities.supportsResultPagination`, which is the same flag the app reads before offering its Load More control. **The database a run reads (optional):** ```json @@ -1873,7 +1873,7 @@ interface DatabaseConnection { apiKeySecret?: string; // the pair's secret half; either alone (after trim) falls back to user/password rather than sending a key built from an empty half } -type DatabaseType = 'postgres' | 'mysql' | 'sqlite' | 'libsql' | 'duckdb' | 'mongodb' | 'redis' | 'oracle' | 'mssql' | 'libredb' | 'couchbase' | 'clickhouse' | 'druid' | 'elasticsearch' | 'opensearch' | 'trino' | 'cassandra' | 'prometheus' | 'kafka' | 'etcd'; +type DatabaseType = 'postgres' | 'mysql' | 'sqlite' | 'libsql' | 'duckdb' | 'mongodb' | 'redis' | 'oracle' | 'mssql' | 'libredb' | 'couchbase' | 'clickhouse' | 'druid' | 'elasticsearch' | 'opensearch' | 'trino' | 'cassandra' | 'prometheus' | 'kafka' | 'etcd' | 'neo4j'; type ConnectionEnvironment = 'production' | 'staging' | 'development' | 'local' | 'other'; ``` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ce767abcb8..193c729c93 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -4,7 +4,7 @@ This document outlines the architectural patterns, tech stack, and system design ## System Overview -LibreDB Studio is a hybrid, cloud-native database management tool that provides an IDE-like experience in the browser. It supports **20 database backends** via a Strategy Pattern abstraction: PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Couchbase, ClickHouse, Apache Druid, Trino, Apache Cassandra, Elasticsearch, OpenSearch, Redis, Prometheus, Apache Kafka, etcd, LibreDB. The count is the `SHIPPED` record in [`src/lib/db/compatibility.ts`](../src/lib/db/compatibility.ts), which is exhaustive over `DatabaseType`; `elasticsearch` and `opensearch` are two ids served by one provider module. +LibreDB Studio is a hybrid, cloud-native database management tool that provides an IDE-like experience in the browser. It supports **21 database backends** via a Strategy Pattern abstraction: PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Couchbase, ClickHouse, Apache Druid, Trino, Apache Cassandra, Elasticsearch, OpenSearch, Redis, Prometheus, Apache Kafka, etcd, Neo4j, LibreDB. The count is the `SHIPPED` record in [`src/lib/db/compatibility.ts`](../src/lib/db/compatibility.ts), which is exhaustive over `DatabaseType`; `elasticsearch` and `opensearch` are two ids served by one provider module. It runs in two modes: as a **standalone Next.js app** and as an **embedded npm package** (`@libredb/studio`) consumed by libredb-platform. See [§4.6](#46-workspace-abstraction-npm-package-embedding). @@ -43,6 +43,7 @@ graph TD DBFactory --> KeyValue[Key-Value Providers] DBFactory --> TimeSeries[Time-Series Providers] DBFactory --> Stream[Stream Providers] + DBFactory --> Graph[Graph Providers] SQL --> PG[(PostgreSQL)] SQL --> MySQL[(MySQL)] @@ -62,6 +63,7 @@ graph TD KeyValue --> Etcd[(etcd)] TimeSeries --> Prometheus[(Prometheus)] Stream --> Kafka[(Apache Kafka)] + Graph --> Neo4j[(Neo4j)] DBFactory --> Embedded[Embedded Providers] Embedded --> LibreDB[(LibreDB)] end @@ -107,6 +109,13 @@ classDiagram +cancelQuery() } + class GraphBaseProvider { + <> + #profile GraphEngineProfile + +query() + +cancelQuery() + } + BaseDatabaseProvider <|-- SQLBaseProvider BaseDatabaseProvider <|-- MongoDBProvider BaseDatabaseProvider <|-- CouchbaseProvider @@ -114,6 +123,7 @@ classDiagram BaseDatabaseProvider <|-- PrometheusProvider BaseDatabaseProvider <|-- KafkaProvider BaseDatabaseProvider <|-- EtcdProvider + BaseDatabaseProvider <|-- GraphBaseProvider BaseDatabaseProvider <|-- LibreDBProvider SQLBaseProvider <|-- PostgresProvider @@ -128,6 +138,8 @@ classDiagram SQLBaseProvider <|-- CassandraProvider SQLBaseProvider <|-- LibSQLProvider SQLBaseProvider <|-- DuckDBProvider + + GraphBaseProvider <|-- Neo4jProvider ``` Each provider implements: @@ -135,6 +147,10 @@ Each provider implements: - **`getLabels()`** - entityName, selectAction, searchPlaceholder, etc. (drives all UI text) - **`prepareQuery()`** - handles query limiting per-provider (SQL LIMIT injection vs MongoDB native) +A graph engine is the one family with a shared base of its own: `GraphBaseProvider` (`src/lib/db/graph/graph-base-provider.ts`) runs every statement through the shared Cypher read policy, the engine's statement gate (which an allowlisted SHOW form skips) and one READ session over the Bolt transport, and the engine supplies a `GraphEngineProfile` (its policy lists, catalog reads, gate and error table) plus its declarations and monitoring reads. +The graph core under `src/lib/db/graph/` is pure and shipped to the browser, where the editor's Cypher language and completion read it; `bolt/` and the base class are server only. +[`ADDING_A_PROVIDER.md`](./ADDING_A_PROVIDER.md#adding-a-graph-engine) describes the layers and the profile, and [`providers/neo4j.md`](./providers/neo4j.md) the one engine on them. + Adding a new database type requires: **1 provider class** + **1 entry in `db-ui-config.ts`**. `CouchbaseProvider` extends `BaseDatabaseProvider` even though SQL++ is a SQL dialect: SQL++ quotes identifiers with doubled backticks, which `escapeIdentifier()` produces for no existing type, so it owns its quoting and declares its SQL-ness through `queryLanguage: 'sql'` instead. Being reached over HTTP is **not** the reason — `ClickHouseProvider`, `DruidProvider` and `TrinoProvider` add no driver either, and all three extend `SQLBaseProvider`, because double-quoted identifiers are correct in each dialect. Each driver-free provider is a directory rather than a single file, with its wire format behind a transport seam that provider logic never bypasses. Trino inherits everything except the limiter: its grammar is `[ OFFSET count ] [ LIMIT count ]` and only that way round, so `prepareQuery()` transposes the clause the shared limiter emits. See [`docs/providers/couchbase.md`](providers/couchbase.md), [`clickhouse.md`](providers/clickhouse.md), [`druid.md`](providers/druid.md) and [`trino.md`](providers/trino.md). @@ -318,7 +334,12 @@ src/ │ │ ├── keyvalue/ # redis, etcd/ (gRPC client seam + an etcdctl subset over etcd's gRPC API via @grpc/grpc-js) │ │ ├── timeseries/ # prometheus/ (transport seam + PromQL over the Prometheus HTTP API) │ │ ├── stream/ # kafka/ (read-client seam + JSON read requests over the Kafka protocol via @platformatic/kafka) + │ │ ├── graph/ # neo4j/ (an engine profile, catalog, statement gate and monitoring on the graph layer below) │ │ └── embedded/ # libredb (built-in embedded provider for the sample connection) + │ ├── graph/ # The graph layer a Cypher-over-Bolt engine extends (docs/ADDING_A_PROVIDER.md, "Adding a graph engine"): + │ │ # cypher/ (lexer, statements, quoting, read policy, generators), objects.ts, values.ts and + │ │ # profile.ts are pure and browser-safe; bolt/ (the GraphClient seam, the URI, the one + │ │ # neo4j-driver-lite client, driver values to JSON) and graph-base-provider.ts are server only │ ├── http/ # endpoint.ts: the validated URL builder every HTTP transport uses (no redirects); node-transport.ts: the shared node:http(s) transport a new REST provider takes (one keep-alive Agent per connection, no proxy variables, a streamed byte cap) │ ├── factory.ts # Provider factory │ ├── query-dialects.ts # The dialect registry: each queryDialect's tab type and row-menu answers @@ -329,7 +350,7 @@ src/ │ # cookie, WebAuthn wrapper, management and sign-in services, browser client ├── llm/ # LLM provider module ├── editor/ # Monaco completions (SQL + MongoDB), the tab-type/language ladder, the - │ # editor registry (dialect-editors.ts) and the LibreDB, Redis and etcd command languages + │ # editor registry (dialect-editors.ts), the LibreDB, Redis and etcd command languages, and Cypher ├── schema-diff/ # Diff engine + migration SQL generator ├── export/ # The writers behind every "save this to disk": RFC 4180 CSV, │ # the SQL INSERT/DDL forms, and the one blob-download path diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 1cb2f2f5b4..485069fb51 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -28,10 +28,10 @@ None of it is a GitHub issue. **Sections** - [SQL statement reading](#sql-statement-reading) — S2–S6 · 4 -- [Drivers and connections](#drivers-and-connections) — D1-D139, U17 · 83 +- [Drivers and connections](#drivers-and-connections) — D1-D144, U17 · 88 - [Value interpolation](#value-interpolation) — V1 - [Row editing](#row-editing) — R1–R3 · 3 -- [Studio UI and query execution](#studio-ui-and-query-execution) — X2-X19, U2-U71 · 58 +- [Studio UI and query execution](#studio-ui-and-query-execution) — X2-X19, U2-U72 · 59 - [Dependencies](#dependencies) — P1-P6 · 6 - [Documentation](#documentation) — DOC3-DOC9 · 6 - [Release pipeline](#release-pipeline) — REL1-REL7 · 7 @@ -41,7 +41,7 @@ None of it is a GitHub issue. - [Security Phase 3 deferrals](#security-phase-3-deferrals) — K4 - [Security scanner triage](#security-scanner-triage) — SCAN1 · 1 - [Agent M1 deferrals (#328)](#agent-m1-deferrals-328) — A1–A8 · 7 -- [Agent M2 deferrals (#329)](#agent-m2-deferrals-329) — B2-B92 · 33 +- [Agent M2 deferrals (#329)](#agent-m2-deferrals-329) — B2-B93 · 34 - [Passkey deferrals (#785)](#passkey-deferrals-785) — PK1-PK9 · 8 - [MCP server deferrals (#246)](#mcp-server-deferrals-246) @@ -1996,6 +1996,55 @@ Not fixed there: the SQLite provider is outside that PR. **Done when:** a statement that changes no row reports no changed rows on both SQLite drivers, read from the statement itself rather than the connection's last count, and a test runs a `DELETE` then a `CREATE TABLE` on one connection and pins the second answer. +### D140. Neo4j calls no APOC procedure, so a measured read-only subset is unavailable + +The Neo4j read policy refuses the `apoc.` and `gds.` namespaces whole and allows eight procedures (`NEO4J_POLICY_PROFILE` in `src/lib/db/providers/graph/neo4j/profile.ts`), because `apoc.load.*` reaches the network and the file system and READ mode does not stop it (`docs/providers/neo4j.md` section 3.4). +Many APOC procedures and functions are pure reads, such as `apoc.meta.*` and the text and collection helpers, and users of a graph with APOC installed reach for them. +The compose image installs no APOC, so no capture says which of them stay within the database. + +Found 2026-10-03 while designing the Neo4j provider (decision N10). + +**Done when:** a named APOC subset is allowlisted by exact name after each one is measured against a server with APOC installed for network, file and write reach, the policy corpus holds each allowed name and a refused neighbour, and the provider doc lists the subset. + +### D141. Memgraph has no provider on the graph layer + +Memgraph speaks Bolt and Cypher, and the graph layer (`src/lib/db/graph/`) was designed for a second engine, but the Memgraph 3.13.1 probe of the Neo4j design found that its catalog has none of the `db.*` procedures, its `EXPLAIN` reports `rw` for reads and writes alike, it enforces READ mode only in an explicit transaction and never on an auto-commit `session.run`, and it stops a running statement only on its timeout or `TERMINATE TRANSACTIONS `, not on a closed session. +So it needs its own catalog and gate, and an execution strategy the Bolt transport does not have yet: explicit READ transactions and a terminate by transaction id. +Memgraph Community is licensed under the Business Source License 1.1, whose clause on competing works is a question for the maintainer before any design. + +Found 2026-10-03 while designing the Neo4j provider (decision N6). + +**Done when:** the licence question has a recorded answer, and then a `memgraph` provider supplies its own `GraphEngineProfile` and adds its execution strategy to the Bolt transport in its own PR, editing nothing under `src/lib/db/providers/graph/neo4j/`, with a live probe of every surface. + +### D142. A Neo4j connection through an SSH tunnel cannot verify the server's certificate + +`boltEndpointOf` in `src/lib/db/graph/bolt/uri.ts` refuses a verifying TLS mode on a tunnelled connection, because the driver would check the certificate against the tunnel's local `127.0.0.1` rather than the server's name, so a tunnelled Neo4j connection runs TLS only with verification off. +`neo4j-driver-lite` 6.2.0 has a `resolver` option that could dial the tunnel while the URI keeps the server's name, and it has not been measured; the provider passes no resolver, by its fixed configuration. + +Found 2026-10-03 while designing the Neo4j provider's TLS mapping. + +**Done when:** the resolver, or another measured route, verifies the far end's certificate through a real tunnel, `verify-full` connects through it and refuses an address the certificate does not carry, and `tests/unit/db/graph/bolt-client.test.ts` pins the configuration. + +### D143. A Neo4j connection reads one database + +A Neo4j connection's one container is its `database`, or the user's home database resolved at connect (`GraphBaseProvider.connect` in `src/lib/db/graph/graph-base-provider.ts`), and another database is another connection. +An Enterprise server with several databases therefore needs one connection per database, and the tree cannot show them side by side. +One container was chosen so that a statement can never run against a database the tree is not showing. + +Found 2026-10-03 while designing the Neo4j provider. + +**Done when:** a connection lists every database the user may read as its containers, each statement runs on the database its tab names and on no other, the database a statement ran on is shown with its result, and a test drives two databases through one connection. + +### D144. Two first requests for one connection can orphan a connected provider + +`getOrCreateProvider` in `src/lib/db/factory.ts` awaits `providerCacheKey`, `createSSHTunnel` and `provider.connect()` between its cache lookup and its `providerCache.set`, and nothing records a creation in flight. +Two requests that miss the cache for the same connection at once each build and connect a provider, and the later `providerCache.set` overwrites the earlier entry, whose provider is never disconnected and is out of reach of the idle sweep. +It predates the Neo4j provider, but a Bolt provider now leaks a driver holding up to four pooled sockets rather than only HTTP handles; `GraphBaseProvider.connect` already makes an overlapping connect on one instance await the attempt in flight, which this race does not reach. + +Found 2026-10-03 while reviewing the Neo4j provider (PR #1239, review N6). + +**Done when:** concurrent `getOrCreateProvider` calls for one cache key share one creation, or the loser disconnects its provider and closes a tunnel it created, and a test drives two overlapping first calls with a provider that counts its connects and disconnects. + ## Value interpolation ### V1. Query history records the placeholders, not the values that were bound @@ -3131,6 +3180,15 @@ Not fixed there: the tab's columns are not declared per provider anywhere, and a **Done when:** the provider declares which table-stat columns it fills, as other capability-driven surfaces do, the tab draws only those, and a test renders it for an engine that fills none of the four. +### U72. Graph results are JSON cells, with no graph view + +A Neo4j result shows nodes, relationships and paths as tagged JSON cells (`"~graph"`, `src/lib/db/graph/values.ts`) with `Node`, `Relationship`, `Path` or `Mixed` in the column header, and nothing draws them as a graph. +The tag was added so that a renderer can find graph values in any `QueryResult` without a provider change. + +Found 2026-10-03 while designing the Neo4j provider (decision N7). + +**Done when:** a result holding graph values offers a graph tab beside the grid that draws its nodes and relationships, bounded by the result's own rows, and a component test renders a path result in it. + ## Dependencies ### P1. The desktop shell's `glib` advisory has no reachable fix while Tauri v2 targets GTK 3 @@ -4646,6 +4704,15 @@ Not fixed there: a model's choice is not something the snapshot can force, and t **Done when:** a server-side check flags a drafted read over a whole partly readable group, by the group's name only so E13 holds, and the answer card shows the flag, with a test that a draft over `/config/` is flagged while one over `/app/` is not. +### B93. Neo4j has no agent execution and no MCP `run_read_query` + +The Neo4j provider implements no `queryReadOnly`, so `AGENT_EXECUTION_ENGINES` does not name it, agent auto mode refuses it, and MCP `run_read_query` answers that the engine is not served; plan mode and the MCP metadata tools work. +Both execution paths guard a statement with SQL readers (`src/lib/db/operations/statement-guard.ts`), and a Cypher statement needs a contract of its own: its read policy, gate and READ session exist, but the agent's and MCP's bounds, audit and refusal shapes have not been measured on it, and the `LOAD CSV` and APOC reach the policy refuses has to be shown refused on that path too. + +Found 2026-10-03 while designing the Neo4j provider (spec 6.4). + +**Done when:** `Neo4jProvider` implements `queryReadOnly` through the same policy, gate and READ session as the editor, `AGENT_EXECUTION_ENGINES` and `RUN_READ_QUERY_ENGINES` name Neo4j, and an agent run and an MCP call each drive a `LOAD CSV` and a write to their refusal in a test. + ## Passkey deferrals (#785) Each was decided when passkey sign-in was designed, not overlooked. diff --git a/docs/BRAND_MESSAGING.md b/docs/BRAND_MESSAGING.md index 84c88b7639..64211071b6 100644 --- a/docs/BRAND_MESSAGING.md +++ b/docs/BRAND_MESSAGING.md @@ -4,7 +4,7 @@ The messaging architecture marketing teams work from: what LibreDB claims, who i ## Positioning statement -> For engineering teams whose databases live in the cloud, LibreDB Studio is the database editor that deploys next to the data instead of onto your laptop: one browser tab for PostgreSQL, MySQL, Oracle, SQL Server, MongoDB, Redis, SQLite, libSQL, DuckDB, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Cassandra, Prometheus, Apache Kafka and etcd, with SSO and audit built in, under MIT with nothing held back. +> For engineering teams whose databases live in the cloud, LibreDB Studio is the database editor that deploys next to the data instead of onto your laptop: one browser tab for PostgreSQL, MySQL, Oracle, SQL Server, MongoDB, Redis, SQLite, libSQL, DuckDB, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Cassandra, Prometheus, Apache Kafka, etcd and Neo4j, with SSO and audit built in, under MIT with nothing held back. One sentence, one reference point. Everything else in this brief either supports it or is cut. @@ -59,7 +59,7 @@ One umbrella claim, three entry doors into it, three assurance layers underneath **The three doors** are three ways into that one claim. A campaign picks a door. It does not argue all three at once, because a piece that opens on three arguments has opened on none. 1. You created the database. The editor is already beside it. -2. One tab, nineteen databases. +2. One tab, twenty databases. 3. Nothing sits behind an Enterprise wall. **Door 3 is deliberately third, and stays third.** Leading with it would define LibreDB as another company's opponent rather than as a position of its own, and it would put our credibility at the mercy of their pricing page. Third, the same fact reads as reassurance rather than accusation. @@ -80,12 +80,12 @@ Each door carries five parts. A promise whose proof does not resolve to a row in This is the sharpest of the three. It is the one claim no competitor can currently make. -### Door 2: One tab, nineteen databases. +### Door 2: One tab, twenty databases. - **Audience:** teams running more than one kind of database, and the engineers who join them. - **Pain:** four databases, four clients, four sets of credentials, and a connection-string hunt for anyone new. -- **Promise:** nineteen engines in one interface, with the same exploration everywhere, and ER diagrams, schema diff and monitoring wherever the engine has something to show. (Not "across all of them": a search cluster declares no foreign keys, so its ER diagram has no edges, and item 4 below forbids the sentence that hides that.) -- **Proof:** nineteen providers, each with its own reference document under `docs/providers/`. +- **Promise:** twenty engines in one interface, with the same exploration everywhere, and ER diagrams, schema diff and monitoring wherever the engine has something to show. (Not "across all of them": a search cluster declares no foreign keys, so its ER diagram has no edges, and item 4 below forbids the sentence that hides that.) +- **Proof:** twenty providers, each with its own reference document under `docs/providers/`. - **Difference:** CloudBeaver Community bundles 18 driver modules and every one of them is SQL. MongoDB and Redis are not among them. The claim here is the span, never the count. See the honesty limits. @@ -124,7 +124,7 @@ Facts drift. Provider counts, channel counts and competitor editions all change, | Claim | Evidence | Source | Verified | | :--- | :--- | :--- | :--- | -| Nineteen database engines | One reference document per engine: PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd. A twentieth, `libredb.md`, is the embedded provider and is not an external engine. The count is derived, not written: `SHIPPED` in `src/lib/db/compatibility.ts` is an exhaustive record over `DatabaseType`, so the compiler refuses a missing id: read the count from there, minus `libredb` | `docs/providers/`, `src/lib/db/compatibility.ts` | 2026-09-23 | +| Twenty database engines | One reference document per engine: PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, MongoDB, Redis, Couchbase, ClickHouse, Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd, Neo4j. A twenty-first, `libredb.md`, is the embedded provider and is not an external engine. The count is derived, not written: `SHIPPED` in `src/lib/db/compatibility.ts` is an exhaustive record over `DatabaseType`, so the compiler refuses a missing id: read the count from there, minus `libredb` | `docs/providers/`, `src/lib/db/compatibility.ts` | 2026-09-23 | | Published as an embeddable npm package | `"name": "@libredb/studio"`, version 0.16.1 | `package.json` | 2026-09-19 | | MIT licensed | "MIT License / Copyright (c) 2025 LibreDB" | `LICENSE` | 2026-08-07 | | 42 distribution channels, 34 live | "42 channels · 34 live · 7 pending · 1 deprecated" | `docs/CHANNELS.md` | 2026-10-01 | @@ -198,7 +198,7 @@ Documented so the message exists when it is wanted. Not part of the current camp An agency that knows what it may not embellish is forced to fill the remaining space with real material. That is the purpose of this section. 1. **Display masking is a presentation feature, not a security control.** It was built for screen sharing, demos and screenshots. It runs in the browser, and the API returns full values to an authorized user. State this as scope, not as a shortcoming. No message may imply that display masking is data protection, access control, or compliance. -2. **Never claim leadership on the number of SQL engines.** CloudBeaver Community bundles 18 driver modules covering 15 distinct engines, against nineteen here. The claim is the span across SQL, NoSQL, analytics and search in one interface, never the count. The count moves, so read it from `SHIPPED` in `src/lib/db/compatibility.ts` (an exhaustive record over `DatabaseType`, minus the embedded `libredb`) rather than from the last campaign; but a bigger number never becomes the claim. +2. **Never claim leadership on the number of SQL engines.** CloudBeaver Community bundles 18 driver modules covering 15 distinct engines, against twenty here. The claim is the span across SQL, NoSQL, analytics and search in one interface, never the count. The count moves, so read it from `SHIPPED` in `src/lib/db/compatibility.ts` (an exhaustive record over `DatabaseType`, minus the embedded `libredb`) rather than from the last campaign; but a bigger number never becomes the claim. 3. **There is no LDAP.** Single sign-on is delivered through OIDC. Teams that require LDAP are told before they install, not after. 4. **Elasticsearch and OpenSearch are a read-only query surface, not a managed one.** Both ship as of [#424](https://github.com/libredb/libredb-studio/issues/424), so both belong in an engine list and in a polyglot story. What may not be implied is parity with PostgreSQL or MySQL. The query language is each product's own SQL endpoint — never ES|QL, which exists on one of the two and is deliberately unused, and never the native JSON DSL. Neither product's SQL grammar has `INSERT`, `UPDATE` or `CREATE TABLE`, so what ships is a query editor plus an index and mapped-field browser plus cluster health and per-index document counts and store sizes. There is no EXPLAIN, no maintenance operation, no slow-query panel and no session list; an ER diagram over a search cluster draws boxes and no edges, because the engine has no foreign keys to declare; aliases and data streams are not in the tree; and on Elasticsearch there is no `OFFSET`, so results after the first page cannot be requested at all. Say "query and browse", never "manage" — and never fold these two into a sentence that says a feature works "across all engines". What the story promises, the product has to hold. 5. **Trino is a query engine, and the story must not sell it as a database.** It ships as of @@ -223,7 +223,7 @@ An engineer speaking to an engineer. - Claim plus proof. Never adjective plus adjective. - No emoji. No exclamation marks. - Competitors are never disparaged by name. A comparison is a table with sources, and the reader draws the conclusion. -- Prefer the concrete number to the impressive word. "Nineteen engines" beats "extensive database support". +- Prefer the concrete number to the impressive word. "Twenty engines" beats "extensive database support". - Say the limitation out loud. Stating scope precisely is what makes the rest of the claims credible to this audience. - Product terms stay in English in every language. LibreDB Studio, not a translated variant. @@ -243,7 +243,7 @@ Direction, not copy. This brief contains no finished copy for any surface. It sa | Surface | Leads with | Why | Current state | | :--- | :--- | :--- | :--- | -| GitHub About | Door 2, in one line | The first thing a repository visitor reads, and the only place a full sentence has to work with no layout around it | Defect: it names four engines against nineteen shipped. Fix before any campaign starts | +| GitHub About | Door 2, in one line | The first thing a repository visitor reads, and the only place a full sentence has to work with no layout around it | Defect: it names four engines against twenty shipped. Fix before any campaign starts | | README opening | Door 1, then Door 3 | Visitors arriving from search need the claim before the feature list | States a category rather than a claim. Rewrite | | Website hero | Door 1 | The site as a whole can carry the story; the hero carries the claim and a single proof | States a category rather than a claim. Rewrite | | Website story section | The brand story, both scenes | Nothing on the site currently says why LibreDB exists | Missing entirely. Add | diff --git a/docs/DATABASE_PROVIDERS.md b/docs/DATABASE_PROVIDERS.md index cdceb28f78..6759b3fd4f 100644 --- a/docs/DATABASE_PROVIDERS.md +++ b/docs/DATABASE_PROVIDERS.md @@ -20,6 +20,11 @@ src/lib/db/ ├── errors.ts # Custom error classes ├── factory.ts # Provider Factory ├── base-provider.ts # Abstract base class +├── graph/ # The graph layer (pure core shipped to the browser; bolt/ and the base server only) +│ ├── cypher/ # lexer, statements, quote, read-policy, generators +│ ├── bolt/ # GraphClient seam, uri, bolt-client (the one neo4j-driver-lite client), record-values +│ ├── objects.ts, values.ts, profile.ts # kinds and path segments, graph JSON forms, policy profile types +│ └── graph-base-provider.ts # GraphBaseProvider: policy, statement gate, READ session, object surface ├── providers/ │ ├── sql/ # SQL Database Providers │ │ ├── sql-base.ts # SQL-specific base class @@ -101,6 +106,15 @@ src/lib/db/ │ │ ├── objects.ts # kinds and the object surface: topics, groups, brokers │ │ ├── monitoring.ts # metadata, configs, log dirs -> monitoring │ │ └── errors.ts # KafkaError category -> the repository's error classes +│ ├── graph/ # Graph Providers, on the graph layer above +│ │ └── neo4j/ # Neo4j Strategy (read-only Cypher over Bolt) +│ │ ├── index.ts # Neo4jProvider: declarations, version read and monitoring delegation +│ │ ├── profile.ts # NEO4J_POLICY_PROFILE: every list of the read policy (browser-safe) +│ │ ├── statement-gate.ts # EXPLAIN classification before a statement runs +│ │ ├── catalog.ts # home database, kind listings, property and index reads +│ │ ├── monitoring-reads.ts, monitoring.ts # the monitoring statements and their shaping +│ │ ├── errors.ts # GraphClientError category -> the repository's error classes +│ │ └── labels.ts # labels, and the plan-mode statement language │ └── embedded/ # Embedded (in-process) Providers │ └── libredb.ts # LibreDB Strategy └── utils/ @@ -132,11 +146,15 @@ BaseDatabaseProvider (abstract) ├── PrometheusProvider ─────────────────────┤ Time series (PromQL over HTTP) ├── KafkaProvider ──────────────────────────┤ Stream (JSON read requests over the Kafka protocol) ├── EtcdProvider ───────────────────────────┤ Key-Value Store (etcdctl commands over gRPC) +├── GraphBaseProvider (abstract) +│ └── Neo4jProvider ──────────────────────┤ Graph (read-only Cypher over Bolt) └── LibreDBProvider ────────────────────────┘ Embedded (key-value) ``` `SQLBaseProvider` provides SQL-specific helpers (LIMIT injection, identifier escaping). Non-SQL databases like MongoDB, Redis, and LibreDB extend `BaseDatabaseProvider` directly. LibreDB is embedded (opened in-process from a file, like SQLite) but, having no SQL, it is a key-value-style provider rather than a SQL one. +Neo4j extends `GraphBaseProvider`, which extends `BaseDatabaseProvider` and runs the query path every graph engine shares; the engine supplies a profile. See [ADDING_A_PROVIDER.md](./ADDING_A_PROVIDER.md#adding-a-graph-engine) and [providers/neo4j.md](./providers/neo4j.md). + Couchbase is the one provider that speaks a SQL dialect (SQL++) without extending `SQLBaseProvider`: SQL++ quotes identifiers with doubled backticks, which `escapeIdentifier()` produces for no existing type, so it owns its quoting and expresses its SQL-ness through `queryLanguage: 'sql'` in the capabilities instead. See [providers/couchbase.md](./providers/couchbase.md). Cassandra is the counter-example to the whole HTTP/driver framing: it needs a driver (a binary @@ -189,7 +207,7 @@ QueryEditor /api/db/query ## Supported Databases -Twenty type-ids are supported by nineteen provider modules: `elasticsearch` and `opensearch` share +Twenty-one type-ids are supported by twenty provider modules: `elasticsearch` and `opensearch` share one, `providers/sql/search/`. The count is derived from the exhaustive `SHIPPED` record in [`src/lib/db/compatibility.ts`](../src/lib/db/compatibility.ts) rather than written here twice. For the per-provider reference (driver, pooling, query format, @@ -216,6 +234,7 @@ monitoring, limitations, …) see the prime docs in **[`docs/providers/`](./prov | Prometheus | `prometheus` | Time series (PromQL over HTTP, read-only) | [providers/prometheus.md](./providers/prometheus.md) | | Apache Kafka | `kafka` | Stream (JSON read requests over the Kafka protocol, read-only) | [providers/kafka.md](./providers/kafka.md) | | etcd | `etcd` | Key-Value (etcdctl commands over gRPC) | [providers/etcd.md](./providers/etcd.md) | +| Neo4j | `neo4j` | Graph (Cypher over Bolt, read-only) | [providers/neo4j.md](./providers/neo4j.md) | | LibreDB | `libredb` | Embedded (key-value) | [providers/libredb.md](./providers/libredb.md) | ## Core Interface @@ -322,6 +341,7 @@ examples live in their prime docs: - **Prometheus** (a PromQL expression): [providers/prometheus.md](./providers/prometheus.md). - **Apache Kafka** (a JSON read request): [providers/kafka.md](./providers/kafka.md). - **etcd** (one etcdctl command): [providers/etcd.md](./providers/etcd.md). +- **Neo4j** (one read-only Cypher statement): [providers/neo4j.md](./providers/neo4j.md). Couchbase is deliberately **not** in that list: SQL++ is a SQL dialect, so a Couchbase connection takes ordinary SQL in the `sql` field and inherits the SQL editor and the shared limiter. @@ -398,7 +418,7 @@ Provider-specific behaviour — pooling model, SSL/encryption, pagination, monit maintenance operations, and known limitations — is documented per provider under [`docs/providers/`](./providers/README.md). Start there for anything specific to PostgreSQL, MySQL, Oracle, SQL Server, SQLite, libSQL, DuckDB, Redis, MongoDB, Couchbase, ClickHouse, Apache Druid, -Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd, or LibreDB. +Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd, Neo4j, or LibreDB. Not every provider has every feature, and the docs record the absences rather than glossing over them. Druid is the sharpest case: its SQL has no `UPDATE`, no `DELETE` and no `CREATE TABLE`, no diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 3a31d094fc..fa976cb73c 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -85,6 +85,12 @@ TLS with a custom CA and client certificates, and SASL PLAIN, SCRAM-SHA-256 and SCRAM-SHA-512 over TLS only. Read-only by construction: the provider never produces, commits an offset, joins a consumer group or creates a topic. See [`providers/kafka.md`](providers/kafka.md). +* **Graph Databases:** + * **Neo4j:** Read-only Cypher over Bolt through `neo4j-driver-lite`, tested on Neo4j 5.26 LTS, on a shared graph layer a second Cypher engine can join. + Node labels and relationship types with their properties as columns, indexes and constraints are browsable, and a click on a label or a relationship type writes a bounded sample read. + Nodes, relationships and paths reach the grid as tagged JSON cells with the graph type in the column header, and a 64-bit integer or a temporal value keeps every digit. + Read-only by construction: every statement passes a read policy (no writes, no `LOAD CSV`, no APOC or GDS, allowlisted procedures, functions and SHOW forms), then the server's own classification (which an allowlisted SHOW form skips), then a READ session. + See [`providers/neo4j.md`](providers/neo4j.md). * **Embedded Stores:** * **LibreDB:** Support for embedded, server-less `.libredb` files via the `@libredb/libredb` package — a small get/put/delete/prefix/range command grammar over the key-value lens, with catalog-aware schema views for relational and document namespaces. * **Connection Pooling:** Configurable pool settings (min/max connections, idle timeout) for production workloads. @@ -111,7 +117,7 @@ Two components are described below and a claim true of one can be false of the o * **Contextual Actions (schema tab):** Quick access menus for each table including "Select Top 50", "Generate Query", "Generate Count Query", and "Copy Name". Action labels adapt per provider (e.g. "Scan Keys" for Redis, "Find Documents" for MongoDB). * **Generate Count Query (both explorers):** Opens an editable count statement in a new tab without running it, so a filter can be added before Run. SQL engines get a qualified, dialect-quoted `SELECT COUNT(*)` (`COUNT_BIG(*)` on SQL Server), and MongoDB gets its `count` document. - Redis, LibreDB, Prometheus, Apache Kafka and etcd have no count grammar here, and a derived key-prefix grouping has nothing to count, so they are not offered it. + Redis, LibreDB, Prometheus, Apache Kafka, etcd and Neo4j have no count grammar here, and a derived key-prefix grouping has nothing to count, so they are not offered it. * **DBA Quick Tools:** (Admin Only) Instant access to "Analyze Table" and "Vacuum Table" directly from the table context menu, on the providers whose rows are real objects. A key-value provider such as Redis, whose rows are derived key-prefix groupings, offers neither -- there is no table for the maintenance page to act on. * **Visual Clarity:** Modern glassmorphic design with Framer Motion animations for smooth transitions. * **Database Stats:** Integrated table counts and connection health monitoring directly in the sidebar. diff --git a/docs/SCHEMA_DIFF.md b/docs/SCHEMA_DIFF.md index 5b5d2d2970..dd0d0669e5 100644 --- a/docs/SCHEMA_DIFF.md +++ b/docs/SCHEMA_DIFF.md @@ -16,7 +16,7 @@ column order or uniqueness replace its old definition; a uniqueness change can f data violates the new constraint. Identifier quoting escapes delimiters; line breaks in informational comments are flattened so metadata cannot start a SQL statement. -MongoDB, Redis, LibreDB, Couchbase, Druid, Elasticsearch, OpenSearch, Prometheus, Apache Kafka and etcd receive an explanatory +MongoDB, Redis, LibreDB, Couchbase, Druid, Elasticsearch, OpenSearch, Prometheus, Apache Kafka, etcd and Neo4j receive an explanatory comment instead of relational table DDL. Trino and ClickHouse refuse foreign-key clauses; Trino refuses primary keys too. Trino has no index grammar, and the diff does not retain enough ClickHouse index metadata to distinguish and recreate its index kinds, so those index changes diff --git a/docs/SECURITY.md b/docs/SECURITY.md index bdf25050fc..17b0e56912 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -52,6 +52,7 @@ Two consequences worth stating before the table: | 3.7 | Every MCP tool call is audited under one correlation id, its decision before any provider is reached, and a call whose decision cannot be recorded does not run | Implemented | [`src/lib/mcp/audit.ts`](../src/lib/mcp/audit.ts), [`src/lib/mcp/output.ts`](../src/lib/mcp/output.ts), [`src/lib/mcp/preprocess.ts`](../src/lib/mcp/preprocess.ts), [`src/lib/mcp/tools/list-connections.ts`](../src/lib/mcp/tools/list-connections.ts), [`src/lib/mcp/tools/inspect-schema.ts`](../src/lib/mcp/tools/inspect-schema.ts), [`src/lib/mcp/tools/run-read-query.ts`](../src/lib/mcp/tools/run-read-query.ts), [`src/lib/audit.ts`](../src/lib/audit.ts) | [`tests/security/mcp-audit.test.ts`](../tests/security/mcp-audit.test.ts), [`tests/integration/mcp/run-read-query.test.ts`](../tests/integration/mcp/run-read-query.test.ts), [`tests/integration/mcp/inspect-schema.test.ts`](../tests/integration/mcp/inspect-schema.test.ts) | | 3.8 | A connection set read-only, and a provider an agent execution profile opens read-only, refuse every write command, value edit and maintenance operation before any request where the engine's provider keeps the mode, and `readOnly: true` is refused before anything is built or dialled on every other engine | Partial | [`src/lib/db/factory.ts`](../src/lib/db/factory.ts), [`src/lib/db/compatibility.ts`](../src/lib/db/compatibility.ts), [`src/lib/seed/types.ts`](../src/lib/seed/types.ts), [`src/lib/seed/connection-filter.ts`](../src/lib/seed/connection-filter.ts), [`src/lib/db/providers/keyvalue/etcd/write-policy.ts`](../src/lib/db/providers/keyvalue/etcd/write-policy.ts) | [`tests/unit/db/etcd/read-only-end-to-end.test.ts`](../tests/unit/db/etcd/read-only-end-to-end.test.ts), [`tests/unit/db/etcd/write-policy.test.ts`](../tests/unit/db/etcd/write-policy.test.ts), [`tests/unit/db/read-only-enforced-capability.test.ts`](../tests/unit/db/read-only-enforced-capability.test.ts), [`tests/unit/seed/types.test.ts`](../tests/unit/seed/types.test.ts), [`tests/isolated/factory.test.ts`](../tests/isolated/factory.test.ts) | | 3.9 | On an etcd connection, a write that touches a Kubernetes storage prefix or kube-apiserver's `compact_rev_key` is refused on the server before any write is sent, and a Kubernetes protobuf or encrypted value or a Kubernetes secret is never shown on any surface | Implemented | [`src/lib/db/providers/keyvalue/etcd/write-policy.ts`](../src/lib/db/providers/keyvalue/etcd/write-policy.ts), [`src/lib/db/providers/keyvalue/etcd/values.ts`](../src/lib/db/providers/keyvalue/etcd/values.ts) | [`tests/unit/db/etcd/write-policy.test.ts`](../tests/unit/db/etcd/write-policy.test.ts), [`tests/unit/db/etcd/values.test.ts`](../tests/unit/db/etcd/values.test.ts) | +| 3.10 | On a Neo4j connection, a statement runs only when the read policy allows it, as one statement with no write clause, no `LOAD CSV`, no APOC or GDS name and only allowlisted procedures, qualified functions and SHOW forms, then only when the server classifies it a read, or `s` when it calls an allowlisted procedure, while an allowlisted SHOW form skips that classification ([neo4j.md section 3.5](providers/neo4j.md#35-the-statement-gate)), and then only in a READ session; a refused statement opens no session | Implemented | [`src/lib/db/graph/cypher/read-policy.ts`](../src/lib/db/graph/cypher/read-policy.ts), [`src/lib/db/providers/graph/neo4j/profile.ts`](../src/lib/db/providers/graph/neo4j/profile.ts), [`src/lib/db/providers/graph/neo4j/statement-gate.ts`](../src/lib/db/providers/graph/neo4j/statement-gate.ts), [`src/lib/db/graph/graph-base-provider.ts`](../src/lib/db/graph/graph-base-provider.ts) | [`tests/unit/db/graph/read-policy.test.ts`](../tests/unit/db/graph/read-policy.test.ts), [`tests/unit/db/neo4j/statement-gate.test.ts`](../tests/unit/db/neo4j/statement-gate.test.ts), [`tests/unit/db/graph/graph-base-provider.test.ts`](../tests/unit/db/graph/graph-base-provider.test.ts) | ## Notes on individual rows @@ -444,13 +445,17 @@ A connection is recorded by its bare seed id and only once it resolved; the conn **3.8.** Partial for one precondition: the mode is a boundary only on a managed seed, which `resolveConnection` re-resolves from the operator's file whatever a caller sends, and only where etcd authenticates the client with a secret that seed keeps on the server, a password or a client certificate. On an etcd that authenticates nobody, and on a connection of the user's own, it is a safety rail, because a `user` can reach the same host and port with a connection of their own and can turn off Read-only on one (Known limits). `readOnly` is checked by one function, `assertReadOnlyHonoured`, before any provider is built and before any SSH tunnel is dialled, so on an engine whose provider does not keep the mode `readOnly: true` is refused rather than ignored, and the seed loader refuses it there too, and on a seed that is not managed. -etcd's provider is the one that keeps the mode today (`READ_ONLY_ENFORCED` in `src/lib/db/compatibility.ts`), and it names where the mode was set in every refusal ([`docs/providers/etcd.md`](./providers/etcd.md) section 3.4). +etcd's and Neo4j's providers keep the mode today (`READ_ONLY_ENFORCED` in `src/lib/db/compatibility.ts`): etcd's names where the mode was set in every refusal ([`docs/providers/etcd.md`](./providers/etcd.md) section 3.4), and Neo4j's refuses every write whatever the mode says (row 3.10). **3.9.** The protected set is the storage roots of the Kubernetes distributions measured for #1089, each with and without its leading `/`: `/registry/`, `registry/`, `/kubernetes.io/`, `kubernetes.io/`, `/openshift.io/`, `openshift.io/`, `/bootstrap/`, `bootstrap/`, `/k3s/` and `k3s/`; RKE2's root, `/rke2/` and `rke2/`, inferred and not measured; and the one exact key `compact_rev_key`, which sits outside every prefix and whose deletion makes each running kube-apiserver panic on its next compaction. RKE2's root comes from its source rather than from a store: RKE2 builds the k3s code with the program name `rke2`, and k3s names its own root after that program name. Every such write is refused before any request but a `lease revoke`, which first reads the keys its lease holds with one `LeaseTimeToLive` and is refused when one of them is protected or when etcd will not show them, because revoking a lease deletes its keys. Under a prefix this set does not name, only a single-key write over a stored protobuf envelope or encrypted value is recognised, and `docs/providers/etcd.md` lists what is not. +**3.10.** The read policy is the boundary and reads tokens, so a keyword inside a string, a comment or a backtick name is never read as one; the statement gate and the READ session are defence in depth. +Neo4j classifies `LOAD CSV` and `TERMINATE TRANSACTIONS` as reads and READ mode does not stop a `LOAD CSV`, which was measured fetching a private address from the server, so `LOAD`, `TERMINATE` and the `apoc.` and `gds.` namespaces are refused by the policy before anything is sent. +Every list is `NEO4J_POLICY_PROFILE`, and `docs/providers/neo4j.md` section 3.2 states each entry, pinned by `tests/unit/db/neo4j/provider-doc.test.ts`. + ## Known limits These are real, current, and not oversights. Each is a decision with a reason. @@ -483,6 +488,7 @@ These are real, current, and not oversights. Each is a decision with a reason. An etcd connection's read-only mode (row 3.8) does not narrow this: it binds a `user` only on a managed seed and only where etcd authenticates the client with a secret only the seeds hold, a password or a client certificate, so on an etcd that authenticates nobody a `user` who knows the address writes through a connection of their own ([`docs/providers/etcd.md`](./providers/etcd.md) section 3.4). Cancelling an etcd write in the editor shows it as cancelled even when etcd applied it, because the editor aborts its own request, posts the cancel and discards the answer that says the write was already sent. The etcd provider's Kubernetes write protection is by prefix, and by content only where a single-key write meets a stored Kubernetes envelope: under a custom `--etcd-prefix`, a range write, a value written between a `txn`'s read of its single-key targets and its send, a `lease revoke`, a new key, and a key holding Kubernetes JSON or CBOR are not recognised. + A Neo4j connection's read policy (row 3.10) does not narrow this either: it decides which statements run on that connection, and a `user` can still post a connection of their own to any host and port. - **Env-mode local passwords are not hashed.** With `STORAGE_PROVIDER=local` they arrive as `ADMIN_PASSWORD` and `USER_PASSWORD`, so the environment already holds the secret. On `sqlite` or `postgres` those variables only seed the account registry, and the stored value is scrypt diff --git a/docs/SEED_CONNECTIONS.md b/docs/SEED_CONNECTIONS.md index 6516620826..81b815f20c 100644 --- a/docs/SEED_CONNECTIONS.md +++ b/docs/SEED_CONNECTIONS.md @@ -58,7 +58,7 @@ defaults: # Optional — merges managed/environment/ssl only connections: - id: "analytics-pg" # Required, unique, lowercase slug [a-z0-9-] name: "Analytics DB" # Required, display name in UI - type: postgres # Required: postgres|mysql|sqlite|libsql|duckdb|mongodb|redis|oracle|mssql|libredb|couchbase|clickhouse|druid|elasticsearch|opensearch|trino|cassandra|prometheus|kafka|etcd + type: postgres # Required: postgres|mysql|sqlite|libsql|duckdb|mongodb|redis|oracle|mssql|libredb|couchbase|clickhouse|druid|elasticsearch|opensearch|trino|cassandra|prometheus|kafka|etcd|neo4j host: "${PG_HOST}" port: 5432 database: analytics @@ -180,7 +180,7 @@ connections: | `connections` | Yes | — | Array of connection definitions (min 1) | | `connections[].id` | Yes | — | Unique slug: `[a-z0-9-]+`, max 64 chars | | `connections[].name` | Yes | — | Display name, max 128 chars | -| `connections[].type` | Yes | - | Database type: `postgres`, `mysql`, `sqlite`, `libsql`, `duckdb`, `mongodb`, `redis`, `oracle`, `mssql`, `libredb`, `couchbase`, `clickhouse`, `druid`, `elasticsearch`, `opensearch`, `trino`, `cassandra`, `prometheus`, `kafka`, `etcd` | +| `connections[].type` | Yes | - | Database type: `postgres`, `mysql`, `sqlite`, `libsql`, `duckdb`, `mongodb`, `redis`, `oracle`, `mssql`, `libredb`, `couchbase`, `clickhouse`, `druid`, `elasticsearch`, `opensearch`, `trino`, `cassandra`, `prometheus`, `kafka`, `etcd`, `neo4j` | | `connections[].host` | No | — | Hostname or IP | | `connections[].port` | No | — | Port number (1-65535) | | `connections[].database` | No | — | Database name (Couchbase: the bucket. Druid has one catalog and ignores it. Trino: the **catalog**) | @@ -195,7 +195,7 @@ connections: | `connections[].connectionString` | No | — | Full connection string (use `${ENV_VAR}`). Druid and Trino have no URI form this build parses — those connections need `host` and are addressed by host and port only | | `connections[].roles` | Yes | — | Access control: `["*"]`, `["admin"]`, `["user"]`, `["admin", "user"]` | | `connections[].managed` | No | from defaults | `true` = admin-controlled: not editable in the UI, its secrets stay on the server; `false` = an editable copy for the user | -| `connections[].readOnly` | No | absent | `true` refuses every write, value edit and maintenance operation on the connection, on an engine whose provider enforces it (etcd); every other engine refuses `readOnly: true` when the file loads, naming the type and the field. Refused with `managed` false, on the connection or through `defaults.managed`, because an editable copy carries the credentials into the browser. A literal boolean: a `${ENV}` reference is refused | +| `connections[].readOnly` | No | absent | `true` refuses every write, value edit and maintenance operation on the connection, on an engine whose provider enforces it (etcd and Neo4j); every other engine refuses `readOnly: true` when the file loads, naming the type and the field. Refused with `managed` false, on the connection or through `defaults.managed`, because an editable copy carries the credentials into the browser. A literal boolean: a `${ENV}` reference is refused | | `connections[].environment` | No | from defaults | Environment badge | | `connections[].group` | No | — | Group label | | `connections[].color` | No | — | Hex color for badge (e.g., `#10B981`) | @@ -218,7 +218,7 @@ With no seed file, or with no entry that opts in for the token's role, `list_con ### A read-only cluster for everyone `readOnly: true` makes a connection refuse every write, value edit and maintenance operation before any request, on an engine whose provider keeps the mode. -etcd's is the one that does today ([providers/etcd.md](providers/etcd.md), section 3.4), and on every other engine the file is refused at load, with a sentence naming the type and the field. +etcd's and Neo4j's do today ([providers/etcd.md](providers/etcd.md), section 3.4, and the Neo4j recipe below), and on every other engine the file is refused at load, with a sentence naming the type and the field. The recipe is two seeds of one cluster: one every role reaches, read-only, and one for the people who may write. ```yaml @@ -266,6 +266,34 @@ A `${ENV}` or `${vault:...}` reference cannot be read at load, so a pair with a A value shaped like `${...}` that the resolver does not resolve, such as `${lower}`, is a literal and is checked as one. An empty `user` with a password of the form `user:password` is read as that pair, so a token that carries a default credential is refused too. +### A read-only Neo4j graph + +A Neo4j connection is read-only whatever `readOnly` says, because its provider refuses every write before it is sent ([providers/neo4j.md](providers/neo4j.md), section 3.1), so `readOnly: true` loads on a managed Neo4j seed and states what the connection does. +One seed every role reaches is enough, and `mcp: true` lets an MCP client list the connection and inspect its schema; `run_read_query` does not serve Neo4j. + +```yaml +version: "1" +connections: + - id: "graph" + name: "Graph" + type: neo4j + host: neo4j.internal + port: 7687 + database: neo4j + user: "reader" + password: "${NEO4J_READER_PASSWORD}" + ssl: + mode: verify-full + caCert: "${NEO4J_CA}" + roles: ["*"] + managed: true + readOnly: true + mcp: true +``` + +Leave `database` out to read the user's home database; another database is another connection. +`verify-full` and `verify-ca` both check the host name, and through an SSH tunnel a verifying mode is refused, so use `require` with verification off there ([providers/neo4j.md](providers/neo4j.md), section 4.5). + --- ## Credential Management diff --git a/docs/providers/README.md b/docs/providers/README.md index 2152d80d3e..9591a0cf01 100644 --- a/docs/providers/README.md +++ b/docs/providers/README.md @@ -25,13 +25,14 @@ in lockstep with the code (see the tri-sync rule in [`../../CLAUDE.md`](../../CL | Prometheus | `prometheus` | Time series | none (HTTP: the Prometheus HTTP API, `/api/v1/*`) | PromQL | [prometheus.md](./prometheus.md) | | Apache Kafka | `kafka` | Stream | `@platformatic/kafka` (pure TypeScript) | JSON (a read request) | [kafka.md](./kafka.md) | | etcd | `etcd` | Key-Value | `@grpc/grpc-js` (pure JavaScript, gRPC) | etcdctl commands (a subset) | [etcd.md](./etcd.md) | +| Neo4j | `neo4j` | Graph | `neo4j-driver-lite` (pure JavaScript, Bolt) | Cypher (read-only) | [neo4j.md](./neo4j.md) | | LibreDB | `libredb` | Embedded (Key-Value) | `@libredb/libredb` | JSON (command grammar) | [libredb.md](./libredb.md) | ## Conventions - **Filename = canonical type-id** (`postgres.md`, `mssql.md`, …), mirroring the source file (`src/lib/db/providers//.ts`, or a `/` directory when a provider is - split across modules, as Couchbase, ClickHouse, Druid, Trino, Cassandra, libSQL, DuckDB, Prometheus, Kafka and etcd are). The official product name (e.g. + split across modules, as Couchbase, ClickHouse, Druid, Trino, Cassandra, libSQL, DuckDB, Prometheus, Kafka, etcd and Neo4j 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/` is `elasticsearch` and `opensearch` — 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 @@ -254,9 +255,10 @@ 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 Neo4j row was verified against the running container on 2026-10-03, by the capture `tests/fixtures/neo4j/5.26.31/README.md` records. -Start the twenty always-on services with a plain `docker compose -f database-compose.yml up -d`: -sixteen engine containers plus the one-shot `couchbase-init`, `trino-init`, `kafka-init` and `etcd-seed` seed sidecars; the `Profile` column names +Start the twenty-one always-on services with a plain `docker compose -f database-compose.yml up -d`: +seventeen engine containers plus the one-shot `couchbase-init`, `trino-init`, `kafka-init` and `etcd-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. @@ -279,6 +281,7 @@ provider's integration pass. | Prometheus | `prometheus` | localhost | 9090 | *none* | *none* | *none* | *none* | | Apache Kafka | `kafka` | localhost | 9092 | *none* | *none* | *none* | *none* | | etcd | `etcd` | localhost | 2379 | *none* | *none* | *none* (one connection is one cluster) | *none* | +| Neo4j | `neo4j` | localhost | 7687 | `neo4j` | `password123` | `neo4j`, or empty for the home database | *none* | | SQLite | *no service* | — | — | — | — | a file path on the Studio host | — | | LibreDB | *no service* | — | — | — | — | a directory on the Studio host | — | @@ -305,6 +308,10 @@ The plain `prometheus` service on 9090 takes no credential at all ([prometheus.m Their certificates and passwords are generated into a volume at first start and never committed; [`docker/etcd/README.md`](../../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](./etcd.md#47-a-user-who-is-not-root)). +**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`](../../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](./neo4j.md#113-the-live-fixture)). + **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 diff --git a/docs/providers/neo4j.md b/docs/providers/neo4j.md new file mode 100644 index 0000000000..d26d8e5917 --- /dev/null +++ b/docs/providers/neo4j.md @@ -0,0 +1,456 @@ +# Neo4j Provider + +The `neo4j` type-id: read-only graph browsing over Bolt, tested and claimed on Neo4j 5.26 LTS. +Source: [`src/lib/db/providers/graph/neo4j/`](../../src/lib/db/providers/graph/neo4j/), on the shared graph layer [`src/lib/db/graph/`](../../src/lib/db/graph/). +Tests: [`tests/unit/db/neo4j/`](../../tests/unit/db/neo4j/), [`tests/unit/db/graph/`](../../tests/unit/db/graph/), [`tests/integration/db/neo4j-provider.test.ts`](../../tests/integration/db/neo4j-provider.test.ts) and [`tests/integration/db/neo4j-bolt-composed.test.ts`](../../tests/integration/db/neo4j-bolt-composed.test.ts). + +## 1. Overview + +Studio reads a Neo4j database through Cypher typed in the editor, shows its node labels, relationship types, indexes and constraints in the object tree, and reads the monitoring panels Community serves. +It writes nothing: every statement passes a read policy before anything is sent, then the server's own classification (an allowlisted SHOW form skips it, and an allowlisted procedure call classified `s` runs, section 3.5), and then runs in a READ session, so a write has to defeat three layers (section 3.1). +Nodes, relationships and paths reach the grid as tagged JSON cells, and the column header names the graph type (section 5.2). +Plan mode drafts Cypher and the MCP metadata tools list Neo4j connections and their shape, while agent execution and MCP `run_read_query` do not serve Neo4j (section 3.6). + +### Concept mapping + +| Neo4j | Studio | +|---|---| +| One database (`neo4j`, or the one the connection names) | One connection, with that database as its one container | +| A node label | A row of Node labels, with its properties as columns | +| A relationship type | A row of Relationship types, with its properties as columns | +| An index, a constraint | A row of Indexes or Constraints, listed with no click action | +| A Cypher read | One statement per run in the editor | +| A node, a relationship, a path in a result | A JSON cell tagged `"~graph"` | + +## 2. Architecture + +### 2.1 Where it sits + +`Neo4jProvider` extends `GraphBaseProvider`, which extends `BaseDatabaseProvider`, in the `graph` family. +`GraphBaseProvider` owns the query path, the object surface, the cancel and the catalog cache, and an engine supplies a `GraphEngineProfile`: its policy lists, its port, its statement gate, its catalog reads and its error table. +So `src/lib/db/providers/graph/neo4j/` holds declarations, the profile and the monitoring reads, and nothing of the query path. +The graph layer is split by what may reach the browser: `src/lib/db/graph/cypher/`, `objects.ts`, `profile.ts` and `values.ts` are pure and shipped to the browser, because the editor's tokens provider, completion and generators read them, while `src/lib/db/graph/bolt/` and `graph-base-provider.ts` run on the server only. +`neo4j-driver-lite` is imported by `bolt/bolt-client.ts` and `bolt/record-values.ts` alone, and `tests/unit/db/graph/seam-guard.test.ts` holds the driver members they may call and the layer direction. + +### 2.2 Modules + +| File | What it owns | +|---|---| +| `graph/cypher/lexer.ts`, `statements.ts`, `quote.ts` | The Cypher lexer the policy and the editor share, the statement splitter, and name and string quoting | +| `graph/cypher/read-policy.ts` | The read policy over tokens, driven by a `GraphPolicyProfile` | +| `graph/cypher/generators.ts` | The tree click's sample reads | +| `graph/objects.ts`, `graph/values.ts`, `graph/profile.ts` | The object kinds and their path segments, the JSON forms of graph values, and the policy profile's types | +| `graph/bolt/client.ts`, `uri.ts`, `bolt-client.ts`, `record-values.ts` | The `GraphClient` seam, the panel to a Bolt URI, the one driver client, and driver values to JSON | +| `graph/graph-base-provider.ts` | The base class every graph engine extends | +| `neo4j/profile.ts` | `NEO4J_POLICY_PROFILE`: every list of the read policy | +| `neo4j/statement-gate.ts`, `catalog.ts`, `errors.ts` | The EXPLAIN gate, the catalog reads, and the error table | +| `neo4j/monitoring-reads.ts`, `monitoring.ts`, `labels.ts`, `index.ts` | The monitoring reads and their shaping, the labels, and the composition root | + +### 2.3 Registration & lifecycle + +The constructor validates nothing and opens nothing, so a provider built from any connection answers its capabilities and labels. +`connect()` builds the Bolt endpoint from the host, the port and the TLS panel (section 4.3), opens one driver, verifies connectivity, resolves the database (section 4.4), clears the catalog cache, and reads the kernel's version and edition once, on the connection's database. +Connectivity verification never touches that database, so this read is the first that does: a database the server does not hold (`Neo.ClientError.Database.DatabaseNotFound`) fails the connect, so Test Connection refuses a misspelled database name. +Any other failed version read leaves the version unknown and does not fail the connection. +A second `connect()` closes the previous driver first and aborts its statements. +A `connect()` that overlaps one still in flight awaits that attempt instead of starting its own, so two calls build one driver. +`disconnect()` aborts every statement in flight and closes the driver. + +### 2.4 The client, and why + +The client is `neo4j-driver-lite` 6.2.0, pinned exactly: the official driver without the RxJS layer, Apache-2.0, pure JavaScript. +Its configuration is fixed and asserted by `tests/unit/db/graph/bolt-client.test.ts`: telemetry off, the product's user agent, the connection's timeout for the socket and for pool acquisition, a pool of four, no transaction retry time, `disableAutoCommitRetries` on, lossless integers, notifications off, and no logger and no resolver. +No logger is passed because at debug level the driver logs every statement, parameter and row. +Only `session.run` is called, in a READ session: `executeQuery`, `executeRead`, `executeWrite` and `beginTransaction` re-run their work on transient errors and are never called, so a statement reaches the server once. +Only the direct `bolt` family is built, never a routing `neo4j://` URI, so no routing table is asked for and no address the server names is dialled. + +## 3. Design decisions + +### 3.1 Read-only by construction: three layers + +Every statement the editor sends, typed, generated by a tree click or drafted by plan mode and then run by the user, takes one road. + +1. The read policy of section 3.2 reads its tokens, and a refusal opens no session. +2. The statement gate of section 3.5 asks the server to classify it with `EXPLAIN` and runs it only as a read, or as `s` when it calls an allowlisted procedure; an allowlisted SHOW form skips the gate. +3. It runs in a READ session, which Neo4j 5.26 enforces on an auto-commit transaction: the server refuses a write there with `Neo.ClientError.Statement.AccessMode`, measured on the compose server. + +The policy is the boundary, and the other two are defence in depth: the gate classifies `LOAD CSV` and `TERMINATE TRANSACTIONS` as `r` (section 3.4), and READ mode is a measured behaviour, not a documented guarantee. +No production seam turns the policy off. + +### 3.2 The read policy + +The policy reads tokens, not text: a word inside a string, a comment or a backtick name is never a keyword, and a keyword split by a comment, `LOAD /* x */ CSV`, is still one word sequence. +A unicode escape, a backslash then `u` then four hex digits, is refused before the text is read, wherever it stands, because the server decodes it before it reads the text. +Measured on the compose 5.26.31 server on 2026-10-03: `` RETURN 1 AS `x\u0060y` `` is a syntax error at the `y`, the decoded backtick having ended the name; `RETURN 1 AS x // \u000a , 2 AS y` returns two columns, the decoded line break having ended the comment; and `RETURN 'a\u0027 AS x, \u0027b' AS y` returns two columns, the decoded quote having ended the string. +So a name, a string or a comment holding the escape hides a clause from the tokens the policy reads: `` MATCH (n) WITH count(*) AS `x\u0060 CALL dbms.components() YIELD \u0060name` RETURN name `` ran the procedure. +A backslash starts an escape only at the end of an odd run of backslashes, as the server reads it, so `'C:\\users'` passes; the server keeps an upper-case `\U0060` as written. +It reads one statement: more than one non-empty statement is refused, while a `;` inside a string or a comment and a trailing `;` are not statements. +Procedures, qualified functions and SHOW forms are allowlists, so a name the profile does not list is refused; clauses are a denylist, backed by the gate and READ mode, because the read clauses of Cypher are many and the writing ones few. + +Denied words, refused wherever they stand outside a string, a comment or a backtick name, even where Cypher would read them as a property or a map key: `CREATE`, `MERGE`, `SET`, `DELETE`, `DETACH`, `REMOVE`, `DROP`, `FOREACH`, `LOAD`, `ALTER`, `RENAME`, `GRANT`, `DENY`, `REVOKE`, `START`, `STOP`, `ENABLE`, `TERMINATE`, `USE`, `INSERT`, `DEALLOCATE`, `REALLOCATE`, `DRYRUN`, `ROWS`, `TRANSACTIONS` and the sequences `IN TRANSACTIONS` and `CONCURRENT TRANSACTIONS`. +INSERT is GQL's spelling of CREATE, which Neo4j accepts since 5.18. +DEALLOCATE, REALLOCATE and DRYRUN are the commands that move databases between the servers of a cluster. +`USE` is denied because the database is the connection's, and `USE system` reaches the administration surface; `IN TRANSACTIONS` commits batches of its own, and `CONCURRENT TRANSACTIONS` is its parallel form, which may carry a count after `IN`. +`ROWS` and `TRANSACTIONS` are denied on their own because a count between `IN` and the words would let both sequences miss another batch spelling, and the lexer reads `1_000` as a number then a word. +5.26.31 rejects `IN 4 ROWS` and `IN 4 TRANSACTIONS` as syntax errors (measured on 2026-10-03), while `EXPLAIN` classifies the batch forms it accepts, `IN TRANSACTIONS OF 4 ROWS` and `IN 4 CONCURRENT TRANSACTIONS`, as reads, so the gate would not stop them and the words are the only layer. +A property, a map key or a label spelled like a denied word is written in backticks, `` n.`set` ``, and the refusal says so. +So is a label or a variable named `CALL` or `SHOW`, `` (n:`CALL`) ``, since outside a property or a map key the policy reads either word as its clause, and the refusal says so. + +Denied namespaces, refused for every qualified name that starts with one, called or not, compared without case: `apoc.` and `gds.`. +The check is fail-closed: `` `apoc`.name ``, a property of a variable named `apoc`, is refused too, and no backtick spelling passes, so such a variable must be renamed; one backticked name holding dots and not called, `` `apoc.x` ``, is a plain name. + +Procedures `CALL` may name, compared as Neo4j spells them, a backticked part unquoted first: `db.labels`, `db.relationshipTypes`, `db.propertyKeys`, `db.schema.visualization`, `db.schema.nodeTypeProperties`, `db.schema.relTypeProperties`, `db.ping` and `dbms.components`. +`CALL {` and `CALL (vars) {` open a subquery, whose body the same rules read. + +Qualified functions, a dotted name followed by `(`, are allowed only when the lowercased name is one of the built-in dotted functions Neo4j 5.26.31 lists (`SHOW FUNCTIONS YIELD name, isBuiltIn WHERE isBuiltIn AND name CONTAINS '.'`): `date.realtime`, `date.statement`, `date.transaction`, `date.truncate`, `datetime.fromepoch`, `datetime.fromepochmillis`, `datetime.realtime`, `datetime.statement`, `datetime.transaction`, `datetime.truncate`, `duration.between`, `duration.indays`, `duration.inmonths`, `duration.inseconds`, `graph.byelementid`, `graph.byname`, `localdatetime.realtime`, `localdatetime.statement`, `localdatetime.transaction`, `localdatetime.truncate`, `localtime.realtime`, `localtime.statement`, `localtime.transaction`, `localtime.truncate`, `point.distance`, `point.withinbbox`, `time.realtime`, `time.statement`, `time.transaction`, `time.truncate`, `vector.similarity.cosine` and `vector.similarity.euclidean`. +An unqualified function is not checked: on Neo4j a user-defined function is always namespaced, so an unqualified name is a built-in one. +A backticked name holding dots and called, `` `apoc.text.join`(...) ``, is read as a qualified name. + +SHOW forms, each optionally followed by `YIELD`, `WHERE`, `RETURN`, `ORDER BY`, `SKIP` and `LIMIT`: `SHOW INDEXES`, `SHOW INDEX`, `SHOW ALL INDEXES`, `SHOW RANGE INDEXES`, `SHOW TEXT INDEXES`, `SHOW POINT INDEXES`, `SHOW LOOKUP INDEXES`, `SHOW FULLTEXT INDEXES`, `SHOW VECTOR INDEXES`, `SHOW CONSTRAINTS`, `SHOW CONSTRAINT`, `SHOW ALL CONSTRAINTS`, `SHOW UNIQUE CONSTRAINTS`, `SHOW NODE UNIQUENESS CONSTRAINTS`, `SHOW RELATIONSHIP UNIQUENESS CONSTRAINTS`, `SHOW EXISTENCE CONSTRAINTS`, `SHOW KEY CONSTRAINTS`, `SHOW PROPERTY TYPE CONSTRAINTS`, `SHOW DATABASES`, `SHOW DATABASE `, `SHOW DEFAULT DATABASE`, `SHOW HOME DATABASE`, `SHOW PROCEDURES`, `SHOW FUNCTIONS`, `SHOW ALL FUNCTIONS`, `SHOW BUILT IN FUNCTIONS` and `SHOW USER DEFINED FUNCTIONS`. +`SHOW USERS`, `SHOW ROLES`, `SHOW PRIVILEGES`, `SHOW SERVERS`, `SHOW SETTINGS` and every other form are refused. +`SHOW TRANSACTIONS` is refused too, because its `YIELD *` returns other sessions' query text and parameters; the Active sessions panel reads transactions with a fixed column list of its own (section 7). + +Prefixes: `CYPHER 5` and `CYPHER 25` pass through to the server, while `EXPLAIN` and `PROFILE` are refused, because the product has no plan view for Cypher and `PROFILE` executes. + +A `$parameter` is refused: every statement is literal text in this version, and the tree's generators write literals. + +### 3.3 Refusals + +Every refusal is a `QueryError` whose sentence names what was refused; a policy refusal carries the offset of what it refused. + +| Refused | Example | The sentence | +|---|---|---| +| A unicode escape | `RETURN 'caf\u00e9' AS s` | The statement holds the escape \u00e9, which the server decodes before it reads the text, so it could end a name, a string or a comment early and the server would run a statement other than the one checked. It was not run: type the character itself. | +| Text that does not lex | `MATCH (n) RETURN 'x` | The statement could not be read: unterminated string at character 18. | +| No statement | an empty or comment-only text | There is no statement to run. | +| More than one statement | `MATCH (n) RETURN n; MATCH (m) RETURN m` | Neo4j runs one statement at a time, and this text holds 2. Run them one by one. | +| `EXPLAIN` or `PROFILE` | `EXPLAIN MATCH (n) RETURN n` | EXPLAIN is not supported on Neo4j connections in this version. | +| A denied word | `MATCH (n) SET n.seen = true` | SET is not allowed: Neo4j connections are read-only in this version. If SET is a name here (a property, a map key or a label), write it in backticks, as `SET`. | +| A denied word used as a name | `MATCH (n) RETURN n.set` | SET is not allowed: Neo4j connections are read-only in this version. If SET is a name here (a property, a map key or a label), write it in backticks, as `set`. | +| A procedure outside the allowlist | `CALL dbms.listConfig()` | CALL dbms.listConfig is not allowed: a read-only Neo4j connection can call only db.labels, db.relationshipTypes, db.propertyKeys, db.schema.visualization, db.schema.nodeTypeProperties, db.schema.relTypeProperties, db.ping, dbms.components. | +| `CALL` used as a name | `MATCH (n:CALL) RETURN n` | CALL is not allowed here: a read-only Neo4j connection can call only db.labels, db.relationshipTypes, db.propertyKeys, db.schema.visualization, db.schema.nodeTypeProperties, db.schema.relTypeProperties, db.ping, dbms.components. If CALL is a name here (a property, a map key or a label), write it in backticks, as `CALL`. | +| A denied namespace | `CALL apoc.load.json('http://10.0.0.5/')` | apoc.* is not allowed on Neo4j connections in this version, because its procedures and functions can reach the network or the file system. A name starting apoc. is refused in any position, called or not, so a variable named apoc must be renamed. | +| A SHOW form outside the allowlist | `SHOW USERS` | SHOW USERS is not allowed on a read-only Neo4j connection. | +| `SHOW` used as a name | `MATCH (n:SHOW) RETURN n` | SHOW is not allowed on a read-only Neo4j connection. If SHOW is a name here (a property, a map key or a label), write it in backticks, as `SHOW`. | +| `SHOW TRANSACTIONS` | `SHOW TRANSACTIONS` | SHOW TRANSACTIONS is not allowed on a read-only Neo4j connection. | +| A qualified function outside the allowlist | `RETURN my.custom(1)` | my.custom() is not allowed: a read-only Neo4j connection calls only built-in functions. | +| A parameter | `MATCH (n) WHERE n.id = $id RETURN n` | Parameters such as $id are not supported on Neo4j connections in this version: write the value into the statement. | +| Parameters bound by the caller | a `params` list that is not empty, from the API | Parameters are not supported for Neo4j in this version. | +| The gate: a write | a statement the server classifies `w` | The server classifies this statement as write, and a read-only Neo4j connection runs only reads. | +| The gate: a read and write | a statement the server classifies `rw` | The server classifies this statement as read and write, and a read-only Neo4j connection runs only reads. | +| The gate: a schema or administration statement | a statement the server classifies `s` that calls no allowlisted procedure | The server classifies this statement as a schema or administration statement, and a read-only Neo4j connection runs only reads. | +| The gate: no classification | a plan that carries no query type | The server did not classify this statement, so it was not run. | +| The gate: the check failed | an `EXPLAIN` the server refused, such as a syntax error | The statement could not be checked by the server, so it was not run: | +| The server's READ mode | a write that reached a READ session | The server refused a write: Neo4j connections are read-only in this version. | + +### 3.4 Why LOAD CSV and APOC are refused + +`LOAD CSV` fetches a URL from the server, and the design's research measured it, through a READ session, reaching a private address the server could see: READ mode does not stop it, and the server classifies `LOAD CSV` as `r` (the capture `tests/fixtures/neo4j/5.26.31/explain-load-csv.json`), so the gate does not stop it either. +APOC's `apoc.load.*` procedures and functions reach the network and the file system the same way, and GDS is a plugin this version has not measured, so its namespace is refused with APOC's. +So `LOAD` is a denied word, `apoc.` and `gds.` are denied namespaces, and every procedure outside the eight above is refused: the policy is the only layer that stops a server-side request to an address the user names. +Widening the allowlist with a measured APOC subset is filed as D140. + +### 3.5 The statement gate + +After the policy allows a statement, the provider sends it prefixed with `EXPLAIN` in a READ session, with the run's own timeout and cancel, and reads `summary.queryType`; a version prefix stays first, as `CYPHER 5 EXPLAIN `, and so do the options after `CYPHER`, as `CYPHER 5 runtime=slotted EXPLAIN `. +Measured on 5.26.31 on 2026-10-03: `CYPHER 5 EXPLAIN runtime=slotted MATCH (n) RETURN n` is a syntax error at `runtime`, while `CYPHER runtime=slotted EXPLAIN MATCH (n) RETURN n`, `EXPLAIN CYPHER runtime=slotted MATCH (n) RETURN n` and `CYPHER 5 runtime=slotted EXPLAIN MATCH (n) RETURN n` each parse and classify as a read. +`r` runs. +`s` runs only when the statement calls an allowlisted procedure, because the policy has already refused every other procedure: `CALL dbms.components()` is classified `s` and runs. +Every other type, a plan with no type, and an `EXPLAIN` that failed refuse the statement, and nothing is run. +An allowed SHOW form skips the gate, because the server classifies the database forms `s`, a class that also holds administration; the policy has already restricted them to the forms of section 3.2. +The gate costs one round trip per statement, which the design accepted because the maintainer's first axis is security. + +### 3.6 Machine access + +Plan mode drafts Cypher and runs nothing: `getLabels().statementLanguage` tells the model what the policy enforces, "Read-only Cypher for Neo4j 5: one statement per run, no writes, no LOAD CSV, no APOC or GDS. CALL is limited to db.labels, db.relationshipTypes, db.propertyKeys, db.schema.visualization, db.schema.nodeTypeProperties, db.schema.relTypeProperties, db.ping and dbms.components. SHOW is limited to indexes, constraints, databases, procedures and functions. Only built-in functions can be called; parameters are not supported. Quote labels, relationship types and property names that are not plain words with backticks.", and a drafted statement meets the policy when the user runs it. +A plan run's fence tag is the type-id, `neo4j`, and a `cypher` block also names Neo4j, as `promql` names Prometheus, while Neo4j is the only engine here that runs Cypher; a second graph engine revisits that. +`list_connections` and `inspect_schema` work through the object surface and run no user Cypher, so a Neo4j seed may set `mcp: true`. +The provider implements no `queryReadOnly`, so agent execution and MCP `run_read_query` do not serve Neo4j: both need a non-SQL statement contract the agent and MCP guards do not have, and the boundary of section 3.4 has to be measured for that path on its own. +That work is filed as B93. + +### 3.7 Measurements + +The cancel, the READ-mode refusal and every capture of section 11 were measured against the compose server on 2026-10-03; section 11.3 records them. + +## 4. Connection + +### 4.1 Configuration fields + +| Field | Meaning | +|---|---| +| Host | A name or address only: the provider builds the `bolt` URI from Host, Port and the SSL panel | +| Port | `7687` by default, Bolt's port; 7474 is Neo4j's HTTP port, which this provider never uses | +| User | The Neo4j user; its write privileges are never used, because the connection is read-only | +| Password | The user's password, sent in Bolt's basic authentication | +| Database | Leave empty to use the server's home database. | +| SSL / TLS | The mode and the CA (section 4.3) | +| SSH Tunnel | A bastion that forwards the one Bolt endpoint | +| Read-only | Accepted, and read-only either way: the provider refuses every write whatever the toggle says. The dialog says so under the toggle: "Neo4j connections are read-only in this version, whether or not this is ticked: this user's write privileges are never used." | + +The Database row is the hint the connection dialog shows under that field, and the Read-only row quotes the sentence under the toggle. +There is no connection string: no `bolt://` or `neo4j://` URI is parsed, so a scheme typed into Host is refused rather than read. +A user name or password written into the host is refused with "Invalid host: a user name or password belongs in the connection's user and password fields, not in the host". +The host and port go through the shared validators of `src/lib/db/http/endpoint.ts`, and the built URI is parsed back and must name the same host and port. + +### 4.2 Authentication + +Studio signs in with Bolt's basic scheme, the User and the Password, or with no authentication when the User is empty. +A refused sign-in reads "Neo4j refused the sign-in:" with the server's words (section 10). +The provider never uses a write privilege the user holds, so a read-only Neo4j user is not needed for safety, though it is a sound second boundary on Enterprise, where roles exist. + +### 4.3 TLS + +| Panel | Scheme | Trust | +|---|---|---| +| No TLS panel, or `disable` | `bolt` | none, plaintext | +| `require` | `bolt+ssc` | any certificate: the chain and the name are not checked | +| `require` with `rejectUnauthorized: true` | `bolt+s` | the system CAs, and the host name | +| `verify-system`, or a panel with no mode | `bolt+s` | the system CAs, and the host name | +| `verify-ca` or `verify-full` with a pasted CA | `bolt+s` | the pasted CA instead of the system CAs, and the host name | +| `verify-ca` or `verify-full` with no CA | `bolt+s` | the system CAs, and the host name | + +`verify-ca` also checks the host name, as `verify-full` does: the driver verifies the chain against the pasted CA and Node verifies the name, and the driver offers no mode that checks one without the other. +A pasted CA reaches the driver as a file, because `neo4j-driver-lite` 6.2.0 reads trusted certificates from paths: it is written once to `/libredb-neo4j-ca/.pem` with mode 0600, in a directory of mode 0700 owned by the Studio process's user, and reused while its content matches. +A CA certificate is public material; the file mode keeps it from being replaced. +A client certificate is refused in every mode with "TLS client certificates are not supported for Neo4j in this version: remove the client certificate and key from the connection's TLS settings". +A self-signed certificate under a verifying mode is refused with a TLS error naming the certificate, never downgraded. + +### 4.4 One database per connection + +A connection reads one database: the Database field, or the user's home database, which `SHOW HOME DATABASE YIELD name` resolves at connect. +The tree shows that one database as its one container, and every object path is checked against it; another database is another connection. +`USE` is a denied word for the same reason. +Browsing several databases in one connection is filed as D143. + +### 4.5 SSH tunnel + +An SSH tunnel is supported, because a `bolt` URI dials the one server it names. +Certificate verification through a tunnel is not: the certificate would be checked against the tunnel's local `127.0.0.1`, so a verifying mode on a tunnelled connection is refused with "Certificate verification through an SSH tunnel is not supported for Neo4j in this version: the certificate would be checked against the tunnel's local address. Use TLS mode require with verification off, or connect without the tunnel". +`require` with verification off connects through a tunnel as `bolt+ssc`. +Verified TLS through a tunnel, with the driver's resolver measured, is filed as D142. + +### 4.6 Server versions + +Neo4j 5.26 LTS is tested and claimed: every capture and the integration test are 5.26.31 Community. +The calendar releases, 2025.x and 2026.x, are not refused and connect untested: constraint type names and `propertyTypes` strings differ there, and they are shown as returned, never parsed. +The overview's version reads `5.26.31 community` on the tested series, `2026.01.0 enterprise (untested)` outside it, and `unknown` when the read at connect failed. +Enterprise connects too; the monitoring differences are in section 7. + +## 5. Query interface + +### 5.1 One statement per run + +The editor sends one Cypher statement, with an optional `CYPHER ` prefix; a trailing `;` is allowed. +To run one statement of several, select it, because the editor then sends the selection alone. + +### 5.2 Result shape + +Every value is converted on the server before it leaves the transport, so the response never meets a driver class, a `BigInt` or a typed array: + +| Bolt value | JSON form | +|---|---| +| INTEGER | a number within the safe integer range, otherwise its exact decimal string | +| FLOAT | the number; `NaN`, `Infinity` and `-Infinity` as those strings | +| DATE, TIME, LOCAL TIME, DATETIME, LOCAL DATETIME, DURATION | the ISO-8601 text with nanoseconds and zone | +| POINT | `{ "srid": , "x": ..., "y": ..., "z"?: ... }` | +| BYTE ARRAY | an array of unsigned byte numbers | +| LIST, MAP | converted recursively | +| NODE | `{ "~graph": "node", "elementId": ..., "labels": [...], "properties": {...} }` | +| RELATIONSHIP | `{ "~graph": "relationship", "elementId": ..., "type": ..., "startNodeElementId": ..., "endNodeElementId": ..., "properties": {...} }` | +| PATH | `{ "~graph": "path", "nodes": [...], "relationships": [...] }`, a zero-length path as its one node and no relationship | +| VECTOR, UUID, a type the driver adds later | its text | + +The deprecated numeric `identity` is never emitted, and `elementId` is stable only within one transaction: do not keep one to find the same node later. +`columnTypes` names `Node`, `Relationship` or `Path` for a column whose every non-null value has that form, `Mixed` when graph forms mix or meet other values, and nothing for a scalar column; the grid shows it in the header. +The `"~graph"` tag is how a later graph view finds graph values in any result without a provider change; the view itself is filed as U72. + +### 5.3 Bounds + +| Bound | Value | What it bounds | +|---|---|---| +| `DEFAULT_QUERY_LIMIT` | 500 | The rows of one result; the driver fetches one more, and on that record the session is closed and the result marked limited | +| `MAX_CELL_JSON_BYTES` | 1,048,576 | One cell's converted JSON in bytes; a larger value becomes `""` and a warning names the column | +| `MAX_CELL_DEPTH` | 32 | How deep one cell's lists and maps nest; a deeper value is replaced the same way | +| `CATALOG_ROW_BOUND` | 10,000 | The rows of one catalog read; a cut listing is counted as a floor and a cut detail is refused, while the tree shows a cut listing's rows with no marker, since its object type has none, and the server log records the cut | +| `CATALOG_CACHE_MS` | 60,000 | How long the property and index reads are kept, in milliseconds (section 6.1) | +| `CATALOG_TIMEOUT_MS` | 30,000 | The Bolt transaction timeout of one catalog read | +| `MONITORING_TIMEOUT_MS` | 30,000 | The Bolt transaction timeout of one monitoring read | +| `TABLE_STATS_LABEL_BOUND` | 50 | The labels the Tables tab counts, the first by name | +| `GRAPH_SAMPLE_LIMIT` | 100 | The `LIMIT` of a tree click's sample read | + +A statement's time is the connection's query timeout, sent as the Bolt transaction timeout, which the server enforces, and kept by the client as its own timer one second later. + +### 5.4 Cancellation and the confirmation gate + +Cancelling a statement aborts its run, the client closes the session, and Neo4j ends the transaction; the result reports a cancel, never a driver fault. +The design's research measured that `session.close()` returns in 1 to 3 ms and the transaction leaves `SHOW TRANSACTIONS` within 1.5 s. +Through the Bolt client as the query route sends it, an abort of a long `UNWIND` rejected the run 1.9 ms later, and the transaction left `SHOW TRANSACTIONS` 5 ms after the abort (section 11.3). +The confirmation gate asks nothing: the destructive vocabulary row for `neo4j` declares no operation and decides alone, because the provider refuses every write before sending, and the gate never reads Cypher as SQL. + +### 5.5 EXPLAIN and PROFILE + +`supportsExplain` is false and a typed `EXPLAIN` or `PROFILE` is refused, because the product has no plan view for Cypher and `PROFILE` runs the statement. + +## 6. Schema introspection + +### 6.1 The object surface (#789) + +| Kind | Role | Folder | Columns | Listing | +|---|---|---|---|---| +| `label` | relation | Node labels | properties | `CALL db.labels() YIELD label RETURN label ORDER BY label` | +| `relationship_type` | relation | Relationship types | properties | `CALL db.relationshipTypes() YIELD relationshipType RETURN relationshipType ORDER BY relationshipType` | +| `index` | config | Indexes | none | `SHOW INDEXES YIELD name, type, entityType, labelsOrTypes, properties, state, owningConstraint`, the default `LOOKUP` indexes left out | +| `constraint` | config | Constraints | none | `SHOW CONSTRAINTS YIELD name, type, entityType, labelsOrTypes, properties` | + +The one container is the database, resolved at connect with `SHOW HOME DATABASE YIELD name` when the connection names none. +An object's path is `[database, segment]`, with a segment that names its kind: a label `(:Name)`, a relationship type `[:NAME]`, an index `INDEX name` and a constraint `CONSTRAINT name`, so a label and a relationship type of one name never collide; the object's name stays the plain name. +A count is the listing's length, and a listing the server refuses answers that kind's refusal sentence instead of a count. +A label's or a relationship type's columns come from `CALL db.schema.nodeTypeProperties() YIELD nodeLabels, propertyName, propertyTypes, mandatory` and `CALL db.schema.relTypeProperties() YIELD relType, propertyName, propertyTypes, mandatory`: the property name, the `propertyTypes` text as returned, and `nullable` false only when every row naming that owner and property says `mandatory`. +A label with no property has no columns. +Its indexes are the index rows whose entity type and labels or types name it. +The property and index reads answer every object at once, so they are shared by concurrent callers and kept for 60 seconds, then read again: a schema change shows in the tree within a minute, and listing the container drops the cache at once. +A tree click on a label writes `` MATCH (n:`Label`) RETURN n LIMIT 100 ``, and on a relationship type `` MATCH (a)-[r:`TYPE`]->(b) RETURN a, r, b LIMIT 100 ``, with every name backticked and every backtick doubled, so a name with a space, a backtick or non-ASCII letters reads back as itself; an index and a constraint have no click action. +The schema diagram is not offered on a `queryLanguage: "cypher"` connection (`offersSchemaDiagram` in `src/lib/db/types.ts`): relationship types are not tables, and the diagram's `_id` heuristic would invent edges. + +### 6.2 Object source (#789) + +No kind has a source, so the provider writes no `readObjectSource`, and the type-id is on the abstainer list of `tests/isolated/object-source-declarations.test.ts`. + +### 6.3 Object edit (#789): nothing to write + +No kind declares `acceptsSourceEdits` or `acceptsRowWrites`: there is no source edit, no grid edit, no Create Table and no Import Data target for Neo4j, because every connection is read-only in this version. +`tests/isolated/object-edit-declarations.test.ts` holds that absence and this section together, over every kind the provider declares. + +## 7. Monitoring & health + +| Surface | Read | On Community 5.26 | +|---|---|---| +| Health | the driver's connectivity check, then `CALL db.ping()` | yes | +| Overview | `CALL dbms.components() YIELD name, versions, edition` at connect, `MATCH (n) RETURN count(n) AS nodes`, `MATCH ()-[r]->() RETURN count(r) AS relationships`, and the label, relationship-type and index listings of section 6.1 | yes | +| Active sessions | `SHOW TRANSACTIONS YIELD database, transactionId, username, currentQuery, startTime, status, elapsedTime` | the user's own transactions; on Enterprise every transaction, with the privilege to see them | +| Slow queries | none | the query log is Enterprise only, and this version does not read it | +| Performance metrics | none | the metrics endpoint is Enterprise only | +| Table stats | the label listing, then `MATCH (n:

    What you get:

      diff --git a/packaging/flatpark/org.libredb.Studio.desktop b/packaging/flatpark/org.libredb.Studio.desktop index d3e1168189..bc5752a202 100644 --- a/packaging/flatpark/org.libredb.Studio.desktop +++ b/packaging/flatpark/org.libredb.Studio.desktop @@ -7,6 +7,6 @@ Exec=libredb-studio Icon=org.libredb.Studio Terminal=false Categories=Development;Database; -Keywords=SQL;Database;PostgreSQL;MySQL;SQLite;libSQL;Turso;DuckDB;MongoDB;Redis;Oracle;SQLServer;ClickHouse;Couchbase;Druid;Elasticsearch;OpenSearch;Trino;Cassandra;Prometheus;PromQL;Kafka;etcd;IDE;Query; +Keywords=SQL;Database;PostgreSQL;MySQL;SQLite;libSQL;Turso;DuckDB;MongoDB;Redis;Oracle;SQLServer;ClickHouse;Couchbase;Druid;Elasticsearch;OpenSearch;Trino;Cassandra;Prometheus;PromQL;Kafka;etcd;Neo4j;Cypher;IDE;Query; StartupNotify=true StartupWMClass=libredb-studio-desktop diff --git a/packaging/flatpark/org.libredb.Studio.metainfo.xml b/packaging/flatpark/org.libredb.Studio.metainfo.xml index 8ec563d670..64baab169f 100644 --- a/packaging/flatpark/org.libredb.Studio.metainfo.xml +++ b/packaging/flatpark/org.libredb.Studio.metainfo.xml @@ -59,6 +59,7 @@
    • Prometheus (read-only)
    • Apache Kafka (read-only)
    • etcd
    • +
    • Neo4j (read-only)

    What you get:

      diff --git a/packaging/linux/nfpm.yaml b/packaging/linux/nfpm.yaml index 72ac824f1f..814429657c 100644 --- a/packaging/linux/nfpm.yaml +++ b/packaging/linux/nfpm.yaml @@ -23,10 +23,10 @@ section: database priority: optional maintainer: cevheri description: | - Web-based SQL IDE for nineteen engines - PostgreSQL, MySQL, SQLite, libSQL, DuckDB, + Web-based SQL IDE for twenty engines - PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, - Apache Kafka and etcd - with AI query assistance. + Apache Kafka, etcd and Neo4j - with AI query assistance. Ships the standalone Next.js server with a bundled private Node.js runtime (no external dependencies) and a systemd unit (libredb-studio) serving the app on port 3000; enable it with diff --git a/packaging/winget/LibreDB.Studio.locale.en-US.yaml.tmpl b/packaging/winget/LibreDB.Studio.locale.en-US.yaml.tmpl index 4575f6d9b0..7235155b4e 100644 --- a/packaging/winget/LibreDB.Studio.locale.en-US.yaml.tmpl +++ b/packaging/winget/LibreDB.Studio.locale.en-US.yaml.tmpl @@ -22,7 +22,7 @@ Description: |- LibreDB Studio is an open-source, web-based SQL IDE for cloud-native teams. It connects to PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, - Trino, Apache Cassandra, Prometheus, Apache Kafka and etcd, and ships a Monaco-powered editor with AI query + Trino, Apache Cassandra, Prometheus, Apache Kafka, etcd and Neo4j, and ships a Monaco-powered editor with AI query assistance, schema browsing, ER diagrams, charts, and import/export tooling. This package installs the standalone server with a bundled Node.js runtime; run `libredb-studio` and open http://127.0.0.1:3000 - the first run prints diff --git a/playwright.config.ts b/playwright.config.ts index 6797ca4d85..8c3844502f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,7 +6,7 @@ import { defineConfig, devices } from "@playwright/test"; // that port is occupied by another instance. const port = Number(process.env.E2E_PORT ?? 3000); -// offline-editor.spec.ts, kafka-provider.spec.ts and etcd-provider.spec.ts get a second server +// offline-editor.spec.ts and the kafka, etcd and neo4j provider specs get a second server // process on its own port - see the projects and the webServer array below for why. Override with // E2E_OFFLINE_PORT under the same collision circumstances as E2E_PORT. const offlinePort = Number(process.env.E2E_OFFLINE_PORT ?? 3010); @@ -51,9 +51,9 @@ export default defineConfig({ { name: "chromium", use: { ...devices["Desktop Chrome"] }, - // offline-editor.spec.ts, kafka-provider.spec.ts and etcd-provider.spec.ts run under their own + // offline-editor.spec.ts and the kafka, etcd and neo4j provider specs run under their own // projects below, against the second server, and passkey.spec.ts against the third. - testIgnore: /(?:offline-editor|base-path|kafka-provider|etcd-provider|passkey)\.spec\.ts/, + testIgnore: /(?:offline-editor|base-path|kafka-provider|etcd-provider|neo4j-provider|passkey)\.spec\.ts/, }, { // Every other spec in this suite signs in as the same shared user@libredb.org account @@ -90,6 +90,13 @@ export default defineConfig({ use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` }, testMatch: /etcd-provider\.spec\.ts/, }, + { + // neo4j-provider.spec.ts drives Test Connection too (two calls, one per refusal it asserts), so it + // takes the second server for the reason kafka-provider.spec.ts does. + name: "chromium-neo4j", + use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` }, + testMatch: /neo4j-provider\.spec\.ts/, + }, { // Passkeys need an account registry, so this server runs in store mode (STORAGE_PROVIDER=sqlite) // with PASSKEY_ORIGIN set; the servers above keep the default local mode, where passkeys are off diff --git a/scripts/security-check.mjs b/scripts/security-check.mjs index b9caff860c..cb55d06cd4 100644 --- a/scripts/security-check.mjs +++ b/scripts/security-check.mjs @@ -70,6 +70,7 @@ export const PROGRAMME_CONTROL_IDS = [ "3.7", "3.8", "3.9", + "3.10", ]; export const STATUSES = new Set(["Implemented", "Partial", "Not implemented"]); diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 2beab63796..f94968b3be 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -37,7 +37,7 @@ description: | Web-based SQL IDE for PostgreSQL, MySQL, SQLite, libSQL, DuckDB, Oracle, SQL Server, MongoDB, Redis, Couchbase, ClickHouse, Apache Druid, Elasticsearch, OpenSearch, Trino, Apache Cassandra, Prometheus, - Apache Kafka and etcd + Apache Kafka, etcd and Neo4j with AI query assistance. Installing the snap starts a background service that serves the app on diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index f8a7ac0998..e3295a8136 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -29,7 +29,7 @@ import { import { DatabaseConnection, SavedQuery, QueryHistoryItem } from "@/lib/types"; import { relationObjects, type DetailedObject } from "@/lib/db/detailed-object"; import { pathKey } from "@/lib/db/object-path"; -import type { ProviderCapabilities } from "@/lib/db/types"; +import { offersSchemaDiagram, type ProviderCapabilities } from "@/lib/db/types"; import { storage } from "@/lib/storage"; import { getDBIcon } from "@/lib/db-ui-config"; @@ -174,7 +174,8 @@ export function CommandPalette({ Monitoring - {activeConnection && ( + {/* Not on a Cypher connection: its relationship types are no tables (SR20). */} + {activeConnection && offersSchemaDiagram(capabilities) && ( runAction(onShowDiagram)}> Schema Diagram (ERD) diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx index f7870054b7..d5a8b86234 100644 --- a/src/components/ConnectionModal.tsx +++ b/src/components/ConnectionModal.tsx @@ -36,6 +36,7 @@ import { getDBConfig, isFileBased, offersSshTunnel, + readOnlyHint, takesConnectionField, type ConnectionField, type DatabaseUIConfig, @@ -458,8 +459,7 @@ export function ConnectionModal({ Read-only

      - Writes, value edits and maintenance are refused on this connection. You can turn this off here, so on - your own connection it is a safety rail, not a permission. + {readOnlyHint(uiConfig)}

)} diff --git a/src/components/QueryEditor.tsx b/src/components/QueryEditor.tsx index 37c9a685d2..5372ace997 100644 --- a/src/components/QueryEditor.tsx +++ b/src/components/QueryEditor.tsx @@ -15,6 +15,9 @@ import { registerLibreDBLanguage } from "@/lib/editor/libredb-language"; import { registerRedisLanguage } from "@/lib/editor/redis-language"; import { registerEtcdLanguage } from "@/lib/editor/etcd-language"; import { registerPromqlLanguage } from "@/lib/editor/promql-language"; +import { CYPHER_LANGUAGE_ID, registerCypherLanguage } from "@/lib/editor/cypher-language"; +import { cypherCompletionSchemaOf, registerCypherCompletionProvider } from "@/lib/editor/cypher-completions"; +import { graphPolicyProfileOf } from "@/lib/db/graph-policy-profiles"; import { configureMonacoLoader } from "@/lib/editor/monaco-loader"; import { defineStudioThemes, STUDIO_THEME_DARK, STUDIO_THEME_LIGHT } from "@/lib/editor/monaco-theme"; import { useEffectiveTheme } from "@/hooks/use-effective-theme"; @@ -72,7 +75,7 @@ interface QueryEditorProps { /** Called when content changes in real-time. Use sparingly as it triggers on every keystroke. */ onContentChange?: (val: string) => void; onExplain?: () => void; - language?: "sql" | "json" | "libredb" | "redis" | "promql" | "etcd"; + language?: "sql" | "json" | "libredb" | "redis" | "promql" | "etcd" | "graph-cypher"; /** * The connected engine, whose grammar decides where a statement ends. * @@ -86,6 +89,8 @@ interface QueryEditorProps { interface ParsedTable { name: string; + /** The object's address; Cypher completion reads a label or relationship type from its last segment. */ + path?: string[]; rowCount?: number; columns?: Array<{ name: string; @@ -486,13 +491,14 @@ export const QueryEditor = forwardRef( }, []); const handleBeforeMount = (monacoInstance: typeof Monaco) => { - // Register the LibreDB, Redis and etcd command languages and PromQL (each idempotent) + // Register the LibreDB, Redis and etcd command languages, PromQL and Cypher (each idempotent) // so their tabs highlight correctly instead of being treated as JSON or SQL - // (#427, #1085, #1089). + // (#427, #1085, #1089, Neo4j spec 6.5). registerLibreDBLanguage(monacoInstance); registerRedisLanguage(monacoInstance); registerPromqlLanguage(monacoInstance); registerEtcdLanguage(monacoInstance); + registerCypherLanguage(monacoInstance); // Suppress Monaco's "Canceled" errors in console (with cleanup tracking) if (!originalConsoleErrorRef.current) { @@ -534,6 +540,22 @@ export const QueryEditor = forwardRef( } }, [monaco, language, schemaCompletionCache, completesMongoDB]); + // Cypher completion provider (Neo4j spec 6.5, SR6). Its names are read from the schema objects' + // kind-qualified paths, never from the SQL cache's lowercased table names, and its CALL and SHOW + // allowlists from the connected engine's policy profile, looked up by type as the SQL provider + // gets its dialect. + const graphPolicy = graphPolicyProfileOf(databaseType); + useEffect(() => { + if (monaco && language === CYPHER_LANGUAGE_ID) { + const disposable = registerCypherCompletionProvider( + monaco, + cypherCompletionSchemaOf(parsedSchema), + graphPolicy, + ); + return () => disposable.dispose(); + } + }, [monaco, language, parsedSchema, graphPolicy]); + // Every model change reaches here: a keystroke, and equally the writes Format, Clear // and the imperative setValue make, since Monaco reports those through the same // change event. All of them are this editor's own text, so all of them are recorded diff --git a/src/components/agent/AnswerCard.tsx b/src/components/agent/AnswerCard.tsx index 9ad4223ea5..2b4b29c540 100644 --- a/src/components/agent/AnswerCard.tsx +++ b/src/components/agent/AnswerCard.tsx @@ -112,7 +112,7 @@ const STATUS_TONES: Readonly> = Object.freeze({ * `"unknown"` is this surface having nothing to go on, which is a different state from * every engine in the ladder and must not collapse into the first of them. */ -type StatementLanguage = "sql" | "json" | "libredb" | "redis" | "promql" | "etcd" | "unknown"; +type StatementLanguage = "sql" | "json" | "libredb" | "redis" | "promql" | "etcd" | "graph-cypher" | "unknown"; /** * Identity, never status. The rail spends amber on "nobody established this", rose on a @@ -127,6 +127,10 @@ type StatementLanguage = "sql" | "json" | "libredb" | "redis" | "promql" | "etcd * * etcd takes sky (#1089), the light blue of its own mark, between SQL's blue and JSON's cyan and * neither of them: its command line is no SQL and no JSON, and no guard here reads it either. + * + * Cypher takes purple (Neo4j spec 6.5, SR20): a hue no other language here uses and none of the three + * status hues above. No guard here reads Cypher either, so its draft too stands beside the amber "not + * checked" chip. */ const LANGUAGE_ACCENTS: Readonly> = Object.freeze({ sql: "border-hue-blue/40", @@ -135,6 +139,7 @@ const LANGUAGE_ACCENTS: Readonly> = Object.fre libredb: "border-hue-violet/40", promql: "border-hue-indigo/40", etcd: "border-hue-sky/40", + "graph-cypher": "border-hue-purple/40", unknown: "border-hairline-strong", }); diff --git a/src/components/icons/db-icons.tsx b/src/components/icons/db-icons.tsx index 348a12eb19..3c9d54a467 100644 --- a/src/components/icons/db-icons.tsx +++ b/src/components/icons/db-icons.tsx @@ -504,3 +504,30 @@ export const EtcdIcon: React.FC = ({ className, ...props }) => ( ); + +/** + * Neo4j: a generic graph glyph, never the vendor's logo. + * + * Three nodes joined by three relationships, drawn as strokes at the house weight with no fill: a property + * graph in its plainest form, which is what Neo4j stores and what stays identifiable at the 14px the + * connection list draws. The edges stop at each circle's outline, so no line crosses a node. + */ +export const Neo4jIcon: React.FC = ({ className, ...props }) => ( + + + + + + + + +); diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx index a45d69648e..dd9720c336 100644 --- a/src/components/sidebar/Sidebar.tsx +++ b/src/components/sidebar/Sidebar.tsx @@ -2,7 +2,7 @@ import React from "react"; import { DatabaseConnection } from "@/lib/types"; -import { keyScanShape, type DatabaseObject } from "@/lib/db/types"; +import { keyScanShape, offersSchemaDiagram, type DatabaseObject } from "@/lib/db/types"; import type { ProviderMetadata } from "@/hooks/use-provider-metadata"; import { Plus, Zap, Layers, LoaderCircle, CircleAlert } from "lucide-react"; import { ScrollArea } from "@/components/ui/scroll-area"; @@ -257,7 +257,8 @@ export const Sidebar = React.memo(function Sidebar({
- {activeConnection && ( + {/* Not on a Cypher connection: its relationship types are no tables (SR20). */} + {activeConnection && offersSchemaDiagram(metadata?.capabilities) && (