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