loom.core.sql¶
Backend-agnostic SQL query subsystem.
Provides an injectable SqlQueryService that executes SQL through
named connections with a fail-closed per-request role policy, readonly
enforcement and pagination. Concrete backends implement the
SqlExecutor port; the first supported backend is ClickHouse
(loom.core.sql.clickhouse, optional extra loom-kernel[clickhouse]).
Work done on behalf of a caller uses CallerBoundSql instead, which
derives the roles from the verified identity rather than from an argument.
- class loom.core.sql.CallerBoundSql(service, config, observability=None)[source]¶
Bases:
objectExecutes SQL with the roles of the verified caller, and only those.
Every query is bound to an
Identity: the effective roles are the intersection of the roles that identity holds with the connection allowlist. There is deliberately norolesparameter — accepting one would reintroduce the very gap this collaborator closes, since the caller’s entitlements would stop being what decides.The identity must come from
Caller(). That marker is the only thing that makes the executor inject the identity the transport verified. Anidentityparameter declared without it is an ordinary primitive parameter, bound from theparamsthe calling code supplies: the caller would then choose its own identity, and with it the roles this class derives. On the agent path it is worse still, because thoseparamsare tool arguments the model writes. A query bound to a forged identity is unbound.A connection with an empty
allowed_rolescannot be queried this way: its only role is the shareddefault_role, which is not derived from anyone, soRolesNotBoundErroris raised. UseSqlQueryServicefor that unbound work.Each accepted query opens one span labelled with the effective roles, the caller subject and the authentication mechanism. Those labels match what the REST endpoint records, but the span is its own: a narrower scope and a different name, so the two are auditable on the same terms rather than being one record. Without an observability runtime there is no span and no other difference.
- Parameters:
service (SqlQueryService) – Underlying query service applying the connection policy.
config (SqlConfig) – Parsed
sql:section holding the per-connection allowlists.observability (ObservabilityRuntime | None) – Runtime the audit span is opened on.
Nonedisables the span; the auto-bootstrapped app injects the registered runtime.
Example:
from loom.core.identity import Identity from loom.core.sql import CallerBoundSql, SqlQueryResult from loom.core.use_case.markers import Caller from loom.core.use_case.use_case import UseCase class ListSales(UseCase[object, SqlQueryResult]): def __init__(self, sql: CallerBoundSql) -> None: self._sql = sql async def execute(self, identity: Identity = Caller()) -> SqlQueryResult: return await self._sql.execute( "SELECT * FROM sales", connection="analytics", identity=identity )
- async execute(sql, *, connection, identity, parameters=None, limit=None, offset=0)[source]¶
Execute sql on connection with the roles identity holds.
- Parameters:
sql (str) – SQL statement with native parameter placeholders.
connection (str) – Name of the configured connection to use.
identity (Identity) – Verified caller injected by the
Caller()marker; the sole source of the effective roles.parameters (Mapping[str, Any] | None) – Values bound server-side by the backend.
limit (int | None) – Requested row limit; clamped to the connection
max_limitand defaulted todefault_limitwhen absent.offset (int) – Number of rows to skip.
- Returns:
The standard tabular result envelope from the executor.
- Raises:
UnknownConnectionError – When connection is not configured.
RolesNotBoundError – When the identity is anonymous, holds no role, or holds none that the connection allowlists. A connection configured without a registered executor is reported this way too when the caller holds no allowlisted role: the roles are checked here, before the service reports the connection as unknown. Both are refusals; only the wording differs.
ConfigError – When the application has no
sql:section.
- Return type:
- class loom.core.sql.NullSqlQueryService[source]¶
Bases:
SqlQueryServiceNull implementation registered when no
sql:section is configured.Keeps
SqlQueryServicealways resolvable from the container so a use case never hits an opaque resolution error: the firstexecutecall raises an actionableConfigErrorinstead.
- class loom.core.sql.SqlQueryService(executors, config)[source]¶
Bases:
objectExecutes SQL through named connections under the fail-closed role policy.
Role resolution, readonly enforcement and limit clamping happen here, before the executor is ever touched. A rejected role never reaches the backend.
This is the unbound path: the roles are an argument, so the code writing the call chooses them, bounded only by the connection allowlist. That is correct for system work with no caller — a scheduled job, a migration, a health probe. Work done on behalf of a caller uses
CallerBoundSql, which derives the roles from the verified identity and accepts norolesargument.- Parameters:
executors (Mapping[str, SqlExecutor]) – Backend executor per connection name.
config (SqlConfig) – Parsed
sql:section with the named connections.
Example — system work with no caller, running as the connection’s own
default_role:service = SqlQueryService(executors=executors, config=sql_config) result = await service.execute( "SELECT count() FROM sales", connection="analytics" )
- async execute(sql, *, connection, roles=None, parameters=None, limit=None, offset=0)[source]¶
Execute sql on connection applying the connection policy.
- Parameters:
sql (str) – SQL statement with native parameter placeholders.
roles (Sequence[str] | None) – Caller roles, each validated against the connection allowlist; the query runs with the union of their privileges. Empty or
Nonefalls back to the connectiondefault_role. They are not checked against any caller: when the query is run on behalf of one, useCallerBoundSql.connection (str) – Name of the configured connection to use.
parameters (Mapping[str, Any] | None) – Values bound server-side by the backend.
limit (int | None) – Requested row limit; clamped to the connection
max_limitand defaulted todefault_limitwhen absent.offset (int) – Number of rows to skip.
- Returns:
The standard tabular result envelope from the executor.
- Raises:
UnknownConnectionError – When connection is not configured.
RoleNotAllowedError – When any requested role is outside the allowlist — the whole request is refused, never filtered.
RoleRequiredError – When no effective role can be resolved.
- Return type:
- loom.core.sql.resolve_query_roles(identity, *, connection, roles_bound, allowed_roles, requested_roles)[source]¶
Resolve the roles one query may use.
- Parameters:
identity (Identity) – Verified caller published by the authentication middleware.
connection (str) – Name of the SQL connection being queried.
roles_bound (bool) – Whether the configured authentication mechanism binds roles to the identity.
Falsemeans the connection declares no binding and is single-role by config.allowed_roles (frozenset[str]) – Connection allowlist, the ceiling of the intersection.
requested_roles (Sequence[str] | None) – Roles asked for in the body; they may only narrow.
- Returns:
The effective roles for this single request.
- Raises:
RolesNotBoundError – When no allowed role can be derived from the verified identity.
RoleNotAllowedError – When the body asks for a role the identity does not hold.
- Return type: