# Changelog All notable changes to SecantusDB are documented here. This file is the **system of record** for what shipped in each release — the per-release blog posts on [secantusdb.com](https://secantusdb.com/categories/releases.html) are generated from these entries via `tools/generate_blog_post.py`. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) with one extension: each release carries a one-to-three-paragraph **prose lede** between the date line and the structured `#### Added` / `#### Changed` / `#### Fixed` subsections. The prose lede is what the blog generator lifts verbatim as the marketing-post body, so it should read as a self-contained narrative — not as "v0.5.1bN ships X." This project adheres roughly to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), but while we're in beta the patch number `bN` rolls forward on every PyPI-visible push; the API surface itself is shaped by Semantic Versioning intent. ## [Unreleased] (No entries yet — the next release will be cut from work landing on `main` after v0.5.1b17.) ## [0.5.1b17] — 2026-05-17 ### `local.oplog.rs` queryable from pymongo, `$merge` pipeline form + `$fill` stage + `$$var.path` resolution Real mongod exposes the oplog as a queryable collection at `local.oplog.rs` — pymongo clients can `db.oplog.rs.find()` against it the same way they would against any collection. Until this release, SecantusDB's oplog was internal only: `Storage.read_oplog` / `oplog_floor_seq` / `oplog_tail_seq` were Python methods but had no wire surface. Now `local.oplog.rs` is a synthetic read-only view — `list_collections("local")` surfaces it, `find` / `count` / `listCollections.options` route to a reader that walks the oplog WT table directly, and write attempts (`insert`, `update`, `delete`, `findAndModify`, `drop`, `create`, `createIndexes`) refuse with code 13 (Unauthorized) like mongod does. The deferred admin UI `/oplog` page is unblocked as a follow-up; for now, debugging an in-flight change-stream pipeline is as simple as `client.local.oplog_rs.find({"op": "u"}).sort("ts", -1).limit(20)`. The aggregation expression library picks up two of the three remaining stages on most "more stages" wishlists. `$merge` was partly implemented; this batch fills in the rest: `whenMatched: []` runs a sub-pipeline against the matched target doc with `$$new` bound to the source doc and any user `let` vars threaded through; `whenMatched: "delete"` (MongoDB 5.0+) removes the matched doc; a unique-index guard refuses non-`_id` `on` fields without a `unique: true` index covering them, matching mongod's rule against silent on-field collapse. `$fill` lands fresh — the 5.3+ stage for filling missing/null fields. Three modes per output field: `{value: }` replaces with an evaluated expression; `{method: "locf"}` carries the last observation forward within the partition's sortBy order; `{method: "linear"}` interpolates between bracketing non-null anchors along the sortBy field (works for numbers and datetimes — timedelta arithmetic divides cleanly to float and multiplies back to timedelta). Partitioning via `partitionByFields` or `partitionBy`; sortBy required when any output uses `method`. The `$merge` pipeline form was the first thing in the repo to exercise `$$var.path` (e.g. `$$new.delta`), and surfaced that the expression evaluator only did exact-name var lookup. Fixed in the same batch: `$$var.field.path` now walks the dotted path into the resolved value across `$$ROOT.f` / `$$CURRENT.f` / user-let vars. #### Added - `local.oplog.rs` synthetic collection: queryable via `find` / `count` / `listCollections`. Walks the existing oplog WT table via a private session for cross-thread visibility. `list_databases` surfaces `local` whenever the oplog is enabled. - `$merge whenMatched: []` with `$$new` binding + `let` clause for user-defined vars (`aggregate._stage_merge`). - `$merge whenMatched: "delete"` (MongoDB 5.0+). - `$merge` unique-index guard on non-`_id` `on` fields. - `$fill` stage with `value`, `locf`, and `linear` modes (`aggregate._stage_fill`). - `$$var.field.path` dotted-path resolution in `expressions._resolve_var`. - `docs/changelog.md` as the system of record (see the [changelog](changelog) itself and the `changelog/` Python package that generates blog posts from it). #### Changed - Writes to `local.oplog.rs` (insert / update / delete / findAndModify / drop / create / createIndexes) refuse with code 13 (Unauthorized). - `$merge` validates `whenMatched` / `whenNotMatched` against the allowed string sets — typos surface as `AggregateError` instead of silently falling through to the default merge. ## [0.5.1b16] — 2026-05-16 ### Sidebar grouping, auto-refreshing connections and cursors, Roles in the nav The `/connections` and `/cursors` admin pages have always been live-data views — they read `currentOp` and render the connection / cursor list each time the page is requested — but they didn't refresh. The dashboard polls 1 Hz over a WebSocket; these two felt stale next to it. v0.5.1b16 extracts each table's tbody into an HTMX partial and lets the tbody itself swap every 5 s via `hx-trigger="every 5s" hx-get="/connections/_rows"`. The page chrome and column headers stay fixed; only the rows refresh. `/connections` also gains a (disabled) Actions column with a tooltip explaining that connection-kill is deferred until SecantusDB grows `killOp` — purely a layout-symmetry fix so the page mirrors the shape of `/cursors`. The sidebar gets two structural fixes. A `Roles` entry now lives directly under `Users` with a sub-nav indent (it was reachable only via the breadcrumb on `/users`, and `roles.html` was setting `active="users"` so the wrong sidebar item highlighted while you were on the page). A second visual separator above `Change stream` marks the boundary between per-target data pages and operational-state pages, mirroring the existing separator below `Server`. A separate fix: 15 `*_via_mongosh` cross-driver smoke tests are now grouped into a single xdist worker. Mongosh launches a full Node-based shell, and under heavy parallel load the PBKDF2 work inside SCRAM-SHA-256 auth could blow past mongosh's connect timeout. Tagged with `@pytest.mark.xdist_group(name="mongosh_smokes")` so they serialize. #### Added - Sidebar `Roles` entry under `Users` with sub-nav indent + correct active highlight on `/roles`. - Sidebar visual separator above the operational-state group (`nav-ops-start` CSS class). - `/connections` + `/cursors` auto-refresh tbody (`hx-trigger="every 5s"`) with new `_rows` partial endpoints. - `/connections` disabled Actions column for layout symmetry with `/cursors`; tooltip explains `killOp` is deferred. #### Fixed - 15 `*_via_mongosh` cross-driver smoke tests serialized via `xdist_group="mongosh_smokes"` to dodge PBKDF2-handshake timeouts under parallel-test CPU contention. ## [0.5.1b15] — 2026-05-16 ### One scaffold for every confirmation modal — escape, focus-trap, restored focus The `secantus-admin` UI has nine confirmation / edit modals (drop-database, drop-collection, drop-index, drop-user, change-password, manage-roles, edit-document, delete-document, kill-cursor). They were assembled at slightly different times and drifted in five different ways — different destructive-button copy, different typed-confirm targets (the delete-document modal asked the user to type the collection name shared by every row; the kill-cursor modal asked for the giant int cursor id), no Escape-to-close, no focus restoration to the trigger element, no focus trap so Tab leaked back into the page behind, and `aria-label="Close"` only on two of nine close buttons. v0.5.1b15 consolidates all nine on a shared scaffold: a new `modal-shell.js` exposes `openModal(url)` / `closeModal()` / `setupModal(el)` plus a global htmx hook that captures the trigger element so `closeModal()` can restore focus. Each modal partial has the same overlay shape — `x-init="setupModal($el)"`, `@click.self="closeModal()"`, `@keydown.escape.window="closeModal()"`, `role="dialog"`, `aria-modal`, `aria-labelledby` — and Tab / Shift+Tab cycle within the modal's focusable children rather than escaping into the page behind. Three substantive fixes ride along with the scaffolding: destructive button copy now always restates action+noun (Kill cursor / Delete document / Drop index / Drop user / Drop database / Drop collection); the delete-document typed-confirm asks for the doc's `_id` value rather than the collection name; the kill-cursor typed-confirm asks for the collection `ns` rather than the unguessable cursor id. None of these change SecantusDB's wire-protocol behaviour. #### Added - `static/js/modal-shell.js`: `openModal(url)`, `closeModal()`, `setupModal(el)`, htmx hook for trigger-element capture. - `[x-cloak]` CSS helper to prevent Alpine flash on first paint. #### Changed - All 9 confirmation / edit modal partials use the shared overlay shape with `role="dialog"` / `aria-modal` / `aria-labelledby`. - Destructive button copy restates action+noun across the board. - `delete-document` typed-confirm uses the doc's `_id` value (was the collection name). - `kill-cursor` typed-confirm uses the collection `ns` (was the cursor id). #### Fixed - Escape now closes every modal. - Focus restored to the triggering element after modal close. - Tab focus-trap inside modals. - `aria-label="Close"` on all 9 close buttons (was on 2). ## [0.5.1b14] — 2026-05-15 ### Admin UI punch list — five silent-failure modes fixed The May 2026 end-to-end review of the `secantus-admin` web UI catalogued five P0s — bugs that didn't crash anything but presented wrong information to the user. v0.5.1b14 fixes all five. None require any database-level change; this is purely admin-UI plumbing, but each one was either lying to the user or hiding a real error behind cheerful copy. The biggest was the **profiler page swallowing every exception** while reading `system.profile`. A bare `except Exception:` rendered "no entries yet — run an operation to see one appear here" no matter what the underlying error was, including the target server being completely unreachable. The clause is now narrowed to `PyMongoError` and the friendly error message gets funnelled into the page's normal error banner. The same page also had a **`flash` keyword argument that the template never rendered** — every settings change returned `HX-Redirect` and the user saw zero confirmation that anything had happened. The POST handler now re-renders the page inline with a flash banner that names the new level / slowms / sampleRate values. The other three are dead-code cleanups: the **doc tour** in `docs/admin.md` walked the user through a `/console` page that was renamed to `/query` two refactors ago; the **Maintenance "Drop collection" form** had an `hx-get` pointing at a route that never existed; and the **dashboard router** still exposed a `GET /_partials/dashboard-tiles` endpoint from before the WebSocket dashboard landed. #### Fixed - Profiler page: narrowed bare `except Exception:` to `PyMongoError` so server-down errors surface (`routers/profiler.py`). - Profiler page: added flash banner block to template + POST handler re-renders inline instead of `HX-Redirect`. - Maintenance "Drop collection" form: dropped dead `hx-get="/maintenance/drop-collection-redirect"` attribute. - Dashboard router: deleted unused `GET /_partials/dashboard-tiles` endpoint, partial template, and the two tests that exercised them. - `docs/admin.md`: replaced stale `### Console` section with `### Query (/query)` + `### Insert (/insert)` + new `### Server (/server)` subsection. ## [0.5.1b13] — 2026-05-15 ### Zero actionable failures — every driver gauge classified, every gap explained Over the past few releases the cross-driver gauge pass rate has been climbing — 99.5% at v0.5.1b4, 99.9% by last week's refresh. The last 0.1% was a handful of failures that either could not be fixed in SecantusDB (a Java-driver SDAM cascade triggered by a server-side `APIStrictError`), reproduced only under heavy parallel load (two `mongo-go-driver` flakes), or assumed a multi-node replica-set deployment SecantusDB deliberately doesn't simulate (Ruby's `w: 2` write-concern test). Reporting them as plain "failures" overstated the gap — but silently dropping them would let real regressions hide in the same column. v0.5.1b13 introduces **`validation_summary/expected_failures.py`** — a small per-gauge registry of `(pattern, rationale)` entries. The cross-driver summary now separates "Failed" (unexpected, a real bug we need to fix) from "Expected" (a documented gap with a one-line reason that ships in the report). A new **Adjusted** column reports the rate excluding expected failures from the denominator — "how much of the conformable surface actually conforms." Current numbers: **7,186 tests, 6,254 passed, 0 unexpected failures, 5 expected failures, 927 skipped — 100.0% adjusted across every driver.** This release also bundles the gauge improvements that landed since v0.5.1b4: `mapReduce` returns a graceful empty result for non-canonical bodies, `$changeStream` against a standalone topology is rejected with code `40573`, Node CSOT explain-plus-`timeoutMS` tests pass via a new `block_connection` / `block_time_ms` failpoint pair, `getParameter` advertises `authenticationMechanisms: ["SCRAM-SHA-256"]`, and `createIndexes` / `create` reject unknown options up-front. #### Added - `validation_summary/expected_failures.py`: per-gauge registry of documented-known failures with rationales. - Cross-driver summary "Expected" + "Adjusted pass rate" columns. - `block_connection` / `block_time_ms` failpoint fields (`failpoints._FailCommand`). #### Changed - `mapReduce` returns a graceful empty result for non-canonical map/reduce bodies (wire-shape probes pass). - `$changeStream` on a standalone topology is rejected with code 40573. - `getParameter` advertises `authenticationMechanisms: ["SCRAM-SHA-256"]`. - `createIndexes` rejects unknown per-index options (`_INDEX_SPEC_KNOWN_OPTIONS` whitelist). - `create` rejects unknown collection options (`_CREATE_KNOWN_OPTIONS` whitelist). - `validate-all` serialized (`max_workers=1`) to dodge load-induced inter-gauge flakes. ## [0.5.1b4] — 2026-05-12 ### Cross-driver conformance summary — 99.5% across 7,186 tests on one page Until this release, comparing SecantusDB's conformance across the five driver gauges (pymongo / mongo-java-driver / mongo-go-driver / mongo-node-driver / mongo-ruby-driver) required opening five different reports and squinting at five different per-category breakdowns whose denominators came from incompatible units of count — JUnit `` versus Mocha test versus RSpec example versus `go test` event versus pytest item. v0.5.1b4 ships **`docs/validation-summary.md`** — a single table that normalises on test count, one row per gauge, the same five columns across the board: tests run, passed, failed, skipped, pass rate. A new `validation_summary` Python module reads each gauge's raw artifact under `.validation/` directly and renders the table; a new `invoke validate-summary` task refreshes it. Current numbers: **7,186 tests, 6,232 passed, 33 failed, 921 skipped — 99.5% pass rate** across all five drivers. Java is biggest by raw count (4,710 tests, 4,242 passed); Node smallest (364). This release also rolls up two driver-gauge fixes that landed since v0.5.1b1: a Java widening to 21 of 112 driver-sync functional classes (+34 passes), and a snapshot-read-concern rejection that turned three `SessionsTest` snapshot-error scenarios from "expected error, got success" into "expected error, got `SnapshotUnavailable` (code 246)". #### Added - `docs/validation-summary.md` cross-driver normalized table. - `validation_summary/` Python module (raw-artifact reader + renderer). - `invoke validate-summary` task. - `snapshot` readConcern rejected with code 246 (`SnapshotUnavailable`). - Java gauge: `ChangeStreamsTest`, `UnifiedWriteConcernTest`, `VersionedApiTest` unified-spec runners (21 of 112 driver-sync functional classes total). #### Fixed - RTD build for v0.5.1b3 failed on a missing toctree entry for the new summary file; b4 is the first release where the docs match what's on PyPI. ## [0.5.1b1] — 2026-05-12 ### Java gauge scope made honest — 18 of 112 driver-sync classes, five named follow-ups The Java gauge passing rate had been reported at "100%" — but only across the 13 driver-sync functional classes the gauge was running. v0.5.1b1 widens the include set to 18 of 112 and adds an explicit **Scope** section to `docs/validation-report-java.md` that surfaces the "X of 112 driver-sync functional classes" denominator so the headline number isn't misleading. The widened set surfaced five real failures, all named and tracked in `tasks/backlog.md` §5: Java apiStrict pool-clear cascade, mapReduce non-canonical bodies, snapshot reads on standalone, distinct apiStrict — none are SecantusDB bugs, but they're now documented expected-fail entries. #### Added - Java gauge include set widened to 18 of 112 driver-sync functional classes (`java_validation/include_modules.py` waves 1 + 2). - "Scope" section in Java validation report exposing the include-set denominator (`java_validation/generate_report.py`). ## [0.5.0b18] — 2026-05-12 ### Ruby gauge climbs to 99%, completing the cross-driver 99–100% band The Ruby gauge had been the weakest of the five at ~95% — a handful of real SecantusDB gaps the Ruby driver exercises but the others don't. v0.5.0b18 closes the high-value ones: `writeConcernError` is now attached on `w > 1` (CannotSatisfyWriteConcern code 100), invalid `wildcardProjection` is rejected on `createIndexes`, `commitQuorum` is validated at the top level, `listIndexes` rejects negative batchSize (code 51024), and `$collStats` surfaces capped-collection bounds (`storageStats.{capped, max, maxSize}`). Net: Ruby gauge from 94.6% → 99.7%, 13 net passes. All five driver gauges now sit in the 99–100% band. #### Added - `writeConcernError` attached on `w > 1` (`CannotSatisfyWriteConcern` code 100). - `createIndexes` validates `wildcardProjection` shape. - `commitQuorum` validated at top-level. - `$collStats` surfaces capped bounds (`storageStats.{capped, max, maxSize}`). #### Changed - `listIndexes` rejects negative `batchSize` with code 51024. ## Older releases Releases before v0.5.0b18 (the `v0.3.0aN` and `v0.4.0bN` lines, and v0.5.0b1 through v0.5.0b3) shipped before this changelog was the system of record. See the [GitHub Releases](https://github.com/jdrumgoole/SecantusDB/releases) page for the auto-generated commit-list notes from those tags. [Unreleased]: https://github.com/jdrumgoole/SecantusDB/compare/v0.5.1b17...HEAD [0.5.1b17]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b17 [0.5.1b16]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b16 [0.5.1b15]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b15 [0.5.1b14]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b14 [0.5.1b13]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b13 [0.5.1b4]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b4 [0.5.1b1]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.1b1 [0.5.0b18]: https://github.com/jdrumgoole/SecantusDB/releases/tag/v0.5.0b18