Updates.

August 2, 2026 · View on GitHub

 ███████╗████████╗██████╗ ██╗   ██╗██╗  ██╗███████╗
 ██╔════╝╚══██╔══╝██╔══██╗╚██╗ ██╔╝██║ ██╔╝██╔════╝
 ███████╗   ██║   ██████╔╝ ╚████╔╝ █████╔╝ █████╗
 ╚════██║   ██║   ██╔══██╗  ╚██╔╝  ██╔═██╗ ██╔══╝
 ███████║   ██║   ██║  ██║   ██║   ██║  ██╗███████╗
 ╚══════╝   ╚═╝   ╚═╝  ╚═╝   ╚═╝   ╚═╝  ╚═╝╚══════╝
                   [ m o n g o ]

CI License: MIT stryke

[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

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:

BSONJSON
Stringstring
Int32, Int64number
Doublenumber
Decimal128{"$numberDecimal": "12.34"}
Booleanbool
Nullnull
Arrayarray
Documentobject
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":...}}

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 → NDJSON
  • find-one → single JSON doc (or null)
  • 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).

OpenLater
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.