Static analyzer internals
Source location
src/sqlmodel_ext/relation_load_checker.py — RelationLoadChecker, RelationLoadCheckMiddleware, run_model_checks
This is the most complex module in the entire project (~2000 lines). It uses AST static analysis to surface potential MissingGreenlet problems at application startup.
Disabled by default since 0.3
Starting with 0.3.0 this module is experimental and requires rlc.check_on_startup = True to opt in. This chapter explains how it works; for steps to enable it in your project, see Prevent MissingGreenlet errors.
Core class
class RelationLoadChecker:
def __init__(self, model_base_class=None):
self.model_base_class = model_base_class
self.warnings: list[RelationLoadWarning] = []Uses Python's ast module to parse source code's abstract syntax tree, rather than executing code:
- No database connection needed
- No business logic execution required
- Can be completed during the import phase
Analysis flow
Detection rules
RLC001: response_model contains relation fields but endpoint doesn't preload
The analyzer:
- Parses
response_model=UserResponse, finds it contains aprofilefield - Checks query calls in the endpoint function body, finds no
load=parameter - Generates a warning
@router.get("/user/{id}", response_model=UserResponse)
async def get_user(session: SessionDep, id: UUID):
return await User.get_exist_one(session, id)
# ⚠ RLC001: response_model contains profile, but query has no load=RLC002: accessing relations after save/update
Tracks variable "expiration state" — after a save() or update() call, all relations on the object are considered expired.
user = await User.get_exist_one(session, id, load=User.profile)
user = await user.update(session, data) # All relations expire after this
return user.profile # RLC002RLC003: accessing unloaded relations (local variables)
Tracks the object types and loaded relations bound to local variables, detecting attribute access on unloaded relations.
RLC007: accessing expired object column attributes after commit
Tracks session.commit() calls — accessing any attribute on related objects afterward is considered dangerous.
RLC008: calling methods on expired objects after commit
Similar to RLC007, but detects method calls rather than attribute access.
RLC009: type annotation resolution errors
Detects issues caused by mixing resolved types with string forward references.
RelationLoadWarning
class RelationLoadWarning:
code: str # "RLC001"
message: str # Human-readable description
location: str # "module.py:42 in function_name"
severity: str # "warning" or "error"mark_app_check_completed()
The middleware check executes only once. After analysis is complete on the first request, mark_app_check_completed() marks it as done, and subsequent requests don't repeat the check.
Why AST instead of runtime checks?
| Approach | Pros | Cons |
|---|---|---|
| AST static analysis | Finds issues at startup, no code execution, covers all paths | Possible false positives, can't analyze dynamic code |
| Runtime checks | 100% accurate | Only checks executed paths |
The static analyzer serves as the "first line of defense" alongside runtime @requires_relations and lazy='raise_on_sql', forming multi-layer protection.
Limitations
- False positives: Static analysis cannot track runtime dynamic behavior (e.g.,
getattr, conditional loading) - Coroutines only: Synchronous functions are not analyzed (no MissingGreenlet issue in sync environments)
- Module scope: Only analyzes imported modules; unimported code is not scanned
False positives & project assumptions
The analyzer's AST rules are tuned to a specific project layout (FastAPI endpoints, STI inheritance conventions, save/update/delete naming). On other projects it may produce false positives or fail to parse. The module API is not in the semver stability promise.