Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/ADDING_A_PROVIDER.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Score a candidate before writing code. Each criterion you fail becomes code you
| 4 | **Is there monitoring data over the same surface?** | Decides how much of the monitoring panel is real rather than honestly empty |
| 5 | **Is there an EXPLAIN?** | Decides `supportsExplain` and whether a strategy is needed |
| 6 | **How complex is auth?** | Basic auth is three lines. SigV4, OAuth2 refresh or Kerberos is a library — and that is usually where the no-dependency promise ends |
| 7 | **Does the data model map onto containers, kinds and objects?** | The object surface addresses an object by a path of segments, so a hierarchy is declared through `containerLevels` and `objectKinds` rather than flattened into a display name |
| 7 | **Does the data model map onto containers, kinds and objects, and is an outer container level alone a real address?** | The object surface addresses an object by a path of segments, so a hierarchy is declared through `containerLevels` and `objectKinds` rather than flattened into a display name. Then choose `containerPathShapes` and declare it in `getCapabilities()`: `exact` when only the declared depth is an address (a PostgreSQL schema, a MongoDB database), `prefixes` when the outer levels alone are one too (a Trino catalog with no schema, a Couchbase bucket with no scope). An absent field reads as `exact`, and the provider's own check and the HTTP object routes both refuse by that one declaration through `acceptedContainerShapes()` in `src/lib/db/object-kinds.ts` |

A good sanity check for criterion 1: **can a browser talk to it?** Couchbase's own Web Console and
the Capella UI are browser applications, so every service had to be reachable over HTTP for the
Expand Down Expand Up @@ -901,7 +901,7 @@ Those three reach code and tests only; the four prose greps of the published blo
- [ ] `src/lib/sql/values.ts`: `LITERAL_ESCAPE`.
- [ ] `src/lib/export/result-export.ts`: `STANDS_ALONE` and `BINARY_LITERAL`, plus a decision on the partial `DIALECT_TYPES`.
- [ ] `tests/helpers/census-connection.ts`: `CENSUS_CONNECTION`, the unconnected connection every census builds through the real factory.
- [ ] `tests/isolated/object-column-declarations.test.ts` (`EXPECTED_COLUMN_KINDS`), `tests/isolated/object-source-declarations.test.ts` (`SOURCE_DECLARATIONS`), `tests/unit/db/result-pagination-capability.test.ts` (`EXPECTED`), `tests/unit/schema-diff/migration-dialects.test.ts` (`COLUMN_GRAMMAR`), `tests/unit/schema-diff/migration-generator.test.ts` (`MODIFIED_COLUMN_COVERAGE`, `TRANSACTION_WRAPPER_COVERAGE`) and `tests/hooks/use-connection-form.test.ts` (`PICKER_COVERAGE`).
- [ ] `tests/isolated/object-column-declarations.test.ts` (`EXPECTED_COLUMN_KINDS`), `tests/isolated/object-source-declarations.test.ts` (`SOURCE_DECLARATIONS`), `tests/unit/db/result-pagination-capability.test.ts` (`EXPECTED`), `tests/unit/db/container-path-shapes-capability.test.ts` (`EXPECTED_CONTAINER_PATH_SHAPES`), `tests/unit/schema-diff/migration-dialects.test.ts` (`COLUMN_GRAMMAR`), `tests/unit/schema-diff/migration-generator.test.ts` (`MODIFIED_COLUMN_COVERAGE`, `TRANSACTION_WRAPPER_COVERAGE`) and `tests/hooks/use-connection-form.test.ts` (`PICKER_COVERAGE`).
- [ ] `tests/unit/lib/db-ui-config.test.ts`: `ALL_TYPES`, which a test holds equal to the keys of `DB_UI_CONFIG`.
- [ ] `tests/helpers/object-edit-expectation.ts`: `EXPECTED_EDIT_ABSTAINERS`, when the new id declares no editable kind.
That is a population, not a record, so the compiler says nothing; `tests/isolated/object-edit-declarations.test.ts` then requires an `Object edit (#789)` heading in the new provider doc naming which absence it is.
Expand Down
39 changes: 36 additions & 3 deletions docs/API_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,40 @@ A `druid` connection fails the second check whatever the `type` is, with `{ "err

A `trino` connection passes it for `kill` and fails it for everything else, which is the difference between an empty supported set and a set of one: `CALL system.runtime.kill_query` really terminates a statement (verified end to end - the target then fails `ADMINISTRATIVELY_KILLED`), while vacuum, reindex, optimize, check and analyze all describe work that belongs to the connector behind a catalog rather than to the engine.

#### Container paths on the object routes

Four object routes take a container path, and they check it by two different rules before the provider is called (#1147).

`container` on `POST /api/db/objects/counts` and `POST /api/db/objects/list`, and every entry of `containers` on `POST /api/db/objects/inventory`, is an address: the container a read binds its segments from.
The route accepts it only in a shape the engine declares as `containerPathShapes` in its capabilities, and it reads that declaration through the same kernel function the provider refuses by, `acceptedContainerShapes()` in `src/lib/db/object-kinds.ts`.
An `exact` engine accepts the declared depth and nothing else.
A `prefixes` engine accepts every depth from one level up to the declared one, so on Trino a catalog alone is an address as well as a catalog and a schema.
An engine that declares no value reads as `exact`.
A path the engine does not accept is refused at the edge, whether it is too short or too long, with one sentence and one wire shape.

| Condition | Status | Body |
|-----------|--------|------|
| `container`, or one entry of `containers`, is not a shape the engine accepts | `400` | `{ "error": "<type> accepts \"<field>\" as <shapes>, received <path>" }` |

The body carries no `code`, like the route's other refusals of a caller mistake, and no listing runs: on the inventory every named entry is checked before the first one is read.
`<shapes>` spells each accepted shape from the engine's level labels, lowercased.
A declaration with no level prints `empty` when only `[]` is accepted, and `nothing: this declaration carries no container level` when no path is.

| Engine | Request field | Answer |
|--------|---------------|--------|
| PostgreSQL | `"container": []` | `400` `{ "error": "postgres accepts \"container\" as [schema], received []" }` |
| PostgreSQL | `"container": ["app", "x"]` | `400` `{ "error": "postgres accepts \"container\" as [schema], received [\"app\",\"x\"]" }` |
| PostgreSQL | `"containers": [["app"], []]` | `400` `{ "error": "postgres accepts \"containers\" as [schema], received []" }` |
| Trino | `"container": ["memory"]` | reaches the engine |
| Trino | `"container": ["memory", "app", "x"]` | `400` `{ "error": "trino accepts \"container\" as [catalog] or [catalog, schema], received [\"memory\",\"app\",\"x\"]" }` |
| SQLite | `"container": ["main"]` | `400` `{ "error": "sqlite accepts \"container\" as empty, received [\"main\"]" }` |

`parent` on `POST /api/db/objects/containers` is a tree cursor rather than an address, and it keeps the depth ceiling on every engine.
Any depth up to and including the declared one is accepted, and a parent at the declared depth answers `[]`, because nothing nests below the last level.
Only a deeper parent is refused, at `400` with `{ "error": "<type> declares a container depth of <n>, and \"parent\" has <m> segments: <path>" }`.

A caller that reaches a provider without these routes, such as the MCP `inspect-schema` tool or a host behind the embedded workspace, is refused by the provider itself under the same rule, in the provider's own words: `A PostgreSQL container path is [schema], received []`.

#### POST /api/db/objects/describe

Read the columns, indexes and foreign keys of ONE object.
Expand Down Expand Up @@ -932,9 +966,8 @@ there.
Build a plan for an edited object definition, and answer what an apply would send.
It executes nothing and writes nothing.

The describe route above is the one Phase 2 sibling documented in this file.
The other six under `/api/db/objects/` (`containers`, `counts`, `list`, `search`, `inventory`,
`source`) are not documented here yet.
The describe route above is the one Phase 2 sibling documented in full in this file.
The other six under `/api/db/objects/` (`containers`, `counts`, `list`, `search`, `inventory`, `source`) are not, except for the container-path rule four of them share, which [Container paths on the object routes](#container-paths-on-the-object-routes) documents.

**Authentication:** Required.
There is NO admin gate on either route, and the reason is measured rather than preferred: a
Expand Down
8 changes: 8 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ Both database and LLM layers use the Strategy Pattern with a factory:

No `isMongoDB` / `=== 'mongodb'` checks outside provider classes. All behavior differences are driven through capabilities and labels.

`src/lib/db/object-kinds.ts` is the kernel through which every provider reads its own declaration, and it takes only facts that follow from the declaration and hold for every engine.
How deep the container chain is (`containerDepth()`), which container paths are an address (`acceptedContainerShapes()`, over `containerPathShapes`) and which object kinds exist (`declaredKinds()`) are such facts.
The HTTP object routes read them through the same functions, so a route refusal and a provider refusal cannot disagree about one declaration (#1147).
A rule that only one engine's reads need stays in that engine's file, next to the reads it protects.
PostgreSQL's `containerSchema()` is the example: it refuses a declaration that names no `schema` level, because the PostgreSQL reads look the schema up by id, so it lives beside those reads rather than in the kernel (#1092).
A descriptor field that only one engine sets is a sign that its rule belongs in that engine.
`ObjectPathShapeEngine.attachedSegment` (#978) is the one pre-existing exception: a per-engine acceptance policy carried in provider descriptors rather than in the declaration, and moving it into the declaration is separate work.

### 4.2. Authentication Flow

```mermaid
Expand Down
7 changes: 3 additions & 4 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4190,10 +4190,9 @@ and a test drives an engine whose top level exceeds the cap.

Reproducible in a browser in one click. Select a depth-0 connection (SQLite), then a depth-2 one
(DuckDB): the first request the tree issues is `POST /api/db/objects/counts` with
`{"connectionId":"seed:t28b-duckdb","container":[]}`, which answers HTTP 400 "A DuckDB container
path is [database] or [database, schema], received []". The tree then re-reads correctly and the
final paint is right, so nothing is visible to the user; the 400 is in the server log on every such
switch.
`{"connectionId":"seed:t28b-duckdb","container":[]}`, which answers HTTP 400.
Since #1147 the route refuses it itself, as `duckdb accepts "container" as [database] or [database, schema], received []`; before that the provider did, as "A DuckDB container path is [database] or [database, schema], received []".
The tree then re-reads correctly and the final paint is right, so nothing is visible to the user; the 400 shows in the browser's network log on every such switch, and since #1147 it is no longer logged as a `Query error` warning by `createErrorResponse` (`src/lib/api/errors.ts`), because the route's own refusals are not.

The cause is a one-commit prop skew rather than anything in the tree: `Sidebar` renders `ObjectTree`
with `activeConnection` and `metadata`, `useProviderMetadata` clears its metadata in an EFFECT, and a
Expand Down
2 changes: 2 additions & 0 deletions docs/providers/cassandra.md
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,8 @@ because there are no table statistics to list at all.)
supportsConnectionString: false, // no URI carries localDataCenter (§4.2)
defaultPort: 9042,
schemaRefreshPattern: "\\b(CREATE|DROP|ALTER)\\b",
containerLevels: [{ id: "schema", label: "Keyspace", labelPlural: "Keyspaces" }], // one level: CQL has none above a keyspace and none below it (§6.4)
containerPathShapes: "exact", // only [keyspace] addresses a container; any other path is refused (§6.4, #1147)
}
```

Expand Down
2 changes: 2 additions & 0 deletions docs/providers/clickhouse.md
Original file line number Diff line number Diff line change
Expand Up @@ -1374,6 +1374,8 @@ rather than silent.
| `maintenanceOperations` | `['optimize', 'analyze', 'kill']` |
| `supportsConnectionString` | `true` |
| `defaultPort` | `8123` |
| `containerLevels` | one level, `schema`, labelled Database: ClickHouse has no schema level below a database ([§6.1](#61-the-object-surface-789)) |
| `containerPathShapes` | `exact`: only `[database]` addresses a container ([§6.1](#61-the-object-surface-789)), so a shorter or a longer path is refused, by the object routes over HTTP and by this provider for a caller that reaches it directly (#1147) |
| `schemaRefreshPattern` | `\b(CREATE\|DROP\|ALTER\|RENAME\|TRUNCATE\|ATTACH\|DETACH)\b` |

`supportsCreateTable: false` is deliberate, not an oversight — see
Expand Down
5 changes: 5 additions & 0 deletions docs/providers/couchbase.md
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,9 @@ provider in [`index.ts`](../../src/lib/db/providers/document/couchbase/index.ts)
| `function` | routine | `[bucket, scope, function]` | `system:functions`, and the only kind here declaring `hasSource` ([§6b](#6b-object-source-789)) |
| `index` | config, `attachedTo: collection` | `[bucket, scope, collection, index]` | `system:indexes` |

A bucket alone is a real address as well as a bucket and a scope, and the declaration states it as `containerPathShapes: "prefixes"` ([§9](#9-capabilities--labels)).
A bucket-level container carries no scope, and that is absent rather than `_default`, because `_default` is a real scope holding real collections.

A collection declares `acceptsRowWrites: true`. That is the per-kind fact and it is deliberately
separate from the engine-wide `supportsInlineRowEdit: false` this provider also declares: the
results grid's `UPDATE ... SET` cannot address a document through the `__id` projection
Expand Down Expand Up @@ -1176,6 +1179,8 @@ stays absent, and that card never renders either.
| `maintenanceOperations` | `['analyze', 'reindex', 'kill']` |
| `supportsConnectionString` | `true` |
| `defaultPort` | `8091` |
| `containerLevels` | two levels, `catalog` labelled Bucket then `schema` labelled Scope ([§6a.1](#6a1-what-is-declared)) |
| `containerPathShapes` | `prefixes`: `[bucket]` and `[bucket, scope]` both address a container, because a bucket alone is a real address ([§6a.1](#6a1-what-is-declared)); the empty path and a longer path are both refused, by the object routes over HTTP and by this provider for a caller that reaches it directly (#1147) |
| `schemaRefreshPattern` | `\b(CREATE\|DROP\|ALTER)\s+(COLLECTION\|SCOPE\|INDEX)\b` |

`supportsCreateTable: false` is deliberate: `CreateTableModal` builds `CREATE TABLE` from a column
Expand Down
1 change: 1 addition & 0 deletions docs/providers/druid.md
Original file line number Diff line number Diff line change
Expand Up @@ -1603,6 +1603,7 @@ Both halves of that are real constraints, not scope cuts made lightly:
| `defaultPort` | `8888` | The Router. `8082` (Broker) is equally valid ([§3.3](#33-router-8888-or-broker-8082--both-work-identically)) |
| `schemaRefreshPattern` | `\b(INSERT\|REPLACE)\b` | The only statements that could change a datasource — and the native engine rejects both, so in practice a query never refreshes the schema, which is correct |
| `containerLevels` | one `schema` level | `INFORMATION_SCHEMA.SCHEMATA` reports one catalog, always `druid`, so there is no second level to add ([§6.1](#61-the-object-surface-789)) |
| `containerPathShapes` | `exact` | Only `[schema]` addresses a container, so a shorter or a longer path is refused, by the object routes over HTTP and by this provider directly (#1147) |
| `objectKinds` | `datasource`, `lookup`, `system_table` | And five kinds ABSENT rather than declared and zero, because `CREATE` is not in the grammar in any form ([§6.1](#61-the-object-surface-789)) |

### `getLabels()` ([`index.ts`](../../src/lib/db/providers/sql/druid/index.ts))
Expand Down
7 changes: 5 additions & 2 deletions docs/providers/duckdb.md
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,8 @@ connection's own file restated: `ATTACH '<file>' AS warehouse` puts another whol
session and a three-part name reaches into it, so one connection genuinely holds databases holding
schemas holding objects. `ATTACH ':memory:' AS name` works too, which is what the tests use.

A database alone is therefore a real address as well as a database and a schema, and the declaration states it as `containerPathShapes: "prefixes"`: a database-level read answers for every schema in that database.

#### Four kinds, and one `duckdb_*` function behind each

| Kind | Role | Catalog function | Note |
Expand Down Expand Up @@ -1054,7 +1056,7 @@ with wording about a keyword the user never typed.

## 9. Capabilities & labels

### 9.1 `getCapabilities()` (`index.ts:380`)
### 9.1 `getCapabilities()` ([`index.ts`](../../src/lib/db/providers/sql/duckdb/index.ts))

| Capability | Value | UI effect |
|---|---|---|
Expand All @@ -1069,13 +1071,14 @@ with wording about a keyword the user never typed.
| `identifierQuoting` | `double` | Generated SQL quotes identifiers with double quotes. |
| `maintenanceOperations` | `vacuum`, `analyze`, `optimize` | Offers only the maintenance operations implemented in §8. |
| `containerLevels` | `Database`, `Schema` | The object browser nests schemas under databases (§6). |
| `containerPathShapes` | `prefixes` | `[database]` and `[database, schema]` both address a container, because an attached database alone is a real address; the empty path and a longer path are both refused, by the object routes over HTTP and by this provider directly (§6, #1147). |
| `objectKinds` | `table`, `view`, `macro`, `sequence` | These are the object folders exposed in the browser (§6). |

The maintenance specs make `vacuum` and `analyze` available both per table and globally.
`optimize` is global only and is labelled **Checkpoint Database** because it runs `CHECKPOINT`.
See §8 for the statements and placement rules.

### 9.2 `getLabels()` (`index.ts:520`)
### 9.2 `getLabels()` ([`index.ts`](../../src/lib/db/providers/sql/duckdb/index.ts))

| Label | UI effect |
|---|---|
Expand Down
1 change: 1 addition & 0 deletions docs/providers/libredb.md
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,7 @@ for a second reason: the rows are derived groupings, see 5.3.
| `supportsConnectionString` | `false` |
| `defaultPort` | `null` |
| `schemaRefreshPattern` | `\\b(put\|delete)\\b` |
| `containerPathShapes` | `exact`: the declaration names no container level (`containerLevels` is absent), so only the empty path `[]` addresses a container and any segment is refused, by the object routes over HTTP and by this provider for a caller that reaches it directly (#1147) |

`schemaRefreshPattern` tells the UI which executed commands should trigger a schema (key-pattern)
refresh — `put` and `delete` both add or remove keys.
Expand Down
1 change: 1 addition & 0 deletions docs/providers/libsql.md
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,7 @@ Measured through the provider against both deployments (fixture: 2 tables, 3 and
| `supportsCreateTable` | `true` | `CREATE TABLE` works as an ordinary SQL statement |
| `schemaRefreshPattern` | `"(CREATE\|DROP\|ALTER\|TRUNCATE\|REINDEX)\\b"` | Matches statements that modify schema or index metadata |
| `containerLevels` | `[]` | Zero-container engine; bare object names throughout ([§6.1](#61-the-object-surface-789)) |
| `containerPathShapes` | `exact` | Only the empty path `[]` addresses a container, so any segment is refused, by the object routes over HTTP and by this provider directly (#1147) |
| `objectKinds` | `LIBSQL_OBJECT_KINDS` | `table` (relation, `acceptsRowWrites`), `view` (relation), `index` (config), `trigger` (attached) |

### Labels — overridden (`getLabels()`, [`src/lib/db/providers/sql/libsql/index.ts`](../../src/lib/db/providers/sql/libsql/index.ts))
Expand Down
1 change: 1 addition & 0 deletions docs/providers/mongodb.md
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,7 @@ request here.
| `defaultPort` | `27017` |
| `schemaRefreshPattern` | `"operation"\s*:\s*"(insert\|delete\|update)` |
| `containerLevels` | one level, `{ id: 'schema', label: 'Database' }` — the object surface's container ([§6](#the-object-surface-789)) |
| `containerPathShapes` | `exact`: only `[database]` addresses a container, so a shorter or a longer path is refused, by the object routes over HTTP and by this provider for a caller that reaches it directly (#1147) |
| `objectKinds` | `collection` (relation, `acceptsRowWrites`) and `view` (relation). No `index`, no routine kind, no `timeseries` kind; each absence is measured in [§6](#what-is-not-declared-and-why-each-absence-is-a-measurement) |

`schemaRefreshPattern` matches write operations in the JSON query so the UI refreshes collections
Expand Down
Loading
Loading