Coverage for snekql/_schema_plan.py: 96%
49 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 model-derived schema planning for backend schema startup."""
3from __future__ import annotations
5from collections.abc import Sequence
6from dataclasses import dataclass
7from typing import Any
9from snekql.errors import SchemaError
10from snekql.indexes import NormalizedIndex
11from snekql.model import Table, require_model_columns, require_model_table_name
12from snekql.storage import Attr, SchemaPolicy
15@dataclass(frozen=True)
16class PlannedColumn:
17 """One model column resolved for schema startup."""
19 column: Attr[Any, Any, Any, Any, Any]
20 name: str
23@dataclass(frozen=True)
24class PlannedModel:
25 """One table model's backend-neutral schema startup inputs."""
27 columns: tuple[PlannedColumn, ...]
28 indexes: tuple[NormalizedIndex, ...]
29 model: type[Table[Any]]
30 table_name: str
33@dataclass(frozen=True)
34class SchemaPlan:
35 """Ordered schema startup plan shared by backend adapters."""
37 models: tuple[PlannedModel, ...]
40def _column_unique_indexes(
41 table_name: str,
42 columns: tuple[PlannedColumn, ...],
43) -> tuple[NormalizedIndex, ...]:
44 return tuple(
45 NormalizedIndex(
46 column_names=(planned_column.name,),
47 name=f"ux_{table_name}_{planned_column.name}",
48 unique=True,
49 )
50 for planned_column in columns
51 if planned_column.column.unique
52 )
55def _model_indexes(
56 model: type[Table[Any]],
57 table_name: str,
58 columns: tuple[PlannedColumn, ...],
59) -> tuple[NormalizedIndex, ...]:
60 table_indexes = getattr(model, "__snekql_indexes__", ())
61 return (*_column_unique_indexes(table_name, columns), *table_indexes)
64def _plan_model(model: type[Table[Any]]) -> PlannedModel:
65 table_name = require_model_table_name(model)
66 columns = tuple(
67 PlannedColumn(column=column, name=name)
68 for name, column in require_model_columns(model).items()
69 )
70 return PlannedModel(
71 columns=columns,
72 indexes=_model_indexes(model, table_name, columns),
73 model=model,
74 table_name=table_name,
75 )
78def build_schema_plan(models: Sequence[type[Table[Any]]]) -> SchemaPlan:
79 """Resolve and validate model-derived schema startup metadata once."""
81 planned_models = tuple(_plan_model(model) for model in models)
82 table_names: set[str] = set()
83 index_names: set[str] = set()
84 for planned_model in planned_models:
85 if planned_model.table_name in table_names:
86 msg = f"duplicate table name: {planned_model.table_name!r}"
87 raise SchemaError(msg)
88 table_names.add(planned_model.table_name)
89 for index in planned_model.indexes:
90 if index.name in index_names:
91 msg = f"duplicate index name: {index.name!r}"
92 raise SchemaError(msg)
93 index_names.add(index.name)
94 return SchemaPlan(models=planned_models)
97def validate_schema_policy(schema_policy: SchemaPolicy) -> None:
98 """Reject unsupported schema policy values."""
100 if schema_policy not in {"strict", "warn"}:
101 msg = "schema_policy must be 'strict' or 'warn'"
102 raise SchemaError(msg)