feat(db): add Apache Druid provider (Druid SQL over HTTP, no native dependency) - #271
Merged
Conversation
…ependency) Closes #265. Extends SQLBaseProvider over the SQL HTTP API, following the ClickHouse (#264) shape rather than the Couchbase directory. package.json is untouched: the provider speaks POST /druid/v2/sql with fetch and nothing else. Verified against a live Apache Druid 37.0.0 cluster throughout, and the live pass corrected three of the issue's own assumptions. EXPLAIN is supported, contrary to the issue's draft. The native-query plan is a genuine nested tree - dataSource recurses through join/query/union - so it renders as { kind: "tree" } honestly, with no metrics at all because Druid's planner emits no cost or row estimates. useNativeQueryExplain=false was rejected: it needs a non-default context flag and is indentation-parsed text where the default is JSON. resultFormat is "array", not "object": the object form silently drops every duplicate output column but the last (SELECT 1 AS c, 2 AS c returns only {c:2}). 64-bit integers arrive as unquoted JSON numbers and Druid has no server-side quoting setting, so JSON.parse rounds 9007199254740993 to ...992 with no error. A string-aware scanner (db/utils/json-integers.ts) quotes unsafe literals before parsing, as the pg driver does for int8. It is shared with the explain strategy, whose second parse of the PLAN column is an independent chance to round the same value. Errors: two envelopes coexist, and `error` is a discriminator whose value is the literal "druidException", so the message always comes from `errorMessage`. Classification is on `category`, never on the HTTP status - SELECT 1/0 answers 500 with persona ADMIN for what is a plain user mistake. Druid SQL cannot write: no UPDATE, no DELETE, no CREATE TABLE, and INSERT/REPLACE need the MSQ task engine. Those are surfaced with Druid's own messages rather than special-cased, and supportsCreateTable / supportsMaintenance are false for reasons the doc records. Also fixes three defects this provider exposed in shared code: - query-generators.ts quoted no Druid identifier, and Calcite reserves many plain lowercase words, so Generate SELECT emitted hard syntax errors for a column named count (Druid's own conventional rollup metric name), value, start, end, date, time, year or rows. Druid now quotes unconditionally, as Couchbase does. - PerformanceMetrics.cacheHitRatio is now optional. A provider that cannot measure one had to report 0, which DEFAULT_THRESHOLDS scores as CRITICAL, so every healthy Druid cluster showed a red cache fault. The monitoring tabs already defaulted the threshold to a healthy 100 when absent; they now also withhold the percentage and the rating instead of printing a fabricated 0.0% / Poor. - The Connections card divided by maxConnections without guarding zero, which means "no limit published" for Druid and for SQL Server, and rendered the literal "NaN% used". database-compose.yml gains seven profile-gated services: Druid is a distributed system with no single-container mode, so `--profile druid` keeps the default `up -d` from growing to 15 containers. Only 8888 and 8082 are published.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds an Apache Druid provider using its SQL HTTP API, integrating querying, schema discovery, monitoring, EXPLAIN, UI configuration, and documentation without a native dependency.
Changes:
- Adds the Druid transport, provider, introspection, EXPLAIN strategy, and exact 64-bit JSON handling.
- Registers Druid across connection, seed, factory, UI, and query-generation surfaces.
- Updates monitoring behavior, development infrastructure, documentation, and comprehensive tests.
Reviewed changes
Copilot reviewed 47 out of 47 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
README.md |
Documents Druid support and setup. |
database-compose.yml |
Adds profile-gated Druid services. |
docs/ADDING_A_PROVIDER.md |
Adds Druid provider guidance. |
docs/API_DOCS.md |
Documents Druid API behavior. |
docs/ARCHITECTURE.md |
Adds Druid to architecture. |
docs/DATABASE_PROVIDERS.md |
Documents provider implementation. |
docs/FEATURES.md |
Lists Druid capabilities. |
docs/SEED_CONNECTIONS.md |
Documents seeded Druid connections. |
docs/providers/README.md |
Registers provider documentation. |
docs/providers/druid.md |
Provides the Druid reference. |
src/components/icons/db-icons.tsx |
Adds the Druid icon. |
src/components/monitoring/tabs/OverviewTab.tsx |
Handles unavailable monitoring values. |
src/components/monitoring/tabs/PerformanceTab.tsx |
Handles unmeasured cache ratios. |
src/hooks/use-connection-form.ts |
Adds Druid to the picker. |
src/lib/db-ui-config.ts |
Defines Druid UI configuration. |
src/lib/db/factory.ts |
Creates Druid providers. |
src/lib/db/providers/document/couchbase/index.ts |
Uses shared cache formatting. |
src/lib/db/providers/sql/clickhouse/index.ts |
Uses shared cache formatting. |
src/lib/db/providers/sql/druid/http-transport.ts |
Implements the HTTP protocol. |
src/lib/db/providers/sql/druid/index.ts |
Implements the provider strategy. |
src/lib/db/providers/sql/druid/introspect.ts |
Implements schema and monitoring reads. |
src/lib/db/providers/sql/druid/transport.ts |
Defines the transport contract. |
src/lib/db/types.ts |
Adds capabilities and optional metrics. |
src/lib/db/utils/json-integers.ts |
Preserves unsafe JSON integers. |
src/lib/explain/druid-native.ts |
Builds Druid EXPLAIN trees. |
src/lib/explain/index.ts |
Registers the EXPLAIN strategy. |
src/lib/monitoring-cache-ratio.ts |
Centralizes cache-ratio formatting. |
src/lib/query-generators.ts |
Adds Druid identifier quoting. |
src/lib/seed/types.ts |
Accepts Druid seed connections. |
src/lib/types.ts |
Adds the Druid type identifier. |
tests/components/monitoring/OverviewTab.test.tsx |
Tests unavailable overview metrics. |
tests/components/monitoring/PerformanceTab.test.tsx |
Tests cache-ratio presentation. |
tests/hooks/use-connection-form.test.ts |
Tests picker registration. |
tests/integration/db/druid-provider.test.ts |
Tests provider integration. |
tests/unit/db/druid/http-transport.test.ts |
Tests HTTP encoding and decoding. |
tests/unit/db/druid/introspect.test.ts |
Tests introspection and monitoring. |
tests/unit/db/druid/seam-guard.test.ts |
Enforces the transport boundary. |
tests/unit/db/druid/transport.test.ts |
Tests the transport contract. |
tests/unit/db/factory.test.ts |
Tests factory registration. |
tests/unit/db/json-integers.test.ts |
Tests integer preservation. |
tests/unit/lib/connection-string-parser.test.ts |
Tests absence of Druid URIs. |
tests/unit/lib/db-icons.test.tsx |
Tests the Druid icon. |
tests/unit/lib/db-ui-config.test.ts |
Tests Druid UI settings. |
tests/unit/lib/explain/druid-native.test.ts |
Tests EXPLAIN conversion. |
tests/unit/lib/monitoring-cache-ratio.test.ts |
Tests ratio formatting. |
tests/unit/lib/query-generators.test.ts |
Tests Druid SQL generation. |
tests/unit/seed/types.test.ts |
Tests Druid seed validation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… check
`buildSql` refused anything not starting literally with SELECT, so the Explain
button was dead for two ordinary statements that Druid explains perfectly well.
Both live-verified as accepted on 37.0.0, and both render a tree end to end:
EXPLAIN PLAN FOR WITH t AS (...) SELECT * FROM t -> tree root "groupBy"
EXPLAIN PLAN FOR -- note\nSELECT ... -> tree root "groupBy"
EXPLAIN PLAN FOR /* header */ SELECT ... -> tree root "scan"
The CTE case was also an internal inconsistency: the shared `analyzeQuery` already
classifies `WITH ... SELECT` as a SELECT and injects a LIMIT into one, so the
explain strategy refusing it disagreed with the rest of the pipeline.
The pattern is deliberately not the obvious spelling. A leading `\s*` in front of
an alternation that also contains `\s` gives two ways to match one run of spaces,
and a lazy `[\s\S]*?\*\/` inside a `*` quantifier lets one iteration swallow
several comments. Both are ambiguous, and a non-matching input pays for it:
20k leading spaces + non-SELECT ambiguous 958ms tempered 0.4ms
4 KB of block comments + non-SELECT ambiguous 852ms tempered 0.0ms
`buildSql` runs on the editor's contents every time a query is executed, so a
buffer opening with a large commented-out block ahead of a non-SELECT is reachable.
The tempered form accepts and rejects exactly the same statements - verified
identical on 21 cases - so this costs nothing. It follows the care that already
made `query-limiter.ts` hand-write its semicolon strip "without regex to avoid
ReDoS".
Tests cover the CTE, both comment styles, comments stacked ahead of a CTE, and the
near misses that must still be refused (comment-only, empty, SELECTED, WITHER, an
unterminated block comment). The backtracking guard asserts both the correct answer
and a bounded time; reverting to the ambiguous pattern fails it at 1274ms against a
200ms bound.
The five sibling strategies still carry the narrow check and are untouched here.
Five findings, all reproduced before being accepted.
1. `__time` is no longer marked `isPrimary`. It is mandatory, it is the partition
and sort key, and it is the only column Druid reports NOT NULL - but a primary
key is UNIQUE and it is not: live, libredb_demo has 50 rows and 30 distinct
`__time` values. Nothing in a datasource is unique. And `isPrimary` is not a
hint - `sql-completions.ts` appends "(PK)", `use-ai-chat.ts` puts ", PK" into
the schema context the model reasons from, and `schema-diff/diff-engine.ts`
reports "Primary key changed", so two datasources differing only in this would
diff as a key change. The time column stays identifiable by name and by being
the one `nullable: false` column.
2. An integral `number` outside the safe range is refused instead of sent. By the
time it arrives it is already wrong: a caller writing 9007199254740993 handed us
9007199254740992, and nothing here can recover the digit. Sending it would
filter on a value the user never wrote and return a plausible wrong row set, so
the error names the fix - pass a bigint, which binds exactly. The same check
catches an integral double past Druid's own BIGINT range.
3. The bigint marker is gone. The parameters array is now serialized by hand and
spliced into the envelope at its closing brace, whose position is known because
JSON.stringify just produced it. The previous NUL sentinel was unsound: a
sentinel is only as private as the values flowing through it, so a caller whose
VARCHAR parameter contained it would have had that string silently unquoted into
a number. Emitting the literal in the first place cannot collide, because no
marker exists. Verified live: a VARCHAR of exactly NUL + digits + NUL now
round-trips as a string, alongside an exact bigint in the same request.
4. A payload shorter than the three header rows now raises instead of reporting an
empty result. Live-verified, there is no legitimate way to receive one: with all
three flags set even a zero-row result answers
[["id","name"],["LONG","STRING"],["BIGINT","VARCHAR"]], and a bare SET - the only
other statement form the grammar accepts - is rejected outright. What does
produce a short payload is a truncated body or a proxy rewrite, and `{rows: []}`
there is the most convincing possible lie: a successful query over the right
datasource that happens to have found nothing. A genuine empty result still
succeeds, fully described.
5. The cache-hit TREND no longer plots zero for missing samples. The current-value
card already withheld the number, but `cacheHistory` still mapped absent to 0,
which drew a measured 0% line - the same fabricated metric. Missing samples are
dropped, and the card says "Not measured" when none remain. Samples that do
carry a ratio still plot.
The sixth finding - that the explainable check rejected CTEs - was already fixed in
the preceding commit.
Test literals for the rounding cases use Number("...") rather than wide numeric
literals: oxlint's no-loss-of-precision is right that such a literal cannot be held
exactly, which is the very thing under test.
…ranch
CodeQL alert 107, and it is right: `--[^\n]*` inside a `*` quantifier is ambiguous.
`[^\n]*` can give characters back and let a later iteration match `--` again, so a
run of bare dashes partitions exponentially. Measured on the previous pattern, with
a tail that never reaches SELECT:
"--" x 12 -> 0.2ms
"--" x 16 -> 7.4ms
"--" x 20 -> 250.7ms
"--" x 22 -> 632.0ms <- a 45-character input
Roughly fourfold per two extra dashes. That is far cheaper to trigger than either
ambiguity fixed in 510c697 - those needed 4 KB to 20 KB of input, this needs
forty-five characters - so removing the `(?:\n|$)` tail in that commit made the
pattern worse overall, not better. Restoring the tail forces the branch to run to
the newline or to the end, leaving nothing to give back.
The fix is to combine all three unambiguous forms rather than choose between them:
no leading `\s*` (whitespace is already an alternative), a tempered block-comment
body that cannot span two comments, and now a line comment anchored to the newline
or end of input. Every adversarial input measured is under 1.5ms, and the pattern
accepts and rejects exactly the same statements - verified identical on 23 cases.
The regression guard gains the bare-dash cases, which is the gap that let this
through: it already covered `-- a\n` repetitions, and the newline is precisely what
makes that branch unambiguous, so no amount of that input could have caught it.
Removing the tail again fails the guard at 636ms.
|
cevheri
added a commit
that referenced
this pull request
Aug 4, 2026
…rategies (#274) * fix(explain): share one explainable-statement check across all six strategies Every strategy carried its own `/^\s*SELECT\b/i`, and every one of them refused two statements its engine explains perfectly well: a CTE, and a SELECT behind a comment. The Explain button was simply absent for both. #271 fixed it for Druid only; this extends it to the other five and moves the check into one place, `explain/select-prefix.ts`. The CTE case was also an internal disagreement: the shared `analyzeQuery` already classifies `WITH ... SELECT` as a SELECT and injects a LIMIT into one, so six strategies declining it contradicted the rest of the pipeline. Verified per dialect rather than assumed - each statement below was wrapped by the REAL strategy, executed against the REAL engine, and rendered: postgres 18 CTE, line comment, block comment all explain mysql 9 CTE, line comment, block comment all explain sqlite CTE, line comment, block comment all explain clickhouse 26.7 CTE, line comment, block comment all explain couchbase 8.0.2 WITH binding, line, block comment all explain druid 37 CTE, line comment, block comment all explain Couchbase needed its own spelling: SQL++ `WITH alias AS (<expression>)` binds a value rather than a subquery. PostgreSQL is the one dialect that could not simply take the shared answer, and finding out why is the reason this was verified per engine. Its strategy emits `EXPLAIN (ANALYZE, ...)`, which EXECUTES the statement, and a data-modifying CTE is a write wearing a WITH: EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) WITH t AS (INSERT INTO probe(id) VALUES (42) RETURNING id) SELECT * FROM t -> 0 rows in the table before, 1 row (42) after. Explaining performed the insert. So accepting WITH there without a screen would have turned the Explain button into a write. `postgres-json.ts` pairs the classification with `hasDataModifyingStatement()` and applies it ONLY to the `with` case: PostgreSQL refuses a data-modifying CTE anywhere but the top level ("WITH clause containing a data-modifying statement must be at the top level", verified), so one cannot hide behind a leading SELECT, and screening SELECTs too would strip the button off `SELECT 'insert'`, which explains fine today. The screen errs toward refusing - a read-only CTE that merely mentions a keyword loses its button rather than risking a write. `classifySelectPrefix` returns `"select" | "with" | null` rather than a boolean precisely so that asymmetry is expressible. The regex keeps the shape arrived at in #271, where all three of its alternatives had to be made unambiguous independently - no leading `\s*`, a line comment anchored to newline-or-end, and a tempered block-comment body. Its bounded-time guard moves here with it, since the property belongs to the regex rather than to Druid; Druid keeps a lighter test proving it still routes through. Six gates green plus coverage at 100% (28534 lines), and all nine explain files at 100% individually. * test(explain): pin the PostgreSQL over-reach at the strategy boundary Review follow-ups on #274, all verified before being accepted. `select-prefix.test.ts` already covered `hasDataModifyingStatement` on a read-only CTE that merely mentions a keyword, but nothing pinned what `postgresJsonStrategy.buildSql` does with it. That is where the documented "errs toward refusing" behaviour actually matters, so it now has a test: WITH t AS (SELECT 'insert' AS x) SELECT * FROM t -> null Paired with the LIMIT of that over-reach, which is the more interesting half and was untested: the word boundary is what keeps the screen from swallowing every CTE that touches an `updated_at` column, so WITH t AS (SELECT updated_at FROM u) SELECT * FROM t still explains. Writing that pair is what caught a wrong assertion in the first draft - I had expected `updated_at` to be refused, and it is not, correctly. Also records two facts about DATA_MODIFYING's membership that were previously assumed: - MERGE is a REAL carrier, not a defensive guess. Live on PostgreSQL 18, `EXPLAIN (ANALYZE, FORMAT JSON) WITH t AS (MERGE INTO probe ... RETURNING id) SELECT * FROM t` really inserted the row. - TRUNCATE is deliberately absent. It cannot ride inside a WITH at all - `WITH t AS (TRUNCATE probe) SELECT 1` is a SYNTAX error - and a statement leading with it never reaches the screen because the prefix classification already refuses anything but SELECT or WITH. Tested for TRUNCATE, DROP and CREATE together. The third review point - that `analyzeQuery` still carries its own regexes - turned out to hide a live defect rather than only a drift risk: a leading comment makes it classify a SELECT as OTHER, so `prepareQuery` injects no LIMIT and the query runs unbounded. Filed as #275 with the reproduction; it feeds the query path of all ten providers and belongs in its own change rather than in an explain PR.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Closes #265.
Apache Druid over its SQL HTTP API, extending
SQLBaseProvider— the ClickHouse (#264) shape, not theCouchbase directory.
package.jsonis untouched: the provider speaksPOST /druid/v2/sqlwithfetchand nothing else.Everything below was verified against a live Apache Druid 37.0.0 cluster, and the live pass
corrected three of the issue's own assumptions.
Where the issue was wrong, and what the cluster said instead
supportsExplain: falsedataSourcerecurses throughjoin/query/union. It renders as{ kind: "tree" }honestly, sosupportsExplain: trueerror,errorMessage,errorClass,hosterroris a discriminator whose value is the literal string"druidException"resultFormat: "object"plus the header flags is enoughobjectsilently drops duplicate columns —SELECT 1 AS c, 2 AS creturns only{c: 2}1. EXPLAIN renders as a tree, and it is honest
The recursion through
dataSourceis the operator tree. What keeps it honest is what is left out:no
metricson any node, because Druid's planner emits no cost and no row estimates.ExplainTreeNode.metricsis optional, so the tree carries structure and nothing it cannot support.useNativeQueryExplain: falsealso works and returns an indented CalciteRelNodeplan. Rejected:it depends on a non-default context flag, and it is indentation-parsed text where the default is
structured JSON.
buildSqlreturnsEXPLAIN PLAN FOR …for both modes, so the direct Explain button is live andnot just the background pre-warm — the defect #265 explicitly warned about.
2. Errors: classify on
category, never on the status codeerroris a discriminator, so showing it would print "druidException" to whoever mistyped adatasource name. The message always comes from
errorMessage. And the status alone misclassifies:A plain user typo, reported as an admin-facing 500.
3. Wire format is
array, for correctnessDuplicate output names are legal SQL and arrive from real joins, so the transport asks for
arraywith three header rows and rebuilds the row objects itself.
The 64-bit integer bug
A
BIGINTholding9007199254740993arrives as an unquoted JSON number, and Druid has noserver-side quoting setting (unlike ClickHouse's
output_format_json_quote_64bit_integers), soJSON.parserounds it to…992with no error:A string-aware scanner (
db/utils/json-integers.ts) quotes unsafe literals before parsing, as thepgdriver already does forint8. It is shared with the explain strategy, whose second parse ofthe
PLANcolumn is an independent chance to round the same value — that was a real bug found inreview and fixed here.
Three defects in shared code that this provider exposed
These are not Druid-specific and are worth reviewing on their own merits.
query-generators.tsquoted no Druid identifier. Calcite reserves many plain lowercase words,so "Generate SELECT" and the auto-executing datasource click emitted hard syntax errors for a
column named
count,value,start,end,date,time,yearorrows.countmattersmost: it is Druid's own conventional rollup metric name, so the standard rollup ingestion creates
one. Druid now quotes unconditionally, exactly as the Couchbase branch above it already does.
PerformanceMetrics.cacheHitRatiois now optional. A provider that cannot measure one had toreport
0, whichDEFAULT_THRESHOLDSscores as critical (direction: "below",critical: 80)— so every healthy Druid cluster showed a red cache fault. The monitoring tabs already defaulted
the threshold to a healthy 100 when absent; they now also withhold the percentage and the
Excellent/Good/Poor rating instead of printing a fabricated
0.0% / Poor. Ripple was two callsites, both routed through one shared formatter so no unreachable branch was added.
maxConnectionswithout guarding zero, which means "no limitpublished" for Druid and for SQL Server (whose own comment says so), and rendered the literal
"NaN% used".
Capabilities, and why each
falseis realsupportsCreateTable: false—CREATE TABLEis not merely unimplemented, it is not in thegrammar:
400 "Incorrect syntax near the keyword 'CREATE'". Datasources come from ingestion.supportsMaintenance: false— nothing inMaintenanceTypehas a SQL-reachable analogue, and thereis no
sys.queriescatalog to read a cancellable query id from.supportsConnectionString: false— Druid has no URI convention for its HTTP SQL API (its JDBCdriver uses
jdbc:avatica:remote:url=…), andhttp(s)://is already claimed by ClickHouse.connection-string-parser.tsis therefore untouched, with negative tests pinning the absence.databasefield in the connection form:INFORMATION_SCHEMA.SCHEMATAreports exactly onecatalog, always
druid, so the control would have no effect.prepareQueryhas exactly one override: a statement ending inOFFSETwith noLIMIT, becauseDruid rejects
OFFSET start LIMIT countoutright. Such a statement is returned untouched — rewritingwrongly fails the query, leaving it alone only returns more rows.
Druid SQL cannot write, and the app says so clearly
Verified through the app's own query route. None of these returns "0 rows"; each returns Druid's own
message, which names the reason and the alternative better than anything we would substitute:
INSERT INTO …INSERT operations are not supported by requested SQL engine [native], consider using MSQ.UPDATE … SET …Unsupported SQL statement [UPDATE]DELETE FROM …Unsupported SQL statement [DELETE]REPLACE INTO …REPLACE operations are not supported by the requested SQL engine [native]. Consider using MSQ.CREATE TABLE …Druid removes data by marking segments unused and running a kill task, through the Coordinator. MSQ
ingestion on
/druid/v2/sql/taskand the async/statementsendpoint are out of scope per the issue,and recorded as follow-ups.
The catalog is a view of what is servable, not of what exists
The most surprising thing about Druid introspection, and the one most likely to be mistaken for a bug
in the editor. Two independent demonstrations:
INFORMATION_SCHEMA.TABLESentirely. Sothere is no empty-datasource case to render — the opposite of Couchbase's empty collection.
INVALID_INPUT,blaming the statement, and is indistinguishable from genuinely mistyping the name. Nothing in
this provider can improve that message, so
docs/providers/druid.mdis the mitigation.Live end-to-end pass
Ran the built app against the real cluster and drove every capability the UI offers, through both the
Router (8888) and the Broker (8082) — the issue asked for both to be confirmed:
Online/ healthy; schema tree lists 2 datasources with 8 columns at their SQL typesSELECT * FROM "libredb_demo" LIMIT 50;— quoted — returning50 rows in 14 ms, with
snowflake_idrendered as the exact"9007199254740993"unbounded (the route injects a LIMIT), and the
OFFSET-only override left untouched at 48 rows37.0.0fromsys.servers, uptime, DB size, per-datasource table stats, andboth new honest empties rendering green rather than red —
Cache Hit: N/A · Not measuredandConnections: 0 · no limit publishedOne pre-existing gap the live pass found
The monitoring Tables tab renders
Analyze/Vacuum/Reindexper row unconditionally —TablesTab.tsxnever readsgetCapabilities()— so for Druid every click answers400 {"error":"Maintenance operations not supported for this database"}. Not introduced here:libredb.tsalso setssupportsMaintenance: falseand has the same dead buttons today, so gatingthat tab is a change for every provider at once. Documented in
docs/providers/druid.mdsection 8rather than left as a doc that describes the intent instead of the software; follow-up to file.
Verification
All six local gates green in a clean worktree, plus the coverage gate:
Every file this PR touches is at 100% individually, verified per-file in the merged lcov.
Reproducing the live pass
database-compose.ymlgains seven profile-gated services. Druid is a distributed system with nosingle-container mode, so gating keeps the default
up -dfrom growing from 8 containers to 15:Only 8888 (Router) and 8082 (Broker) are published; 8091 would have collided with Couchbase. A
datasource can only be created by ingestion, and
docs/providers/druid.mdshows the native batch taskwith an inline input source so the next person can load data.
Review notes
docs/providers/druid.md(1483 lines) andtests/integration/db/druid-provider.test.tsall land together.docs/ADDING_A_PROVIDER.mdhad listed Druid as a candidate whose EXPLAIN "does not fit the treerender model". That verdict is now corrected, and the HTTP-traps section gained the generalisable
lesson about unquoted 64-bit integers.
=== "druid"anywhere outside the provider directory anddb-ui-config.ts.