yggdrasil.data.executor¶
executor ¶
Backend-agnostic statement executor.
Public surface¶
- :class:
ExecutionOptions— cross-backend execution policy (waiting, raise-on-failure, parallelism). Not statement configuration — parameters, external tables, byte/row limits, routing hints all live as typed fields on the :class:PreparedStatementsubclass. - :class:
StatementExecutor— abstract base with a single subclass hook (:meth:_submit_statement). Coercion, batching, lifecycle, dispose semantics, and theexecute/execute_manydriver methods are provided here.
Subclassing¶
Subclasses pin their concrete types via the three :class:ClassVar
attributes _PREPARED_CLASS, _RESPONSE_CLASS,
_BATCH_CLASS, and implement :meth:_submit_statement.
Cross-cutting behavior (logging, retries, metrics) is best added by
overriding :meth:_execute — it sees an already-coerced statement and a
resolved :class:ExecutionOptions, so it doesn't have to re-implement
the kwargs dance.
ExecutionOptions
dataclass
¶
ExecutionOptions(
wait: WaitingConfigArg = True,
raise_error: bool = True,
parallel: Optional[int] = None,
)
Cross-backend execution policy.
Decoupled from statement-level config (parameters, external tables,
byte/row limits, etc.) which lives as typed fields on the concrete
:class:PreparedStatement subclass. An ExecutionOptions instance
only describes how to run, never what.
Fields¶
wait
Waiting policy. True (default polling), False (return
once submitted), or a :class:WaitingConfig for custom timing.
raise_error
Whether to raise on a backend-reported failure. When False,
the caller inspects result.failed / result.raise_for_status
themselves.
parallel
Used only by :meth:StatementExecutor.execute_many. None =
executor default. <= 1 = sequential wait.
Construction¶
Build with ExecutionOptions(wait=False, parallel=4) or coerce
from a kwargs dict via :meth:from_kwargs. :meth:replace returns a
derived options object with overrides applied — useful for layered
overrides (e.g. a batch-level default + per-statement tweaks).
from_
classmethod
¶
from_(
value: "ExecutionOptions | Mapping[str, Any] | None" = None,
**overrides: Any
) -> "ExecutionOptions"
Coerce to an :class:ExecutionOptions, applying any overrides.
None-> defaults- an existing instance -> returned with
replace()applied - a Mapping -> constructed from it, then overrides applied
StatementExecutor ¶
Bases: Singleton, Disposable, Generic[PS, SR, SB]
Abstract base for backend-specific statement executors.
A :class:StatementExecutor IS a :class:Session over a transport
that speaks SQL instead of HTTP: the same prepare → send pipeline
drives both, and the same singleton-by-config + pickle pattern
keeps connection pools and in-flight result maps shared across
callers in-process.
Subclasses implement exactly one hook — :meth:_submit_statement —
which turns a coerced :class:PreparedStatement into a backend-
specific :class:StatementResult. The base provides the
Session-shaped surface:
- :meth:
prepare— coerce raw input into the typed :class:PreparedStatementsubclass (analogue of :meth:Session.prepare_request_before_send), - :meth:
send— dispatch a prepared statement and return its :class:StatementResult.start=Falsereturns an idled result whose backend submission is deferred until :meth:StatementResult.startfires — same shape as :meth:Session.sendwithstart=False, - :meth:
execute/ :meth:execute_many— kwargs-friendly sugar that resolves :class:ExecutionOptionsand waits.
Singleton + pickle¶
Concrete subclasses opt into the inherited :class:Session /
:class:Singleton cache by:
- setting
_SINGLETON_TTL = None(process-lifetime caching), - overriding :meth:
_singleton_keyto project the identity- bearing constructor arguments into a hashable tuple, - guarding
__init__withif getattr(self, "_initialized", False): returnso Python's re-entry after a cache hit doesn't clobber live state, - extending
_TRANSIENT_STATE_ATTRSwith any non-picklable handles they hold (locks, urllib3 pools, live SDK sessions).
The base default — _SINGLETON_TTL = ... (from :class:Singleton) —
keeps caching opt-in so executor subclasses that genuinely don't
have a stable identity (a hand-rolled test double, an anonymous
executor) still work without surprise sharing.
Class-level configuration¶
The prepared / response / batch types are pinned on the inherited
:class:Session ClassVars — :attr:Session._PREPARED_CLASS /
:attr:Session._RESPONSE_CLASS / :attr:Session._BATCH_CLASS —
overridden here to the SQL-shaped defaults. Backend subclasses
(SQLWarehouse, SparkStatementExecutor, …) re-pin them to
their concrete types so the prepare → send pipeline produces the
right shape without per-call coercion.
prepare ¶
Coerce statement into this executor's prepared-statement type.
Mirrors :meth:Session.prepare_request_before_send: takes
whatever the caller passed (raw string, cross-backend
:class:PreparedStatement, already-typed instance) and
returns the concrete :attr:_PREPARED_CLASS every downstream
hook expects. Subclasses that need to inject per-statement
defaults (warehouse routing, catalog binding, SELECT-rewrite
for cluster execution) override this — same shape as Session's
hook.
send ¶
Dispatch statement and return its tracking :class:StatementResult.
Mirrors :meth:Session.send. start=True (default) fires
the backend submission eagerly — the result comes back in
flight (or already terminal for synchronous backends).
start=False returns the idled :class:StatementResult
whose backend submission is deferred until
:meth:StatementResult.start fires.
The returned result is always bound to this executor — every
subclass _submit_statement is supposed to thread
executor=self through the constructor, but that's easy
to forget and downstream code (StatementResult.wait,
retry, raise_for_status) needs the back-reference.
Setting it here when it's missing makes the contract
enforceable from one place instead of audited per backend.
execute ¶
execute(
statement: "PS | PreparedStatement | str",
*,
options: Optional[ExecutionOptions] = None,
wait: WaitingConfigArg = True,
raise_error: bool = True,
start: bool = True
) -> SR
Submit a single statement and optionally wait for completion.
Two ways to pass execution policy:
- Per-call kwargs
wait/raise_error(ergonomic, matches the previous public API). - An :class:
ExecutionOptionsviaoptions=(when you want to reuse the same policy across many calls or compose from layered defaults).
The two are merged: options provides the base, kwargs override
any field they explicitly set. Unknown kwargs go nowhere — they
are not forwarded to the backend. Use a typed
:class:PreparedStatement subclass for backend-specific
configuration (parameters, byte limits, routing, etc.).
execute_many ¶
execute_many(
statements: Iterable["PS | PreparedStatement | str"],
*,
options: Optional[ExecutionOptions] = None,
wait: WaitingConfigArg = True,
raise_error: bool = True,
parallel: Optional[int] = None,
**batch_kwargs: Any
) -> SB
Run several statements as a batch and return the populated batch.
Convenience wrapper around :meth:batch: enqueues every statement,
submits, and (by default) waits. parallel controls the wait
phase only — submission itself is sequential, since most backends
either accept fast or reject fast.
**batch_kwargs are forwarded to the batch constructor (e.g.
external_paths= for :class:WarehouseStatementBatch).
batch ¶
batch(
statements: Optional[Iterable["PS | PreparedStatement | str"]] = None,
*,
executor: "StatementExecutor | None" = None,
parallel: Optional[int] = None,
**kwargs: Any
) -> SB
Construct a batch bound to this executor.
open ¶
Acquire the resource and cascade into owned children.
Order:
- Run our own :meth:
_acquire(subclass body). - Flip :attr:
openedto True and mark_self_opened. -
For each owned child, in registration order:
-
If the child is already opened, just :meth:
_claimit. It stays self-opened — the existing self-open is what keeps it alive after we let go. - Otherwise, call :meth:
openon the child (which recursively cascades into ITS owned children), then clear the child's_self_openedflag so the child knows its open is parent-driven, then :meth:_claimit. Without that flag clear, the eventual :meth:_unclaimwould refuse to close — it would see "I'm self-opened, someone explicitly opened me, leave me alone."
Both branches record the child in our per-frame scratch
list so :meth:_release knows what to unclaim.
Transactional rollback: if any child's open or claim raises,
we walk back through the children we already touched (in
reverse), unclaim each, then call our own :meth:_release
with committed=False and re-raise the original exception.
From the caller's view, the open atomically either succeeded
with the whole graph live, or failed with nothing changed.
Not reentrant: raises :class:RuntimeError if already opened.
Nesting is expressed via with self: blocks, not via paired
:meth:open calls.
close ¶
Release the resource and cascade into owned children.
Order:
- If currently held open by an outside parent claim
(
_claim_count > 0) AND we are not in self-opened state, this is a no-op — the parents that opened us still need us live. (Handled inside :meth:_do_close.) - Walk our scratch list of acquired children in REVERSE
registration order; :meth:
_unclaimeach. A child whose claim count hits zero and isn't otherwise self-opened closes itself. - Run :meth:
_before_release, then :meth:_release— withcommittedreflecting the dirty bit (cleared on exception by__exit__).
Idempotent: no-op when already closed, unless force.
force=True runs teardown even when :attr:closed.
Intended for error-recovery paths where subclass state
might be inconsistent.
Does NOT touch :attr:depth — the with-stack counter
belongs to :meth:__enter__/:meth:__exit__ exclusively.
If a caller calls :meth:close inside an active with
block, the outer :meth:__exit__ will harmlessly skip the
now-no-op close on unwind.
to_singleton ¶
Promote this instance into the per-class _INSTANCES cache.
Hot listing paths (iterdir / _ls / glob) build
children with singleton_ttl=False so the bounded cache
doesn't fill up with thousands of short-lived entries. When a
caller decides one of those children is worth keeping around
(handing it to a long-running worker, returning it from an
API), :meth:to_singleton registers self into the cache
so the next constructor call with the same key collapses to
the same instance.
ttl defaults to the subclass's _SINGLETON_TTL
(... = no caching, None = process lifetime, or a
seconds count). When a different instance is already cached
under this key, that pre-existing one wins and is returned
unchanged — the cache is the source of truth.
invalidate_singleton ¶
Pop self from the per-class _INSTANCES cache.
Mutating ops on a Singleton-cached object (writes, deletes,
schema invalidations on a Databricks table, put_object on
an :class:S3Path) want to make sure the next caller asking
for the same key gets a fresh build rather than collapsing
onto this stale handle — that's what remove_global=True
(the default) does. The pop is :meth:identity-guarded:
only an entry that still points at self is removed, so
a concurrent re-construction that already raced past this
thread is left alone.
remove_global=False is a no-op. The keyword exists so
subclass invalidators (invalidate_singleton,
_invalidate_entity_tag_cache, …) can offer the same
switch without branching at the call site.