RESULT_OBJECT.md¶
Design notes — read the user guide first
Shipped result types are documented in the API guide. Prefer Rejection policies for callback / quarantine / log behavior shipped in 0.6.0; sections below may include deferred design detail.
RowGuard Result Object¶
Purpose¶
The QueryResult object is the primary buffered result returned by RowGuard.
It provides a structured, typed, and observable representation of a completed query operation, including:
Validated Pydantic models
Rejected rows
Execution statistics
Diagnostics
The executed SQLAlchemy statement
Timing information
The result object should make successful and failed row handling equally visible.
Core Contract¶
QueryResult[T]
Where T is the target Pydantic model type.
Example:
result: QueryResult[UserRead] = rowguard.select(
session=session,
table=users,
model=UserRead,
on_reject="collect",
)
Suggested Data Model¶
from dataclasses import dataclass
from datetime import timedelta
from typing import Generic, TypeVar
T = TypeVar("T")
@dataclass(frozen=True, slots=True)
class QueryResult(Generic[T]):
models: tuple[T, ...]
rejected: tuple[RejectedRow, ...]
statistics: QueryStatistics
statement: object
diagnostics: tuple[Diagnostic, ...]
The result should be immutable after construction.
Models¶
result.models
Contains every successfully validated Pydantic model.
Recommended type:
tuple[T, ...]
A tuple communicates that query results are complete and immutable.
Convenience aliases may include:
result.valid
result.accepted
The canonical public name should remain models.
Rejected Rows¶
result.rejected
Contains every rejected row when the rejection policy retains them.
Each item should be a RejectedRow containing:
Raw database row
Adapted mapping
Pydantic validation error
Target model
Row index or sequence number
Optional source metadata
Diagnostics
Example:
for rejected in result.rejected:
print(rejected.validation_error)
print(rejected.mapping)
QueryStatistics¶
Suggested structure:
@dataclass(frozen=True, slots=True)
class QueryStatistics:
rows_read: int
rows_validated: int
rows_valid: int
rows_rejected: int
execution_time_ns: int
adaptation_time_ns: int
validation_time_ns: int
rejection_time_ns: int
Derived properties:
statistics.acceptance_rate
statistics.rejection_rate
statistics.total_time_ns
The statistics object should avoid floating-point values until requested.
Statement¶
result.statement
Contains the SQLAlchemy statement that RowGuard executed.
This is useful for:
Debugging
Logging
Testing
Observability
Reproducing query behavior
RowGuard should preserve the SQLAlchemy expression object rather than rendering a SQL string by default.
Diagnostics¶
result.diagnostics
Diagnostics may include:
SQLRules pushdown decisions
Skipped constraints
Field mappings
Alias resolution
Rejection-policy actions
Adapter warnings
Query planning notes
Diagnostics should be structured objects, not plain strings.
Convenience Properties¶
Recommended properties:
result.valid_count
result.rejected_count
result.total_count
result.has_rejections
result.is_clean
Suggested semantics:
result.valid_count == len(result.models)
result.rejected_count == len(result.rejected)
result.has_rejections is bool(result.rejected)
result.is_clean is not result.has_rejections
total_count should reflect rows processed, not merely retained objects.
Iteration¶
A QueryResult may iterate over valid models:
for user in result:
...
Equivalent to:
for user in result.models:
...
This should be a convenience only. Explicit access through result.models
remains clearer in most code.
Length¶
len(result)
Recommended meaning:
len(result.models)
This matches the iterable behavior.
Rejected rows are available separately.
Indexing¶
Optional support:
result[0]
Equivalent to:
result.models[0]
This makes buffered results list-like without hiding rejected-row metadata.
Truthiness¶
QueryResult should not overload truthiness ambiguously.
Possible options:
Always truthy.
Truthy only when valid models exist.
Disallow implicit boolean conversion.
Recommended behavior:
bool(result) == bool(result.models)
This is intuitive, but it must be documented because a result may contain only rejections.
Applications checking query success should use explicit properties such as:
result.is_clean
result.has_rejections
Rejection Policy Effects¶
The result shape depends on the configured rejection policy.
collect¶
Rejected rows are included in result.rejected.
skip¶
Rejected rows may not be retained, but statistics must still count them.
raise¶
No QueryResult is returned if validation fails.
log¶
Rejected rows may or may not be retained depending on configuration.
callback¶
Retention is configurable.
quarantine¶
Rejected rows may be represented by lightweight references if raw data is moved to another destination.
The result object must clearly distinguish:
Rejected rows encountered
Rejected rows retained
Suggested statistics:
rows_rejected
rows_rejected_retained
QueryOutcome vs QueryResult¶
Internally, RowGuard may distinguish between:
QueryOutcome[T]
and:
QueryResult[T]
QueryOutcome could represent mutable in-progress execution state, while
QueryResult is the immutable public object assembled after completion.
This prevents partially constructed public results.
Streaming Results¶
Streaming requires a separate abstraction because the complete result is not known upfront.
Suggested type:
StreamResult[T]
Possible API:
with rowguard.stream(...) as stream:
for model in stream:
...
statistics = stream.statistics
StreamResult should not pretend to be a complete QueryResult.
After a stream is exhausted, it may expose:
Final statistics
Retained rejected rows
Diagnostics
Async Results¶
Buffered async execution should return the same QueryResult[T] type as
synchronous execution.
Example:
result = await rowguard.aselect(...)
This keeps result handling consistent across sync and async APIs.
Async streaming should use a separate AsyncStreamResult[T].
Serialization¶
The result object itself should not automatically serialize SQLAlchemy statements, raw rows, or Pydantic errors.
Optional export helpers may provide:
result.model_dump()
result.to_dict()
result.to_json()
However, serialization must be explicit because rejected rows may contain:
Sensitive values
Non-JSON-native database types
Large payloads
SQLAlchemy objects
Recommended default export shape:
{
"models": [...],
"rejections": [...],
"statistics": {...},
"diagnostics": [...],
}
The executed statement should be omitted unless explicitly requested.
Memory Behavior¶
Buffered results retain valid models and, depending on policy, rejected rows.
For large datasets, users should prefer streaming.
The result object should avoid retaining duplicate representations of data unless needed for diagnostics.
Potential options:
retain_raw_rows=False
retain_adapted_rows=True
retain_validation_input=False
These should be configured during execution, not mutated afterward.
Type Safety¶
QueryResult[T] should preserve the target model type for static type checkers.
Example:
result = rowguard.select(
session=session,
table=users,
model=UserRead,
)
reveal_type(result) # QueryResult[UserRead]
reveal_type(result.models) # tuple[UserRead, ...]
Public APIs should use generics consistently.
Equality and Hashing¶
QueryResult should not be hashable by default because it may contain:
SQLAlchemy statement objects
Unhashable diagnostics
Complex rejected-row payloads
Structural equality may be useful in tests, but statement identity can make equality surprising.
Recommended approach:
Dataclass equality may be disabled.
Tests compare explicit fields.
Dedicated snapshot helpers may be added later.
RejectedRow Structure¶
Suggested model:
@dataclass(frozen=True, slots=True)
class RejectedRow:
index: int
raw_row: object | None
mapping: Mapping[str, object] | None
validation_error: ValidationError | None
adaptation_error: Exception | None
model: type[BaseModel]
diagnostics: tuple[Diagnostic, ...]
A rejected item may originate from either:
Row adaptation failure
Pydantic validation failure
Reject-handler failure, if safely recoverable
The rejection reason should therefore be explicit.
Diagnostic Structure¶
Suggested base model:
@dataclass(frozen=True, slots=True)
class Diagnostic:
code: str
message: str
severity: DiagnosticSeverity
metadata: Mapping[str, object]
Diagnostic codes should be stable enough for programmatic use.
Examples:
sqlrules.constraint_skipped
adapter.alias_applied
validation.row_rejected
rejection.row_quarantined
Partial Results¶
Buffered APIs should not return partial results after unrecoverable execution errors unless the user explicitly opts into partial-result behavior.
Possible future option:
on_execution_error="raise"
on_execution_error="return_partial"
If partial results are supported, the result must expose:
result.complete
result.execution_error
This is deferred beyond the MVP because partial query semantics can be subtle.
Result Invariants¶
Every completed QueryResult must satisfy:
statistics.rows_valid == len(models)
When all rejected rows are retained:
statistics.rows_rejected == len(rejected)
Always:
statistics.rows_read >= statistics.rows_valid
statistics.rows_read >= statistics.rows_rejected
statistics.rows_valid + statistics.rows_rejected == statistics.rows_classified
Additional states such as adaptation failures should be counted explicitly rather than hidden.
Error Handling¶
Constructing an inconsistent result should raise an internal error.
Suggested exception:
ResultAssemblyError
Examples:
Negative statistics
Valid count differs from model count
Retained rejection count exceeds total rejection count
Missing required statement metadata
These failures indicate a RowGuard bug or invalid plugin behavior.
Extension Points¶
Future result plugins may provide:
Pandas export
Polars export
Metrics export
Audit-record generation
JSON-safe serialization
Quarantine summaries
Web framework response adapters
Plugins should consume the immutable result rather than mutate it.
Testing Requirements¶
Tests should cover:
Generic typing
Immutability
Convenience properties
Iteration
Length and indexing
Clean results
Mixed valid and rejected rows
All-rejected results
Statistics invariants
Rejection retention policies
Serialization helpers
Statement preservation
Sync/async parity
Streaming finalization behavior
MVP Scope¶
The initial release should implement:
QueryResult[T]RejectedRowQueryStatisticsStructured diagnostics
Validated models
Retained rejections for
collectStatement preservation
Basic convenience properties
Immutable public objects
Deferred:
Partial results
Automatic JSON serialization
DataFrame exporters
Rich report rendering
Result pagination
Result persistence
Design Principles¶
Make accepted and rejected rows equally visible.
Preserve strong generic typing.
Keep public results immutable.
Separate buffered and streaming abstractions.
Record what happened, not just what succeeded.
Avoid hidden serialization or data retention.
Enforce result invariants during assembly.