Typed-SQL column-mapping contract (SQL in, frozen dataclasses out).
Chirp's data layer maps SQL rows onto frozen dataclasses via
db.fetch(cls, sql) / db.fetch_one(cls, sql) / db.stream(cls, sql)
(chirp.data._mapping). A column SELECTed by the SQL that exists on
neither the target dataclass nor the declared table schema is drift: the
query runs, the column is silently ignored bymap_row(extra columns are
dropped), and the bug only surfaces when the row is missing data at runtime.
This rule promotes that drift to anapp.check() ERRORwhen it is
statically analyzable. It is deliberately conservative -- it does not become an
ORM, a model registry, or a SQL engine. It only fires when all of the following
can be resolved from static handler source:
- a literal first positional
clsargument that names a frozen dataclass reachable in the handler's module globals, and - a string-literal SQL with an explicit
SELECT col, col, ...list (SELECT *, expressions, joins with ambiguous columns, and dynamic SQL are skipped -- no false positives).
A SELECTed column is flagged only when it is absent from the dataclass fields and (when a declared schema is available) absent from every table column in that schema. That double-guard keeps the check quiet for column aliases the schema cannot see and for db-less / schema-less apps, honoring the data steward's "data stays optional" rule and the contracts steward's "no noisy warnings" rule.
Severity isERRORand overridable via
app.override_contract_severity("data", Severity.ERROR/WARNING/INFO).
contracts.rules_data_shapes
| Name | Type | Default | Description |
|---|---|---|---|
type
|
|
— | |
qualified_name
|
|
— | |
element_type
|
|
— | |
description
|
|
— | |
source_file
|
|
— | |
line_number
|
|
— | |
is_autodoc
|
|
— | |
autodoc_element
|
|
— | |
_autodoc_template
|
|
— | |
_autodoc_url_path
|
|
— | |
_autodoc_page_type
|
|
— | |
title
|
|
— | |
doc_content_hash
|
|
— |
Symbols on this page
Returnsqlwith SQL comments replaced by a single space, string-aware.
_parse_select_columnsis the single-source-of-truth SELECT parser (also consumed by codegen and the data…
Split a projection on top-level commas (ignoring commas inside parens).
Return the output column names of a simpleSELECT a, bquery.
ReturnsNone(skip -- not statically analyzable) when the projection is
SELECT…
Return the field-name set for a frozen dataclass type, elseNone.
Resolve aclsargument AST node to a runtime object, if statically known.
Only resolves bareNamereferences against the handler's module globals
(e.g. …
True when the call receiver looks like a chirp.dataDatabasehandle.
Recognizesdb / database / *_dbnames, attribute access ending in
those (…
Yield(cls_node, sql_literal)for statically analyzable fetch calls.
A call is analyzable when it is
<db>.fetch|fetch_one|stream(CLS, "literal sql", ...) where <db>is an idiomatic…
Union of all column names across all tables in the declared schema.
ReturnsNonewhen no schema is available -- the check then relies…
Normalize indentation soast.parseaccepts a nested handler source.
Flagdb.fetch(cls, sql)columns that map to no dataclass field.
For each statically-resolvable fetch call on a route handler:
- resolve
clsto a frozen…
_strip_sql_comments
function
def _strip_sql_comments(sql: str) -> str
Returnsqlwith SQL comments replaced by a single space, string-aware.
_parse_select_columnsis the single-source-of-truth SELECT parser (also
consumed by codegen and the data contract, and -- viashapes._scope_injectable
-- the tenant-scope injectability gate). A comment in the PROJECTION list
(SELECT id /* note */, name FROM t or SELECT id -- c\nFROM t) is NOT
opaque SQL, but a naive regex parse choked on it and returnedNone--
surfacing a misleading "CTE / UNION / SELECT * / derived-table" rejection for
what is really just a comment. We strip comments first so a commented column
list still parses.
Comment forms (mirroring the SQL standard): line comments-- ... EOLand
block comments/* ... */(non-nesting). Each is replaced by a single
space (never deleted) so adjacent tokens cannot merge (a/**/bmust not
becomeab). The scan is string-literal aware: a -- / /*inside
a'...' or "..." literal (honoring doubled-quote '' / ""
escapes) is NOT a comment and is preserved verbatim, so a quoted identifier or
string body is never corrupted. A line comment's terminating newline is
preserved (kept outside the replacement) so line structure is unchanged.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
sql
|
str
|
— |
_split_top_level
function
def _split_top_level(projection: str) -> list[str]
Split a projection on top-level commas (ignoring commas inside parens).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
projection
|
str
|
— |
_parse_select_columns
function
def _parse_select_columns(sql: str) -> tuple[str, ...] | None
Return the output column names of a simpleSELECT a, bquery.
ReturnsNone(skip -- not statically analyzable) when the projection is
SELECT *, contains an expression / function call / subquery / DISTINCT,
or the SQL has no analyzableSELECT ... FROMshape. Returns the resolved
output names (alias-aware) otherwise.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
sql
|
str
|
— |
_dataclass_fields
function
def _dataclass_fields(cls: Any) -> set[str] | None
Return the field-name set for a frozen dataclass type, elseNone.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
cls
|
Any
|
— |
_resolve_cls
function
def _resolve_cls(node: ast.expr, handler_globals: dict[str, Any]) -> Any
Resolve aclsargument AST node to a runtime object, if statically known.
Only resolves bareNamereferences against the handler's module globals
(e.g.db.fetch(User, ...) where Useris a module-level dataclass).
Attribute access, subscripts, calls, and locals are not resolvable -> skip.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
node
|
ast.expr
|
— | |
handler_globals
|
dict[str, Any]
|
— |
_is_db_receiver
function
def _is_db_receiver(value: ast.expr) -> bool
True when the call receiver looks like a chirp.dataDatabasehandle.
Recognizesdb / database / *_dbnames, attribute access ending in
those (self.db, app.db, g.db), and get_db()calls. Anything
else (an arbitrary object that merely exposes afetch/streammethod)
is rejected so the rule never ERRORs on an unrelated API.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
value
|
ast.expr
|
— |
_iter_fetch_calls
function
def _iter_fetch_calls(tree: ast.AST) -> list[tuple[ast.expr, str]]
Yield(cls_node, sql_literal)for statically analyzable fetch calls.
A call is analyzable when it is
<db>.fetch|fetch_one|stream(CLS, "literal sql", ...) where <db>is an
idiomatic chirp.dataDatabase receiver (see _is_db_receiver()),
with a string-literal SQL and at least theclspositional argument.
Dynamic SQL (f-strings, concatenation, names) and calls on unrelated
receivers are skipped.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
tree
|
ast.AST
|
— |
_schema_columns
function
def _schema_columns(schema: SchemaSnapshot | None) -> set[str] | None
Union of all column names across all tables in the declared schema.
ReturnsNonewhen no schema is available -- the check then relies on the
dataclass fields alone and only flags columns that match no field, which is
still a real drift (a SELECTed column the dataclass cannot receive).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
schema
|
SchemaSnapshot | None
|
— |
_dedent
function
def _dedent(src: str) -> str
Normalize indentation soast.parseaccepts a nested handler source.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
src
|
str
|
— |
check_data_shapes
function
def check_data_shapes(router: object, schema: SchemaSnapshot | None) -> list[ContractIssue]
Flagdb.fetch(cls, sql)columns that map to no dataclass field.
For each statically-resolvable fetch call on a route handler:
- resolve
clsto a frozen dataclass (skip if not resolvable), - parse the SELECT column list (skip
SELECT */ expressions / dynamic SQL), - a SELECTed column absent from the dataclass fields and (when a schema is
available) absent from every declared table column is real drift ->
ERROR.
Returns one issue per (route, dataclass, offending column).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
router
|
object
|
— | |
schema
|
SchemaSnapshot | None
|
— |
View source · /home/runner/work/chirp/chirp/site/../src/chirp/contracts/rules_data_shapes.py:1