The two servers¶
SecantusDB ships two separate servers that speak the same MongoDB wire protocol. You run one or the other — there is no in-process engine switching, and a client never sees a mix of the two.
The Rust server — the flagship: a self-contained Rust server (its own wire / dispatch / cursors / accept loop over the pure-Rust engines and a WiredTiger-backed store) that runs its accept loop off the GIL. It ships in the
SecantusDBwheel as an embedded lifecycle handle (RustServer) and as the standalonesecantusd-rsbinary; Python is only the launcher, never in the request path.The Python server — the original pure-Python
SecantusDBServer. It is the readable reference: every operator, stage and error message lands here first.
Each is held to mongod, never to the other. Comparing the two servers
to each other detects drift; it never says which one is right.
Both store data on the same vendored WiredTiger engine mongod ships, so
the on-disk durability story is identical. The difference is the layers above
storage — command dispatch, query planning, the operator engines — which are
Python in one server and Rust in the other.
Note
The old in-process accelerator (SECANTUS_ENGINE=rust /
SecantusDBServer(engine=...)) has been retired in favour of this
two-server split. The Python server is always pure-Python; the Rust engines
live only in the Rust server.
Which one should I use?¶
Rust server |
Python server |
|
|---|---|---|
Package |
the |
|
Run it as |
|
|
Conformance |
99.5% of pymongo’s own suite |
see the validation report |
Speed |
within 1.0×–3.2× of |
2×–27× |
Request path |
pure Rust (off the GIL) |
pure Python |
Use the Rust server to run SecantusDB — in tests, in CI, in a container. Reach for the Python server when you want to read how something works, or need one of the few features only it has (listed below and in the Feature comparison). Speed figures are from Benchmark.
The same split exists on the PostgreSQL side: the Rust PostgreSQL server
(secantusd-pg) and the Python one
(SecantusPGServer / secantusd-py-pg). See the
SQL / PostgreSQL interface.
Versioning¶
The two servers are separate deliverables on independent version lines;
they diverged at 0.5.2 and advance independently:
Python server —
0.6.0bN(PEP 440). This is the PyPI package version inpyproject.toml/secantus.__version__.Rust server —
0.5.3-beta.N(SemVer pre-release), carried in lockstep across thecrates/*workspace and surfaced over the wire asbuildInfo.secantusVersion, by thesecantusd-rs --versionflag, and by the embedded handle’sRustServer.version.
A change that touches only one server bumps only that server’s version.
Running each server¶
Python server¶
from pymongo import MongoClient
from secantus import SecantusDBServer
with SecantusDBServer(port=27017) as server:
client = MongoClient(server.uri)
client["mydb"]["users"].insert_one({"_id": 1, "name": "Joe"})
Or as a daemon — pip install puts a secantusd-py script on PATH:
secantusd-py --host 127.0.0.1 --port 27017
See Quickstart and Installation.
Rust server¶
The Rust server ships as the secantus-mdb crate on crates.io, not in the
Python wheel. cargo install builds it (WiredTiger included) and puts the
secantusd-rs daemon on PATH:
cargo install secantus-mdb --version 0.5.3-beta.196
secantusd-rs --host 127.0.0.1 --port 27017
From a Rust test, the same crate starts the server in-process as
secantus_mdb::Server. From a Python test, run secantusd-rs as a
subprocess and point pymongo at it, or use the Python server, which has
the same pymongo surface.
Standalone secantusd-rs archives (no Python needed) are attached to the
secantusdb-v* tags on
GitHub Releases. Both
Mongo daemons read the same secantusd.toml config (see
Configuration).
SQL / PostgreSQL servers¶
The Rust PostgreSQL server is the secantus-pg crate on crates.io (cargo install secantus-pg --version 0.1.0-beta.17), and also ships as a standalone
secantusd-pg archive on
the secantusd-pg-v* tags on
GitHub Releases:
secantusd-pg ./secantus-pg-data 127.0.0.1:5432
To offer TLS, give it a PEM certificate chain and its private key:
secantusd-pg ./secantus-pg-data 127.0.0.1:5432 \
--tls-cert-file server.crt --tls-key-file server.key
A client that asks for TLS then gets it (sslmode=require, verify-ca,
verify-full); one that does not ask is still served in the clear. Without
the pair the server answers a TLS request with “not supported”, as PostgreSQL
does with ssl = off. These two options are in 0.1.0-beta.9 and later.
An encrypted key – PKCS#8 (-----BEGIN ENCRYPTED PRIVATE KEY-----), or
OpenSSL’s legacy form (Proc-Type: 4,ENCRYPTED, AES or DES-EDE3 in CBC
mode) – needs --tls-key-passphrase-command CMD, PostgreSQL’s
ssl_passphrase_command: the first line the shell command prints is the
passphrase, %p in it is the prompt and %% a %. It runs again on each
reload.
A password login over TLS is offered SCRAM channel binding
(SCRAM-SHA-256-PLUS), so a client with channel_binding=require connects.
Three more options stand in for the pg_hba.conf this server does not have.
Channel binding and these options are in 0.1.0-beta.10 and later.
Option |
What it does |
PostgreSQL’s equivalent |
|---|---|---|
|
Refuses a connection that is not TLS. |
A |
|
Asks a TLS client for a certificate. One it sends must be signed by a CA in this PEM file; a client that sends none still connects. |
|
|
A certificate must be sent. |
|
|
A certificate must be sent and its common name must be the user name. A role with a password still gives it. |
|
|
The same check, and no password is asked for. |
The |
--tls-client-cert needs --tls-ca-file, and it refuses a connection that
is not TLS, as PostgreSQL allows clientcert on a hostssl line only.
secantusd-pg ./secantus-pg-data 127.0.0.1:5432 \
--tls-cert-file server.crt --tls-key-file server.key \
--tls-ca-file ca.pem --tls-client-cert verify-full
pg_stat_ssl has a row for each connection: whether it is TLS, the protocol
version, the cipher under OpenSSL’s name, and the client certificate’s
subject, serial number and issuer.
Four more, since 0.1.0-beta.12, say which certificates are accepted and which roles they may log in as:
Option |
What it does |
PostgreSQL’s equivalent |
|---|---|---|
|
Refuses a client certificate that a revocation list in this PEM file names. |
|
|
Compares the certificate’s whole subject with the user name, in place of its common name. With |
|
|
A user name map file. With |
|
|
Under |
|
With a revocation list, every certificate in a client’s chain needs a list
from its issuer that is in date, as OpenSSL requires of PostgreSQL: a
certificate whose CA has no list in the file, or only an expired one, is
refused. A list with no extensions, which openssl ca -gencrl writes by
default, is read. A list scoped by an issuing distribution point applies
only to the certificates it covers, and a delta list is read as a full one,
as OpenSSL applies them for PostgreSQL. A list with any other critical
extension stops the server from starting.
The subject under --tls-client-name DN is written as RFC 2253 has it, the
last attribute first: CN=alice,O=Example. That is seldom a user name, so
it goes with a map:
# pg_ident.conf: map name, what the certificate says, role
staff "/^CN=(.*),O=Example$" \1
staff build-agent ci
A line whose second field begins with / is a regular expression, and \1
in the role is what its first group matched. The expressions are the Rust
regex crate’s, which agree with PostgreSQL’s on what such a map is
written with; one that does not compile stops the server from starting.
The TLS files are read again on SIGHUP, and on select pg_reload_conf()
from a superuser: the certificate and key, the CA file, the revocation list
and the map. A connection made from then on gets what they hold. When they
cannot be used the server logs why and keeps what it had. SIGHUP reloads
only on a server started with TLS; without TLS it stops the server, as
before.
A certificate whose signature is RSA-PSS is bound to with the hash its
parameters name. One signed with Ed25519 names no hash: channel binding is
offered, as PostgreSQL offers it, and a login that takes it up fails with
“could not find digest for NID UNDEF”. libpq takes it up by default, so a
client of such a server needs channel_binding=disable.
Not supported: a key with a passphrase.
A pg_hba.conf¶
The options above apply one rule to every connection. --hba-file (since
0.1.0-beta.12) takes a pg_hba.conf in their place, and its lines decide
each login as PostgreSQL’s do: the first line whose type, database, user
and address match the connection says how it must prove who it is, and a
connection no line matches is refused.
# TYPE DATABASE USER ADDRESS METHOD
host all postgres 127.0.0.1/32 trust
hostssl appdb +staff 10.0.0.0/8 scram-sha-256
hostssl all agent all cert map=agents
host all all all reject
secantusd-pg ./secantus-pg-data 0.0.0.0:5432 \
--tls-cert-file server.crt --tls-key-file server.key --tls-ca-file ca.pem \
--hba-file pg_hba.conf --ident-file pg_ident.conf
Part of a line |
What is read |
|---|---|
Type |
|
Database |
|
User |
|
Address |
|
Method |
|
Options |
|
With a file, a role that has no password cannot pass a scram-sha-256 line,
and a user name with no role is refused, both as on PostgreSQL. The file is
read again on SIGHUP and on select pg_reload_conf(); one that cannot be
read leaves the old one in force.
A line this server cannot apply stops it from starting, with the line’s
number, so that no rule is silently skipped: a method that needs a service
outside the server (peer, ident, gss, sspi, ldap, radius, pam,
bsd). A host name for the address is matched by a reverse lookup of the
client’s address confirmed forward, as PostgreSQL’s is. @file inclusion reads a file of
names beside the pg_hba.conf, as PostgreSQL 15 does. samehost
and samenet match by this host’s interface addresses and their networks,
as PostgreSQL’s do.
--hba-file cannot be given with --tls-require, --tls-client-cert,
--tls-client-name, --tls-user-map or --trust-unknown-users: the file
says what those say.
A password file (~/.pgpass, or libpq’s passfile) works as it does with
PostgreSQL. It is read by the client library, which sends the password it
finds there; the server sees an ordinary password login.
A user name there is no role for is refused, as PostgreSQL refuses a role
that does not exist: it is asked for a password, as PostgreSQL asks one
under a scram-sha-256 line, and none it gives is right. Connect as
postgres, create the role first, or start the server with
--trust-unknown-users (trust_unknown_users=True on the embedded
handles), which lets such a user in without a password. A role with no
password connects without one, so give postgres a password
(ALTER ROLE postgres PASSWORD '...') and every login then needs one.
Until 0.1.0-beta.17 unknown users were trusted by default and
--refuse-unknown-users refused them; that flag is still accepted, and
changes nothing.
The Python one needs the sql extra (pip install "SecantusDB[sql]") and
runs as secantusd-py-pg:
secantusd-py-pg --host 127.0.0.1 --port 5432 --storage-path ./secantus-data
See the SQL / PostgreSQL interface.
What each server does not support¶
Both servers share the project-wide non-goals — anything that depends on real
cluster topology (multi-node replica sets, sharding, elections, cross-node
oplog), auth mechanisms beyond SCRAM (SHA-1 / SHA-256) and MONGODB-X509,
text / hashed / wildcard indexes, and
$where / $function / $accumulator / JS mapReduce (no embedded JS
runtime).
These are out of scope for both servers; the per-feature detail lives in
Compatibility.
Python server¶
The Python server implements the full in-scope wire surface. Its remaining
divergences are the stopgaps and known edge cases enumerated in
Compatibility — the _id numeric-type bridge is
undefined for NaN / infinity, top counters are always zero, and a handful
of date-format and
$group-ordering edge cases. It does not speak OP_COMPRESSED, which the
Rust server does (snappy, zstd, zlib); a driver asked to compress
simply sends uncompressed to it.
Rust server¶
The Rust server passes 99.5% of pymongo’s suite. The remaining feature differences (full three-way matrix in the Feature comparison) are:
Point-in-time restore over the wire —
secantusAdmin.restoreToTimestampis Python-server-only; the Rust server does the same restore via thesecantusd-rs restoreCLI subcommand.Session lifecycle —
endSessions/refreshSessions/killSessionsare acknowledged no-ops on the Rust server; the Python server tracks sessions with a 30-minute idle TTL.Operator edges — a handful of
$dateFromString/$dateToStringformat directives, Decimal128 arithmetic edges, and mixed-type sort orderings the Rust engine rejects rather than risk diverging frommongod.Thinner diagnostics —
serverStatus/dbStats/collStatsreturn a smaller subset of fields than the Python server’s replies.
Both servers mint resume tokens in SecantusDB’s own {s, t, n, k} layout
rather than mongod’s keystring format — tokens round-trip within SecantusDB but
cannot be presented to a real mongod (or vice versa).
The current Rust-server pass rate, and the exact set of failing pymongo tests, is regenerated each run into the Rust-server validation report. The gap against the Python-server report is the canonical, machine-checked statement of what the Rust server doesn’t support yet.