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
are generated from these entries via tools/generate_blog_post.py.
Format follows Keep a Changelog
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, 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: [<pipeline>]
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: <expr>} 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.rssynthetic collection: queryable viafind/count/listCollections. Walks the existing oplog WT table via a private session for cross-thread visibility.list_databasessurfaceslocalwhenever the oplog is enabled.$merge whenMatched: [<pipeline>]with$$newbinding +letclause for user-defined vars (aggregate._stage_merge).$merge whenMatched: "delete"(MongoDB 5.0+).$mergeunique-index guard on non-_idonfields.$fillstage withvalue,locf, andlinearmodes (aggregate._stage_fill).$$var.field.pathdotted-path resolution inexpressions._resolve_var.docs/changelog.mdas the system of record (see the changelog itself and thechangelog/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).$mergevalidateswhenMatched/whenNotMatchedagainst the allowed string sets — typos surface asAggregateErrorinstead of silently falling through to the default merge.
0.5.1b16 — 2026-05-16¶
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-documenttyped-confirm uses the doc’s_idvalue (was the collection name).kill-cursortyped-confirm uses the collectionns(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:toPyMongoErrorso 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-tilesendpoint, partial template, and the two tests that exercised them.docs/admin.md: replaced stale### Consolesection 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_msfailpoint fields (failpoints._FailCommand).
Changed¶
mapReducereturns a graceful empty result for non-canonical map/reduce bodies (wire-shape probes pass).$changeStreamon a standalone topology is rejected with code 40573.getParameteradvertisesauthenticationMechanisms: ["SCRAM-SHA-256"].createIndexesrejects unknown per-index options (_INDEX_SPEC_KNOWN_OPTIONSwhitelist).createrejects unknown collection options (_CREATE_KNOWN_OPTIONSwhitelist).validate-allserialized (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 <testcase>
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.mdcross-driver normalized table.validation_summary/Python module (raw-artifact reader + renderer).invoke validate-summarytask.snapshotreadConcern rejected with code 246 (SnapshotUnavailable).Java gauge:
ChangeStreamsTest,UnifiedWriteConcernTest,VersionedApiTestunified-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.pywaves 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¶
writeConcernErrorattached onw > 1(CannotSatisfyWriteConcerncode 100).createIndexesvalidateswildcardProjectionshape.commitQuorumvalidated at top-level.$collStatssurfaces capped bounds (storageStats.{capped, max, maxSize}).
Changed¶
listIndexesrejects negativebatchSizewith 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 page for
the auto-generated commit-list notes from those tags.