Skip to content

feat(db): add Couchbase provider (SQL++ over Query REST, no native dependency) - #263

Merged
cevheri merged 6 commits into
mainfrom
feat/262-couchbase-provider
Aug 3, 2026
Merged

feat(db): add Couchbase provider (SQL++ over Query REST, no native dependency)#263
cevheri merged 6 commits into
mainfrom
feat/262-couchbase-provider

Conversation

@cevheri

@cevheri cevheri commented Aug 3, 2026

Copy link
Copy Markdown
Member

Closes #262.

Adds Couchbase as the ninth database provider, speaking SQL++ over the documented Query Service and
management REST APIs. The provider adds no runtime dependency of any kindpackage.json is
untouched, so the Docker image, Snap, AppImage, Flatpak, deb/rpm and the @libredb/studio npm
package that libredb-platform consumes all stay exactly the same size.

Why no SDK

The official couchbase SDK is 64.6 MB unpacked across 3765 files, depends on cmake-js and
node-addon-api, and runs a postinstall step that downloads a prebuilt binary or compiles from
source. The capability it would buy turned out to be very small: USE KEYS reads a document by key
with no index, ACID transactions work over REST, and — verified live on 8.0.2 — the Query Service
performs a sequential scan for un-indexed collections, so even KV range scan is not a gap on Server
7.6+. Only non-JSON documents and subdocument operations remain SDK-only.

HTTP is also the more deployable choice in enterprise environments: nothing to download during an
air-gapped install, REST traverses corporate HTTP proxies while the binary KV protocol on 11210
does not, site firewall policy commonly opens 8091/18093 and not 11210, and container images need
no native module or glibc/musl matching.

The transport is still a seam (CouchbaseTransport) with one implementation, so adopting the SDK
later is a new file rather than a rewrite. A guard test keeps the REST envelope from leaking out of
http-transport.ts, which is what makes that promise hold.

What it does

  • SQL++ as a first-class SQL dialect. queryLanguage: "sql" inherits Monaco highlighting, the
    shared query limiter, the sql tab type, NL2SQL and saved queries with no extra code.
  • Keyspace mapping follows the PostgreSQL rule. The bucket is pinned by the connection; a
    collection in _default shows as airline, anything else as inventory.hotel — the same rule as
    postgres.ts:818.
  • Ports are discovered, not configured. Only the management port is stored; query ports come
    from /pools/default/nodeServices, including alternateAddresses for NAT and Docker. Capella SRV
    endpoints resolve via dns.promises.resolveSrv with an A-record fallback.
  • Schema from system:* plus INFER, at a bounded concurrency, where a collection the user
    cannot read yields empty columns instead of failing the whole tree.
  • EXPLAIN via a new couchbase-json strategy reusing the existing tree render model. analyze
    returns null rather than silently degrading to an estimate, matching Explain on a non-SELECT statement executes it with the safety check bypassed #201.
  • Monitoring degrades to empty, never throws — the monitoring catalogs need the Query System
    Catalog RBAC role, so a denial is the normal case for a restricted user.
  • Maintenance maps to UPDATE STATISTICS, BUILD INDEX and killing a request; the
    Enterprise-only refusal of UPDATE STATISTICS is surfaced verbatim rather than hidden.

Non-obvious behaviours handled

Each of these silently produces wrong output if missed, and each has a test:

  • A 200 response can carry status: "errors". The payload is checked before the HTTP code, or a
    syntax error reads as "0 rows".
  • SELECT * nests documents under the keyspace name and omits the key, so generated statements
    alias and project META(d).id.
  • A wildcard in the signature does not describe the rows. { id, "*" } would name a literal *
    column and hide every expanded field, so any wildcard falls back to deriving fields from rows.
  • The query context is pinned to the bucket. SQL++ reads a bare inventory.hotel as
    bucket.collection, so without query_context the generated statement dies with "Ambiguous
    reference to field 'inventory'".
  • bucket, scope and using are reserved words in the system catalogs and must be
    backtick-quoted.
  • scan_consistency defaults to request_plus so a user always sees their own writes. The
    default not_bounded returned zero rows immediately after an INSERT in testing.

Verification

Six local gates green: format, lint (0 errors), typecheck, knip, test, build. Coverage
is at the required 100%.

Verified end to end against a real Couchbase Server 8.0.2 Community node driving the running
application, not mocks: full INSERT / UPDATE / SELECT / DELETE through both the query API and the
browser UI, read-your-writes, the un-indexed collection, both error paths, schema introspection with
INFER-derived column types, the Explain tree including the Parallel operator's nested ~child,
every monitoring surface, and all three maintenance operations.

That pass found three defects, each fixed with a test first: the wildcard signature, the missing
query context, and a documentation claim that un-indexed collections cannot be listed.

Not in this PR

The SDK and what it gates (non-JSON documents, subdocument operations); Analytics, Full-Text Search
and Eventing; Capella management APIs; multi-bucket browsing from one connection. Chart and operator
bundle versions are untouched — those gates trigger on a package.json version change, which
belongs to a separate release step.

cevheri added 4 commits August 3, 2026 04:15
Implements the Couchbase provider as a directory under providers/document,
speaking SQL++ over the documented Query Service and management REST APIs.
The provider adds no runtime dependency: package.json is untouched, so every
distribution channel keeps its current size.

The transport is a seam with one HTTP implementation, so adopting the official
SDK later is a new file rather than a rewrite. Its result type is deliberately
neutral instead of the REST envelope, and a guard test keeps envelope
identifiers from leaking outside http-transport.ts.

Behaviours that silently produce wrong output if missed, each covered by a test:

- A 200 response can carry a payload status of "errors", so the payload is
  inspected before the HTTP code. Skipping this reports a syntax error as
  "0 rows".
- SELECT * nests each document under the keyspace name and omits the document
  key, so statements alias and project META().id.
- A wildcard in the signature does not describe the rows: taking those keys
  verbatim would name a literal "*" column and hide every expanded field, so
  any wildcard falls back to deriving fields from the rows.
- The query context is pinned to the connection's bucket. SQL++ reads a bare
  two-part name as bucket.collection, so without it the displayed keyspace
  name resolves to a non-existent bucket.
- bucket, scope and using are reserved words in the system catalogs and are
  backtick-quoted everywhere.
- scan_consistency defaults to request_plus so a user always sees their own
  writes; the server default returned zero rows immediately after an insert.

Schema comes from system:keyspaces and system:scopes with columns inferred by
INFER at a bounded concurrency, where a collection the user cannot read yields
empty columns rather than failing the whole tree. Monitoring degrades to empty
on a denial, because the monitoring catalogs require an RBAC role most users
lack. EXPLAIN reuses the existing tree render model; analyze returns null
rather than degrading to an estimate, matching the behaviour set in #201.
…tors (#262)

Adds the union member and every exhaustive map that depends on it. The
Record<DatabaseType, ...> in db-ui-config and the PICKER_COVERAGE map in the
connection-form test are both exhaustive by design, so the compiler refuses to
build until each is updated - they are the checklist for this commit.

The connection form labels the database field Bucket, since only the management
port is configured and query ports are discovered from the cluster. Connection
strings accept the couchbase and couchbases schemes, including Capella SRV
endpoints that carry no port or path.

The generated statement aliases the keyspace and projects META().id as __id.
Without the alias SELECT * would nest each document under the keyspace name,
and without the projection the document key would never appear in a result set
at all.
Adds docs/providers/couchbase.md following the structure of the sibling
provider references, and lists Couchbase everywhere the other providers are
enumerated. This repo treats the provider triad as an invariant: code, docs and
tests move together.

The limitations section is written from live verification against Server 8.0.2
Community rather than from the design assumptions. Two corrections came out of
that: an un-indexed collection IS readable from Server 7.6 onward through a
sequential scan backed by a KV range scan, so error 4000 is an older-server
fallback rather than the headline behaviour, and the capability originally
listed as SDK-only for that reason is not a gap on current servers. What
remains genuinely SDK-only is non-JSON documents and subdocument operations.

The compose service pins the community image and its init sidecar creates the
bucket with the couchstore storage backend and zero replicas, because Community
Edition rejects the Magma default that couchbase-cli would otherwise pick and
a single node cannot satisfy the default replica count.
The default Array.prototype.sort compares UTF-16 code units, not alphabetical
order. The observed types here are lowercase ASCII so the rendered union is
unchanged today, but the comparator matches what the MongoDB provider already
does at mongodb.ts:525 - which is what the function comment claims - and makes
the ordering correct for any type name that is not plain lowercase ASCII.

Clears the SonarCloud reliability gate on this PR (typescript:S2871).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Couchbase as a first-class SQL++ database provider using REST APIs without new runtime dependencies.

Changes:

  • Registers Couchbase across provider, UI, seed, URI, and query-generation layers.
  • Implements HTTP transport, schema inference, monitoring, maintenance, and visual EXPLAIN.
  • Adds extensive tests, documentation, and local Docker Compose support.

Reviewed changes

Copilot reviewed 39 out of 39 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/lib/types.ts Adds the Couchbase database type.
src/lib/seed/types.ts Allows Couchbase seed connections.
src/lib/query-generators.ts Generates quoted Couchbase SQL++.
src/lib/explain/index.ts Registers Couchbase EXPLAIN.
src/lib/explain/couchbase-json.ts Parses Couchbase plan trees.
src/lib/db/types.ts Adds Couchbase explain format.
src/lib/db/providers/document/couchbase/transport.ts Defines the transport contract.
src/lib/db/providers/document/couchbase/keyspace.ts Maps and quotes keyspaces.
src/lib/db/providers/document/couchbase/introspect.ts Implements schema introspection.
src/lib/db/providers/document/couchbase/index.ts Implements the provider.
src/lib/db/providers/document/couchbase/http-transport.ts Implements REST and TLS transport.
src/lib/db/factory.ts Registers provider construction.
src/lib/db-ui-config.ts Adds Couchbase UI metadata.
src/lib/connection-string-parser.ts Parses Couchbase URIs.
src/hooks/use-connection-form.ts Adds Couchbase form behavior.
src/components/icons/db-icons.tsx Adds a Couchbase icon.
src/components/ConnectionModal.tsx Adds bucket labels and URI hints.
tests/unit/seed/types.test.ts Tests seed type acceptance.
tests/unit/lib/query-generators.test.ts Tests SQL++ generation.
tests/unit/lib/explain/couchbase-json.test.ts Tests plan parsing.
tests/unit/lib/db-ui-config.test.ts Tests Couchbase UI configuration.
tests/unit/lib/db-icons.test.tsx Tests icon rendering.
tests/unit/lib/connection-string-parser.test.ts Tests Couchbase URI parsing.
tests/unit/db/factory.test.ts Tests provider creation.
tests/unit/db/couchbase/seam-guard.test.ts Guards the transport boundary.
tests/unit/db/couchbase/keyspace.test.ts Tests keyspace mapping.
tests/unit/db/couchbase/introspect.test.ts Tests schema inference.
tests/unit/db/couchbase/http-transport.test.ts Tests REST, discovery, and TLS.
tests/integration/db/couchbase-provider.test.ts Covers provider behavior end to end.
tests/hooks/use-connection-form.test.ts Tests Couchbase form state.
tests/components/ConnectionModal.test.tsx Tests Couchbase-specific labels.
README.md Adds Couchbase user-facing references.
docs/providers/README.md Adds Couchbase to the provider index.
docs/FEATURES.md Documents Couchbase capabilities.
docs/DATABASE_PROVIDERS.md Documents provider architecture.
docs/ARCHITECTURE.md Updates architecture diagrams and counts.
docs/API_DOCS.md Documents Couchbase API usage.
database-compose.yml Adds local Couchbase services.
Suppressed comments (2)

src/lib/db/providers/document/couchbase/index.ts:371

  • Connection-string mode is not actually usable for Couchbase. The form deliberately clears host, port, user, and password when it stores a URI, but this normalization restores only the hostname. As a result, URI credentials are sent as Basic Og==, a bucket in the URI is not available to validate(), and couchbases:// still uses plaintext unless SSL is configured separately. Normalize the full URI (credentials, bucket, secure scheme, and management-port policy) before validation and transport construction.
  private transportConfig(): DatabaseConnection {
    if (this.config.host) return this.config;
    const host = this.hostFromConnectionString();
    return host ? { ...this.config, host } : this.config;

src/lib/db/providers/document/couchbase/http-transport.ts:361

  • The TLS default is unreachable from the normal host/port form: selecting Couchbase preloads port 8091 and buildConnection() always serializes that numeric port, so enabling SSL makes this expression keep 8091 and sends HTTPS to the plaintext management port. The tests only cover port: undefined. Track whether the port was explicitly overridden, or update the untouched Couchbase default to 18091 when TLS is enabled.
    this.managementPort = config.port ?? (this.tls ? DEFAULT_MANAGEMENT_TLS_PORT : DEFAULT_MANAGEMENT_PORT);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +214 to +215
rows: Array.isArray(record.results) ? (record.results as CouchbaseRow[]) : [],
fieldNames: fieldNamesFromSignature(record.signature),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reproduced against a live cluster before fixing. Worse than described: SELECT RAW h.name produced fields: ["0","1",...,"13"] (one column per character index of the longest string), and SELECT RAW h.nonexistent returned HTTP 500 "Cannot convert undefined or null to object".

Fixed with your second suggestion - converting at the provider boundary, not in the transport. The transport cast has to stay, because INFER returns its flavour array as rows[0] and introspect.ts:294 reads that raw payload; reshaping in the transport would break schema loading. toQueryResult now maps every row through normalizeRow, wrapping anything that is not a plain object into a single __value column, named after the existing __id convention.

Verified live after the fix: [{"__value":"Grand Plaza"},...] with fields: ["__value"], and the null projection returns rows instead of a 500. Commit d438c57.

Comment thread src/lib/seed/types.ts
Comment on lines +22 to +32
const SeedDatabaseType = z.enum([
"postgres",
"mysql",
"sqlite",
"mongodb",
"redis",
"oracle",
"mssql",
"libredb",
"couchbase",
]);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed - docs/SEED_CONNECTIONS.md:103 had no couchbase, so the seed type this PR adds was undiscoverable from its own reference document. Added. Commit 7a02ccc.

Comment thread README.md Outdated
| `connections[].id` | Yes | Unique slug (`[a-z0-9-]+`, max 64 chars) |
| `connections[].name` | Yes | Display name in UI |
| `connections[].type` | Yes | `postgres`, `mysql`, `sqlite`, `mongodb`, `redis`, `oracle`, `mssql` |
| `connections[].type` | Yes | `postgres`, `mysql`, `sqlite`, `mongodb`, `couchbase`, `redis`, `oracle`, `mssql` |

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. libredb was missing here before this PR, but the line is one this PR edits and it claims to enumerate the union, so it is in scope. Added, and reordered to match the order in src/lib/types.ts so the two stay comparable line by line. Commit 7a02ccc.

Comment thread docs/API_DOCS.md Outdated
}

type DatabaseType = 'postgres' | 'mysql' | 'sqlite' | 'mongodb' | 'redis' | 'oracle' | 'mssql';
type DatabaseType = 'postgres' | 'mysql' | 'sqlite' | 'mongodb' | 'couchbase' | 'redis' | 'oracle' | 'mssql';

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. Added libredb and matched the union order from src/lib/types.ts, so the documented type is now character-for-character comparable with the real one. Commit 7a02ccc.

Comment thread docs/ARCHITECTURE.md Outdated
## System Overview

LibreDB Studio is a hybrid, cloud-native database management tool that provides an IDE-like experience in the browser. It supports **7 database backends** via a Strategy Pattern abstraction: PostgreSQL, MySQL, SQLite, Oracle, SQL Server, MongoDB, Redis.
LibreDB Studio is a hybrid, cloud-native database management tool that provides an IDE-like experience in the browser. It supports **8 database backends** via a Strategy Pattern abstraction: PostgreSQL, MySQL, SQLite, Oracle, SQL Server, MongoDB, Couchbase, Redis.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed - this PR updated the list but left the count at eight and still omitted LibreDB. Now reads nine and includes it. Commit 7a02ccc.

Comment on lines +279 to +280
supportsExplain: true,
explainFormat: "couchbase-json",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and this was the more damaging of the two. use-query-execution.ts:165 always builds the direct action with mode analyze, so the strategy returning null meant every click hit the refusal path while only the background pre-warm (:231, mode estimate) worked. My own end-to-end pass missed it because I saw the Explain tab populated by that pre-warm and concluded the button worked.

The design decision behind the null was mine and it diverged from the established precedent: sqlite-queryplan.ts ignores the mode entirely and returns EXPLAIN QUERY PLAN for both, for exactly the same reason (SQLite has no EXPLAIN ANALYZE either). Declining the mode does not narrow the feature, it disables it - the #194 dead-button defect.

Both modes now return the estimate; a non-SELECT is still declined in both. Re-verified in the browser on a never-run statement so no pre-warmed plan could mask it: plan tree renders, no refusal toast. Commit d438c57.

cevheri added 2 commits August 3, 2026 08:22
…ion (#262)

Two defects found in review, both reproduced against a live cluster first.

SELECT RAW and SELECT VALUE project bare values, so a row can be a scalar, an
array or null rather than the object the grid's row contract assumes. Passed
through unchanged, deriveFields called Object.keys on a string and produced one
column per character index, and on a null row it threw outright - a RAW
projection of a missing field returned HTTP 500 "Cannot convert undefined or
null to object". Anything that is not a plain object is now wrapped in a single
__value column.

The normalization lives at the provider boundary rather than in the transport
on purpose: INFER returns its flavour array as rows[0] and introspection reads
that raw payload, so reshaping in the transport would break schema loading.

The direct Explain action was dead. use-query-execution.ts:165 always builds it
with mode "analyze", and the strategy returned null for that mode, so every
click was refused with "Only SELECT statements can be explained" while only the
background pre-warm worked. SQL++ has no EXPLAIN ANALYZE, but declining the
mode disables the feature rather than narrowing it. Both modes now return the
estimate, matching sqlite-queryplan.ts, which ignores the mode for exactly the
same reason. A non-SELECT is still declined in both modes.

Verified live: the RAW projections return one honest column instead of crashing,
and clicking Explain on a never-run statement renders the plan tree with no
refusal toast.
Four enumerations of DatabaseType had drifted from src/lib/types.ts, three of
them on lines this PR already touched:

- SEED_CONNECTIONS.md omitted couchbase entirely, so the seed type this PR adds
  was undiscoverable from its own reference document.
- README.md and API_DOCS.md list the union but omit libredb, an omission that
  predates this PR and that adding couchbase beside it made load-bearing.
- ARCHITECTURE.md still claimed eight backends and left LibreDB out of the list;
  there are nine.

All four now match the union's own order, which appends new members rather than
inserting them, so the docs and the type stay comparable line by line.
@sonarqubecloud

sonarqubecloud Bot commented Aug 3, 2026

Copy link
Copy Markdown

@cevheri
cevheri merged commit 5135a9c into main Aug 3, 2026
18 checks passed
@cevheri
cevheri deleted the feat/262-couchbase-provider branch August 3, 2026 05:35
cevheri added a commit that referenced this pull request Aug 3, 2026
Nine review findings, all verified against the code before changing anything.
Four were inherited from the guide's previous home and one was a factual error
this repo has been shipping since #263.

The consequential one: the guide claimed SQLBaseProvider "owns pooled-driver
mechanics a stateless HTTP transport does not have". It does not. sql-base.ts is
153 lines of pure SQL text helpers keyed off this.type - escaping, LIMIT clause
building, placeholder style, read-only detection - plus a prepareQuery() that
applies the shared query limiter. Nothing in it touches a pool, a driver or a
connection. As written, the guide would have told the ClickHouse provider (#264)
to reimplement all of it for no reason. The real criterion is whether the
dialect fits the helpers, which is why Couchbase does not use it: SQL++ needs
doubled backticks that escapeIdentifier() emits for no existing type. The same
false sentence in providers/couchbase.md is corrected too, along with its claim
that transactions and cancelQuery live in that base - they do not.

Related: the rubric promised that queryLanguage: "sql" inherits the shared query
limiter. It does not. BaseDatabaseProvider.prepareQuery() is a pass-through;
Couchbase overrides it explicitly to get limiting back.

The rest:

- The Strategy Pattern guarantee of "no changes to routes, components or
  existing providers" is contradicted by the worked example, which touched the
  union, the exhaustive UI maps, the factory, the connection-string parser, the
  query generators and the explain registry. It now says what the pattern
  actually buys: provider logic stays self-contained, registration does not.
- The capabilities reference omitted explainFormat, which is required whenever
  supportsExplain is true. Omitting it leaves a visible, dead Explain control -
  the exact defect found in review on #263.
- The connection-picker example silently dropped sqlite. Copying it as
  instructed would have removed SQLite from the UI.
- Verification listed two checks; the repo contract is six gates plus the
  coverage gate for executable changes.
- "Return [] from monitoring methods that don't apply" does not type-check for
  getOverview() and getPerformanceMetrics(), which return DTOs.
- The transport's neutral result type declared rows as Record<string, unknown>[]
  while the traps section explains that wire rows can be scalars, arrays or
  null. It is unknown[] now, with the narrowing at the provider boundary, and
  the note that the Couchbase transport still declares the narrower type.
- The seam's kind discriminator was the literal "http", which would have stopped
  the future native adapter the seam exists for from naming itself.
- The prerequisites intro said the third decision was the consequential one
  after the driver decision had been moved to first.
cevheri added a commit that referenced this pull request Aug 3, 2026
* docs: split provider reference from the add-a-provider guide

DATABASE_PROVIDERS.md was doing two jobs: describing the architecture and the
providers that ship today, and walking through how to add a new one. Those are
read by different people at different times, so the how-to moves to its own
document and the reference keeps only what exists.

The moved guide also gains the material the Couchbase provider produced, which
had nowhere to live before:

- A rubric for the decision that now comes first - does this database need a
  driver at all? Seven criteria, each failure being code you hand-write.
- The transport seam: one interface, one implementation, a neutral result type
  rather than the wire envelope, and a guard test so the boundary actually holds.
- The failure modes specific to HTTP databases. Each of these silently produces
  wrong output and each was found against a real server, not a mock: a 200 that
  carries an error, an envelope that does not describe the rows, rows that are
  not objects, implicit name resolution, a consistency default that is not
  read-your-writes, differing pagination models, and the hard edge of having no
  session.
- The requirement to verify against a real server before opening a PR, with the
  specific checks that caught real defects on #263.
- The candidate list, assessed against the rubric, so the next provider starts
  from evidence rather than from a guess.

Also adds the roadmap phases this leaves standing: Couchbase as the first
driver-free provider, ClickHouse (#264) and Druid (#265) next, and Trino marked
deliberately unscheduled with the product question that blocks it written down.

* docs: correct the provider guide after review

Nine review findings, all verified against the code before changing anything.
Four were inherited from the guide's previous home and one was a factual error
this repo has been shipping since #263.

The consequential one: the guide claimed SQLBaseProvider "owns pooled-driver
mechanics a stateless HTTP transport does not have". It does not. sql-base.ts is
153 lines of pure SQL text helpers keyed off this.type - escaping, LIMIT clause
building, placeholder style, read-only detection - plus a prepareQuery() that
applies the shared query limiter. Nothing in it touches a pool, a driver or a
connection. As written, the guide would have told the ClickHouse provider (#264)
to reimplement all of it for no reason. The real criterion is whether the
dialect fits the helpers, which is why Couchbase does not use it: SQL++ needs
doubled backticks that escapeIdentifier() emits for no existing type. The same
false sentence in providers/couchbase.md is corrected too, along with its claim
that transactions and cancelQuery live in that base - they do not.

Related: the rubric promised that queryLanguage: "sql" inherits the shared query
limiter. It does not. BaseDatabaseProvider.prepareQuery() is a pass-through;
Couchbase overrides it explicitly to get limiting back.

The rest:

- The Strategy Pattern guarantee of "no changes to routes, components or
  existing providers" is contradicted by the worked example, which touched the
  union, the exhaustive UI maps, the factory, the connection-string parser, the
  query generators and the explain registry. It now says what the pattern
  actually buys: provider logic stays self-contained, registration does not.
- The capabilities reference omitted explainFormat, which is required whenever
  supportsExplain is true. Omitting it leaves a visible, dead Explain control -
  the exact defect found in review on #263.
- The connection-picker example silently dropped sqlite. Copying it as
  instructed would have removed SQLite from the UI.
- Verification listed two checks; the repo contract is six gates plus the
  coverage gate for executable changes.
- "Return [] from monitoring methods that don't apply" does not type-check for
  getOverview() and getPerformanceMetrics(), which return DTOs.
- The transport's neutral result type declared rows as Record<string, unknown>[]
  while the traps section explains that wire rows can be scalars, arrays or
  null. It is unknown[] now, with the narrowing at the provider boundary, and
  the note that the Couchbase transport still declares the narrower type.
- The seam's kind discriminator was the literal "http", which would have stopped
  the future native adapter the seam exists for from naming itself.
- The prerequisites intro said the third decision was the consequential one
  after the driver decision had been moved to first.
cevheri added a commit that referenced this pull request Aug 3, 2026
…two providers actually touched

The checklist listed six integration points and asserted "no other files should
need changes", while Couchbase (#263) and ClickHouse (#264) each touched 27 files
under src/ and tests/. That gap is not cosmetic: the ExplainFormat union in
src/lib/db/types.ts was absent from the list, and its registry is exhaustive, so
omitting it fails the build in a way the list gave no warning about.

Splits the list into always-required and conditional, names the exhaustive-map
tests that are the real checklist, and points at `git grep -l <previous-type-id>`
as authoritative over a hand-maintained list. Keeps the Strategy Pattern claim but
scopes it to provider logic, which is what it actually spares you, rather than to
registration, which it does not.
cevheri added a commit that referenced this pull request Aug 3, 2026
…tive dependency) (#270)

* feat(db): add ClickHouse provider (SQL over the HTTP interface, no native dependency) (#264)

Implements ClickHouse as a directory under providers/sql, speaking SQL over the
documented HTTP interface on port 8123. The provider adds no runtime dependency:
package.json is untouched, so every distribution channel keeps its current size.

Unlike Couchbase it extends SQLBaseProvider rather than BaseDatabaseProvider, which
is the case ADDING_A_PROVIDER.md names ClickHouse for: double-quoted identifiers and
LIMIT n OFFSET m are both correct here, so identifier quoting and the shared query
limiter are inherited instead of reimplemented. The transport is a seam with one HTTP
implementation, its result type is deliberately neutral rather than the JSON envelope,
and a guard test keeps envelope identifiers from leaking outside http-transport.ts.

Behaviours verified against a live 26.7.1 server, each of which silently produces
wrong output if missed, and each covered by a test:

- Errors use real HTTP status codes, so failure is detected by status - the opposite
  of Couchbase's rule. But ACCESS_DENIED arrives as 500, not 403, so failures are
  CLASSIFIED by the numeric exception code; the 497 message says "Not enough
  privileges" and contains neither "access denied" nor "permission denied", so
  sniffing the message text would miss every real denial.
- Error bodies are plain text even under an application/json content type, so an
  error body is never handed to JSON.parse.
- 64-bit integers arrive unquoted by default and JSON.parse silently rounds them
  (18446744073709551615 -> ...552000). The transport always sends
  output_format_json_quote_64bit_integers=1, matching pg's int8-as-string behaviour.
- A statement that fails after output has started answers 200 with a truncated body
  and the real exception in an __exception__ trailer, keyed on the per-request
  X-ClickHouse-Exception-Tag. The check runs before the format branch and before
  parsing, because the fence is format-independent: checking it only for JSON
  reported 805000 lost rows as a successful short result. The buffered variant is
  different again - 500, no fence, the partial body ahead of the exception - so the
  error path trims that prefix off the message.
- FORMAT and SETTINGS are trailing clauses and the inherited limiter appends LIMIT at
  the very end, which is a hard syntax error after either. prepareQuery refuses to
  rewrite such a statement.
- Writes answer 200 with an empty body; counts come from X-ClickHouse-Summary, whose
  values are strings. For ALTER TABLE ... UPDATE and lightweight DELETE the server
  reports zero even on success, and that is what the provider reports.
- Multi-statement is rejected by the server itself, so no client-side splitting.
- supportsCreateTable is false, live-disproving the issue's own guess: ENGINE and
  ORDER BY turned out to have working defaults, but CreateTableModal's default output
  emits SERIAL (UNKNOWN_TYPE) and UNIQUE (SYNTAX_ERROR).
- A pasted https:// URL carries sslMode so it cannot become a plaintext POST to the
  TLS port, which is the common ClickHouse Cloud case.

Schema comes from system.tables, system.columns and system.data_skipping_indices,
each filtered by database and each degrading to empty on its own denial - only
data_skipping_indices needs a separate grant. Nullable total_rows/total_bytes are
reported as unknown rather than zero, so a view does not claim to have no rows.
Column types are the declared strings verbatim. Monitoring reads system.query_log,
system.processes, system.metrics and system.parts and degrades to empty or zeroed
panels, with the overview split into five reads so a restricted user keeps the panels
it can still see. EXPLAIN reuses the existing tree render model and returns the same
estimate for both modes, since ClickHouse EXPLAIN never executes.

Tri-sync per CLAUDE.md: provider code, docs/providers/clickhouse.md and
tests/integration/db/clickhouse-provider.test.ts land together. Two shared features
that generate SQL - inline row editing and the schema-diff migration generator - are
not dialect-aware and are documented as limitations, tracked in #269 rather than
fixed here, since both are pre-existing and neither lives in this provider.

* docs: correct the add-a-provider registration checklist against what two providers actually touched

The checklist listed six integration points and asserted "no other files should
need changes", while Couchbase (#263) and ClickHouse (#264) each touched 27 files
under src/ and tests/. That gap is not cosmetic: the ExplainFormat union in
src/lib/db/types.ts was absent from the list, and its registry is exhaustive, so
omitting it fails the build in a way the list gave no warning about.

Splits the list into always-required and conditional, names the exhaustive-map
tests that are the real checklist, and points at `git grep -l <previous-type-id>`
as authoritative over a hand-maintained list. Keeps the Strategy Pattern claim but
scopes it to provider logic, which is what it actually spares you, rather than to
registration, which it does not.

* test(db): pin backslash escaping in ClickHouse literals, and state the overview's honest zeroes

Review of PR #270 noted that literal() escapes both the backslash and the quote but
only the quote had a test. The escaping was already correct - live-verified on 26.7.1
that `name = 'back\\slash'` matches a table genuinely named `back\slash` - so this
adds the missing tests rather than changing behaviour, including the combined
backslash-then-quote case that a quote-only escape would let close the literal.

Also documents two overview values that are structurally zero rather than measured:
maxConnections where system.server_settings is unavailable, and index scan counts,
which ClickHouse exposes nowhere the HTTP interface can reach.

* fix(db): bound ClickHouse requests client-side, let a URL scheme override TLS, and strip FORMAT from EXPLAIN

Four defects from the PR #270 review, each live-reproduced before fixing.

The advertised query timeout did not cover the transport. max_execution_time only
starts counting once ClickHouse has accepted a statement, so a stalled DNS lookup,
connect or TLS handshake - or a response body that stopped arriving - waited on the
runtime's own unrelated timeout instead. The seam gains a neutral timeoutMs (any
implementation can honour a deadline) and the HTTP transport arms it as an
AbortSignal covering the body read as well, since headers can arrive promptly and
the stream stall afterwards. Every call site passes one.

A URL scheme could not turn TLS off. Both the pasted path and the hand-typed
connection string only ever ADDED TLS for https://, so an explicit http:// URL
deferred to whatever ssl setting the form still carried from an earlier edit and
sent HTTPS to a plaintext endpoint, failing with a bare "fetch failed". The scheme
is the more specific statement of intent, so http:// now disables TLS and https://
requires it, while the scheme-neutral clickhouse:// remains the one form that
defers.

The explain builder disagreed with prepareQuery about what a trailing FORMAT is.
It only matched a format anchored at the very end, so `FORMAT TSV;` and
`FORMAT TSV SETTINGS max_threads=1` - both of which prepareQuery already treats as
trailing clauses, and both covered by its own tests - kept the clause, ClickHouse
formatted the EXPLAIN output as TSV, and no plan could be rendered. The tail is now
a lookahead so SETTINGS survives, which is correct: it applies to the inner
statement, whereas FORMAT reformats the plan itself.

The key-expression splitter treated quoted spans as syntax. A legal
``ORDER BY `region,code` `` was reported as two columns, and an unbalanced
parenthesis inside quotes corrupted the depth counter and swallowed every later
top-level comma. Both scans now skip quoted spans, honouring backslash and
doubled-quote escapes.

Also documents what was accurate but under-stated: clickhouse-client speaks the
NATIVE protocol, not the HTTP interface this provider uses (the overview claimed
otherwise while section 3.1 said the opposite); the maintenance API contract, since
analyze deliberately takes no target; and the dotted-name display ambiguity, which
is a cross-provider TableSchema limitation rather than a ClickHouse one. The compose
comment no longer points at a gitignored spec path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Add Couchbase provider (SQL++ over Query REST, no native dependency)

2 participants