// STRYKE-MONGO — MONGODB CLIENT FOR STRYKE // CRUD + AGGREGATION + INDEX ADMIN

stryke package · cdylib libstryke_mongo.{dylib,so} · dlopen'd in-process on use Mongo · opt-in (kept out of stryke core)

Report GitHub Issues
// Color scheme

>_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 oneval $r = Mongo::insert_one "app/users", { name => "alice", age => 30 }
countp Mongo::count "app/users"
find with filter + sortval @over30 = Mongo::find "app/users", filter => { age => { '$gt' => 30 } }, sort => { age => -1 }, limit => 100
update oneMongo::update_one "app/users", { name => "alice" }, { '$set' => { role => "superadmin" } }
aggregation pipelineval @top = Mongo::aggregate "app/orders", [ { '$match' => { status => "paid" } }, { '$group' => { _id => '$customer', total => { '$sum' => '$amount' } } } ]
create an indexval $idx = Mongo::create_index "app/users", { email => 1 }
discoveryval @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 | 0count($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, Nullstring / number / number / bool / null
Array, Documentarray / object
Decimal128{"$numberDecimal": "12.34"}
ObjectId{"$oid": "65f9b1d2c3f6e9..."}
DateTime{"$date": "2026-05-17T02:30:00Z"}
Binary{"$binary": {"base64":"…","subType":"00"}}
UUIDwrapped 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.stkinsert_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.stkcreate_index (unique, named) / indexes / drop_index
examples/counts.stkper-collection document counts across all databases (read-only)
examples/discover.stkversion + 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.

OpenChange Streams (replica set); multi-doc transactions (replica set); bulk_write (mixed ordered/unordered)
LaterGridFS 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: