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.1b24.)
0.5.1b24 — 2026-05-19¶
Geo: legacy $near sibling form, 2d quadtree covering, java gauge¶
Three geo improvements that close the long-standing tail of the phase 1/2 geo work and lift the mongo-java-driver gauge into the geo surface for the first time.
Legacy mongod 2d shape — {geo: {$near: [x, y], $maxDistance: r, $minDistance: r2}} with the distance bounds at sibling level
rather than nested inside $near — now matches end-to-end through
both the operator matcher and the 2d-index picker. This is exactly
what mongo-java-driver’s Filters.near(field, x, y, max, min)
and Filters.nearSphere(...) build. Unit conventions match
mongod: legacy $near takes the bound in input units (planar
Pythagoras); legacy $nearSphere takes radians on the unit sphere
(picker converts to meters for 2dsphere and to degrees for 2d).
The 2d range scan picks tighter Z-order ranges via a quadtree
decomposition of the bbox: each 2^k × 2^k power-of-2-aligned
quadtree cell that lands fully inside the bbox emits one
contiguous Z-range (the invariant that makes Z-order indexes
work). Partial-overlap cells recurse; pure-outside cells are
skipped. Falls back to the single coarse range if the
decomposition would exceed max_ranges=32. Tightens the WT range
scan on wider query polygons; correctness is unchanged
(per-doc verifier filters false positives either way).
mongo-java-driver’s GeoJsonFiltersFunctionalSpecification and
GeoFiltersFunctionalSpecification (driver-core functional)
joined the java gauge include list and both pass 10/10. They
exercise $geoWithin / $geoIntersects / $near / $nearSphere
through the driver’s Filters builder against a real 2d and
2dsphere index — the kind of integration coverage neither the
pymongo conformance gauge nor our in-tree pymongo tests reach.
Added¶
secantus.geo_index.planar_2d_covering_ranges()— quadtree Z-order range decomposition for 2d index scans. Returns up to 32 tight(lo, hi)ranges; falls back to a single coarse range on cap overflow.6 new tests in
tests/test_geo_query.py/tests/test_geo.py: sibling-form$nearwith$maxDistance, sibling-form annulus (max+min), sibling-form$nearSpherewith radians convention, single-range quadtree for an aligned bbox, multi-range quadtree for an off-axis bbox, fallback to single range under cap._DRIVER_CORE_FUNCTIONAL_INCLUDESinjava_validation/include_modules.py: brings the two upstream geo functional specs into the java gauge as:driver-core:testfiltered runs.docs/geospatial.md— dedicated reference page: operator-by-operator, both index types, doc-side shapes accepted, the legacy / GeoJSON / spherical distance-unit conventions, a worked deployment example, validation surface summary. Linked from the Highlights list and added to the Sphinx toctree.docs/indexes.md— new geospatial section pointing at the dedicated page; the “Acceleration summary across index types” table now covers2d,2dsphere, and compound geo + scalar.
Changed¶
_parse_near_specnow returns a 5-tuple(center, max_d, min_d, spherical, legacy_form); consumers use the newlegacy_formflag to pick the right unit conversion (legacy+spherical → radians; legacy+planar → input units; GeoJSON → meters).2d-index picker uses the multi-range coverer; existing single- range
planar_2d_coveringkept as the coarse fallback.docs/indexes.md— “What’s still missing” list rewritten. Multi-field sort acceleration, multikey indexing, and basic collation all shipped long ago and shouldn’t have been on the gap list; the actual remaining gaps (per-index collation, TTL background sweeper, text / hashed indexes) replace the stale entries.docs/production.md— added a paragraph on per-writewriteConcern: {j: true}routing as the finer-grained alternative to the daemon-widesync_on_commit = trueknob.
Fixed¶
Legacy mongod
{geo: {$near: [x, y], $maxDistance: r}}previously raisedunsupported query operator: $maxDistancebecause the dispatcher treated the sibling bound as a standalone operator. The matcher now skips the sibling keys when iterating and passes them into_op_geo_near.2d-index picker no longer over-filters on
$nearSpherelegacy form: the radians bound is converted to degrees before building the planar disk, matching mongod’s behaviour against a 2d index.
0.5.1b23 — 2026-05-19¶
Native TLS + mTLS + per-write j:true — production gaps closed¶
Three slices land together against the production-readiness gaps
called out in the docs/production.md page.
[tls] cert_file + [tls] key_file (in secantusdb.toml) or
--tls-cert-file / --tls-key-file (CLI) makes the daemon wrap
every accepted socket in TLS before the wire protocol starts.
Clients connect with mongodb://host:port/?tls=true&tlsCAFile=<ca>
and SecantusDB negotiates the TLS handshake itself; the
connection thread then sees an encrypted socket-like object and
serves mongo wire frames over it unchanged. This closes one of
the biggest production-deployment gaps the docs/production.md
page called out — operators no longer need to terminate TLS at an
nginx / HAProxy / stunnel reverse proxy that becomes part of the
trust boundary.
mTLS lands as a layer on top: set [tls] ca_file and the daemon
asks connecting clients for their own X.509 cert during the TLS
handshake, verifying it against the configured CA bundle. Set
[tls] require_client_cert = true to reject clients that don’t
present a cert; the default (false, CERT_OPTIONAL) verifies a
cert if presented and accepts clients without one — useful for
staged rollouts. mTLS is a coarse-grained “you’re someone we
approved of” gate; SCRAM-SHA-256 still identifies the specific
user on top. mongod’s MONGODB-X509 auth mechanism
(cert-subject-DN as the username, no SCRAM step) is a separate
follow-on slice.
Python’s PROTOCOL_TLS_SERVER (TLS 1.2+, no SSLv2/3 fallback,
default cipher list) is the only protocol mode. The SSLContext
is built once at startup and cached — hot cert rotation requires
a daemon restart. certbot renew --post-hook 'systemctl reload secantusdb' is the standard pattern. Without the cert / key
kwargs the daemon stays plaintext exactly as before — no
regression risk for the 1300+ existing tests.
The b20 sync_on_commit knob enabled per-commit fsync at the
connection level — every write on the daemon shared the same
durability mode. The third slice finishes the story: the per-write
writeConcern.j flag now threads from the wire layer through
Storage.insert / update_matching / delete_matching (and all
four findAndModify paths) into
_batch_transaction(sync=True), which calls
session.commit_transaction("sync=on"). A client can now mix
j: true and j: false writes against one daemon: the j:true
subset pays the per-commit fsync cost (closes the durability gap),
the rest stays fast.
Added¶
[tls]table insecantusdb.toml(cert_file,key_file,ca_file,require_client_cert). Half-configured TLS (only one of cert/key set) raisesValueErrorat startup so deployment mistakes can’t silently fall back to plaintext.--tls-cert-file/--tls-key-file/--tls-ca-file/--tls-require-client-certCLI flags. Standard precedence: SecantusConfig defaults < TOML < explicit CLI.SecantusDBServer(tls_cert_file=..., tls_key_file=..., tls_ca_file=..., tls_require_client_cert=...)kwargs. When cert/key are set anssl.SSLContextis built in__init__and used to wrap accepted sockets in_serve_forever. When ca_file is also set, the context asks clients for an X.509 cert during the handshake and verifies it against that CA.tests/test_tls.py: 12 tests viatrustmefor ephemeral CA + client cert fixtures. Covers TLS round-trip, non-TLS-client rejection, no-args plaintext path (no regression), half-configured raises, missing-cert startup error, active_conns leak guard, and the four mTLS modes (required + valid cert / required + no cert / required + foreign-CA cert / optional + both modes).journal: bool = Falsekwarg onStorage.insert/update_matching/delete_matching. When True, the WT transaction commits withsession.commit_transaction("sync=on")— forces a per-commit fsync of the log regardless of the connection’stransaction_syncconfig._batch_transaction(*, sync: bool = False)context-manager kwarg. The per-commit-fsync escape hatch the newjournalwrite kwargs route through.tests/test_write_concern_journal.py: 10 tests covering the storage-layer kwarg threading (_batch_transactionis invoked withsync=True/Falseappropriately), wire-level happy paths on insert / update / delete / findAndModify, and the positive + negative routing assertions.
Changed¶
TLS / mTLS handshake errors are logged + the socket closed + the active-connection slot released; the daemon keeps serving everyone else.
writeConcern: {j: true}is now honoured per-write: the wire layer extracts the flag and threads it through to_batch_transaction(sync=True). Previously the flag was accepted on the wire but had no effect — only the daemon-widesync_on_commitknob (b20) could enable per-commit fsync.docs/production.mdupdated: “Native TLS” is no longer in the gaps list; the dedicated TLS section now shows the in-process config plus the mTLS opt-in instead of an nginx-stream-module example.docs/configuration.mddocuments the full[tls]schema (cert / key / ca / require_client_cert), the hot-rotation caveat, and the cipher-suite “out of scope for v1” note.
Dependencies¶
trustme>=1.2added to thedevextra for the test CA fixture (transitively pullscryptography).
0.5.1b20 — 2026-05-19¶
secantusdb.toml config file, native checkpoint restore, j:true durability knob¶
Two production-shaping slices land together. A new
secantusdb.toml configuration file exposes every CLI flag plus
the WT and oplog knobs that were previously hard-coded — including
cache_size (so you can size the engine for your dataset instead
of running with the 1 GB test default) and a sync_on_commit
switch that closes the long-standing writeConcern: {j: true}
durability gap by enabling WT’s per-commit fsync. The loader
auto-discovers ./secantusdb.toml, ~/.secantus/secantusdb.toml,
and /etc/secantus/secantusdb.toml; an explicit --config PATH
overrides the search. CLI flags still win over file values, so the
file is a deployment baseline rather than a lock-in.
A new secantusAdmin.restoreArchive wire command and matching
secantusdb-restore-archive offline CLI close out the backup
story started in b18 — extract a backup .tar.gz into a target
directory the operator then points a fresh SecantusDB process at.
The admin UI’s per-row Restore button now adapts to backup type:
mongodump directories still call mongorestore; native .tar.gz
archives surface an inline target-dir field and an Extract action
that hits the new endpoint. Restore intentionally doesn’t try to
swap the WT home under a running server (the connection-thread
session-caching layer would need a wholesale rework first), and
matches how real mongod restore tooling already trains operators.
Drive-by fix: the admin UI’s “Existing backups” list now also
includes .tar.gz files. The native archives created by the b18
backup button were previously invisible because list_backups
only enumerated directories.
The new Running in production doc page ties the
config-file, native-backup, and restore work together — honest
comparison vs single-node Postgres (the more useful framing than
“SecantusDB vs mongod”), the gaps you have to accept, and a
concrete systemd / TLS / backup / monitoring deployment shape.
Added¶
Running in production docs page — honest comparison vs single-node Postgres (the more useful framing than “SecantusDB vs mongod-for-prod”), the gaps you must accept (no native TLS, no PITR, no replication, beta maturity), and a concrete deployment shape:
systemdunit,secantusdb.tomlwithsync_on_commit = true, SCRAM auth provisioning, nginx stream TLS termination, hourly native checkpoint backups with off-host sync, the restore drill,serverStatusscraping for Prometheus / Datadog, and capacity sizing notes forcache_size.secantusdb.tomlconfiguration file (see Configuration for the full schema). Auto- discovered from./secantusdb.toml,~/.secantus/secantusdb.toml,/etc/secantus/secantusdb.toml;--config PATHdisables discovery and loads a specific file. Unknown keys / unknown top-level tables fail loudly at startup so typos can’t silently leave the engine running on the hard-coded default.secantus.config.SecantusConfigdataclass +load_config()/apply_overrides()helpers. CLI flags’ argparse defaults are nowNone(the “user did not pass this” sentinel) so the precedence chain isSecantusConfig defaults < secantusdb.toml < explicit CLI flag— file is a per-deployment baseline, the CLI overrides for one-off runs.New CLI flags exposing previously-hard-coded knobs:
--cache-size,--session-max,--sync-on-commit,--oplog-retention-seconds,--oplog-max-entries. Each has a matching[storage]/[oplog]key in the config file.Storage.__init__acceptscache_size,session_max,sync_on_commitkwargs. The WT engine config string is built from these instead of being a hard-coded literal.secantusAdmin.restoreArchivewire command. AcceptsarchivePath(server-side path to.tar.gz),targetDir(extraction destination), and optionalallowExisting(overlay into a non-empty dir). Returns{targetDir, fileCount, archive, ok: 1}. RBAC:fsyncaction, cluster scope.secantus.storage.extract_backup_archive(archive_path, target_dir, *, allow_existing=False)— module-level helper shared by the wire command, the admin route, and the CLI. Validates that the archive contains aWiredTigermetadata file before unpacking, so a malformed tarball can’t pollute the target.secantusdb-restore-archiveconsole script (new[project.scripts]entry). Same validation as the wire command, no server needed.Admin UI per-row Extract action on
.tar.gzrows, posting toPOST /backup/restore-archivewith editable target-dir form field; the existingRestorebutton still handles mongodump directories.
Changed¶
writeConcern: {j: true}is now honourable end-to-end via[storage] sync_on_commit = true(or--sync-on-commit), which sets WT’stransaction_sync=(enabled=true,method=fsync). Closes the long-standing durability gap previously documented in the backlog. Off by default (matches mongod’s default{w:1, j:false}) since the throughput cost is significant.secantus.admin.backup.list_backups()now includes*.tar.gzfiles alongside directories. Native-archive backups produced by b18’s backup button were previously invisible in the admin UI’s “Existing backups” list.MongoFacade.restore_archive(archive_path, target_dir, *, allow_existing=False)— new admin client facade method.
Fixed¶
“Existing backups” table on
/backupwas silently dropping every.tar.gzproduced by the native checkpoint backup path introduced in v0.5.1b18 (only dump directories were listed). Both kinds now render with the correct per-row restore action.
0.5.1b18 — 2026-05-18¶
Native WT-checkpoint backups, admin UI /oplog page, and change-stream fidelity wins¶
The natural follow-on to v0.5.1b17’s local.oplog.rs synthetic
collection lands as the admin UI /oplog page: a paged entry
browser with a window selector (last 50 / 500 / 5000), op-checkbox
filter (i / u / d / c / n), ns substring filter, and a
per-row expandable JSON body. Auto-refreshes every 5 s. The data
source is just client.local.oplog_rs.find() — no new server-side
surface needed, only the page chrome and an _rows partial that
follows the same pattern as /connections + /cursors.
showExpandedEvents on change streams now matches mongod: the flag
defaults to false, and DDL “expanded” events (createIndexes,
dropIndexes) are suppressed unless the user opts in via
coll.watch(show_expanded_events=True). Previously these surfaced
unconditionally — more permissive than mongod, and broke the
conformance contract for tests that assume the stable v1 event set.
killOp lands as a real wire command that closes the target
connection’s socket via shutdown(SHUT_RDWR). Any in-flight command
finishes, the per-connection thread’s next recv returns 0, the
loop exits, and the connection unregisters cleanly. Real mongod uses
a per-op interrupt flag, which would need cancellation infrastructure
SecantusDB doesn’t carry — but “close the socket” is the visible
end-state users care about, and the kill-and-reap admin button on
/connections is now functional.
$sample becomes deterministic when SECANTUS_SAMPLE_SEED=<n> is
set in the environment. Builds a dedicated random.Random(seed)
instance at module load instead of mutating the global random
state, so other code sharing the process keeps its own entropy.
Closes the long-standing test-flake source where $sample results
varied run-to-run.
Added¶
Admin UI
/oplogpage (routers/oplog.py+templates/pages/oplog.html+templates/partials/oplog_rows.html): window / op / ns filters, expandable per-row JSON, 5 s auto-refresh, sidebar entry between Profiler and Maintenance.killOpwire command +kill(conn_id)onConnectionRegistry(shuts down the socket viashutdown(SHUT_RDWR)). Per-connection sockets are now stashed on the registry at_handle_clienttime.A_KILLOPprivilege action insecantus.rbac; granted byclusterAdminandroot.Admin UI
/connectionsKill button (was a placeholder), typed-confirm modal (partials/connection_kill_modal.html), facadekill_connection(conn_id)method.ChangeStreamSpec.show_expanded_eventsparsed from$changeStream.showExpandedEvents; threaded intochangestreams.project.SECANTUS_SAMPLE_SEEDenv var (read ataggregatemodule import) —$sampleuses a dedicatedrandom.Random(seed)when set.secantusAdmin.backupArchivewire command +Storage.create_archiveadmin UI “Run native checkpoint backup” button: forces a WT checkpoint then tars the storage directory into a single
.tar.gz. Faster + atomic vsmongodump; restore is “extractstart a new SecantusDB pointing at it”. Rigorous round-trip test coverage in
tests/test_backup_restore.py(doc identity at scale, every non-default index shape, oplog tail continuity, capped collection options + FIFO state, SCRAM users / roles, concurrent-writes consistency, archive portability, repeated- backup idempotency).
$densifymonth / quarter / year units viadateutil.relativedelta.quarteris canonically 3 months. Addspython-dateutil>=2.8to the runtime dependencies (pure Python, available almost everywhere as a transitive dep).
Changed¶
changestreams.projectsuppressescreateIndexes/dropIndexesevents unless the caller passedshow_expanded_events=True(mongod-faithful default-off). The three existing tests + cross-driver DDL smokes (mongosh / node / go / java) all set the opt-in.
Fixed¶
Closes backlog entry
$sample uses random.sample without a fixed seed— deterministic via env var.Closes backlog entry
killOp / connection-close command— admin UI Kill button is functional.Closes backlog entry
showExpandedEvents — accepted, ignored.Closes backlog entry
Admin UI /oplog page.updateDescription.truncatedArraysnow emits for any array shrink (not just strict head-prefix), with indexedupdatedFieldsfor kept-prefix changes — matches mongod’s $v:2 in-place diff rather than wholesale-replacing on any reshape. Same-length-with- changes arrays also produce indexedarr.<i>updates now (previously wholesale). Closes the §3.2 backlog entry.
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.