sqlphilosophy 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.
- sqlphilosophy/VERSION +1 -0
- sqlphilosophy/__init__.py +3 -0
- sqlphilosophy/aio/__init__.py +3 -0
- sqlphilosophy/aio/protocols.py +26 -0
- sqlphilosophy/aio/query.py +396 -0
- sqlphilosophy/aio/repository.py +400 -0
- sqlphilosophy/audit/__init__.py +3 -0
- sqlphilosophy/audit/context.py +37 -0
- sqlphilosophy/audit/fields.py +24 -0
- sqlphilosophy/audit/listener.py +99 -0
- sqlphilosophy/audit/model.py +59 -0
- sqlphilosophy/py.typed +0 -0
- sqlphilosophy/sorting.py +97 -0
- sqlphilosophy/sql.py +532 -0
- sqlphilosophy/sync/__init__.py +3 -0
- sqlphilosophy/sync/protocols.py +26 -0
- sqlphilosophy/sync/query.py +392 -0
- sqlphilosophy/sync/repository.py +360 -0
- sqlphilosophy/types.py +61 -0
- sqlphilosophy-0.1.0.dist-info/METADATA +134 -0
- sqlphilosophy-0.1.0.dist-info/RECORD +24 -0
- sqlphilosophy-0.1.0.dist-info/WHEEL +5 -0
- sqlphilosophy-0.1.0.dist-info/licenses/LICENSE +21 -0
- sqlphilosophy-0.1.0.dist-info/top_level.txt +1 -0
sqlphilosophy/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Portable async repository factory protocol (no Phobos or app imports)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
from typing import TypeVar
|
|
6
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
7
|
+
from sqlphilosophy.aio.query import AsyncStatementQueryBuilder
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T", bound=DeclarativeBase)
|
|
10
|
+
R = TypeVar("R")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AsyncRepositoryFactory(Protocol):
|
|
14
|
+
"""Session-scoped factory for async statement builders and entity repositories."""
|
|
15
|
+
|
|
16
|
+
def create_statement(self, model: type[T]) -> AsyncStatementQueryBuilder[T]:
|
|
17
|
+
"""Return a fluent async read builder for ``model``."""
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
def get_repository(self, repo_class: type[R]) -> R:
|
|
21
|
+
"""Return a cached typed entity repository."""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
def repository(self, model: type[T]) -> object:
|
|
25
|
+
"""Return generic CRUD helpers for ``model`` (``AsyncBaseRepository`` in Phobos)."""
|
|
26
|
+
...
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""Async StatementQueryBuilder for typed entity-repo reads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from abc import ABC
|
|
5
|
+
from abc import abstractmethod
|
|
6
|
+
from typing import cast
|
|
7
|
+
from sqlalchemy import func
|
|
8
|
+
from sqlalchemy import select
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
11
|
+
from sqlalchemy.sql import Select
|
|
12
|
+
from sqlphilosophy.sorting import ListQuery
|
|
13
|
+
from sqlphilosophy.sorting import OrderByMap
|
|
14
|
+
from sqlphilosophy.sorting import SortConfig
|
|
15
|
+
from sqlphilosophy.sql import row_mapping
|
|
16
|
+
from sqlphilosophy.sql import row_mapping_opt
|
|
17
|
+
from sqlphilosophy.sql import rows_mapping
|
|
18
|
+
from sqlphilosophy.types import RowMapping
|
|
19
|
+
from sqlphilosophy.types import SqlFilter
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _AsyncMappingResult(ABC):
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def all(self) -> list[RowMapping]:
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
async def first(self) -> RowMapping | None:
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
async def one(self) -> RowMapping:
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _AsyncScalarResult[T: DeclarativeBase](ABC):
|
|
37
|
+
@abstractmethod
|
|
38
|
+
async def all(self) -> list[T]:
|
|
39
|
+
raise NotImplementedError
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
async def first(self) -> T | None:
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AsyncStatementQueryBuilder[T: DeclarativeBase](ABC):
|
|
47
|
+
"""Fluent multi-clause async read builder (joins, filters, mappings)."""
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def select_entity(self) -> AsyncStatementQueryBuilder[T]:
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def select_table(self) -> AsyncStatementQueryBuilder[T]:
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def select_columns(self, *columns: object) -> AsyncStatementQueryBuilder[T]:
|
|
59
|
+
raise NotImplementedError
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def select_from(self, from_clause: object) -> AsyncStatementQueryBuilder[T]:
|
|
63
|
+
raise NotImplementedError
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def join(
|
|
67
|
+
self,
|
|
68
|
+
target: object,
|
|
69
|
+
onclause: object | None = None,
|
|
70
|
+
*,
|
|
71
|
+
isouter: bool = False,
|
|
72
|
+
) -> AsyncStatementQueryBuilder[T]:
|
|
73
|
+
raise NotImplementedError
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def outerjoin(
|
|
77
|
+
self,
|
|
78
|
+
target: object,
|
|
79
|
+
onclause: object | None = None,
|
|
80
|
+
) -> AsyncStatementQueryBuilder[T]:
|
|
81
|
+
raise NotImplementedError
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def where(self, *criteria: SqlFilter) -> AsyncStatementQueryBuilder[T]:
|
|
85
|
+
raise NotImplementedError
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def filter_by(self, **kwargs: object) -> AsyncStatementQueryBuilder[T]:
|
|
89
|
+
raise NotImplementedError
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def distinct(self, *columns: object) -> AsyncStatementQueryBuilder[T]:
|
|
93
|
+
raise NotImplementedError
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def group_by(self, *clauses: object) -> AsyncStatementQueryBuilder[T]:
|
|
97
|
+
raise NotImplementedError
|
|
98
|
+
|
|
99
|
+
@abstractmethod
|
|
100
|
+
def correlate(self, *from_clauses: object) -> AsyncStatementQueryBuilder[T]:
|
|
101
|
+
raise NotImplementedError
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
def correlate_except(self, *from_clauses: object) -> AsyncStatementQueryBuilder[T]:
|
|
105
|
+
raise NotImplementedError
|
|
106
|
+
|
|
107
|
+
@abstractmethod
|
|
108
|
+
def as_lateral(self, name: str) -> object:
|
|
109
|
+
raise NotImplementedError
|
|
110
|
+
|
|
111
|
+
@abstractmethod
|
|
112
|
+
def as_cte(self, name: str) -> object:
|
|
113
|
+
raise NotImplementedError
|
|
114
|
+
|
|
115
|
+
@abstractmethod
|
|
116
|
+
def order_by(self, *clauses: object) -> AsyncStatementQueryBuilder[T]:
|
|
117
|
+
raise NotImplementedError
|
|
118
|
+
|
|
119
|
+
@abstractmethod
|
|
120
|
+
def apply_sort(
|
|
121
|
+
self,
|
|
122
|
+
sort: SortConfig,
|
|
123
|
+
order_by: OrderByMap | None = None,
|
|
124
|
+
) -> AsyncStatementQueryBuilder[T]:
|
|
125
|
+
raise NotImplementedError
|
|
126
|
+
|
|
127
|
+
@abstractmethod
|
|
128
|
+
def limit(self, limit: int) -> AsyncStatementQueryBuilder[T]:
|
|
129
|
+
raise NotImplementedError
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
def offset(self, offset: int) -> AsyncStatementQueryBuilder[T]:
|
|
133
|
+
raise NotImplementedError
|
|
134
|
+
|
|
135
|
+
@abstractmethod
|
|
136
|
+
def with_for_update(
|
|
137
|
+
self,
|
|
138
|
+
*,
|
|
139
|
+
of: object | None = None,
|
|
140
|
+
skip_locked: bool = False,
|
|
141
|
+
) -> AsyncStatementQueryBuilder[T]:
|
|
142
|
+
raise NotImplementedError
|
|
143
|
+
|
|
144
|
+
@abstractmethod
|
|
145
|
+
async def count(self) -> int:
|
|
146
|
+
raise NotImplementedError
|
|
147
|
+
|
|
148
|
+
@abstractmethod
|
|
149
|
+
async def count_distinct(self, *columns: object) -> int:
|
|
150
|
+
raise NotImplementedError
|
|
151
|
+
|
|
152
|
+
@abstractmethod
|
|
153
|
+
def with_params(self, params: RowMapping) -> AsyncStatementQueryBuilder[T]:
|
|
154
|
+
raise NotImplementedError
|
|
155
|
+
|
|
156
|
+
@abstractmethod
|
|
157
|
+
async def scalar(self) -> object | None:
|
|
158
|
+
raise NotImplementedError
|
|
159
|
+
|
|
160
|
+
@abstractmethod
|
|
161
|
+
def mappings(self) -> _AsyncMappingResult:
|
|
162
|
+
raise NotImplementedError
|
|
163
|
+
|
|
164
|
+
@abstractmethod
|
|
165
|
+
async def fetch_page(
|
|
166
|
+
self,
|
|
167
|
+
list_query: ListQuery,
|
|
168
|
+
*,
|
|
169
|
+
sort: SortConfig | None = None,
|
|
170
|
+
) -> tuple[list[RowMapping], int]:
|
|
171
|
+
"""Count matching rows, then return one page of row mappings."""
|
|
172
|
+
|
|
173
|
+
@abstractmethod
|
|
174
|
+
def scalars(self) -> _AsyncScalarResult[T]:
|
|
175
|
+
raise NotImplementedError
|
|
176
|
+
|
|
177
|
+
@abstractmethod
|
|
178
|
+
def build_select(self) -> Select[tuple[object, ...]]:
|
|
179
|
+
"""Return the composed SELECT (debug/logging only; execute via builder methods)."""
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class _AsyncSqlAlchemyMappingResult(_AsyncMappingResult):
|
|
183
|
+
def __init__(
|
|
184
|
+
self,
|
|
185
|
+
session: AsyncSession,
|
|
186
|
+
stmt: object,
|
|
187
|
+
params: RowMapping | None = None,
|
|
188
|
+
) -> None:
|
|
189
|
+
self._session = session
|
|
190
|
+
self._stmt = stmt
|
|
191
|
+
self._params = params or {}
|
|
192
|
+
|
|
193
|
+
async def all(self) -> list[RowMapping]:
|
|
194
|
+
result = await self._session.execute(self._stmt, self._params)
|
|
195
|
+
mapped = result.mappings()
|
|
196
|
+
rows = mapped.all() if hasattr(mapped, "all") else mapped
|
|
197
|
+
return rows_mapping(rows)
|
|
198
|
+
|
|
199
|
+
async def first(self) -> RowMapping | None:
|
|
200
|
+
result = await self._session.execute(self._stmt.limit(1), self._params)
|
|
201
|
+
row = result.mappings().first()
|
|
202
|
+
return row_mapping_opt(row)
|
|
203
|
+
|
|
204
|
+
async def one(self) -> RowMapping:
|
|
205
|
+
result = await self._session.execute(self._stmt, self._params)
|
|
206
|
+
row = result.mappings().one()
|
|
207
|
+
return row_mapping(row)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class _AsyncSqlAlchemyScalarResult[T: DeclarativeBase](_AsyncScalarResult[T]):
|
|
211
|
+
def __init__(
|
|
212
|
+
self,
|
|
213
|
+
session: AsyncSession,
|
|
214
|
+
stmt: object,
|
|
215
|
+
params: RowMapping | None = None,
|
|
216
|
+
) -> None:
|
|
217
|
+
self._session = session
|
|
218
|
+
self._stmt = stmt
|
|
219
|
+
self._params = params or {}
|
|
220
|
+
|
|
221
|
+
async def all(self) -> list[T]:
|
|
222
|
+
result = await self._session.scalars(self._stmt, self._params)
|
|
223
|
+
return list(result.all())
|
|
224
|
+
|
|
225
|
+
async def first(self) -> T | None:
|
|
226
|
+
result = await self._session.scalars(self._stmt.limit(1), self._params)
|
|
227
|
+
return result.first()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class AsyncSqlAlchemyStatementBuilder[T: DeclarativeBase](AsyncStatementQueryBuilder[T]):
|
|
231
|
+
def __init__(self, session: AsyncSession, entity_class: type[T]) -> None:
|
|
232
|
+
self._session = session
|
|
233
|
+
self._entity_class = entity_class
|
|
234
|
+
self._stmt = select(entity_class)
|
|
235
|
+
self._params: RowMapping = {}
|
|
236
|
+
|
|
237
|
+
def select_entity(self) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
238
|
+
self._stmt = select(self._entity_class)
|
|
239
|
+
return self
|
|
240
|
+
|
|
241
|
+
def select_table(self) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
242
|
+
self._stmt = select(self._entity_class.__table__)
|
|
243
|
+
return self
|
|
244
|
+
|
|
245
|
+
def select_columns(self, *columns: object) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
246
|
+
self._stmt = select(*columns)
|
|
247
|
+
return self
|
|
248
|
+
|
|
249
|
+
def select_from(self, from_clause: object) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
250
|
+
self._stmt = self._stmt.select_from(from_clause)
|
|
251
|
+
return self
|
|
252
|
+
|
|
253
|
+
def join(
|
|
254
|
+
self,
|
|
255
|
+
target: object,
|
|
256
|
+
onclause: object | None = None,
|
|
257
|
+
*,
|
|
258
|
+
isouter: bool = False,
|
|
259
|
+
) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
260
|
+
if onclause is not None:
|
|
261
|
+
self._stmt = self._stmt.join(target, onclause, isouter=isouter)
|
|
262
|
+
else:
|
|
263
|
+
self._stmt = self._stmt.join(target, isouter=isouter)
|
|
264
|
+
return self
|
|
265
|
+
|
|
266
|
+
def outerjoin(
|
|
267
|
+
self,
|
|
268
|
+
target: object,
|
|
269
|
+
onclause: object | None = None,
|
|
270
|
+
) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
271
|
+
if onclause is not None:
|
|
272
|
+
self._stmt = self._stmt.outerjoin(target, onclause)
|
|
273
|
+
else:
|
|
274
|
+
self._stmt = self._stmt.outerjoin(target)
|
|
275
|
+
return self
|
|
276
|
+
|
|
277
|
+
def where(self, *criteria: SqlFilter) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
278
|
+
self._stmt = self._stmt.where(*criteria)
|
|
279
|
+
return self
|
|
280
|
+
|
|
281
|
+
def filter_by(self, **kwargs: object) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
282
|
+
self._stmt = self._stmt.filter_by(**kwargs)
|
|
283
|
+
return self
|
|
284
|
+
|
|
285
|
+
def distinct(self, *columns: object) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
286
|
+
self._stmt = self._stmt.distinct(*columns)
|
|
287
|
+
return self
|
|
288
|
+
|
|
289
|
+
def group_by(self, *clauses: object) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
290
|
+
self._stmt = self._stmt.group_by(*clauses)
|
|
291
|
+
return self
|
|
292
|
+
|
|
293
|
+
def correlate(self, *from_clauses: object) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
294
|
+
self._stmt = self._stmt.correlate(*from_clauses)
|
|
295
|
+
return self
|
|
296
|
+
|
|
297
|
+
def correlate_except(self, *from_clauses: object) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
298
|
+
self._stmt = self._stmt.correlate_except(*from_clauses)
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
def as_lateral(self, name: str) -> object:
|
|
302
|
+
return self._stmt.lateral(name)
|
|
303
|
+
|
|
304
|
+
def as_cte(self, name: str) -> object:
|
|
305
|
+
return self._stmt.cte(name)
|
|
306
|
+
|
|
307
|
+
def order_by(self, *clauses: object) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
308
|
+
self._stmt = self._stmt.order_by(*clauses)
|
|
309
|
+
return self
|
|
310
|
+
|
|
311
|
+
def apply_sort(
|
|
312
|
+
self,
|
|
313
|
+
sort: SortConfig,
|
|
314
|
+
order_by: OrderByMap | None = None,
|
|
315
|
+
) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
316
|
+
return self.order_by(*sort.order_clauses(order_by))
|
|
317
|
+
|
|
318
|
+
def limit(self, limit: int) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
319
|
+
if limit < 0:
|
|
320
|
+
raise ValueError("limit must be >= 0")
|
|
321
|
+
self._stmt = self._stmt.limit(limit)
|
|
322
|
+
return self
|
|
323
|
+
|
|
324
|
+
def offset(self, offset: int) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
325
|
+
if offset < 0:
|
|
326
|
+
raise ValueError("offset must be >= 0")
|
|
327
|
+
self._stmt = self._stmt.offset(offset)
|
|
328
|
+
return self
|
|
329
|
+
|
|
330
|
+
def with_for_update(
|
|
331
|
+
self,
|
|
332
|
+
*,
|
|
333
|
+
of: object | None = None,
|
|
334
|
+
skip_locked: bool = False,
|
|
335
|
+
) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
336
|
+
if of is not None:
|
|
337
|
+
self._stmt = self._stmt.with_for_update(of=of, skip_locked=skip_locked)
|
|
338
|
+
else:
|
|
339
|
+
self._stmt = self._stmt.with_for_update(skip_locked=skip_locked)
|
|
340
|
+
return self
|
|
341
|
+
|
|
342
|
+
def with_params(self, params: RowMapping) -> AsyncSqlAlchemyStatementBuilder[T]:
|
|
343
|
+
self._params = {**self._params, **params}
|
|
344
|
+
return self
|
|
345
|
+
|
|
346
|
+
async def count(self) -> int:
|
|
347
|
+
froms = self._stmt.get_final_froms()
|
|
348
|
+
if len(froms) > 1:
|
|
349
|
+
from_clause = froms[-1] # pragma: no cover
|
|
350
|
+
elif froms:
|
|
351
|
+
from_clause = froms[0]
|
|
352
|
+
else:
|
|
353
|
+
from_clause = self._entity_class
|
|
354
|
+
count_stmt = select(func.count()).select_from(from_clause)
|
|
355
|
+
if self._stmt.whereclause is not None:
|
|
356
|
+
count_stmt = count_stmt.where(self._stmt.whereclause)
|
|
357
|
+
result = await self._session.execute(count_stmt, self._params)
|
|
358
|
+
return int(result.scalar_one() or 0)
|
|
359
|
+
|
|
360
|
+
async def count_distinct(self, *columns: object) -> int:
|
|
361
|
+
count_stmt = select(func.count(func.distinct(*columns)))
|
|
362
|
+
count_stmt = count_stmt.select_from(*self._stmt.get_final_froms())
|
|
363
|
+
if self._stmt.whereclause is not None:
|
|
364
|
+
count_stmt = count_stmt.where(self._stmt.whereclause)
|
|
365
|
+
result = await self._session.execute(count_stmt, self._params)
|
|
366
|
+
return int(result.scalar_one() or 0)
|
|
367
|
+
|
|
368
|
+
async def scalar(self) -> object | None:
|
|
369
|
+
result = await self._session.execute(self._stmt, self._params)
|
|
370
|
+
return result.scalar_one()
|
|
371
|
+
|
|
372
|
+
def mappings(self) -> _AsyncSqlAlchemyMappingResult:
|
|
373
|
+
return _AsyncSqlAlchemyMappingResult(self._session, self._stmt, self._params)
|
|
374
|
+
|
|
375
|
+
async def fetch_page(
|
|
376
|
+
self,
|
|
377
|
+
list_query: ListQuery,
|
|
378
|
+
*,
|
|
379
|
+
sort: SortConfig | None = None,
|
|
380
|
+
) -> tuple[list[RowMapping], int]:
|
|
381
|
+
if list_query.limit < 0:
|
|
382
|
+
raise ValueError("limit must be >= 0")
|
|
383
|
+
if list_query.offset < 0:
|
|
384
|
+
raise ValueError("offset must be >= 0")
|
|
385
|
+
builder = self
|
|
386
|
+
if sort is not None:
|
|
387
|
+
builder = builder.apply_sort(sort, list_query.order_by)
|
|
388
|
+
total = await builder.count()
|
|
389
|
+
rows = await builder.limit(list_query.limit).offset(list_query.offset).mappings().all()
|
|
390
|
+
return rows, total
|
|
391
|
+
|
|
392
|
+
def scalars(self) -> _AsyncSqlAlchemyScalarResult[T]:
|
|
393
|
+
return _AsyncSqlAlchemyScalarResult(self._session, self._stmt, self._params)
|
|
394
|
+
|
|
395
|
+
def build_select(self) -> Select[tuple[object, ...]]:
|
|
396
|
+
return cast(Select[tuple[object, ...]], self._stmt)
|