rowguard 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rowguard/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ from rowguard.api import execute, select, stream, validate_rows
2
+ from rowguard.errors import (
3
+ ConfigurationError,
4
+ QueryExecutionError,
5
+ RejectHandlerError,
6
+ RowAdaptationError,
7
+ RowGuardError,
8
+ RowValidationError,
9
+ )
10
+ from rowguard.results.query_result import QueryResult
11
+ from rowguard.results.rejected_row import RejectedRow
12
+ from rowguard.statistics import QueryStatistics
13
+
14
+ __version__ = "0.1.0"
15
+
16
+ __all__ = [
17
+ "ConfigurationError",
18
+ "QueryExecutionError",
19
+ "QueryResult",
20
+ "QueryStatistics",
21
+ "RejectHandlerError",
22
+ "RejectedRow",
23
+ "RowAdaptationError",
24
+ "RowGuardError",
25
+ "RowValidationError",
26
+ "__version__",
27
+ "execute",
28
+ "select",
29
+ "stream",
30
+ "validate_rows",
31
+ ]
@@ -0,0 +1,6 @@
1
+ """Row adapter protocols and implementations."""
2
+
3
+ from rowguard.adapters.base import AdaptedRow, RowAdapter
4
+ from rowguard.adapters.sqlalchemy_row import SQLAlchemyRowAdapter
5
+
6
+ __all__ = ["AdaptedRow", "RowAdapter", "SQLAlchemyRowAdapter"]
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from typing import Protocol
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class AdaptedRow:
10
+ mapping: Mapping[str, object]
11
+ raw_row: object | None = None
12
+
13
+
14
+ class RowAdapter(Protocol):
15
+ def adapt(self, row: object) -> AdaptedRow: ...
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+
5
+ from rowguard.adapters.base import AdaptedRow
6
+ from rowguard.errors import RowAdaptationError
7
+
8
+
9
+ class SQLAlchemyRowAdapter:
10
+ """Adapt SQLAlchemy rows or mappings into Pydantic-ready dictionaries."""
11
+
12
+ def __init__(self, field_map: Mapping[str, str] | None = None) -> None:
13
+ self._field_map = dict(field_map) if field_map else None
14
+
15
+ def adapt(self, row: object) -> AdaptedRow:
16
+ source = getattr(row, "_mapping", None)
17
+ if source is None:
18
+ if isinstance(row, Mapping):
19
+ source = row
20
+ else:
21
+ raise RowAdaptationError(f"Unsupported row type: {type(row).__name__}")
22
+
23
+ mapping: dict[str, object] = dict(source)
24
+ if self._field_map is not None:
25
+ remapped: dict[str, object] = {}
26
+ missing: list[str] = []
27
+ for field_name, column_key in self._field_map.items():
28
+ if column_key not in mapping:
29
+ missing.append(f"{field_name}->{column_key}")
30
+ continue
31
+ remapped[field_name] = mapping[column_key]
32
+ if missing:
33
+ raise RowAdaptationError(
34
+ "field_map source key(s) missing from row: " + ", ".join(missing)
35
+ )
36
+
37
+ reserved_sources = set(self._field_map.values())
38
+ reserved_destinations = set(self._field_map.keys())
39
+ for key, value in mapping.items():
40
+ if key in reserved_sources or key in reserved_destinations:
41
+ continue
42
+ if key not in remapped:
43
+ remapped[key] = value
44
+ mapping = remapped
45
+
46
+ return AdaptedRow(mapping=mapping, raw_row=row)
rowguard/api.py ADDED
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Mapping
4
+ from typing import Any, TypeVar
5
+
6
+ from pydantic import BaseModel
7
+ from sqlalchemy.sql import Select
8
+
9
+ from rowguard.adapters.sqlalchemy_row import SQLAlchemyRowAdapter
10
+ from rowguard.errors import ConfigurationError
11
+ from rowguard.execution.sync import SyncExecutionEngine
12
+ from rowguard.planning.compiler import QueryPlanner
13
+ from rowguard.planning.execution_plan import ExecutionPlan
14
+ from rowguard.planning.request import QueryRequest
15
+ from rowguard.rejection.base import RejectionPolicy
16
+ from rowguard.rejection.policies import CollectPolicy, RaisePolicy, SkipPolicy
17
+ from rowguard.results.query_result import QueryResult
18
+ from rowguard.validation.pydantic import PydanticValidator
19
+
20
+ T = TypeVar("T", bound=BaseModel)
21
+
22
+ _POLICIES: dict[str, type[RejectionPolicy]] = {
23
+ "raise": RaisePolicy,
24
+ "collect": CollectPolicy,
25
+ "skip": SkipPolicy,
26
+ }
27
+
28
+
29
+ def select(
30
+ *,
31
+ session: Any | None = None,
32
+ connection: Any | None = None,
33
+ table: Any,
34
+ model: type[T],
35
+ where: Iterable[Any] = (),
36
+ field_map: Mapping[str, str] | None = None,
37
+ column_map: Mapping[str, Any] | None = None,
38
+ parameters: Mapping[str, object] | None = None,
39
+ on_reject: str = "raise",
40
+ use_sqlrules: bool = True,
41
+ ) -> QueryResult[T]:
42
+ """Build and execute a validation-first SQLAlchemy SELECT query."""
43
+ request: QueryRequest[T] = QueryRequest(
44
+ model=model,
45
+ source=table,
46
+ session=session,
47
+ connection=connection,
48
+ where=tuple(where),
49
+ field_map=field_map,
50
+ column_map=column_map,
51
+ parameters=dict(parameters or {}),
52
+ on_reject=on_reject,
53
+ use_sqlrules=use_sqlrules,
54
+ )
55
+ plan = QueryPlanner[T]().compile(request)
56
+ return SyncExecutionEngine[T]().execute(plan)
57
+
58
+
59
+ def execute(
60
+ *,
61
+ session: Any | None = None,
62
+ connection: Any | None = None,
63
+ statement: Select[Any],
64
+ model: type[T],
65
+ source: Any | None = None,
66
+ where: Iterable[Any] = (),
67
+ field_map: Mapping[str, str] | None = None,
68
+ column_map: Mapping[str, Any] | None = None,
69
+ parameters: Mapping[str, object] | None = None,
70
+ on_reject: str = "raise",
71
+ use_sqlrules: bool = True,
72
+ ) -> QueryResult[T]:
73
+ """Execute an existing SQLAlchemy statement and validate every row."""
74
+ request: QueryRequest[T] = QueryRequest(
75
+ model=model,
76
+ source=source,
77
+ statement=statement,
78
+ session=session,
79
+ connection=connection,
80
+ where=tuple(where),
81
+ field_map=field_map,
82
+ column_map=column_map,
83
+ parameters=dict(parameters or {}),
84
+ on_reject=on_reject,
85
+ use_sqlrules=use_sqlrules,
86
+ )
87
+ plan = QueryPlanner[T]().compile(request)
88
+ return SyncExecutionEngine[T]().execute(plan)
89
+
90
+
91
+ def stream(
92
+ *,
93
+ session: Any,
94
+ statement: Select[Any],
95
+ model: type[T],
96
+ on_reject: str = "raise",
97
+ ) -> Iterable[T]:
98
+ """Stream validated models without buffering all accepted rows.
99
+
100
+ Deferred until 0.3.0.
101
+ """
102
+ raise NotImplementedError(
103
+ "stream() is deferred to RowGuard 0.3.0; use select()/execute() for buffered results"
104
+ )
105
+
106
+
107
+ def validate_rows(
108
+ *,
109
+ rows: Iterable[Mapping[str, object]],
110
+ model: type[T],
111
+ field_map: Mapping[str, str] | None = None,
112
+ on_reject: str = "raise",
113
+ ) -> QueryResult[T]:
114
+ """Validate row mappings without executing SQL."""
115
+ policy_cls = _POLICIES.get(on_reject)
116
+ if policy_cls is None:
117
+ raise ConfigurationError(
118
+ f"Unsupported on_reject policy: {on_reject!r}. "
119
+ f"Supported: {', '.join(sorted(_POLICIES))}"
120
+ )
121
+
122
+ plan: ExecutionPlan[T] = ExecutionPlan(
123
+ statement=None,
124
+ model=model,
125
+ adapter=SQLAlchemyRowAdapter(field_map=field_map),
126
+ validator=PydanticValidator(model),
127
+ rejection_policy=policy_cls(),
128
+ use_sqlrules=False,
129
+ )
130
+ return SyncExecutionEngine[T]().validate_rows(plan=plan, rows=rows)
rowguard/cache.py ADDED
@@ -0,0 +1,26 @@
1
+ from collections import OrderedDict
2
+ from typing import Generic, TypeVar
3
+
4
+ K = TypeVar("K")
5
+ V = TypeVar("V")
6
+
7
+
8
+ class LRUCache(Generic[K, V]):
9
+ def __init__(self, max_entries: int = 512) -> None:
10
+ self._max_entries = max_entries
11
+ self._items: OrderedDict[K, V] = OrderedDict()
12
+
13
+ def get(self, key: K) -> V | None:
14
+ value = self._items.get(key)
15
+ if value is not None:
16
+ self._items.move_to_end(key)
17
+ return value
18
+
19
+ def set(self, key: K, value: V) -> None:
20
+ self._items[key] = value
21
+ self._items.move_to_end(key)
22
+ while len(self._items) > self._max_entries:
23
+ self._items.popitem(last=False)
24
+
25
+ def clear(self) -> None:
26
+ self._items.clear()
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime, timezone
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class Diagnostic:
10
+ code: str
11
+ severity: str
12
+ execution_id: str
13
+ row_index: int | None = None
14
+ metadata: Mapping[str, object] = field(default_factory=dict)
15
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
rowguard/errors.py ADDED
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import ValidationError
6
+
7
+
8
+ class RowGuardError(Exception):
9
+ """Base exception for all RowGuard errors."""
10
+
11
+
12
+ class ConfigurationError(RowGuardError):
13
+ """Invalid or incompatible RowGuard configuration."""
14
+
15
+
16
+ class QueryExecutionError(RowGuardError):
17
+ """SQLAlchemy query execution failed."""
18
+
19
+
20
+ class RowAdaptationError(RowGuardError):
21
+ """A database row could not be adapted safely."""
22
+
23
+ def __init__(
24
+ self,
25
+ message: str,
26
+ *,
27
+ model: type[Any] | None = None,
28
+ row_index: int | None = None,
29
+ ) -> None:
30
+ self.model = model
31
+ self.row_index = row_index
32
+ super().__init__(message)
33
+
34
+
35
+ class RowValidationError(RowGuardError):
36
+ """A row failed Pydantic validation under the raise policy."""
37
+
38
+ def __init__(
39
+ self,
40
+ *,
41
+ model: type[Any],
42
+ validation_error: ValidationError,
43
+ row_index: int | None = None,
44
+ ) -> None:
45
+ self.model = model
46
+ self.validation_error = validation_error
47
+ self.row_index = row_index
48
+ super().__init__(
49
+ f"Row {row_index!r} failed validation for {model.__name__} "
50
+ f"with {validation_error.error_count()} error(s)."
51
+ )
52
+
53
+
54
+ class RejectHandlerError(RowGuardError):
55
+ """A rejection callback or quarantine provider failed."""
56
+
57
+
58
+ class ResultAssemblyError(RowGuardError):
59
+ """An inconsistent public result was about to be created."""
@@ -0,0 +1,6 @@
1
+ """Execution engines and per-row processing."""
2
+
3
+ from rowguard.execution.processor import ProcessedRow, process_row
4
+ from rowguard.execution.sync import SyncExecutionEngine
5
+
6
+ __all__ = ["ProcessedRow", "SyncExecutionEngine", "process_row"]
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from time import perf_counter_ns
5
+ from typing import Generic, TypeVar
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from rowguard.errors import RowAdaptationError
10
+ from rowguard.planning.execution_plan import ExecutionPlan
11
+ from rowguard.rejection.base import RejectionDecision
12
+ from rowguard.results.rejected_row import RejectedRow
13
+
14
+ T = TypeVar("T", bound=BaseModel)
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class ProcessedRow(Generic[T]):
19
+ model: T | None
20
+ rejected: RejectedRow | None
21
+ retain_rejection: bool
22
+ continue_processing: bool
23
+ adaptation_time_ns: int = 0
24
+ validation_time_ns: int = 0
25
+ rejection_time_ns: int = 0
26
+ validated: bool = False
27
+
28
+
29
+ def process_row(
30
+ *,
31
+ row: object,
32
+ index: int,
33
+ plan: ExecutionPlan[T],
34
+ ) -> ProcessedRow[T]:
35
+ """Adapt, validate, and apply rejection policy for a single row."""
36
+ adaptation_time_ns = 0
37
+ validation_time_ns = 0
38
+
39
+ started = perf_counter_ns()
40
+ try:
41
+ adapted = plan.adapter.adapt(row)
42
+ except RowAdaptationError as error:
43
+ adaptation_time_ns = perf_counter_ns() - started
44
+ rejected = RejectedRow(
45
+ index=index,
46
+ model=plan.model,
47
+ mapping=None,
48
+ validation_error=None,
49
+ adaptation_error=error,
50
+ raw_row=row,
51
+ )
52
+ return _handle_rejection(
53
+ plan,
54
+ rejected,
55
+ adaptation_time_ns=adaptation_time_ns,
56
+ validated=False,
57
+ )
58
+ adaptation_time_ns = perf_counter_ns() - started
59
+
60
+ started = perf_counter_ns()
61
+ outcome = plan.validator.validate(adapted.mapping)
62
+ validation_time_ns = perf_counter_ns() - started
63
+
64
+ if outcome.accepted:
65
+ assert outcome.model is not None
66
+ return ProcessedRow(
67
+ model=outcome.model,
68
+ rejected=None,
69
+ retain_rejection=False,
70
+ continue_processing=True,
71
+ adaptation_time_ns=adaptation_time_ns,
72
+ validation_time_ns=validation_time_ns,
73
+ validated=True,
74
+ )
75
+
76
+ rejected = RejectedRow(
77
+ index=index,
78
+ model=plan.model,
79
+ mapping=adapted.mapping,
80
+ validation_error=outcome.error,
81
+ raw_row=adapted.raw_row,
82
+ )
83
+ return _handle_rejection(
84
+ plan,
85
+ rejected,
86
+ adaptation_time_ns=adaptation_time_ns,
87
+ validation_time_ns=validation_time_ns,
88
+ validated=True,
89
+ )
90
+
91
+
92
+ def _handle_rejection(
93
+ plan: ExecutionPlan[T],
94
+ rejected: RejectedRow,
95
+ *,
96
+ adaptation_time_ns: int = 0,
97
+ validation_time_ns: int = 0,
98
+ validated: bool = False,
99
+ ) -> ProcessedRow[T]:
100
+ started = perf_counter_ns()
101
+ decision: RejectionDecision = plan.rejection_policy.handle(rejected)
102
+ rejection_time_ns = perf_counter_ns() - started
103
+ return ProcessedRow(
104
+ model=None,
105
+ rejected=rejected,
106
+ retain_rejection=decision.retain_rejection,
107
+ continue_processing=decision.continue_processing,
108
+ adaptation_time_ns=adaptation_time_ns,
109
+ validation_time_ns=validation_time_ns,
110
+ rejection_time_ns=rejection_time_ns,
111
+ validated=validated,
112
+ )
@@ -0,0 +1,14 @@
1
+ from collections.abc import Iterator
2
+ from typing import Generic, TypeVar
3
+
4
+ from pydantic import BaseModel
5
+
6
+ T = TypeVar("T", bound=BaseModel)
7
+
8
+
9
+ class StreamResult(Generic[T], Iterator[T]):
10
+ def __iter__(self) -> "StreamResult[T]":
11
+ return self
12
+
13
+ def __next__(self) -> T:
14
+ raise StopIteration
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Mapping
4
+ from dataclasses import dataclass, field
5
+ from time import perf_counter_ns
6
+ from typing import Any, Generic, TypeVar
7
+
8
+ from pydantic import BaseModel
9
+
10
+ from rowguard.diagnostics import Diagnostic
11
+ from rowguard.errors import QueryExecutionError, ResultAssemblyError, RowGuardError
12
+ from rowguard.execution.processor import ProcessedRow, process_row
13
+ from rowguard.planning.execution_plan import ExecutionPlan
14
+ from rowguard.results.query_result import QueryResult
15
+ from rowguard.results.rejected_row import RejectedRow
16
+ from rowguard.statistics import QueryStatistics
17
+
18
+ T = TypeVar("T", bound=BaseModel)
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class _MutableStatistics:
23
+ rows_read: int = 0
24
+ rows_validated: int = 0
25
+ rows_accepted: int = 0
26
+ rows_rejected: int = 0
27
+ execution_time_ns: int = 0
28
+ adaptation_time_ns: int = 0
29
+ validation_time_ns: int = 0
30
+ rejection_time_ns: int = 0
31
+
32
+ def snapshot(self) -> QueryStatistics:
33
+ return QueryStatistics(
34
+ rows_read=self.rows_read,
35
+ rows_validated=self.rows_validated,
36
+ rows_accepted=self.rows_accepted,
37
+ rows_rejected=self.rows_rejected,
38
+ execution_time_ns=self.execution_time_ns,
39
+ adaptation_time_ns=self.adaptation_time_ns,
40
+ validation_time_ns=self.validation_time_ns,
41
+ rejection_time_ns=self.rejection_time_ns,
42
+ )
43
+
44
+
45
+ @dataclass(slots=True)
46
+ class ExecutionState(Generic[T]):
47
+ plan: ExecutionPlan[T]
48
+ statistics: _MutableStatistics = field(default_factory=_MutableStatistics)
49
+ accepted: list[T] = field(default_factory=list)
50
+ rejected: list[RejectedRow] = field(default_factory=list)
51
+ diagnostics: list[Diagnostic] = field(default_factory=list)
52
+
53
+
54
+ class SyncExecutionEngine(Generic[T]):
55
+ def execute(self, plan: ExecutionPlan[T]) -> QueryResult[T]:
56
+ state = ExecutionState(plan=plan)
57
+ state.diagnostics.extend(plan.diagnostics)
58
+ started = perf_counter_ns()
59
+ result: Any | None = None
60
+
61
+ try:
62
+ result = self._execute_statement(plan)
63
+ for index, row in enumerate(result):
64
+ processed = process_row(row=row, index=index, plan=plan)
65
+ if not self._consume_processed(state, processed):
66
+ break
67
+ except RowGuardError:
68
+ raise
69
+ except Exception as exc:
70
+ raise QueryExecutionError(f"Query execution failed: {exc}") from exc
71
+ finally:
72
+ if result is not None:
73
+ close = getattr(result, "close", None)
74
+ if callable(close):
75
+ close()
76
+ state.statistics.execution_time_ns = perf_counter_ns() - started
77
+
78
+ return self._assemble(state)
79
+
80
+ def validate_rows(
81
+ self,
82
+ *,
83
+ plan: ExecutionPlan[T],
84
+ rows: Iterable[Mapping[str, object]],
85
+ ) -> QueryResult[T]:
86
+ state = ExecutionState(plan=plan)
87
+ state.diagnostics.extend(plan.diagnostics)
88
+ started = perf_counter_ns()
89
+
90
+ try:
91
+ for index, row in enumerate(rows):
92
+ processed = process_row(row=row, index=index, plan=plan)
93
+ if not self._consume_processed(state, processed):
94
+ break
95
+ finally:
96
+ state.statistics.execution_time_ns = perf_counter_ns() - started
97
+
98
+ return self._assemble(state)
99
+
100
+ def _consume_processed(
101
+ self,
102
+ state: ExecutionState[T],
103
+ processed: ProcessedRow[T],
104
+ ) -> bool:
105
+ """Update state from a processed row. Returns whether to continue."""
106
+ state.statistics.rows_read += 1
107
+ state.statistics.adaptation_time_ns += processed.adaptation_time_ns
108
+ state.statistics.validation_time_ns += processed.validation_time_ns
109
+ state.statistics.rejection_time_ns += processed.rejection_time_ns
110
+ if processed.validated:
111
+ state.statistics.rows_validated += 1
112
+
113
+ if processed.model is not None:
114
+ state.statistics.rows_accepted += 1
115
+ state.accepted.append(processed.model)
116
+ return True
117
+
118
+ state.statistics.rows_rejected += 1
119
+ if processed.retain_rejection and processed.rejected is not None:
120
+ state.rejected.append(processed.rejected)
121
+ return processed.continue_processing
122
+
123
+ def _execute_statement(self, plan: ExecutionPlan[T]) -> Any:
124
+ params = dict(plan.parameters) if plan.parameters else {}
125
+ if plan.session is not None:
126
+ if params:
127
+ return plan.session.execute(plan.statement, params)
128
+ return plan.session.execute(plan.statement)
129
+ if plan.connection is not None:
130
+ if params:
131
+ return plan.connection.execute(plan.statement, params)
132
+ return plan.connection.execute(plan.statement)
133
+ raise QueryExecutionError("No session or connection available for execution")
134
+
135
+ def _assemble(self, state: ExecutionState[T]) -> QueryResult[T]:
136
+ stats = state.statistics.snapshot()
137
+ if stats.rows_accepted != len(state.accepted):
138
+ raise ResultAssemblyError("Accepted count does not match models")
139
+ if stats.rows_rejected < len(state.rejected):
140
+ raise ResultAssemblyError("Rejected count is less than retained rejections")
141
+ if stats.rows_accepted + stats.rows_rejected != stats.rows_read:
142
+ raise ResultAssemblyError("Read rows are not fully classified")
143
+ if stats.rows_validated > stats.rows_read:
144
+ raise ResultAssemblyError("Validated count exceeds rows read")
145
+
146
+ return QueryResult(
147
+ models=tuple(state.accepted),
148
+ rejected=tuple(state.rejected),
149
+ statistics=stats,
150
+ statement=state.plan.statement,
151
+ diagnostics=tuple(state.diagnostics),
152
+ )
@@ -0,0 +1,11 @@
1
+ """Database and SQLRules integrations."""
2
+
3
+ from rowguard.integrations.sqlalchemy_core import apply_where, build_select
4
+ from rowguard.integrations.sqlrules import CompiledPushdown, SQLRulesBridge
5
+
6
+ __all__ = [
7
+ "CompiledPushdown",
8
+ "SQLRulesBridge",
9
+ "apply_where",
10
+ "build_select",
11
+ ]
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from typing import Any, cast
5
+
6
+ from sqlalchemy import Select, select
7
+ from sqlalchemy.sql import ColumnElement
8
+
9
+
10
+ def build_select(source: object) -> Select[Any]:
11
+ """Build a SELECT over a Core table or selectable."""
12
+ return cast(Select[Any], select(source)) # type: ignore[call-overload]
13
+
14
+
15
+ def apply_where(statement: Select[Any], expressions: Iterable[object]) -> Select[Any]:
16
+ """Return a new statement with additional WHERE expressions."""
17
+ exprs = tuple(expressions)
18
+ if not exprs:
19
+ return statement
20
+ return statement.where(*exprs) # type: ignore[arg-type]
21
+
22
+
23
+ def is_select(value: object) -> bool:
24
+ return isinstance(value, Select)
25
+
26
+
27
+ def is_column_element(value: object) -> bool:
28
+ return isinstance(value, ColumnElement)
@@ -0,0 +1,5 @@
1
+ """Deferred ORM integration.
2
+
3
+ Milestone 0.1.0 should focus on SQLAlchemy Core. Keep this module as the future
4
+ boundary for mapped-class and entity adaptation.
5
+ """
@@ -0,0 +1,5 @@
1
+ """Deferred SQLModel integration.
2
+
3
+ SQLModel remains responsible for mapping and persistence. RowGuard will add
4
+ explicit validation-first query execution in a later milestone.
5
+ """