WHY_NOT_SQLMODEL.md¶
Why RowGuard Is Not SQLModel¶
Purpose¶
This document explains the relationship between RowGuard and SQLModel.
RowGuard is not a replacement for SQLModel. The two libraries solve different problems and are designed to work together.
The motivation for RowGuard comes from a gap that appears in many production applications:
Data may already exist in a database that no longer satisfies the Pydantic model the application expects.
SQLModel validates objects that it constructs. RowGuard validates the rows returned by arbitrary SQLAlchemy queries, regardless of how those rows entered the database.
Different Responsibilities¶
SQLModel |
RowGuard |
|---|---|
ORM + data model |
Query validation layer |
Defines tables |
Consumes tables |
Persists data |
Reads data |
Uses SQLAlchemy |
Builds on SQLAlchemy |
Uses Pydantic |
Uses Pydantic |
Validates application-created objects |
Validates database-returned rows |
CRUD-oriented |
Read/query-oriented |
These responsibilities complement one another.
The Long-Standing Gap¶
Consider a read model:
class UserRead(BaseModel):
id: int
age: Annotated[int, Field(ge=18)]
Suppose legacy data exists:
id=1
age=12
A plain SQLAlchemy or SQLModel query may successfully return this row because the database is not required to enforce the application’s Pydantic constraints.
The application now receives data that violates its own contract.
RowGuard inserts a validation step after the query:
Database
↓
SQLAlchemy
↓
RowGuard
↓
Pydantic
↓
Accepted or Rejected
SQLModel Already Validates…¶
Yes—but it validates objects during normal construction and persistence.
RowGuard addresses a different question:
“Does every row returned by this query still satisfy the model I expect?”
That question becomes important when:
legacy data exists
multiple applications write to the same database
migrations were incomplete
raw SQL modified data
ETL pipelines bypassed validation
manual fixes occurred
constraints changed over time
Query Validation vs Persistence Validation¶
Persistence validation answers:
“Can I write this object?”
RowGuard answers:
“Can I trust what I just read?”
Those are different guarantees.
Why Not Just Call model_validate() Yourself?¶
You can.
For small applications:
rows = session.execute(stmt).mappings()
models = [
UserRead.model_validate(r)
for r in rows
]
RowGuard standardizes and extends this pattern with:
SQLRules pushdown
typed results
rejected-row handling
streaming
async support
diagnostics
statistics
quarantine
callbacks
plugin system
planning
caching
SQLRules + RowGuard¶
SQLRules attempts to reduce the number of candidate rows sent back from the database.
RowGuard still validates every returned row with Pydantic.
Pushdown is an optimization—not a replacement for validation.
Why Not Put This Into SQLModel?¶
Keeping RowGuard separate has several advantages.
Works With Plain SQLAlchemy¶
Many applications intentionally avoid SQLModel.
RowGuard works directly with SQLAlchemy Core and ORM.
Multiple Read Models¶
One table may have many read contracts.
Examples:
Public API
Internal API
Analytics
Admin
ETL
RowGuard validates against whichever model the query requests.
Existing Projects¶
Large SQLAlchemy projects can adopt RowGuard without migrating to SQLModel.
Separation of Concerns¶
SQLModel focuses on modeling and persistence.
RowGuard focuses on query correctness.
Complementary Example¶
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
Later:
class PublicUser(BaseModel):
id: int
name: Annotated[str, Field(min_length=3)]
Query:
result = rowguard.select(
session=session,
statement=select(User),
model=PublicUser,
)
SQLModel manages persistence.
RowGuard validates the returned data against the API contract.
What RowGuard Does Not Replace¶
RowGuard does not replace:
SQLModel tables
SQLAlchemy ORM
Sessions
Migrations
Relationships
Identity map
Persistence
Unit of work
It simply validates query results.
Choosing Between Them¶
Use SQLModel when you want:
ORM models
table definitions
CRUD applications
simple persistence
Use RowGuard when you need:
validated query results
rejected-row handling
SQLRules pushdown
streaming validation
diagnostics
data-quality auditing
Many applications will use both.
Design Principles¶
SQLModel and RowGuard solve different problems.
RowGuard builds on SQLAlchemy rather than replacing it.
Every accepted row should satisfy the requested Pydantic model.
Validation after a query is a distinct capability from validation before a write.
SQLRules optimizes reads; Pydantic remains the source of truth.