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 ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,3 @@
1
+ """SQLAlchemy repository kit — sync/async CRUD, query builders, sort, and SQL helpers."""
2
+
3
+ __all__: list[str] = []
@@ -0,0 +1,3 @@
1
+ """Async repository subpackage."""
2
+
3
+ __all__: list[str] = []
@@ -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)