Updates.
August 2, 2026 · View on GitHub
███████╗████████╗██████╗ ██╗ ██╗██╗ ██╗███████╗
██╔════╝╚══██╔══╝██╔══██╗╚██╗ ██╔╝██║ ██╔╝██╔════╝
███████╗ ██║ ██████╔╝ ╚████╔╝ █████╔╝ █████╗
╚════██║ ██║ ██╔══██╗ ╚██╔╝ ██╔═██╗ ██╔══╝
███████║ ██║ ██║ ██║ ██║ ██║ ██╗███████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝
[ m o n g o ]
[MONGODB CLIENT FOR STRYKE // CRUD + AGGREGATION + INDEX ADMIN]
"Documents, one stryke pipe at a time."
MongoDB client for stryke. CRUD, aggregation, index admin against any MongoDB 5.0+ standalone, replica set, or sharded cluster. Opt-in package tier.
strykelang · MenkeTechnologiesMeta · stryke-mysql · stryke-postgres · stryke-redis · stryke-demo
Read the Docs · Engineering Report
Table of Contents
- [0x00] Install
- [0x01] Quick start
- [0x03] API reference
- [0x04] BSON type encoding
- [0x05] FFI layer
- [0x06] Tests
- [0x07] Dev workflow
- [0x08] Layout
- [0x09] Roadmap
- [0xFF] License
[0x00] Install
From a release (no rustc on the consumer machine):
s pkg install -g github.com/MenkeTechnologies/stryke-mongo
From a local checkout:
cd ~/projects/stryke-mongo
cargo build --release
s pkg install -g .
Or:
make install
The cdylib is dlopened in-process on first use Mongo. A shared tokio
runtime + mongodb::Client cache keyed by connection URI is held in
OnceCell — no fork-per-call, no fresh TCP+TLS+auth handshake.
[0x01] Quick start
use Mongo
$ENV{MONGODB_URI} = "mongodb://localhost:27017"
# Insert / find / update / delete — target is `DB/COLLECTION`.
val $r = Mongo::insert_one "app/users",
{ name => "alice", age => 30, role => "admin" }
p to_json $r->{inserted_id} # { "$oid": "65f9..." }
Mongo::insert_many "app/users", [
{ name => "bob", age => 25 },
{ name => "charlie", age => 35 },
]
p Mongo::count "app/users"
# Filters and operators use full Mongo query syntax.
val @over30 = Mongo::find "app/users",
filter => { age => { '$gt' => 30 } },
sort => { age => -1 },
limit => 100
@over30 |> ep
# Callback-per-doc variant (cdylib materializes the result, then iterates).
Mongo::find_stream "app/events",
filter => { ts => { '$gte' => $cutoff } },
callback => fn { process _ }
# Updates.
Mongo::update_one "app/users",
{ name => "alice" },
{ '$set' => { role => "superadmin" } }
Mongo::update_many "app/users", { active => 1 }, { '$inc' => { score => 1 } }
# Replace whole document.
Mongo::replace_one "app/users", { _id => $oid }, { ...new doc... }
# Aggregation pipeline (any standard stage).
val @top = Mongo::aggregate "app/orders", [
{ '$match' => { status => "paid" } },
{ '$group' => { _id => '$customer', total => { '$sum' => '$amount' } } },
{ '$sort' => { total => -1 } },
{ '$limit' => 10 },
]
# Index admin.
val $idx = Mongo::create_index "app/users", { email => 1 }
Mongo::indexes "app/users" |> ep
Mongo::drop_index "app/users", $idx
# Discovery.
val @dbs = Mongo::list_databases
val @colls = Mongo::list_collections "app"
URI overrides on every public fn:
val %prod = (uri => "mongodb+srv://user:pass\@cluster.example.com")
Mongo::find "logs/errors", filter => {...}, %prod
[0x03] API reference
Read paths
Mongo::find $target, %opts → @docs # opts: filter, projection, sort, limit, skip
Mongo::find_one $target, %opts → \%doc | undef
Mongo::find_stream $target, %opts → $count # callback per doc
Mongo::count $target, %opts → $n
Mongo::aggregate $target, \@pipeline, %opts → @docs
Convenience composites
Pure-stryke helpers over count / find_one / list_collections — no
extra round trips beyond the primitive they wrap.
Mongo::exists $target, $filter, %opts → 1 | 0 # count($filter) > 0
Mongo::find_value $target, $filter, $field, %opts → $value | undef # one field, projected
Mongo::collection_exists $db, $coll, %opts → 1 | 0 # $coll in list_collections($db)
Write paths
Mongo::insert_one $target, \%doc, %opts → { inserted_id }
Mongo::insert_many $target, \@docs, %opts → $inserted_count
Mongo::update_one $target, \%filter, \%update, %opts → { matched_count, modified_count, upserted_id }
Mongo::update_many $target, \%filter, \%update, %opts → { matched_count, modified_count, upserted_id }
Mongo::replace_one $target, \%filter, \%doc, %opts → { matched_count, modified_count, upserted_id }
Mongo::delete_one $target, \%filter, %opts → $deleted_count
Mongo::delete_many $target, \%filter, %opts → $deleted_count
Write %opts: upsert (insert when no match), array_filters (for
positional $[<id>] updates).
Atomic findAndModify
Mongo::find_one_and_update $target, \%filter, \%update, %opts → \%doc | undef
Mongo::find_one_and_replace $target, \%filter, \%doc, %opts → \%doc | undef
Mongo::find_one_and_delete $target, \%filter, %opts → \%doc | undef
%opts: return ("before" | "after"), upsert, sort, projection,
array_filters (update only).
Aggregation helpers
Mongo::distinct $target, $field, %opts → @values # opts: filter
Mongo::estimated_count $target, %opts → $n # fast metadata count
Metadata + admin
Mongo::list_databases %opts → @names
Mongo::list_collections $db, %opts → @names
Mongo::create_collection $db, $coll, %opts → { ok, created }
Mongo::drop_collection $target, %opts → { ok, dropped }
Mongo::drop_database $db, %opts → { ok, dropped } # delete a whole db
Mongo::create_index $target, \%keys, %opts → $index_name
Mongo::create_indexes $target, \@indexes, %opts → \%result # [{keys, name?, unique?}]
Mongo::drop_index $target, $name, %opts → 1 | ""
Mongo::drop_indexes $target, %opts → 1 | "" # drop ALL indexes except _id
Mongo::indexes $target, %opts → @specs
Mongo::aggregate_db $db, \@pipeline, %opts → @docs # database-level aggregation ($currentOp, $listLocalSessions)
Mongo::run_command $db, \%command, %opts → \%result # arbitrary db command
Mongo::rename_collection $target, $to, %opts → { ok, renamed } # opt: drop_target
Mongo::collection_specs $db, %opts → @specs # full listCollections specs (name, type, options, info); opt: filter
Mongo::validate $target, %opts → \%result # server-side integrity check; opts: full, repair
Mongo::coll_stats $target, %opts → \%stats # collStats
Mongo::db_stats $db, %opts → \%stats # dbStats
Mongo::explain $target, %opts → \%plan # opts: filter | pipeline, verbosity
Mongo::server_status %opts → \%status # opts: db (default admin)
Mongo::ping %opts → 1 | ""
Pure helpers (no connection)
Mongo::parse_connection_string($uri) → { scheme, srv, user, password, hosts:[{host,port}], database, params }
Mongo::build_connection_string(%opts) → $uri # inverse: hosts/srv/user/password/database/params → mongodb[+srv]:// URI (percent-encoded)
Mongo::valid_connection_string($uri) → { uri, valid, reason } # non-throwing predicate; enforces host rules (mongodb:// ≥1 host, mongodb+srv:// exactly 1 host + no port)
Mongo::redact_connection_string($uri, %opts) → $uri # mask the password (default ***) for safe logging; rest preserved byte-for-byte; opts: mask
Mongo::parse_namespace($ns) → { db, collection } # split on first dot
Mongo::build_namespace($db, $coll) → $ns # join db.collection; inverse of parse_namespace
Mongo::valid_collection_name($name, $db?) → { name, valid, reason } # MongoDB rules: no $, no null, not empty, no system. prefix; 255-byte ns with $db
Mongo::valid_database_name($name) → { name, valid, reason } # MongoDB db rules: not empty, <64 chars, no null/space/ /\."$*<>:|?
Mongo::valid_namespace($ns) → { namespace, valid, reason, database, collection } # validate a full db.collection in one call (splits on first dot; checks both parts + 255-byte limit)
Mongo::valid_field_name($name) → { name, valid, reason } # BSON key rules: not empty, no $ prefix, no . separator, no null
Mongo::escape_regex($value) → { value, escaped } # escape PCRE metacharacters (. ^ $ * + ? ( ) [ ] { } | \) for a literal $regex match
Mongo::unescape_regex($escaped) → { escaped, value } # inverse of escape_regex: recover the literal; rejects a real regex (unescaped metachar / dangling backslash)
Mongo::is_valid_objectid($id) → 1 | "" # 24-hex, validated via bson
Mongo::new_objectid() → $hex # fresh 24-hex ObjectId
Mongo::objectid_timestamp($id) → { epoch_seconds, epoch_millis, iso } # creation time from leading 4 bytes
Mongo::parse_objectid($id) → { hex, epoch_seconds, iso, random, counter } # full decomposition: timestamp + 5-byte random + 3-byte counter
Mongo::objectid_compare($a, $b) → { a, b, cmp, equal, older } # natural _id sort order (12 bytes lexicographic); finer than timestamp — counter breaks same-second ties; older = earlier-sorting id
Mongo::build_objectid($epoch_seconds, $random, $counter) → { oid, epoch_seconds, random, counter } # inverse: reassemble an ObjectId from its parts (random = 10 hex chars, counter = 24-bit)
Mongo::objectid_from_time(%opts) → $oid # {epoch_seconds|epoch_millis|iso} → boundary ObjectId (createFromTime); inverse of objectid_timestamp
Mongo::objectid_max_from_time(%opts) → $oid # same input → LARGEST ObjectId for that second (trailing 0xFF); $lte bound for _id time ranges
Mongo::objectid_range(\%start, \%end) → { start_epoch_seconds, end_epoch_seconds, min, max } # inclusive _id window: {_id:{$gte:min,$lte:max}} selects docs created in [start,end] (min=from_time, max=max_from_time)
Unlike a SQL DSN, parse_connection_string returns a host list (replica
sets) and recognizes mongodb+srv:// — it parses structure only, never
resolving SRV DNS.
Query builders (no connection)
Pure helpers that assemble filter / update / sort / projection / index-key
documents from convenient shapes — no FFI to mongo, no client. Hand the output
straight to find / update_one / create_index / etc.
Mongo::merge_filters(\@filters) → \%filter # combine filter hashrefs: shallow merge when keys are disjoint, {$and:[…]} when any key is shared (no clause lost); [] → {} ; single → unchanged
Mongo::build_update(%opts) → \%update # opts: set (→$set), unset (arrayref|hashref →$unset, values normalized to ""), inc (→$inc); ≥1 required
Mongo::build_sort(\@fields) → \%sort # entries: "field" (asc) | "-field" (desc) | [field, 1|-1|"asc"|"desc"]; order preserved
Mongo::build_projection(%opts) → \%projection # opts: include (→1) XOR exclude (→0); id (bool) controls _id even in the opposite mode
Mongo::normalize_index_keys(\@keys) → \%keys # entries: "field" | "-field" | [field, 1|-1|"2dsphere"|"text"|"hashed"…]; compound order preserved
Mongo::in_filter($field, \@values, %opts) → \%filter # { $field: { $in: […] } }; opt negate → $nin
Mongo::between_filter($field, %opts) → \%filter # opts: gte/gt (lower), lte/lt (upper); a bound + its strict variant on one side are mutually exclusive
Mongo::build_regex_filter($field, $value, %opts) → \%filter # literal (regex-escaped) $regex; opts: anchor (prefix|suffix|exact|contains), ignore_case
Mongo::or_filter(\@filters) → \%filter # disjunction: { $or: [...] }; [] → {} ; single → unchanged (counterpart to merge_filters' $and)
Mongo::exists_filter($field, %opts) → \%filter # { $field: { $exists: true } }; opt exists (bool, default true)
Mongo::elem_match_filter($field, \%query) → \%filter # { $field: { $elemMatch: {…} } }; all conditions bind to ONE array element
Mongo::text_filter($search, %opts) → \%filter # { $text: { $search: … } }; opts: language, case_sensitive, diacritic_sensitive (needs a text index)
Mongo::not_filter($field, \%expr) → \%filter # { $field: { $not: {…} } }; negates an operator expression (rejects bare values / $or/$and/$nor)
Plumbing
Mongo::version() → $version_string # cdylib's CARGO_PKG_VERSION
[0x04] BSON type encoding
The cdylib converts BSON ↔ JSON via MongoDB's relaxed extended JSON format, so non-JSON types round-trip cleanly:
| BSON | JSON |
|---|---|
String | string |
Int32, Int64 | number |
Double | number |
Decimal128 | {"$numberDecimal": "12.34"} |
Boolean | bool |
Null | null |
Array | array |
Document | object |
ObjectId | {"$oid": "65f9b1d2c3f6e9...."} |
DateTime | {"$date": "2026-05-17T02:30:00Z"} |
Binary | {"$binary": {"base64":"…","subType":"00"}} |
UUID | wrapped via $binary subtype 04 |
Regex | {"$regularExpression": {"pattern":"^foo","options":"i"}} |
Timestamp | {"$timestamp": {"t":..., "i":...}} |
You can pass extended-JSON wrappers back through filters / updates: a
filter like {"_id": {"$oid": "65f9…"}} will be re-parsed to a real
ObjectId before hitting the wire.
[0x05] FFI layer
Each Mongo::* wrapper builds a JSON args dict and calls a sibling
mongo__* symbol resolved out of libstryke_mongo.{dylib,so}. The
cdylib is dlopened in-process on first use Mongo (via stryke's
pkg::commands::try_load_ffi_for resolver hook). Its exports cover
version/ping, discovery, find/count/aggregate, write paths, index admin,
and connection-free helpers (mongo__parse_connection_string,
mongo__build_connection_string, mongo__valid_connection_string,
mongo__redact_connection_string,
mongo__parse_namespace, mongo__build_namespace, mongo__escape_regex, mongo__unescape_regex,
mongo__is_valid_objectid,
mongo__new_objectid, mongo__objectid_timestamp, mongo__parse_objectid, mongo__objectid_compare). The authoritative
list is [ffi].exports in
stryke.toml.
Errors come back as a {error} JSON payload; the stryke wrapper dies
with Mongo::<op>: <reason>.
v1 wire shape (historical helper binary)
stryke-mongo-helper find app/users --filter='{"name":"alice"}' --limit=10
stryke-mongo-helper insert-one app/users --doc='{"name":"alice"}'
cat docs.ndjson | stryke-mongo-helper insert-many app/users
stryke-mongo-helper aggregate app/orders \
--pipeline='[{"$group":{"_id":"$customer","total":{"$sum":"$amount"}}}]'
Output:
find,aggregate,list-databases,list-collections,indexes→ NDJSONfind-one→ single JSON doc (ornull)- writes / metadata → single JSON summary
- errors → stderr + non-zero exit
[0x06] Tests
cargo test # compiles, no live calls
MONGODB_URI=mongodb://localhost s test t/ # live round-trip
Tests use a unique stryke_test_$$ collection name and clean up.
Local test server:
brew install mongodb-community
mongod --dbpath /tmp/mdb --port 27017 &
[0x07] Dev workflow
make # release build
make debug
make test
make install
make clean
[0x08] Layout
stryke-mongo/
stryke.toml # stryke package manifest
Cargo.toml # Rust helper crate manifest
Makefile
src/lib.rs # single-file cdylib
lib/
Mongo.stk # `use Mongo`
t/
test_mongo.stk # live round-trip
test_stryke_mongo_surface.stk
examples/
crud.stk
aggregate.stk
index_admin.stk
counts.stk
discover.stk
.github/workflows/
ci.yml # mongo:7 service + live round-trip
release.yml # cross-compile + GH release on tag push
[0x09] Roadmap
Shipped: CRUD + aggregate (collection and database level) + index admin (create/
drop/list, plus drop-all), atomic findAndModify (update/replace/delete), distinct,
estimated count, collection create/drop/rename, database drop, arbitrary
run_command, upsert / array_filters write options, and connection-free
query builders (filter merge, update / sort / projection / index-key assembly,
$in / range / literal-$regex filters).
| Open | Later |
|---|---|
| Change Streams (requires replica set) | GridFS read/write |
| Multi-doc transactions (replica set required) | Canonical extended JSON for $numberLong precision |
bulk_write (mixed ordered/unordered) | Connection pool / persistent serve daemon |
[0xFF] License
MIT.