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
19 changes: 17 additions & 2 deletions docs/API_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,8 @@ admin routes use.
"password": "secret"
},
"type": "vacuum",
"target": "users"
"target": "users",
"container": "app"
}
```

Expand All @@ -789,6 +790,13 @@ admin routes use.
| `connection` | object | Yes | Database connection configuration |
| `type` | string | Yes | Maintenance operation type |
| `target` | string | No | Target table name or PID (for kill). Also selects the *placement* the request is validated as: absent or empty means whole-database, any name means one object |
| `container` | string | No | The container the target lives in, as the row carries it in `schemaName`: the schema on PostgreSQL and SQL Server, the database on ClickHouse, the bucket on a document store. A non-string value (an object, a number, an array, `null`) returns `400`. Absent or empty means the request names no container and the provider falls back to its own reading of `target` |

`container` is what disambiguates a target whose namespace the name alone cannot settle:
`app.orders` and `public.orders` carry the same `target` and different `container` values, and the
provider qualifies with it rather than splitting the name. Engines with one attached namespace
(SQLite, libSQL, Trino's query-id `kill`) ignore it; each provider's own meaning is in
`docs/providers/<engine>.md`. The maintenance audit event records it beside `target`.

**Maintenance Types:**

Expand Down Expand Up @@ -833,7 +841,14 @@ admin routes use.

The handler validates against the target provider's capabilities: `type` is required (`{ "error": "Maintenance type is required" }`), the provider must support maintenance at all, and the requested operation must be in that provider's supported set (see the matrix above) — otherwise a `400` is returned listing what the provider does support.

A fourth `400` gates what the operation may be *pointed at*. Each provider declares that separately
`container` is type-checked before any provider is opened: a value that is neither absent nor a string
answers `{ "error": "\"container\" must be a string naming the target's container" }` with `400`.
Without this the value reached the provider's identifier escaper, where it failed as
`identifier.replace is not a function` and the caller read a `500` for a malformed request. An
empty string is not malformed: it reads as a request that named no container, the same way an empty
`target` reads as the whole-database form.

A fifth `400` gates what the operation may be *pointed at*. Each provider declares that separately
(`maintenanceOperationSpecs`, documented per engine under `docs/providers/`), and `target` selects
which half of the declaration this request is: absent or empty is a whole-database request, a name
is a per-object one. When the provider says that placement is not offered for this operation while
Expand Down
49 changes: 4 additions & 45 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ None of it is a GitHub issue.
**Sections**

- [SQL statement reading](#sql-statement-reading) — S2–S6 · 4
- [Drivers and connections](#drivers-and-connections) — D1-D129, U17 · 74
- [Drivers and connections](#drivers-and-connections) — D1-D129, U17 · 73
- [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-U54 · 42
Expand Down Expand Up @@ -516,47 +516,6 @@ current database, and that permission cannot be granted in `master`.
rather than published. Measured on a real instance with a login that has neither grant, because the
whole entry rests on a permission boundary no fixture can prove.

### D49. Per-table maintenance drops the schema, so every table outside the default one refuses

Found 2026-08-27 in the BROWSER while registering `duckdb` (issue #424). Not DuckDB's defect - the
provider is the half that behaves - and no gate could have caught it: the six local gates, 100%
line coverage and a four-lens adversarial review all passed over it, because the two halves are
correct in isolation and only the running product puts them together.

`TablesTab.tsx:390` calls `handleMaintenance(type, table.tableName)` - the BARE table name - from a
row whose very next line (`:350`) renders `table.schemaName` beside it. Every provider's
`qualifyMaintenanceTarget` then supplies a default schema for an unqualified target:
`postgres.ts:1287` returns `"public." + escapeIdentifier(target)`, and
`duckdb/index.ts:712` returns `"main"."<target>"`. So the statement names a table that is not there.

Measured on DuckDB v1.5.5, clicking **Analyze Table** on the `analytics.events` row:

```
Catalog Error: Table with name events does not exist! Did you mean "analytics.events"?
LINE 1: ANALYZE "main"."events"
```

`POST /api/db/maintenance` answers 400 and the panel prints the engine's message, so it is visible
rather than silent - but the button cannot succeed on any table outside the default schema, on any
engine. It went unnoticed because the fixtures the other engines are exercised with keep their
tables in the default schema; DuckDB is simply the first whose fixture carries a second one.

This is #U9 one layer up. #U9 was an operation DECLARED in the wrong placement (Oracle offered
`optimize` per table, and the target it sent was rejected); this is the right placement sending an
under-qualified target.

Deliberately not fixed in the provider PR that found it. The one-line repair - passing
`` `${table.schemaName}.${table.tableName}` `` - changes the target string reaching all TWELVE
providers that implement `runMaintenance` (postgres, mysql, mssql, oracle, sqlite, libsql, duckdb,
clickhouse, cassandra, druid, trino, search), and each has its own qualification and its own
statement grammar: SQLite has no user schemas, MySQL's `OPTIMIZE TABLE` takes `db.table`, and the
HTTP engines build their own paths. That is a twelve-engine live verification, not a provider
change.

**Done when:** the row passes the qualified name, every one of the twelve providers has been
measured against a table outside its default schema (or recorded as having no such concept), and a
component test pins the target the row sends so it cannot silently revert to the bare name.

### D51. Four providers degrade a refused monitoring read to no rows, then read the absent row as 0

Found 2026-08-27 in the #517 review, which asked whether the search provider really held the last
Expand Down Expand Up @@ -1779,13 +1738,13 @@ Not fixed there: the cache key is shared by every engine.

Found 2026-09-24 in the browser while verifying #843 (PR #1106), on a connection opened with `appdata` whose tree also lists `analytics`.
Both hold a collection called `events`.
The tree's **Validate Collection** on `analytics > events` opens `/admin/operations?path=analytics&path=events`, and that page lists the connected database's collections, `appdata . events` among them: `getTableStats()`, `getIndexStats()` and `runMaintenance()` in `src/lib/db/providers/document/mongodb.ts` all read `this.db`, the connected database, and `runMaintenance(type, target)` takes a bare collection name.
The tree's **Validate Collection** on `analytics > events` opens `/admin/operations?path=analytics&path=events`, and that page lists the connected database's collections, `appdata . events` among them: `getTableStats()`, `getIndexStats()` and `runMaintenance()` in `src/lib/db/providers/document/mongodb.ts` all read `this.db`, the connected database, and since #1091 `runMaintenance()` takes the row's container but refuses one that is not the connected database.
So the row a person presses for the collection they chose is the connected database's same-named collection, which is the shape #843 removed from the query path.

Not fixed in #1106, which is scoped to the statement grammar.
The target is a bare string in `runMaintenance(type, target)`'s contract for every provider, so passing a path is the D49 change, and the monitoring tabs are session-scoped on every engine.
Since #1091 the contract carries the container, `runMaintenance(type, target, container)`, so what is left is the provider running in a database other than the connected one, and the monitoring tabs, which are session-scoped on every engine.

**Done when:** a MongoDB maintenance target names its database, the deep link either opens the collection's own database or refuses a path outside the connected one, and a test pins that `Validate` on `analytics.events` reaches `analytics`.
**Done when:** the deep link either opens the collection's own database or refuses a path outside the connected one, and a test pins that `Validate` on `analytics.events` reaches `analytics`.

### D119. On RisingWave every column reads nullable, a `NOT NULL` column and a primary key included

Expand Down
2 changes: 1 addition & 1 deletion docs/DATABASE_PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ interface DatabaseProvider {
getHealth(): Promise<HealthInfo>;

// Maintenance operations
runMaintenance(type: MaintenanceType, target?: string): Promise<MaintenanceResult>;
runMaintenance(type: MaintenanceType, target?: string, container?: string): Promise<MaintenanceResult>;

// Validation
validate(): void;
Expand Down
32 changes: 25 additions & 7 deletions docs/providers/clickhouse.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,18 @@ ClickHouseProvider (clickhouse/index.ts)
Couchbase does — because the dialect really is standard on the points the shared helpers care
about: double-quoted identifiers and `LIMIT n OFFSET m` are both correct here, live-verified
(`SELECT "id" FROM "probe"` and the bare unquoted form both parse). This is exactly the case
[`docs/ADDING_A_PROVIDER.md`](../ADDING_A_PROVIDER.md) names ClickHouse for. Only `prepareQuery()`
is overridden, for the trailing-clause trap in [§3.8](#38-the-preparequery-override).
[`docs/ADDING_A_PROVIDER.md`](../ADDING_A_PROVIDER.md) names ClickHouse for. `escapeIdentifier()`
carries one dialect correction, below; `prepareQuery()` is overridden for the trailing-clause trap
in [§3.8](#38-the-preparequery-override).

### 2.3 What `SQLBaseProvider` gives for free

`ClickHouseProvider` reuses these inherited members rather than reimplementing them:
`ClickHouseProvider` reuses these inherited members rather than reimplementing them, except where
the table says otherwise:

| Member | Purpose |
|--------|---------|
| `escapeIdentifier()` | Double-quoted, since `this.type` (`clickhouse`) falls through to the default branch — the same quoting PostgreSQL uses. Both quoted and unquoted forms parse (live-verified) |
| `escapeIdentifier()` | **Overridden here.** Double-quoted, the same quoting PostgreSQL uses, plus a doubled BACKSLASH: the inherited form doubles only the quote character, and a backslash is an ESCAPE inside a quoted identifier on this engine, so a name ending in one swallowed its own closing quote (#1091 review). See [§8](#8-maintenance) |
| `buildLimitClause()` | `LIMIT n` / `LIMIT n OFFSET m` |
| `shouldEnableSSL()` | Inherited but **never called**, and deliberately so. It infers TLS from substrings in the host (`cloud`, `aws`, …), which would silently switch a self-hosted node whose hostname merely contains one of them. TLS here comes from the connection's own `ssl` config or from an `https://` scheme, never from a guess ([§4.3](#43-tls)) |
| `prepareQuery()` (base) | The shared query limiter; `ClickHouseProvider` calls it first and only overrides the trailing-clause case |
Expand Down Expand Up @@ -1264,8 +1266,10 @@ curl -s "http://127.0.0.1:8123/?user=libredb&password=$CH_PASSWORD&database=demo

### 6.3 Object edit (#789)

This engine is a REFUSAL, and the reason is that no measured escaper exists for its identifiers.
A backslash inside a quoted identifier is an ESCAPE in both the double-quote and the backtick form on 26.7.1.1315, and all three identifier quoters in this tree emit `"x\"` for the name `x\`, so the statement a plan would carry is not the statement the author addressed.
This engine is a REFUSAL, recorded because no escaper for its identifiers had been measured.
A backslash inside a quoted identifier is an ESCAPE in both the double-quote and the backtick form on 26.7.1.1315, and the shared quoters (the default branch of `SQLBaseProvider.escapeIdentifier`, `quoteIdentifier` in [`src/lib/sql/identifier.ts`](../../src/lib/sql/identifier.ts) and `escapeIdentifier` in [`pool-manager.ts`](../../src/lib/db/utils/pool-manager.ts)) emit `"x\"` for the name `x\`, so a statement built through any of them is not the statement the author addressed.
Since #1091 this provider's own `escapeIdentifier()` escapes the backslash as well, and it is measured ([§8](#8-maintenance)).
No edit statement is built through it and none has been measured, so the refusal stands.
One question here is UNMEASURED and is recorded as such rather than answered: whether a dictionary's credential is really redacted in the text the Phase 2 read returns.
No kind here declares `acceptsSourceEdits`, and `tests/isolated/object-edit-declarations.test.ts` is what holds that absence and this section together.

Expand Down Expand Up @@ -1300,10 +1304,15 @@ Two honest zeroes in the overview, so neither reads as a measurement:

## 8. Maintenance

`runMaintenance(type, target?)`
`runMaintenance(type, target?, container?)`
([`index.ts`](../../src/lib/db/providers/sql/clickhouse/index.ts)). `optimize` and `kill` **require**
a target; `analyze` does not.

A `container` is the DATABASE the row carries as `schemaName` (#772), used as the database outright:
`database.table` cannot be told apart from a name that contains a dot, while a container is already
the database on its own. Without one the old reading stands, splitting the name and falling back to
the pinned database.

| Type | ClickHouse action | Notes |
|------|--------------------|-------|
| `optimize` | `OPTIMIZE TABLE <db>.<table> FINAL` | Merges the table down to one part per partition and applies pending mutations — the operation a ClickHouse user reaches for where another engine would vacuum. A target is mandatory, because `OPTIMIZE` names a table |
Expand All @@ -1317,6 +1326,15 @@ Calling `runMaintenance` with one directly throws a `QueryError` naming the thre
operations. A target is qualified through
`escapeIdentifier()` (`"database"."table"`, defaulting the database to the pinned one when the
target names none), so a hostile or oddly-named table cannot break out of the generated statement.
That helper is OVERRIDDEN here rather than inherited, and the reason is the identifier escape
measured in [§6.3](#63-object-edit-789): a backslash inside a quoted identifier is an escape on this
engine, so the inherited form, which doubles only the quote character, left a name ending in one
with its closing quote swallowed and the rest of the statement reparsed around it. A container of
`x\` was the reachable case (#1091 review): the target that followed became more statement text
rather than a second segment. The override escapes the backslash first, the order `literal()` in
`objects.ts` uses.
Live-verified on 26.7.1.1315 against the fixture's own `` demo.`bs_one\` ``: the target `bs_one\`, the target `demo.bs_one\`, and the target `bs_one\` with the container `demo` all optimize it, where the inherited spelling answers `Double quoted string is not closed` on the same table.
The container `x\` with the target `.t FINAL SETTINGS optimize_throw_if_noop = 1 --` is refused with `UNKNOWN_DATABASE`, for a database named `x\`.

### Where each operation may be offered (`maintenanceOperationSpecs`)

Expand Down
11 changes: 9 additions & 2 deletions docs/providers/couchbase.md
Original file line number Diff line number Diff line change
Expand Up @@ -1114,13 +1114,20 @@ edge one. Omitted, the same panels render `N/A` / "Not measured" and score the c

## 8. Maintenance

`runMaintenance(type, target?)`
`runMaintenance(type, target?, container?)`
([`index.ts`](../../src/lib/db/providers/document/couchbase/index.ts)). All three operations
**require** a target.

A `container` is the row's `schemaName` (#772), and the keyspace it addresses is decided from it:
the bucket's own name (the only Tables row this provider has, `getTableStats()`) means the
bucket's default collection, so the row's Analyze button addresses `` `bucket`.`_default`.`_default` ``
rather than a scope that does not exist; any other container is the SCOPE the collection sits in,
used as one instead of being parsed back out of the display name. Without a container the
display-name rule stands: `scope.collection`, or the default scope for a bare name.

| Type | Couchbase action | Notes |
|------|------------------|-------|
| `analyze` | `UPDATE STATISTICS FOR <keyspace> INDEX ALL` | **Enterprise Edition only.** A Community cluster answers "'Update Statistics' is an enterprise level feature." — returned verbatim as a failed result, not swallowed or reworded |
| `analyze` | `UPDATE STATISTICS FOR <keyspace> INDEX ALL` | **Enterprise Edition only.** A Community cluster answers "'Update Statistics' is an enterprise level feature.", returned verbatim as a failed result, not swallowed or reworded. The success reply names the same keyspace the statement addressed (``Updated statistics for `travel`.`inventory`.`hotel` ``), so a row whose target is the bucket cannot report as if the bucket itself had been touched (#1091 review) |
| `reindex` | `BUILD INDEX ON <keyspace>(...)` over the keyspace's deferred indexes | Reports "No deferred indexes on X" when there are none |
| `kill` | `DELETE FROM system:active_requests WHERE requestId = $1` | Target is the request id shown in active sessions |

Expand Down
6 changes: 6 additions & 0 deletions docs/providers/duckdb.md
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,12 @@ per-entity control would fail at the point the user clicked it. `runMaintenance`
withheld types **here**, naming the reason, rather than sending a statement the engine will reject
with wording about a keyword the user never typed.

`runMaintenance(type, target?, container?)` takes a `container` as the SCHEMA the row carries as
`schemaName` (#772), the same reading PostgreSQL uses: the schema is quoted whole and prefixed to
the quoted table name, and never recovered by splitting the target - a schema is allowed to contain
a dot. Without one the old readings stand: `schema.table` is quoted part by part and a bare name
falls back to `main`.

---

## 9. Capabilities & labels
Expand Down
3 changes: 3 additions & 0 deletions docs/providers/libsql.md
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,9 @@ Measured through the provider against both deployments (fixture: 2 tables, 3 and
| `check` | globally | `PRAGMA integrity_check`, and the ANSWER is read — a corrupt database reports damage in its row while the statement itself succeeds |
| `vacuum`, `analyze`, `optimize`, `kill` | withheld | Refused by the server (§3.5); a direct API call is refused by the provider with the reason |

A `container` is deliberately ignored (#772): a libSQL connection resolves names against its one
attached database, exactly as `sqlite.ts` does.

---

## 9. Capabilities & labels
Expand Down
8 changes: 7 additions & 1 deletion docs/providers/mongodb.md
Original file line number Diff line number Diff line change
Expand Up @@ -913,9 +913,15 @@ something was measured:

## 8. Maintenance

`runMaintenance(type, target?)` ([`mongodb.ts`](../../src/lib/db/providers/document/mongodb.ts))
`runMaintenance(type, target?, container?)` ([`mongodb.ts`](../../src/lib/db/providers/document/mongodb.ts))
maps the generic operations onto MongoDB admin commands:

A `container` is a DATABASE name (#772). The provider is bound to one database and no admin command
can retarget mid-command, so the bound name is accepted and any OTHER name is refused with
`bound to the database "<name>"` rather than quietly acted on against the wrong one. The comparison
uses `getDatabaseName()`, the name `connect()` opened - a connection-string connection sets no
`config.database`, and comparing with that alone refused the bound database itself.

| Type | MongoDB action |
|------|----------------|
| `analyze` | `validate` (one collection, or every collection) |
Expand Down
6 changes: 4 additions & 2 deletions docs/providers/mssql.md
Original file line number Diff line number Diff line change
Expand Up @@ -1251,8 +1251,10 @@ boundary preserves those states without a falsy test that would erase a genuine

## 9. Maintenance

`runMaintenance(type, target?)` ([`mssql.ts`](../../src/lib/db/providers/sql/mssql.ts)); targets
are bracket-escaped (`]` → `]]`):
`runMaintenance(type, target?, container?)` ([`mssql.ts`](../../src/lib/db/providers/sql/mssql.ts)); targets
are bracket-escaped (`]` → `]]`). A `container` is the SCHEMA the row carries as `schemaName`
(#772), emitted as `[schema].[table]`; without one a bare target keeps the previous reading, where
the connected default schema applies.

| Type | With target | Without target |
|------|-------------|----------------|
Expand Down
Loading
Loading