Skip to content

feat(providers): Elasticsearch and OpenSearch, two type-ids over one SQL transport (#424) - #429

Merged
cevheri merged 8 commits into
mainfrom
feat/phase1-search-providers
Aug 19, 2026
Merged

cevheri merged 8 commits into
mainfrom
feat/phase1-search-providers

Conversation

@cevheri

@cevheri cevheri commented Aug 19, 2026

Copy link
Copy Markdown
Member

Phase 1 of #424 — the largest gap on the coverage map. One implementation in src/lib/db/providers/sql/search/ serves both type-ids; everything the two products disagree about is data in a dialect table, never a branch on the type-id.

The query surface was decided by measurement

Elasticsearch 9.1.4 (basic) OpenSearch 3.8.0
SQL _sql?format=json, {columns, rows} _plugins/_sql, {schema, datarows}
ES|QL works does not exist (405)
Licence basic, no gate Apache-2.0

Both speak SQL with no licence gate, so SQL is the shared language and ES|QL cannot be. That puts the provider on SQLBaseProvider, which is what makes the explorer, ER diagrams, schema diff, the query limiter and agent plan mode work with no per-provider cost. The schema still comes from _mapping, never from SQL — SELECT * describes the statement, and an index mapping flattened/nested answers it with zero columns.

Three defects the live probes found

A truncated result presented as complete. An Elasticsearch aggregation is paged by the engine even with no fetch_size: a GROUP BY over 1500 distinct values answers 1000 rows plus a cursor. Dropping it returned two thirds of the buckets and labelled the result complete — worse than an error, because a user reading a GROUP BY has no way to notice 500 missing groups. query() now follows the cursor (page two carries rows and no column declaration, so page one's names are carried forward) and refuses loudly at a bound rather than truncating quietly. Verified live: 1500 rows where it previously returned 1000.

A wrong column label. SELECT customer AS who declares name: "who" on Elasticsearch and name: "customer", alias: "who" on OpenSearch. The transport read name only, so the same statement's column was labelled who on one product and customer on the other.

Silently wrong rows from a generated query. query-generators.ts derives its dialect from defaultPort — an assumption that held only while every engine had a distinct one. Both products are 9200 and they disagree about the quote character: on OpenSearch a double-quoted identifier is a string literal, so WHERE "customer" = 'acme' answers HTTP 200 with total: 0 while the backtick form returns the row. No error, just no rows for data that exists. Fixed with a declared capability (ProviderCapabilities.identifierQuoting) rather than a type-id check; absent means "keep deriving it from the port", so no existing provider changed.

Two places the honest answer was to offer less

  • Multi-fields are not listed as columns, on either product. OpenSearch refuses SELECT note.keyword in every spelling (SemanticCheckException) while Elasticsearch selects it fine, and dynamic mapping creates such a child for every text field — so keeping them would break the generated starter query on nearly any dynamically-mapped OpenSearch index. Keeping them only on Elasticsearch would make the dialect decide a behaviour, which introspect.ts's own recorded rule forbids. The Elasticsearch-side loss is written down, not papered over.
  • supportsExplain: false. Neither SQL endpoint returns a plan tree, so an Explain tab would render for a plan nothing can parse.

Registration surfaces

Recon measured which surfaces the compiler enforces by adding the union member and running tsc: eight fail the build, and roughly as many fail silently — the factory switch and the literal type list in its error message, the seed zod enum, selectableTypes, the icon exports, the grammar maps. All are registered, and three that had no test at all now do: the icon list, both factory cases (each asserted to produce its own class), and the error-message list, which now iterates SHIPPED_DATABASE_TYPES instead of naming three engines.

SHIPPED_DATABASE_TYPES itself was the one mandatory surface with no guard — a hand-written array whose count test read the same constant on both sides. It is now derived from an exhaustive Record, so an omission is a compile error.

Claim surfaces this work falsified

docs/BRAND_MESSAGING.md carried a standing rule: "There is no Elasticsearch or OpenSearch provider. Neither may appear as an example, in any list of engines, or in any story about polyglot teams." Shipping the provider makes that false while the rule keeps forbidding us to mention it. Nothing in the repo would have flagged this — it is not a provider file, a test, or a doc anyone greps when adding an engine. Updated, along with its engine count and the candidate row in docs/ADDING_A_PROVIDER.md.

Verification

Seven gates of #424's Definition of Done:

Gate Evidence
1 Tri-sync provider + docs/providers/{elasticsearch,opensearch}.md + one integration test per type-id
2 Registration 8 type-enforced + ~8 silent surfaces, listed above
3 Unit + integration check-coverage: OK — 37635/37635 lines (100.00%)
4 Live probe both engines, real provider, every introspection surface: zero failures, QueryError for a missing index, versions Elasticsearch 9.1.4 / OpenSearch 3.8.0
5 E2E e2e/search-providers.spec.ts — both drivers selectable, port prefilled, no connection-string toggle, and no unearned compatibility hint
6 Functional + gates format · lint (0 errors) · typecheck · knip · test · build all clean
7 Agent plan mode both type-ids ground via provider-inventory (#414's non-catalog path), asserted in context-snapshot.test.ts

Container fixtures added to database-compose.yml with the exact configuration the probed containers ran, so gate 4 is reproducible.

Recorded limitations

No EXPLAIN. Aliases and data streams are not in the schema tree (_cat/indices does not list them). No maintenance operations, no active sessions, no slow queries. totalHits is unavailable on Elasticsearch. A trailing semicolon is a syntax error there, and OFFSET is too. Nested-mapping document counts exceed what a SELECT returns. No result-export.ts dialect entry: both engines reject CREATE TABLE, so no type spelling makes an exported file replayable into its own engine — the #422 criterion cannot be met here, and inventing a row would bury a promise the product cannot keep.

…SQL transport (#424)

Phase 1 of #424, the largest gap on the coverage map. One implementation in
`providers/sql/search/` serves both type-ids; everything the two products disagree
about is data in a dialect table, never a branch on the type-id.

**The query surface was decided by measurement, not by the docs.** Both products
expose an SQL endpoint with no licence gate - `_sql?format=json` on Elasticsearch
9.1.4 basic, `_plugins/_sql` on OpenSearch 3.8.0. ES|QL also works, and exists only
on Elasticsearch (OpenSearch answers 405), so it cannot be the shared language.
Choosing SQL puts the provider on `SQLBaseProvider`, which is what makes the
explorer, ER diagrams, schema diff, the query limiter and agent plan mode work with
no per-provider cost. The schema still comes from `_mapping`, never from SQL:
`SELECT *` describes the statement, and an index mapping `flattened`/`nested`
answers it with zero columns.

Three defects the live probes found, all of which mocks would have missed:

- **A truncated result presented as complete.** An Elasticsearch aggregation is
  paged by the engine even with no `fetch_size`: a `GROUP BY` over 1500 distinct
  values answers 1000 rows plus a cursor. Dropping that cursor returned two thirds
  of the buckets and called it the answer. `query()` now follows the cursor (page
  two carries rows and no column declaration), and refuses loudly at a bound rather
  than truncating silently.
- **A wrong column label.** `SELECT customer AS who` declares `name: "who"` on
  Elasticsearch and `name: "customer", alias: "who"` on OpenSearch, so the grid
  labelled the same statement's column differently per product.
- **Silently wrong rows from a generated query.** `query-generators.ts` derives its
  dialect from `defaultPort`, which held only while every engine had a distinct one.
  Both products are 9200 and they disagree about the quote character: on OpenSearch
  a double-quoted identifier is a STRING LITERAL, so `WHERE "customer" = 'acme'`
  answers 200 with `total: 0` while backticks return the row. Fixed through a
  declared capability (`identifierQuoting`), not a type-id check - absent means
  "keep deriving it from the port", so no existing provider changed.

Two places where the honest answer was to offer LESS. Multi-fields are dropped from
the column list on both products because OpenSearch cannot select `note.keyword` in
any spelling and dynamic mapping creates one for every text field - keeping them
only on Elasticsearch would make the dialect decide a behaviour, which this
provider's own rule forbids. And `supportsExplain` is false: neither SQL endpoint
returns a plan tree, so an Explain tab would render for a plan nothing can parse.

Also updated: `docs/BRAND_MESSAGING.md`, whose standing rule said no such provider
exists and forbade mentioning either product - a claim this work falsifies, and one
nothing would have flagged.
The login showcase that #428 added ranks every engine through an exhaustive `Record<DatabaseType, number>`, so the two type-ids this branch adds broke typecheck on the missing keys - the compile-time guard doing exactly its job. Elasticsearch and OpenSearch take ranks 7 and 8, ahead of the analytical stores: Elasticsearch is one of the better-known names on that page and OpenSearch reads as its sibling, which is what the code says too - one HTTP SQL transport behind both. The test-side exhaustive lists in db-ui-config.test.ts move with them.
Both engines were driven in Chrome against real clusters - Elasticsearch 9.1.4 and
OpenSearch 3.8.0, seeded with a microservice-shaped dataset (orders with an object
mapping and a multi-field, 1500-document app_logs, Turkish names throughout). Every
finding below is a browser measurement, and each one is invisible to the unit suite
because the wrong answer only appears once a real engine and the real UI are both in
the loop.

**The first click on an index failed on Elasticsearch.** "Select Top 50 Documents"
generated `SELECT * FROM orders LIMIT 50;` and this grammar has no terminator:
`parsing_exception`, "extraneous input ';' expecting <EOF>". "Generate Query" failed
the same way. `docs/providers/elasticsearch.md` had already recorded the defect as a
fact about the engine; it is now fixed instead. `ProviderCapabilities.statementTerminator`
is `"none"` on both products - the absence runs on OpenSearch too, so one answer serves
both type-ids rather than a branch on dialect - and `query-generators.ts` asks the
capability.

**The grid showed NULL over values the API had returned.** TanStack reads a dot in
`accessorKey` as a path into the row, so `shipping.city` was fetched as
`row.shipping.city` while the row carries that flat key. Measured: the API answered
`{"order_id":"o-001","shipping.city":"İzmir"}` and the cell read NULL, while the CSV
export - which reads `row[column]` - wrote İzmir the whole time. Every object field in
a mapping flattens to a dotted leaf, so on a search cluster this was most of a user's
columns, and the screen was the only surface that lied. Now `id` + `accessorFn`, with
`Object.hasOwn` for the same reason the export path has it.

**Plan mode wrote the wrong language.** A live plan run on an OpenSearch connection,
asked for one runnable statement, answered with a native aggregation body -
`{"size":0,"aggs":{…}}` - correct for the product and unrunnable through the SQL
endpoint this provider speaks to. The guard then declined to classify it
(NO_STATEMENT), so nothing ran; the user was still handed a plan they could not
execute. `queryLanguage: "sql"` was already true and settled nothing, because the
engine's NAME carries the stronger prior. `ProviderLabels.statementLanguage` now names
the endpoint and rules out each product's own alternatives (ES|QL upstream, PPL on the
fork), and the plan contract states it. Verified after the fix: the run drafted
`SELECT service, COUNT(*), AVG(latency_ms) … GROUP BY service`, the guard read it as a
bounded read, and the statement runs on the cluster.

**Two maintenance entries led nowhere.** An index is addressable, so the
`tablesAreDerivedGroupings` gate #427 added did not catch it: "Index Statistics" and
"Merge Segments" rendered on an engine declaring `supportsMaintenance: false`, and
/admin/operations gates its Global Operations card on that same capability - so the
click landed on a page with no controls, no error, and none of the wording written for
exactly that moment. The gate now asks for an addressable row AND a declared
capability.

Also: both provider docs said "multi-fields kept" in their §11 coverage summaries
while `introspect.ts` drops them by a measured, documented rule - the tri-sync
invariant, wrong in the one phrase that matters.

Three findings are recorded in docs/BACKLOG.md instead of fixed here, each because it
predates this PR and spans every engine: the monitoring Queries tab tells all eleven
engines to enable `pg_stat_statements` (U12), BEGIN/SANDBOX render where no provider
declares transactions and the route refuses with 400 (U13), and monitoring reports
relational zeros as measurements where the engine keeps no such counter (U14).
@cevheri

cevheri commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

Live-cluster verification (Chrome, both engines)

Driven in a real browser against Elasticsearch 9.1.4 and OpenSearch 3.8.0 (the pinned database-compose.yml services), seeded with a microservice-shaped dataset: orders with an object mapping and a text+keyword multi-field, customers, a 1500-document app_logs, Turkish names and city names throughout, plus .internal_bookkeeping and OpenSearch's own .plugins-ml-config / top_queries-* to test index hiding.

What already worked, measured rather than assumed

Surface Result
Connection dialog Elasticsearch and OpenSearch both offered, default port 9200, no connection-string field, TLS/SSH panels present; Test Connection → Connected successfully (19ms) with no database name
Schema explorer "Search indices or fields…", 3 indices; .internal_bookkeeping, .plugins-ml-config and the dotless top_queries-2026.08.19-* all hidden; object leaves flattened to shipping.city / shipping.country; multi-field customer_name.raw absent — and SELECT customer_name.raw is refused live on OpenSearch (SemanticCheckException), which is the measurement that rule was written from
Engine-initiated paging GROUP BY trace_id over 1500 groups returned 1500 rows through the cursor, trace-0001 … trace-1500
ES has no OFFSET Load More refused with the provider's own sentence as a toast, row count stayed at 500 — no fabricated page
OpenSearch has OFFSET Load More 500 → 1000, no duplicates; LIMIT 5 OFFSET 5 returns o-006
Writes INSERT / UPDATE refused by both grammars with the engine's own message; 1/0 → HTTP 500 categorised as a user error
Gating No EDIT, no Explain button, no Explain tab, no Create Table
ER diagram 3 boxes, 0 edges, per-field types — exactly what declaresForeignKeys: false promises
Monitoring OpenSearch 3.8.0, DB size 381.68 KB, 3 tables, "no limit published" for connections, "Not measured" for cache hit; Sessions empty and honest
Turkish data Şeyma Çağlar, İbrahim Öztürk, Gökhan Şahin, İzmir, Şanlıurfa intact through query, grid and ER labels

Four defects the browser found, all fixed in ca98d72

  1. The first click on an index failed on Elasticsearch. "Select Top 50 Documents" generated SELECT * FROM orders LIMIT 50; — parsing_exception, "extraneous input ';' expecting <EOF>". "Generate Query" failed identically. elasticsearch.md §5.4 had already recorded this as a fact about the engine rather than fixing it. Now ProviderCapabilities.statementTerminator: "none" on both products (the absence runs on OpenSearch too, so no dialect branch) and the generators ask the capability.
  2. The grid rendered NULL over values the API returned. TanStack reads a dot in accessorKey as a deep path, so shipping.city was looked up as row.shipping.city while the row carries that flat key. The API answered {"order_id":"o-001","shipping.city":"İzmir"}; the cell said NULL; the CSV export wrote İzmir. Every object field in a mapping flattens to a dotted leaf, so this was most columns on a search cluster — and the screen was the only surface that lied. Fixed with id + accessorFn.
  3. Plan mode wrote the wrong language. A plan run on OpenSearch, asked for one runnable statement, answered with a native aggregation body ({"size":0,"aggs":{…}}) — correct for the product, unrunnable through the SQL endpoint. The guard declined to classify it (NO_STATEMENT), so nothing ran, but the deliverable was unusable. queryLanguage: "sql" was already true and settled nothing: the engine's name carries the stronger prior. ProviderLabels.statementLanguage now names the endpoint and rules out each product's own alternatives (ES|QL upstream, PPL on the fork). After the fix the same objective drafted SELECT service, COUNT(*) AS error_count, AVG(latency_ms) … GROUP BY service ORDER BY error_count DESC, the guard read it as a bounded read, and the statement runs on the cluster.
  4. Two maintenance entries led nowhere. An index is addressable, so Redis schema-explorer actions generate MongoDB syntax: nothing the sidebar produces can be executed #427's tablesAreDerivedGroupings gate did not catch it: "Index Statistics" and "Merge Segments" rendered on an engine declaring supportsMaintenance: false, and /admin/operations gates its Global Operations card on that same capability — so the click landed on a page with no controls, no error, and none of the wording written for exactly that moment. The gate now requires an addressable row and a declared capability.

Plus a tri-sync fix: both docs said "multi-fields kept" in their §11 coverage summaries while introspect.ts drops them by a measured rule.

Re-verified in the browser after the fixes: the generated query is SELECT * FROM orders LIMIT 50 (runs, 40 rows), shipping.city renders İzmir / Ankara, the two maintenance entries are gone while Profile Table / Generate Code / Generate Test Data remain, and "Generate Query" emits "shipping.city" double-quoted (ES's form, from identifierQuoting) and runs.

Deliberately not fixed here — recorded in docs/BACKLOG.md, each pre-dating this PR and spanning every engine: the monitoring Queries tab tells all eleven engines to enable pg_stat_statements (U12); BEGIN/SANDBOX render where no provider declares transactions and /api/db/transaction refuses with 400 (U13); monitoring reports Buffer Pool 0% / Deadlocks 0 as measurements on engines that keep no such counter (U14).

Gates after the fixes: format · lint · typecheck · knip · test (7827 pass, 0 fail) · build, plus test:coverage + coverage:check at 38144/38144 lines (100.00%), build:lib and attw-relevant surfaces rebuilt.

…he credentials that work (#424)

The section was titled "database compose connection tests" and listed five services -
the HTTP ones - which read as the fixture's whole surface while eight more were in the
file. Engines were columns, so each new provider meant re-drawing the grid; they are
rows now, one per type-id, the same set the table at the top of this file carries.

Every row was verified against its running container rather than transcribed from the
compose file, and twice that mattered:

- Oracle's environment sets `ORACLE_PDB: ORCLPDB1`, a variable `gvenzl/oracle-xe` does
  not read - it takes `ORACLE_DATABASE`. The only PDB on the node is the image default
  `XEPDB1` (`SELECT name FROM v$pdbs`), and the listener refuses the other name.
- SQL Server's `MSSQL_DATABASE: mssql` is ignored by the official image, which creates
  no database. `master` is what the fixture guarantees (`SELECT name FROM sys.databases`).

Also recorded: SQLite and LibreDB have no service and never reach a network, so they
are rows that say so and point at their sample connections; Redis genuinely has no
password (`CONFIG GET requirepass` answers empty) rather than a blank default; and the
`Profile` column names Druid's seven containers as the one block a plain `up -d` leaves
out.
#424)

The fixture table's hosts are all `localhost`, which is only true for a Studio running
on the host. A plain `docker run -p 3000:3000 ghcr.io/libredb/libredb-studio` reaches
none of these services, and the failure is quiet in the worst way: from inside a
container `curl localhost:9200` returns no HTTP status at all, which reads like a
network problem rather than "you asked the wrong machine".

Three routes, measured on this machine rather than recited:

- Joining `libredb-studio_default` and addressing services by name, which is the one
  that needs no published ports. It carries its own port table because the numbers
  differ: **OpenSearch is 9200 inside the network**, not the 9201 the host table shows -
  9201 exists only to dodge a collision with the `elasticsearch` service, and there is
  no collision inside. Verified `http://opensearch:9200/` reports distribution
  `opensearch` 3.8.0, and `elasticsearch:9200` / `clickhouse:8123` answer too.
- `--add-host=host.docker.internal:host-gateway`, with the note that the name resolves
  by itself only on Docker Desktop. Verified on Linux: 9200 and 9201 both answer 200
  through it, so the host table applies as written.
- `--network host`, last, with what it costs stated: Linux only, no `-p`, and the whole
  host network namespace for the sake of one database.

Also recorded: the root `docker-compose.yml` and the fixture both name a container
`libredb-postgres`, so running both fails on the name.
…ion. because docker logs show the initial admin password
… the localhost trap (#424)

The Docker Hub page had drifted in five places, and the first two are the ones a reader
acts on:

**The engine list stopped at seven.** Couchbase, ClickHouse and Apache Druid had been
shipping for months, LibreDB is in the connection dialog, and this PR adds Elasticsearch
and OpenSearch - none of them appeared, on the page whose whole job is to say what the
image can talk to. All twelve are listed now, with the three read-only engines named as
read-only *because their own SQL is*, and the nine wire-compatible engines (MariaDB,
Citus, Valkey, DragonflyDB, KeyDB, FerretDB, CockroachDB, Materialize, RisingWave) get a
table of their own with the support level each one measured.

**SQLite's driver was wrong.** The page said `better-sqlite3`; the DB provider is
`bun:sqlite` / `node:sqlite`, selected by the runtime with a `LIBREDB_SQLITE_DRIVER`
override. `better-sqlite3` is the storage layer, which is a different thing on a
different code path.

**Plan mode's grounding claim predated #414.** It said a plan run is grounded on
PostgreSQL and SQLite; since #414 it is grounded on every engine, reads the inventory
before the model's first turn, and asks for a statement in that engine's own language.
Agent mode is still the two dialects, and that half was already right.

**Nothing said what `localhost` means inside a container** - on the page written for
people who run `docker run -p 3000:3000`. It is the container's own loopback, so a
database on the host or in another container is simply not there, and the failure
arrives as a timeout that never mentions the host. A new section gives the four cases
(container network by service name, `host.docker.internal` with the `host-gateway` flag
Linux needs, `--network host`, a managed service) plus the two facts that decide between
them: the port inside a network is the engine's own rather than the one you published,
and `host.docker.internal` resolves by itself only on Docker Desktop.

Also: the pin example was `0.9.16` (now 0.12.0), and the DBA line promised
VACUUM/ANALYZE/REINDEX as if every engine had them - it now says maintenance is offered
in each engine's own terms, and not at all where an engine has no maintenance statement.
…at ship (#424)

Twenty-three files told a prospective user this product speaks seven engines.
Couchbase, ClickHouse and Apache Druid have been shipping since 0.11.0, LibreDB is in
the connection dialog, and #424 adds Elasticsearch and OpenSearch - none of that had
reached the text people actually read before installing.

Each was edited in the form its channel allows rather than by pasting one sentence
everywhere:

- **Store listings with hard limits** got a fitted rewrite. Azure's search summary
  (100 chars) and short description (256) are asserted by
  `tests/unit/build-azure-package.test.ts`, so both were measured, not guessed - 98 and
  253. Chocolatey's summary, WinGet's ShortDescription and Homebrew's `desc` name a few
  engines and the count instead of listing twelve inside a one-line field.
- **The OLM bundle was regenerated, not hand-edited.** `operator/config` carries the
  description and `make -C operator bundle` produced the bundle copy; CI runs the same
  command and fails on drift, and the regenerated file matches this commit apart from
  the `createdAt` stamp. `operator-sdk bundle validate --select-optional
  suite=operatorframework` passes.
- **The two Flatpak descriptor sets** list engines as `<li>` items, so the new ones are
  items - with the read-only three marked read-only, and Redis's wire-compatible family
  (Valkey, DragonflyDB, KeyDB) named on its own line since the probe covered them.
  Flathub's copy is updated too: the channel is deprecated, not deleted.
- **Desktop and Flatpak `Keywords=`** gain SQLServer, ClickHouse, Couchbase, Druid,
  Elasticsearch and OpenSearch, which is what a store search matches on.
- **The FlatPark network comment** said the app reaches its databases "over TCP"; five
  of the twelve are HTTP, and the sandbox rationale is more honest for saying so.
- **CLAUDE.md's own overview** named seven engines, so the repo's instructions
  disagreed with the repo.
- Two docs were wrong about a different count: `docs/editor/query-optimization.md` said
  "the two non-SQL types" when LibreDB makes three, and `docs/AGENT_GUIDE.md` listed
  the engines where agent mode is unsupported without the two search engines - which is
  exactly the list a user checks before opening a run.

**Left alone deliberately:** `deploy/rancher/CATALOG_LISTING.md`, which already carries
an accuracy gate explaining that its prose is version-scoped to a released tag and that
the twelve-engine wording waits for a release. It is pinned on purpose, not stale.
`README.md`'s Phase 4 roadmap line is history and stays as written.

Gates: format, lint, typecheck, knip, `bun run test` (7827 pass, 0 fail),
`node scripts/distribution-check.mjs --strict`, and every edited JSON/YAML/XML parsed.
@sonarqubecloud

Copy link
Copy Markdown

@cevheri
cevheri merged commit 25712e6 into main Aug 19, 2026
25 of 26 checks passed
@cevheri
cevheri deleted the feat/phase1-search-providers branch August 19, 2026 13:54
cevheri added a commit that referenced this pull request Aug 19, 2026
)

Chart-only release: `appVersion` stays 0.12.0 and the deployed image is the one 0.1.37
already installs. The two versions move independently, and this one moves alone.

`description` named seven engines while twelve ship. That string is not internal - it is
what Rancher renders on its Apps catalog card and what ArtifactHub shows beside the
chart - so it was the last listing still telling a prospective user this product stops
at Redis, after every other channel was refreshed with #424 Phase 1. Couchbase,
ClickHouse and Apache Druid have shipped since 0.11.0; Elasticsearch and OpenSearch
landed in #429.

The same five engines are now chart `keywords`. ArtifactHub search matches on those, so
an engine that is only in prose is an engine nobody finds.

`artifacthub.io/containsSecurityUpdates` flips to **false**, and that is a statement
rather than a default: this chart version carries no application change at all, so
claiming a security update would be false. The app-level fixes stay recorded on 0.1.37,
the chart version that first shipped them. `artifacthub.io/changes` is rewritten for the
same reason - the entries a user reads on 0.1.38 describe 0.1.38, and the
`Track app release` line stays last so the next `chart:bump` can still find it.

Mechanics, in the order `.claude/skills/cut-release/SKILL.md` requires: `Chart.yaml` and
the chart README hand-edited first (`chart:bump` moves `version:` only when
`package.json` disagrees, which it does not here), then `chart:bump` last to refresh the
vendored `operator/helm-charts/` copy. `chart:check` reports 0.1.38 / 0.12.0 in sync and
`helm lint --strict` passes. `make -C operator bundle` produces no change beyond its
volatile `createdAt`, which the CI gate ignores, so the bundle is left alone.

The runbook gains the reminder that would have caught this earlier: checking
`description` and `keywords` against `SHIPPED` in `src/lib/db/compatibility.ts` is now
step 3 of the version bump, beside the `artifacthub.io/changes` rule that exists for the
same reason - a field `chart:bump` never touches goes stale silently - plus a Traps row.
cevheri added a commit that referenced this pull request Aug 20, 2026
…nd make every engine count agree (#440)

Elasticsearch and OpenSearch shipped in #429, but the epic's Phase 1 wrap-up never closed, so this
finishes it. Three things, none of them a behaviour change.

**Which distribution, and under which licence.** The box asked for it and its own premise was wrong:
it said "Elasticsearch is SSPL / Elastic License". The pinned image
`docker.elastic.co/elasticsearch/elasticsearch:9.1.4` ships **Elastic License 2.0** and says so in
its own `/usr/share/elasticsearch/LICENSE.txt`, read out of the running container rather than
recalled; `opensearchproject/opensearch:3.8.0` ships Apache-2.0 the same way. The blur actually
worth removing turned out to be a different one: both docs said "basic licence", where **basic** is
the self-generated subscription tier `GET /_license` reports and not the software licence at all.
Each doc now separates the two, and seven older sites that used "licence" in the tier sense are
reworded, because introducing the distinction is what made them ambiguous.

**Gate 7, run rather than argued.** #429 recorded it as "asserted rather than assumed" - both ids
ground through #414's provider-inventory path, so plan mode should work at no per-provider cost.
The epic's own wording is "Expected is not verified". Both are now run against the live containers:
plan mode captured 3 indices on each - its word, not "tables" - and drafted a statement over the
real `orders` index and `shipping.country`, a nested field it could only know from the mapping
capture. Applied unmodified it answered `TR | 40` on both, columns typed `keyword` and `long`.

**One engine count, stated with its denominator.** Three numbers were in circulation and each was
locally true: the issue said twelve, the README says thirteen, the login page says fourteen. They
count different sets, and none said which. The external count is thirteen and the internal one is
fourteen with the embedded `libredb`; every surface now agrees with its own list. That reached
further than expected - twenty-six files still enumerated engines without Trino, including the OLM
bundle, the Azure and Rancher listings and five packaging manifests. `snap/snapcraft.yaml` gains the
missing names but deliberately no number: Store copy can only be corrected by a re-release, so
introducing a count there creates drift nothing can fix in between.

The operator CSV was regenerated with `make -C operator bundle` rather than hand-mirrored, and the
generator reproduced it exactly.
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.

1 participant