Coverage for snekql/_model_materialization.py: 89%
55 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-07 21:13 +0300
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-07 21:13 +0300
1"""Shared Table Model row encoding and fetched-row materialization."""
3from __future__ import annotations
5from collections.abc import Mapping
6from typing import Any, Literal, cast
8from snekql.errors import (
9 ModelDeclarationError,
10 ModelValidationError,
11 QueryConstructionError,
12)
13from snekql.storage import MISSING, Attr
15type StorageBackend = Literal["mariadb", "sqlite"]
18def _require_model_columns(
19 model: type[object],
20) -> dict[str, Attr[Any, Any, Any, Any, Any]]:
21 columns = getattr(model, "__snekql_columns__", None)
22 if not isinstance(columns, dict):
23 msg = "schema setup requires snekql table models"
24 raise ModelDeclarationError(msg)
25 return cast("dict[str, Attr[Any, Any, Any, Any, Any]]", columns)
28def _require_insert_model(row: object) -> type[object]:
29 model_class = row.__class__
30 try:
31 _ = _require_model_columns(model_class)
32 except ModelDeclarationError as error:
33 msg = "insert requires a snekql model instance"
34 raise QueryConstructionError(msg) from error
35 return model_class
38def encode_column_value(
39 column: Attr[Any, Any, Any, Any, Any],
40 value: object,
41 *,
42 backend: StorageBackend,
43) -> object:
44 """Encode one logical model value through a backend-specific column codec."""
46 if backend == "mariadb":
47 return column.encode_mariadb(value)
48 return column.encode_sqlite(value)
51def decode_column_value(
52 column: Attr[Any, Any, Any, Any, Any],
53 value: object,
54 *,
55 backend: StorageBackend,
56) -> object:
57 """Decode one database value through a backend-specific column codec."""
59 if backend == "mariadb":
60 return column.decode_mariadb(value)
61 return column.decode_sqlite(value)
64def encode_model_row(
65 row: object,
66 *,
67 backend: StorageBackend,
68) -> tuple[type[object], dict[str, object]]:
69 """Encode a Pending Model into table metadata and backend row values."""
71 model_class = _require_insert_model(row)
72 encoded_row: dict[str, object] = {}
73 for name, column in _require_model_columns(model_class).items():
74 value = getattr(row, name)
75 if value is MISSING:
76 continue
77 encoded_row[name] = encode_column_value(column, value, backend=backend)
78 return model_class, encoded_row
81def decode_model_row(
82 model: type[object],
83 row: Mapping[str, object],
84 *,
85 backend: StorageBackend,
86) -> object:
87 """Materialize a Fetched Model from backend row values."""
89 remaining_values = dict(row)
90 model_instance = object.__new__(model)
91 storage = cast(
92 "dict[str, object]",
93 object.__getattribute__(model_instance, "__dict__"),
94 )
95 storage["_snekql_frozen"] = False
96 storage["_snekql_state"] = "Fetched"
97 for name, column in _require_model_columns(model).items():
98 if name not in remaining_values:
99 msg = f"missing database value for {name!r}"
100 raise ModelValidationError(msg)
101 value = decode_column_value(
102 column,
103 remaining_values.pop(name),
104 backend=backend,
105 )
106 setattr(model_instance, name, value)
107 if remaining_values:
108 names = ", ".join(sorted(remaining_values))
109 msg = f"unknown database values: {names}"
110 raise ModelValidationError(msg)
111 storage["_snekql_frozen"] = True
112 return model_instance