mixle.data.sources.mongo_source module¶
MongoDB connector – a collection query into a DataSource.
Optional: requires pymongo (pip install mixle[mongo]). The _id field is dropped; supply a
Schema to coerce the loosely-typed BSON documents. Like a SQL result set (see
mixle.data.sources.sql_source), a MongoDB query result is not a cheap thing to hold in memory – a
large collection query must not have to fit in the driver, or in mixle, before the first document reaches
an encoder. See MongoCursorSource for the streaming, disposal, replay, and grouped-ordering
contract (MXR-080-1666: “Mongo reads exhaust the cursor before yielding and never close the client” –
independently numbered from, but the same architectural gap as, the SQL connector’s MXR-080-0064 fix: a
different backend, the same “materialize the whole result before the first record is usable” defect).
- class MongoCursorSource(uri, database, collection, query=None, columns=None, *, structure=EXCHANGEABLE, schema=None, batch_size=_STREAM_BATCH_DOCS, sort=None)[source]
Bases:
objectA cursor-backed, bounded-memory
DataSourceover a MongoDB query.Unlike
LazySource(read a factory once, cache the result as a list – the right contract for a connector that is cheap to hold in memory), this source never builds a Python list of the full result set internally.records()/encode()open a fresh client and pull documents off a batched cursor (find(...).batch_size(batch_size)) straight into the caller/encoder, so a collection query with more documents than fit in memory can still be consumed one bounded batch at a time (MXR-080-1666)..materialize()remains available as an explicit, honestly-named opt-in for callers who really do want the whole result set as a list (e.g. a small lookup collection) – it is no longer whatrecords()/encode()do by default.Replay policy: each call to
records()/encode()/materialize()re-runs the query from scratch, on a freshMongoClient. A live cursor cannot be rewound, so “replay” here means “run the query again” (as re-running it by hand would), not “replay a cached snapshot” – if the underlying collection changes between calls, later calls see the new data. Two calls are fully independent: nothing about this object is safe to share a single in-flight iterator across two consumers, but taking two separaterecords()calls is always fine, including concurrently.Disposal: every client this object opens is closed in a
finally, deterministically – on normal exhaustion, on an exception raised while a document is being processed downstream (the exception unwinds the consuming frame, which drops the last reference to the generator; CPython finalizes it synchronously, running the pendingfinally), and on early abandonment (explicitly call.close()on the iterator returned byrecords()for an immediate, GC-independent close; letting it fall out of scope also works under CPython’s refcounting but is not a language guarantee). The live cursor itself is also closed explicitly (telling the server to free it immediately) before the client that owns it. There is deliberately no object-levelclose()/context-manager pair here (contrastHDF5ArraySource): the replay-by-re-execute design means no connection is held between calls, so there is nothing to close while the object is idle.Count resolution: a streaming cursor does not know its size up front, so a
chunk_sizetarget is resolved viacount_documents()– one bounded integer, never the document data. Simpler thanSqlCursorSource’sCOUNT(*)subquery:count_documentstakes the same filter dict_stream()already builds directly, with no query-text wrapping/parsing involved (there is no raw query string here to manipulate in the first place).Projection:
columns=Nonefetches whole documents (minus_id, always dropped); an explicitly emptycolumns=[]projects to zero columns (every record becomes()), matchingtext_source.read_csv’s and the fixedhadoop_source.read_remote’scolumns=[]semantics (MXR-080-0065’s sibling truthiness bug:if columnstreats[]andNonealike, which they are not) – implemented as a{"_id": 1}projection (the cheapest possible per-document payload), since an empty Mongo projection document{}means “no restriction” (returns full documents), not “zero fields”: the two only look alike under Python’sif columnstruthiness, not in what either actually asks the server for. Requesting'_id'itself viacolumnsis rejected at construction (it is unconditionally dropped, so honoring it would either silently do nothing or raise a confusingKeyErroronce the query actually runs).Grouped (partially-exchangeable) structure: assigning whole groups to partitions needs to know a group is complete before it can be flushed, which a single forward pass can only bound in memory if the query already yields documents contiguously by group key – this is required and enforced, not assumed: a group key reappearing after the stream has moved on to a different group raises
ValueErrorrather than silently splitting the group across partitions. UnlikeSqlCursorSource(which cannot inject anORDER BYinto a caller’s raw SQL text), this connector builds its query from structured pieces, so a plain stringbyis auto-sorted server-side (see_resolved_sort) – contiguous by construction, no caller action needed. A callablebyhas no server-side sort equivalent (Mongo cannot sort by an arbitrary Python function), so it still needs the caller to pass an explicitsort(or otherwise guarantee contiguous order) – exactly SQL’sORDER BY-by-hand requirement, just for a narrower case. Either way, a stringbyalso requirescolumns=None: a projected record is a tuple/scalar (positional), not a document dict, soSampleStructure.group_keycannot look a field up by name on it (mirrorsSqlCursorSource, whose rows are always tuples and so always need a callableby) – rejected at construction rather than raising a confusingAttributeErrordeep inencode().- Parameters:
- records()[source]
Stream one full query execution’s documents. Call again to re-run the query (see class docstring).
- materialize()[source]
Run the query and return every document as a list – an explicit opt-in that bypasses the bounded-memory path; use only when the result is known to be small enough to hold in memory.
- encode(encoder, num_chunks=1, chunk_size=None, batch_size=None)[source]
Partition and encode straight off the cursor, in bounded batches – never a full record list.
Strideable structures (everything but partially-exchangeable) round-robin record
iinto partitioni % n_partsas it streams by, matching therecords[k::n]stridingpartition_records()uses on a materialized list – bit-identical partitioning, just computed without ever holdingrecordsitself. Partially-exchangeable (grouped) structure streams whole groups round-robin, in first-encountered order, exactly aspartition_records()does, but requires the query to already yield documents contiguous by group key (see class docstring).Like
_encode_indexable()(MXR-080-0056’s sibling fix), a partition with no documents contributes nothing to the returned list rather than an explicit(0, ...)entry, and a partition may legitimately contribute more than one entry (one per flushed batch, in read order) instead of exactly one – every existing caller sums/iterates/ concatenates over however many entries the list contains rather than assuming exactlynum_chunksof them, so this is not a behavior change from their point of view.
- read_mongo(uri, database, collection, query=None, columns=None, *, structure=EXCHANGEABLE, schema=None, batch_size=_STREAM_BATCH_DOCS, sort=None)[source]
Read documents matching
queryfromdatabase.collectionaturiinto a cursor-backed, bounded-memoryMongoCursorSource– see its docstring for the streaming, disposal, replay, and grouped-ordering contract.