>_STRYKE-MONGO
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.
Overview
stryke-mongo is the MongoDB connector in the stryke package family. It exposes 75 public Mongo::* functions spanning CRUD, aggregation (collection- and database-level), atomic findAndModify, index administration, collection/database admin, server diagnostics, and a large set of connection-free pure helpers (URI parsing, namespace validation, ObjectId decomposition, and query/filter builders). It targets MongoDB 5.0+ standalone servers, replica sets, and sharded clusters, over both mongodb:// and mongodb+srv:// connection strings.
The driver itself — the official mongodb Rust crate (v3, server compat profile compat-3-0-0) plus tokio, rustls, a DNS resolver, and the BSON stack — ships as a single Rust cdylib (libstryke_mongo.{dylib,so}) that stryke dlopens in-process on the first use Mongo. Core stryke is never linked against these dependencies; you pay for the driver only in scripts that ask for it.
Of the 75 public functions, 71 are direct FFI exports (the authoritative list is [ffi].exports in stryke.toml); the remaining 4 (find_stream, exists, find_value, collection_exists) are pure-stryke composites built over the primitives, adding no extra round trips beyond the call they wrap.
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 # produces target/release/libstryke_mongo.{dylib,so}
s pkg install -g . # cdylib lands in ~/.stryke/store/mongo@<version>/
# one-liner
make install
The -g flag installs globally (no surrounding stryke project required). The cdylib is dlopened in-process on first use Mongo — no helper-binary fork per call. Any stryke script that declares use Mongo resolves the package automatically. The Makefile also wraps the common flows: make (release build), make debug, make test, make install, make clean.
Quick start: use Mongo
Targets are written DB/COLLECTION (or DB.COLLECTION). Filters, updates, and pipelines use full MongoDB query syntax. Set the server with $ENV{MONGODB_URI}; the default is mongodb://127.0.0.1:27017.
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
# 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
| insert one | val $r = Mongo::insert_one "app/users", { name => "alice", age => 30 } |
| count | p Mongo::count "app/users" |
| find with filter + sort | val @over30 = Mongo::find "app/users", filter => { age => { '$gt' => 30 } }, sort => { age => -1 }, limit => 100 |
| update one | Mongo::update_one "app/users", { name => "alice" }, { '$set' => { role => "superadmin" } } |
| aggregation pipeline | val @top = Mongo::aggregate "app/orders", [ { '$match' => { status => "paid" } }, { '$group' => { _id => '$customer', total => { '$sum' => '$amount' } } } ] |
| create an index | val $idx = Mongo::create_index "app/users", { email => 1 } |
| discovery | val @colls = Mongo::list_collections "app" |
Connection URI + per-call overrides
The default URI comes from $ENV{MONGODB_URI} (falling back to mongodb://127.0.0.1:27017). Every public function accepts a trailing uri => option that overrides the connection for that single call — the cdylib keeps a mongodb::Client cache keyed by URI, so multiple URIs each get their own pooled client.
val %prod = (uri => "mongodb+srv://user:pass@cluster.example.com")
Mongo::find "logs/errors", filter => { level => "error" }, %prod
The find_stream default connect timeout is short (v0.2.1+), so an unreachable server trips quickly — eval { Mongo::ping() } is the idiomatic liveness guard before live work (used by every example).
CLI: mongo
The package also ships a mongo command for shell-pipeline use. Writes read NDJSON from stdin where noted; reads (find, aggregate, list-databases, list-collections, indexes) emit NDJSON.
mongo find app/users --filter='{"age":{"$gt":30}}' --sort='{"age":-1}' --limit=20
mongo find-one app/users --filter='{"name":"alice"}'
mongo insert-one app/users --doc='{"name":"alice","age":30}'
cat docs.ndjson | mongo insert-many app/users
mongo update-one app/users --filter='{"name":"alice"}' --update='{"$set":{"role":"admin"}}'
mongo update-many app/users --filter='{"active":true}' --update='{"$inc":{"score":1}}'
mongo replace-one app/users --filter='{"_id":{"$oid":"..."}}' --doc='{"name":"x"}' --upsert
mongo delete-one app/users --filter='{"name":"alice"}'
mongo delete-many app/users --filter='{}'
mongo count app/users [--filter='{...}']
mongo aggregate app/users --pipeline='[{"$group":{"_id":"$role","n":{"$sum":1}}}]'
mongo list-databases
mongo list-collections app
mongo create-index app/users --keys='{"email":1}' --unique --name=uniq_email
mongo drop-index app/users uniq_email
mongo indexes app/users
mongo ping
mongo build # cargo build --release
mongo version
Global flag (also an env var): -u, --uri URI → $MONGODB_URI — a mongodb:// or mongodb+srv:// connection string.
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 — calls callback => fn { … } 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 trip 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 projected field |
| Mongo::collection_exists | $db, $coll, %opts → 1 | 0 |
API reference: 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 |
API reference: 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 } |
| Mongo::rename_collection | $target, $to, %opts → { ok, renamed } — opt: drop_target |
| 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::collection_specs | $db, %opts → @specs — full listCollections specs; opt: filter |
| Mongo::aggregate_db | $db, \@pipeline, %opts → @docs — database-level aggregation |
| Mongo::run_command | $db, \%command, %opts → \%result — arbitrary db command |
| Mongo::validate | $target, %opts → \%result — 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)
These never open a client — they run entirely in the cdylib's BSON/URI/ObjectId logic. parse_connection_string returns a host list (replica sets) and recognizes mongodb+srv://; it parses structure only and never resolves SRV DNS.
| Mongo::parse_connection_string | $uri → { scheme, srv, user, password, hosts:[{host,port}], database, params } |
| Mongo::build_connection_string | %opts → $uri — inverse; percent-encoded |
| Mongo::valid_connection_string | $uri → { uri, valid, reason } — non-throwing predicate; host rules |
| Mongo::redact_connection_string | $uri, %opts → $uri — mask password (opt: mask, default ***) |
| Mongo::parse_namespace | $ns → { db, collection } — split on first dot |
| Mongo::build_namespace | $db, $coll → $ns |
| Mongo::valid_collection_name | $name, $db? → { name, valid, reason } |
| Mongo::valid_database_name | $name → { name, valid, reason } |
| Mongo::valid_namespace | $ns → { namespace, valid, reason, database, collection } |
| Mongo::valid_field_name | $name → { name, valid, reason } |
| Mongo::escape_regex | $value → { value, escaped } — escape PCRE metacharacters |
| Mongo::unescape_regex | $escaped → { escaped, value } — inverse; rejects a real regex |
| Mongo::is_valid_objectid | $id → 1 | "" — 24-hex via bson |
| Mongo::new_objectid | → $hex — fresh 24-hex ObjectId |
| Mongo::objectid_timestamp | $id → { epoch_seconds, epoch_millis, iso } |
| Mongo::parse_objectid | $id → { hex, epoch_seconds, iso, random, counter } |
| Mongo::objectid_compare | $a, $b → { a, b, cmp, equal, older } — natural _id sort order |
| Mongo::build_objectid | $epoch_seconds, $random, $counter → { oid, epoch_seconds, random, counter } |
| Mongo::objectid_from_time | %opts → $oid — boundary ObjectId (createFromTime) |
| Mongo::objectid_max_from_time | %opts → $oid — LARGEST ObjectId for that second ($lte bound) |
| Mongo::objectid_range | \%start, \%end → { start_epoch_seconds, end_epoch_seconds, min, max } |
Query builders (no connection)
Pure helpers that assemble filter / update / sort / projection / index-key documents from convenient shapes — no FFI to a live server. Hand the output straight to find / update_one / create_index.
| Mongo::merge_filters | \@filters → \%filter — shallow merge when keys disjoint, {$and:[…]} when shared |
| Mongo::or_filter | \@filters → \%filter — { $or: [...] } |
| Mongo::build_update | %opts → \%update — opts: set ($set), unset ($unset), inc ($inc); ≥1 required |
| Mongo::build_sort | \@fields → \%sort — "field" | "-field" | [field, 1|-1|"asc"|"desc"] |
| Mongo::build_projection | %opts → \%projection — include XOR exclude; id controls _id |
| Mongo::normalize_index_keys | \@keys → \%keys — "field" | "-field" | [field, 1|-1|"2dsphere"|"text"|"hashed"] |
| Mongo::in_filter | $field, \@values, %opts → \%filter — $in; opt negate → $nin |
| Mongo::between_filter | $field, %opts → \%filter — opts: gte/gt, lte/lt |
| Mongo::build_regex_filter | $field, $value, %opts → \%filter — literal $regex; opts: anchor, ignore_case |
| Mongo::exists_filter | $field, %opts → \%filter — $exists; opt exists (default true) |
| Mongo::elem_match_filter | $field, \%query → \%filter — $elemMatch (one array element) |
| Mongo::text_filter | $search, %opts → \%filter — $text; opts: language, case_sensitive, diacritic_sensitive |
| Mongo::not_filter | $field, \%expr → \%filter — $not over an operator expression |
BSON type encoding
The cdylib converts BSON ↔ JSON via MongoDB's relaxed extended JSON, so non-JSON types round-trip cleanly. You can pass extended-JSON wrappers back through filters / updates: a filter like {"_id": {"$oid": "65f9…"}} is re-parsed to a real ObjectId before hitting the wire.
| String, Int32/Int64, Double, Boolean, Null | string / number / number / bool / null |
| Array, Document | array / object |
| Decimal128 | {"$numberDecimal": "12.34"} |
| 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":...}} |
Why a package, not a builtin
The official mongodb Rust crate pulls in tokio, rustls, a DNS resolver, and the BSON stack. Ships once as an opt-in package.
The stryke side is a thin .stk wrapper that calls mongo__* FFI symbols on the cdylib; the heavy code lives in libstryke_mongo.{dylib,so}, dlopened in-process on first use Mongo. Core stryke is never linked against this package's deps.
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); a shared tokio runtime + a mongodb::Client cache keyed by connection URI is held in OnceCell — no fork-per-call, no fresh TCP+TLS+auth handshake per call. Responses are JSON; errors come back as a {"error": "<msg>"} payload and the wrapper dies with Mongo::<op>: <reason>. The authoritative export list is [ffi].exports in stryke.toml (71 symbols).
Examples
Runnable scripts live under examples/; each guards live work behind eval { Mongo::ping() } so it exits cleanly with no server.
| examples/crud.stk | insert_one / insert_many / count / find / update_one / find_one / delete_many round-trip |
| examples/aggregate.stk | $group + $sum + $sort pipeline over seeded orders |
| examples/index_admin.stk | create_index (unique, named) / indexes / drop_index |
| examples/counts.stk | per-collection document counts across all databases (read-only) |
| examples/discover.stk | version + list_databases + list_collections tour |
# run any example (defaults to mongodb://127.0.0.1:27017) s examples/crud.stk MONGODB_URI=mongodb://localhost s examples/aggregate.stk
Tests + troubleshooting
cargo test # compiles, no live calls MONGODB_URI=mongodb://localhost s test t/ # live round-trip # local test server brew install mongodb-community mongod --dbpath /tmp/mdb --port 27017 &
Tests use a unique stryke_test_$$ collection name and clean up after themselves. If a call dies with Mongo::<op>: <reason>, the <reason> is the driver/server error carried back across the FFI {"error": …} envelope. A long-hanging call against an unreachable host means the connect timeout has not yet elapsed — guard live work with eval { Mongo::ping() } and check $@.
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.
| Open | Change Streams (replica set); multi-doc transactions (replica set); bulk_write (mixed ordered/unordered) |
| Later | GridFS read/write; canonical extended JSON for $numberLong precision; connection pool / persistent serve daemon |
Layout
stryke-mongo/ ├── Cargo.toml # cdylib crate manifest (crate-type = ["cdylib"], publish = false) ├── src/ │ └── lib.rs # cdylib — mongo__* extern "C" exports ├── lib/ # stryke .stk wrapper(s) — `use Mongo` ├── stryke.toml # stryke package manifest ├── t/ # zunit-style tests ├── examples/ # runnable .stk examples ├── Makefile # `make install` builds + installs └── docs/ # this site (GitHub Pages)
Sibling packages
Part of the stryke package family. Browse the others via the MenkeTechnologiesMeta umbrella repo:
- stryke-arrow — Apache Arrow / Parquet / Feather
- stryke-aws — S3, DynamoDB, SQS, Lambda, STS
- stryke-azure — Blob, Queues, Cosmos DB, Key Vault, Entra
- stryke-clickhouse — ClickHouse
- stryke-demo — live demos for every connector
- stryke-docker — Docker daemon API
- stryke-duckdb — embedded DuckDB SQL engine
- stryke-email — email + campaign client
- stryke-fleet — parallel expect/PTY automation
- stryke-gcp — Cloud Storage, Pub/Sub, Secret Manager, BigQuery, Firestore
- stryke-grpc — reflection-based gRPC client
- stryke-gui — mouse / keyboard / screen / clipboard automation
- stryke-k8s — Kubernetes
- stryke-kafka — Apache Kafka
- stryke-mcpd — MCP servers without a runtime
- stryke-mssql — Microsoft SQL Server
- stryke-mysql — MySQL / MariaDB
- stryke-neo4j — Neo4j graph (Bolt)
- stryke-office — Office docs / PDF / images
- stryke-parquet — Parquet file inspector
- stryke-polars — Polars + ndarray + linalg + FFT
- stryke-postgres — PostgreSQL
- stryke-redis — Redis / Valkey
- stryke-scrape — web scraping / crawling
- stryke-scylla — ScyllaDB / Cassandra
- stryke-search — Elasticsearch / OpenSearch
- stryke-selenium — browser automation (WebDriver)
- stryke-spark — Spark Connect (no JVM)
- stryke-utils — pure-stryke utility belt
- stryke-zmq — ZeroMQ messaging