mixle.data.sources.sql_source module¶
SQL connector – any SQLAlchemy URL (Postgres / MySQL / SQLite / …) into a DataSource.
Optional: requires sqlalchemy (pip install mixle[sql]). One connector covers every RDBMS. Unlike
most connectors here, a SQL result set is not a cheap thing to hold in memory – a large query must not
have to fit in the driver, or in mixle, before the first record reaches an encoder. See
SqlCursorSource for the streaming, disposal, and replay contract (MXR-080-0064).
- class SqlCursorSource(url, query, columns=None, *, structure=EXCHANGEABLE, schema=None, batch_size=_STREAM_BATCH_ROWS)[source]
Bases:
objectA cursor-backed, bounded-memory
DataSourceover a SQL 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 connection and pull rows off a streaming server-side cursor (stream_results=True, fetchedbatch_sizeat a time via SQLAlchemy’syield_per) straight into the caller/encoder, so a query with more rows than fit in memory can still be consumed one bounded batch at a time (MXR-080-0064)..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 table) – it is no longer whatrecords()/encode()do by default.Replay policy: each call to
records()/encode()/materialize()re-executes the query from scratch, on a fresh connection. A live server-side 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 table 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 connection/engine this object opens is disposed in a
finally, deterministically – on normal exhaustion, on an exception raised while a row 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). 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.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 rows contiguously by group key (e.g.
ORDER BYthe group column) – this is required and enforced, not assumed: a group key reappearing after the stream has moved on to a different group raisesValueErrorrather than silently splitting the group across partitions.- Parameters:
- records()[source]
Stream one full query execution’s rows. Call again to re-run the query (see class docstring).
- materialize()[source]
Run the query and return every row 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 rows contiguous by group key (see class docstring).Like
_encode_indexable()(MXR-080-0056’s sibling fix), a partition with no rows 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_sql(url, query, columns=None, *, structure=EXCHANGEABLE, schema=None, batch_size=_STREAM_BATCH_ROWS)[source]
Read the rows of
queryagainst the database aturlinto a cursor-backed, bounded-memorySqlCursorSource– see its docstring for the streaming, disposal, and replay contract.