sqlspec 0.16.2__cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.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.

Potentially problematic release.


This version of sqlspec might be problematic. Click here for more details.

Files changed (148) hide show
  1. 51ff5a9eadfdefd49f98__mypyc.cpython-39-aarch64-linux-gnu.so +0 -0
  2. sqlspec/__init__.py +92 -0
  3. sqlspec/__main__.py +12 -0
  4. sqlspec/__metadata__.py +14 -0
  5. sqlspec/_serialization.py +77 -0
  6. sqlspec/_sql.py +1782 -0
  7. sqlspec/_typing.py +680 -0
  8. sqlspec/adapters/__init__.py +0 -0
  9. sqlspec/adapters/adbc/__init__.py +5 -0
  10. sqlspec/adapters/adbc/_types.py +12 -0
  11. sqlspec/adapters/adbc/config.py +361 -0
  12. sqlspec/adapters/adbc/driver.py +512 -0
  13. sqlspec/adapters/aiosqlite/__init__.py +19 -0
  14. sqlspec/adapters/aiosqlite/_types.py +13 -0
  15. sqlspec/adapters/aiosqlite/config.py +253 -0
  16. sqlspec/adapters/aiosqlite/driver.py +248 -0
  17. sqlspec/adapters/asyncmy/__init__.py +19 -0
  18. sqlspec/adapters/asyncmy/_types.py +12 -0
  19. sqlspec/adapters/asyncmy/config.py +180 -0
  20. sqlspec/adapters/asyncmy/driver.py +274 -0
  21. sqlspec/adapters/asyncpg/__init__.py +21 -0
  22. sqlspec/adapters/asyncpg/_types.py +17 -0
  23. sqlspec/adapters/asyncpg/config.py +229 -0
  24. sqlspec/adapters/asyncpg/driver.py +344 -0
  25. sqlspec/adapters/bigquery/__init__.py +18 -0
  26. sqlspec/adapters/bigquery/_types.py +12 -0
  27. sqlspec/adapters/bigquery/config.py +298 -0
  28. sqlspec/adapters/bigquery/driver.py +558 -0
  29. sqlspec/adapters/duckdb/__init__.py +22 -0
  30. sqlspec/adapters/duckdb/_types.py +12 -0
  31. sqlspec/adapters/duckdb/config.py +504 -0
  32. sqlspec/adapters/duckdb/driver.py +368 -0
  33. sqlspec/adapters/oracledb/__init__.py +32 -0
  34. sqlspec/adapters/oracledb/_types.py +14 -0
  35. sqlspec/adapters/oracledb/config.py +317 -0
  36. sqlspec/adapters/oracledb/driver.py +538 -0
  37. sqlspec/adapters/psqlpy/__init__.py +16 -0
  38. sqlspec/adapters/psqlpy/_types.py +11 -0
  39. sqlspec/adapters/psqlpy/config.py +214 -0
  40. sqlspec/adapters/psqlpy/driver.py +530 -0
  41. sqlspec/adapters/psycopg/__init__.py +32 -0
  42. sqlspec/adapters/psycopg/_types.py +17 -0
  43. sqlspec/adapters/psycopg/config.py +426 -0
  44. sqlspec/adapters/psycopg/driver.py +796 -0
  45. sqlspec/adapters/sqlite/__init__.py +15 -0
  46. sqlspec/adapters/sqlite/_types.py +11 -0
  47. sqlspec/adapters/sqlite/config.py +240 -0
  48. sqlspec/adapters/sqlite/driver.py +294 -0
  49. sqlspec/base.py +571 -0
  50. sqlspec/builder/__init__.py +62 -0
  51. sqlspec/builder/_base.py +473 -0
  52. sqlspec/builder/_column.py +320 -0
  53. sqlspec/builder/_ddl.py +1346 -0
  54. sqlspec/builder/_ddl_utils.py +103 -0
  55. sqlspec/builder/_delete.py +76 -0
  56. sqlspec/builder/_insert.py +421 -0
  57. sqlspec/builder/_merge.py +71 -0
  58. sqlspec/builder/_parsing_utils.py +164 -0
  59. sqlspec/builder/_select.py +170 -0
  60. sqlspec/builder/_update.py +188 -0
  61. sqlspec/builder/mixins/__init__.py +55 -0
  62. sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
  63. sqlspec/builder/mixins/_delete_operations.py +41 -0
  64. sqlspec/builder/mixins/_insert_operations.py +244 -0
  65. sqlspec/builder/mixins/_join_operations.py +149 -0
  66. sqlspec/builder/mixins/_merge_operations.py +562 -0
  67. sqlspec/builder/mixins/_order_limit_operations.py +135 -0
  68. sqlspec/builder/mixins/_pivot_operations.py +153 -0
  69. sqlspec/builder/mixins/_select_operations.py +604 -0
  70. sqlspec/builder/mixins/_update_operations.py +202 -0
  71. sqlspec/builder/mixins/_where_clause.py +644 -0
  72. sqlspec/cli.py +247 -0
  73. sqlspec/config.py +395 -0
  74. sqlspec/core/__init__.py +63 -0
  75. sqlspec/core/cache.cpython-39-aarch64-linux-gnu.so +0 -0
  76. sqlspec/core/cache.py +871 -0
  77. sqlspec/core/compiler.cpython-39-aarch64-linux-gnu.so +0 -0
  78. sqlspec/core/compiler.py +417 -0
  79. sqlspec/core/filters.cpython-39-aarch64-linux-gnu.so +0 -0
  80. sqlspec/core/filters.py +830 -0
  81. sqlspec/core/hashing.cpython-39-aarch64-linux-gnu.so +0 -0
  82. sqlspec/core/hashing.py +310 -0
  83. sqlspec/core/parameters.cpython-39-aarch64-linux-gnu.so +0 -0
  84. sqlspec/core/parameters.py +1237 -0
  85. sqlspec/core/result.cpython-39-aarch64-linux-gnu.so +0 -0
  86. sqlspec/core/result.py +677 -0
  87. sqlspec/core/splitter.cpython-39-aarch64-linux-gnu.so +0 -0
  88. sqlspec/core/splitter.py +819 -0
  89. sqlspec/core/statement.cpython-39-aarch64-linux-gnu.so +0 -0
  90. sqlspec/core/statement.py +676 -0
  91. sqlspec/driver/__init__.py +19 -0
  92. sqlspec/driver/_async.py +502 -0
  93. sqlspec/driver/_common.py +631 -0
  94. sqlspec/driver/_sync.py +503 -0
  95. sqlspec/driver/mixins/__init__.py +6 -0
  96. sqlspec/driver/mixins/_result_tools.py +193 -0
  97. sqlspec/driver/mixins/_sql_translator.py +86 -0
  98. sqlspec/exceptions.py +193 -0
  99. sqlspec/extensions/__init__.py +0 -0
  100. sqlspec/extensions/aiosql/__init__.py +10 -0
  101. sqlspec/extensions/aiosql/adapter.py +461 -0
  102. sqlspec/extensions/litestar/__init__.py +6 -0
  103. sqlspec/extensions/litestar/_utils.py +52 -0
  104. sqlspec/extensions/litestar/cli.py +48 -0
  105. sqlspec/extensions/litestar/config.py +92 -0
  106. sqlspec/extensions/litestar/handlers.py +260 -0
  107. sqlspec/extensions/litestar/plugin.py +145 -0
  108. sqlspec/extensions/litestar/providers.py +454 -0
  109. sqlspec/loader.cpython-39-aarch64-linux-gnu.so +0 -0
  110. sqlspec/loader.py +760 -0
  111. sqlspec/migrations/__init__.py +35 -0
  112. sqlspec/migrations/base.py +414 -0
  113. sqlspec/migrations/commands.py +443 -0
  114. sqlspec/migrations/loaders.py +402 -0
  115. sqlspec/migrations/runner.py +213 -0
  116. sqlspec/migrations/tracker.py +140 -0
  117. sqlspec/migrations/utils.py +129 -0
  118. sqlspec/protocols.py +407 -0
  119. sqlspec/py.typed +0 -0
  120. sqlspec/storage/__init__.py +23 -0
  121. sqlspec/storage/backends/__init__.py +0 -0
  122. sqlspec/storage/backends/base.py +163 -0
  123. sqlspec/storage/backends/fsspec.py +386 -0
  124. sqlspec/storage/backends/obstore.py +459 -0
  125. sqlspec/storage/capabilities.py +102 -0
  126. sqlspec/storage/registry.py +239 -0
  127. sqlspec/typing.py +299 -0
  128. sqlspec/utils/__init__.py +3 -0
  129. sqlspec/utils/correlation.py +150 -0
  130. sqlspec/utils/deprecation.py +106 -0
  131. sqlspec/utils/fixtures.cpython-39-aarch64-linux-gnu.so +0 -0
  132. sqlspec/utils/fixtures.py +58 -0
  133. sqlspec/utils/logging.py +127 -0
  134. sqlspec/utils/module_loader.py +89 -0
  135. sqlspec/utils/serializers.py +4 -0
  136. sqlspec/utils/singleton.py +32 -0
  137. sqlspec/utils/sync_tools.cpython-39-aarch64-linux-gnu.so +0 -0
  138. sqlspec/utils/sync_tools.py +237 -0
  139. sqlspec/utils/text.cpython-39-aarch64-linux-gnu.so +0 -0
  140. sqlspec/utils/text.py +96 -0
  141. sqlspec/utils/type_guards.cpython-39-aarch64-linux-gnu.so +0 -0
  142. sqlspec/utils/type_guards.py +1139 -0
  143. sqlspec-0.16.2.dist-info/METADATA +365 -0
  144. sqlspec-0.16.2.dist-info/RECORD +148 -0
  145. sqlspec-0.16.2.dist-info/WHEEL +7 -0
  146. sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
  147. sqlspec-0.16.2.dist-info/licenses/LICENSE +21 -0
  148. sqlspec-0.16.2.dist-info/licenses/NOTICE +29 -0
@@ -0,0 +1,830 @@
1
+ """Filter system for SQL statement manipulation.
2
+
3
+ This module provides filters that can be applied to SQL statements to add
4
+ WHERE clauses, ORDER BY clauses, LIMIT/OFFSET, and other modifications.
5
+
6
+ Components:
7
+ - StatementFilter: Abstract base class for all filters
8
+ - BeforeAfterFilter: Date range filtering
9
+ - InCollectionFilter: IN clause filtering
10
+ - LimitOffsetFilter: Pagination support
11
+ - OrderByFilter: Sorting support
12
+ - SearchFilter: Text search filtering
13
+ - Various collection and negation filters
14
+
15
+ Features:
16
+ - Parameter conflict resolution
17
+ - Type-safe filter application
18
+ - Cacheable filter configurations
19
+ """
20
+
21
+ import uuid
22
+ from abc import ABC, abstractmethod
23
+ from collections import abc
24
+ from collections.abc import Sequence
25
+ from datetime import datetime
26
+ from typing import TYPE_CHECKING, Any, Generic, Literal, Optional, Union
27
+
28
+ from sqlglot import exp
29
+ from typing_extensions import TypeAlias, TypeVar
30
+
31
+ if TYPE_CHECKING:
32
+ from sqlglot.expressions import Condition
33
+
34
+ from sqlspec.core.statement import SQL
35
+
36
+ __all__ = (
37
+ "AnyCollectionFilter",
38
+ "BeforeAfterFilter",
39
+ "FilterTypeT",
40
+ "FilterTypes",
41
+ "InAnyFilter",
42
+ "InCollectionFilter",
43
+ "LimitOffsetFilter",
44
+ "NotAnyCollectionFilter",
45
+ "NotInCollectionFilter",
46
+ "NotInSearchFilter",
47
+ "OffsetPagination",
48
+ "OnBeforeAfterFilter",
49
+ "OrderByFilter",
50
+ "PaginationFilter",
51
+ "SearchFilter",
52
+ "StatementFilter",
53
+ "apply_filter",
54
+ )
55
+
56
+ T = TypeVar("T")
57
+ FilterTypeT = TypeVar("FilterTypeT", bound="StatementFilter")
58
+
59
+
60
+ class StatementFilter(ABC):
61
+ """Abstract base class for filters that can be appended to a statement."""
62
+
63
+ __slots__ = ()
64
+
65
+ @abstractmethod
66
+ def append_to_statement(self, statement: "SQL") -> "SQL":
67
+ """Append the filter to the statement.
68
+
69
+ This method should modify the SQL expression only, not the parameters.
70
+ Parameters should be provided via extract_parameters().
71
+ """
72
+
73
+ def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
74
+ """Extract parameters that this filter contributes.
75
+
76
+ Returns:
77
+ Tuple of (positional_parameters, named_parameters) where:
78
+ - positional_parameters: List of positional parameter values
79
+ - named_parameters: Dict of parameter name to value
80
+ """
81
+ return [], {}
82
+
83
+ def _resolve_parameter_conflicts(self, statement: "SQL", proposed_names: list[str]) -> list[str]:
84
+ """Resolve parameter name conflicts.
85
+
86
+ Args:
87
+ statement: The SQL statement to check for existing parameters
88
+ proposed_names: List of proposed parameter names
89
+
90
+ Returns:
91
+ List of resolved parameter names (same length as proposed_names)
92
+ """
93
+ existing_params = set(statement._named_parameters.keys())
94
+ existing_params.update(statement.parameters.keys() if isinstance(statement.parameters, dict) else [])
95
+
96
+ resolved_names = []
97
+ for name in proposed_names:
98
+ if name in existing_params:
99
+ unique_suffix = str(uuid.uuid4()).replace("-", "")[:8]
100
+ resolved_name = f"{name}_{unique_suffix}"
101
+ else:
102
+ resolved_name = name
103
+ resolved_names.append(resolved_name)
104
+ existing_params.add(resolved_name)
105
+
106
+ return resolved_names
107
+
108
+ @abstractmethod
109
+ def get_cache_key(self) -> tuple[Any, ...]:
110
+ """Return a cache key for this filter's configuration.
111
+
112
+ Returns:
113
+ Tuple of hashable values representing the filter's configuration
114
+ """
115
+
116
+
117
+ class BeforeAfterFilter(StatementFilter):
118
+ """Filter for datetime range queries.
119
+
120
+ Applies WHERE clauses for before/after datetime filtering.
121
+ """
122
+
123
+ __slots__ = ("_param_name_after", "_param_name_before", "after", "before", "field_name")
124
+
125
+ field_name: str
126
+ before: Optional[datetime]
127
+ after: Optional[datetime]
128
+
129
+ def __init__(self, field_name: str, before: Optional[datetime] = None, after: Optional[datetime] = None) -> None:
130
+ """Initialize the BeforeAfterFilter.
131
+
132
+ Args:
133
+ field_name: Name of the model attribute to filter on.
134
+ before: Filter results where field earlier than this.
135
+ after: Filter results where field later than this.
136
+ """
137
+ self.field_name = field_name
138
+ self.before = before
139
+ self.after = after
140
+
141
+ self._param_name_before: Optional[str] = None
142
+ self._param_name_after: Optional[str] = None
143
+
144
+ if self.before:
145
+ self._param_name_before = f"{self.field_name}_before"
146
+ if self.after:
147
+ self._param_name_after = f"{self.field_name}_after"
148
+
149
+ def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
150
+ """Extract filter parameters."""
151
+ named_parameters = {}
152
+ if self.before and self._param_name_before:
153
+ named_parameters[self._param_name_before] = self.before
154
+ if self.after and self._param_name_after:
155
+ named_parameters[self._param_name_after] = self.after
156
+ return [], named_parameters
157
+
158
+ def append_to_statement(self, statement: "SQL") -> "SQL":
159
+ """Apply filter to SQL expression only."""
160
+ conditions: list[Condition] = []
161
+ col_expr = exp.column(self.field_name)
162
+
163
+ # Resolve parameter name conflicts
164
+ proposed_names = []
165
+ if self.before and self._param_name_before:
166
+ proposed_names.append(self._param_name_before)
167
+ if self.after and self._param_name_after:
168
+ proposed_names.append(self._param_name_after)
169
+
170
+ if not proposed_names:
171
+ return statement
172
+
173
+ resolved_names = self._resolve_parameter_conflicts(statement, proposed_names)
174
+
175
+ param_idx = 0
176
+ result = statement
177
+ if self.before and self._param_name_before:
178
+ before_param_name = resolved_names[param_idx]
179
+ param_idx += 1
180
+ conditions.append(exp.LT(this=col_expr, expression=exp.Placeholder(this=before_param_name)))
181
+ result = result.add_named_parameter(before_param_name, self.before)
182
+
183
+ if self.after and self._param_name_after:
184
+ after_param_name = resolved_names[param_idx]
185
+ conditions.append(exp.GT(this=col_expr, expression=exp.Placeholder(this=after_param_name)))
186
+ result = result.add_named_parameter(after_param_name, self.after)
187
+
188
+ final_condition = conditions[0]
189
+ for cond in conditions[1:]:
190
+ final_condition = exp.And(this=final_condition, expression=cond)
191
+ return result.where(final_condition)
192
+
193
+ def get_cache_key(self) -> tuple[Any, ...]:
194
+ """Return cache key for this filter configuration."""
195
+ return ("BeforeAfterFilter", self.field_name, self.before, self.after)
196
+
197
+
198
+ class OnBeforeAfterFilter(StatementFilter):
199
+ """Data required to filter a query on a ``datetime`` column."""
200
+
201
+ __slots__ = ("_param_name_on_or_after", "_param_name_on_or_before", "field_name", "on_or_after", "on_or_before")
202
+
203
+ field_name: str
204
+ on_or_before: Optional[datetime]
205
+ on_or_after: Optional[datetime]
206
+
207
+ def __init__(
208
+ self, field_name: str, on_or_before: Optional[datetime] = None, on_or_after: Optional[datetime] = None
209
+ ) -> None:
210
+ """Initialize the OnBeforeAfterFilter.
211
+
212
+ Args:
213
+ field_name: Name of the model attribute to filter on.
214
+ on_or_before: Filter results where field is on or earlier than this.
215
+ on_or_after: Filter results where field on or later than this.
216
+ """
217
+ self.field_name = field_name
218
+ self.on_or_before = on_or_before
219
+ self.on_or_after = on_or_after
220
+
221
+ self._param_name_on_or_before: Optional[str] = None
222
+ self._param_name_on_or_after: Optional[str] = None
223
+
224
+ if self.on_or_before:
225
+ self._param_name_on_or_before = f"{self.field_name}_on_or_before"
226
+ if self.on_or_after:
227
+ self._param_name_on_or_after = f"{self.field_name}_on_or_after"
228
+
229
+ def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
230
+ """Extract filter parameters."""
231
+ named_parameters = {}
232
+ if self.on_or_before and self._param_name_on_or_before:
233
+ named_parameters[self._param_name_on_or_before] = self.on_or_before
234
+ if self.on_or_after and self._param_name_on_or_after:
235
+ named_parameters[self._param_name_on_or_after] = self.on_or_after
236
+ return [], named_parameters
237
+
238
+ def append_to_statement(self, statement: "SQL") -> "SQL":
239
+ conditions: list[Condition] = []
240
+
241
+ # Resolve parameter name conflicts
242
+ proposed_names = []
243
+ if self.on_or_before and self._param_name_on_or_before:
244
+ proposed_names.append(self._param_name_on_or_before)
245
+ if self.on_or_after and self._param_name_on_or_after:
246
+ proposed_names.append(self._param_name_on_or_after)
247
+
248
+ if not proposed_names:
249
+ return statement
250
+
251
+ resolved_names = self._resolve_parameter_conflicts(statement, proposed_names)
252
+
253
+ param_idx = 0
254
+ result = statement
255
+ if self.on_or_before and self._param_name_on_or_before:
256
+ before_param_name = resolved_names[param_idx]
257
+ param_idx += 1
258
+ conditions.append(
259
+ exp.LTE(this=exp.column(self.field_name), expression=exp.Placeholder(this=before_param_name))
260
+ )
261
+ result = result.add_named_parameter(before_param_name, self.on_or_before)
262
+
263
+ if self.on_or_after and self._param_name_on_or_after:
264
+ after_param_name = resolved_names[param_idx]
265
+ conditions.append(
266
+ exp.GTE(this=exp.column(self.field_name), expression=exp.Placeholder(this=after_param_name))
267
+ )
268
+ result = result.add_named_parameter(after_param_name, self.on_or_after)
269
+
270
+ final_condition = conditions[0]
271
+ for cond in conditions[1:]:
272
+ final_condition = exp.And(this=final_condition, expression=cond)
273
+ return result.where(final_condition)
274
+
275
+ def get_cache_key(self) -> tuple[Any, ...]:
276
+ """Return cache key for this filter configuration."""
277
+ return ("OnBeforeAfterFilter", self.field_name, self.on_or_before, self.on_or_after)
278
+
279
+
280
+ class InAnyFilter(StatementFilter, ABC, Generic[T]):
281
+ """Subclass for methods that have a `prefer_any` attribute."""
282
+
283
+ __slots__ = ()
284
+
285
+ def append_to_statement(self, statement: "SQL") -> "SQL":
286
+ raise NotImplementedError
287
+
288
+
289
+ class InCollectionFilter(InAnyFilter[T]):
290
+ """Filter for IN clause queries.
291
+
292
+ Constructs WHERE ... IN (...) clauses.
293
+ """
294
+
295
+ __slots__ = ("_param_names", "field_name", "values")
296
+
297
+ field_name: str
298
+ values: Optional[abc.Collection[T]]
299
+
300
+ def __init__(self, field_name: str, values: Optional[abc.Collection[T]]) -> None:
301
+ """Initialize the InCollectionFilter.
302
+
303
+ Args:
304
+ field_name: Name of the model attribute to filter on.
305
+ values: Values for ``IN`` clause. An empty list will return an empty result set,
306
+ however, if ``None``, the filter is not applied to the query, and all rows are returned.
307
+ """
308
+ self.field_name = field_name
309
+ self.values = values
310
+
311
+ self._param_names: list[str] = []
312
+ if self.values:
313
+ for i, _ in enumerate(self.values):
314
+ self._param_names.append(f"{self.field_name}_in_{i}")
315
+
316
+ def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
317
+ """Extract filter parameters."""
318
+ named_parameters = {}
319
+ if self.values:
320
+ for i, value in enumerate(self.values):
321
+ named_parameters[self._param_names[i]] = value
322
+ return [], named_parameters
323
+
324
+ def append_to_statement(self, statement: "SQL") -> "SQL":
325
+ if self.values is None:
326
+ return statement
327
+
328
+ if not self.values:
329
+ return statement.where(exp.false())
330
+
331
+ # Resolve parameter name conflicts
332
+ resolved_names = self._resolve_parameter_conflicts(statement, self._param_names)
333
+
334
+ placeholder_expressions: list[exp.Placeholder] = [
335
+ exp.Placeholder(this=param_name) for param_name in resolved_names
336
+ ]
337
+
338
+ result = statement.where(exp.In(this=exp.column(self.field_name), expressions=placeholder_expressions))
339
+
340
+ # Add parameters with resolved names
341
+ for resolved_name, value in zip(resolved_names, self.values):
342
+ result = result.add_named_parameter(resolved_name, value)
343
+ return result
344
+
345
+ def get_cache_key(self) -> tuple[Any, ...]:
346
+ """Return cache key for this filter configuration."""
347
+ values_tuple = tuple(self.values) if self.values is not None else None
348
+ return ("InCollectionFilter", self.field_name, values_tuple)
349
+
350
+
351
+ class NotInCollectionFilter(InAnyFilter[T]):
352
+ """Data required to construct a ``WHERE ... NOT IN (...)`` clause."""
353
+
354
+ __slots__ = ("_param_names", "field_name", "values")
355
+
356
+ field_name: str
357
+ values: Optional[abc.Collection[T]]
358
+
359
+ def __init__(self, field_name: str, values: Optional[abc.Collection[T]]) -> None:
360
+ """Initialize the NotInCollectionFilter.
361
+
362
+ Args:
363
+ field_name: Name of the model attribute to filter on.
364
+ values: Values for ``NOT IN`` clause. An empty list or ``None`` will return all rows.
365
+ """
366
+ self.field_name = field_name
367
+ self.values = values
368
+
369
+ self._param_names: list[str] = []
370
+ if self.values:
371
+ for i, _ in enumerate(self.values):
372
+ self._param_names.append(f"{self.field_name}_notin_{i}_{id(self)}")
373
+
374
+ def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
375
+ """Extract filter parameters."""
376
+ named_parameters = {}
377
+ if self.values:
378
+ for i, value in enumerate(self.values):
379
+ named_parameters[self._param_names[i]] = value
380
+ return [], named_parameters
381
+
382
+ def append_to_statement(self, statement: "SQL") -> "SQL":
383
+ if self.values is None or not self.values:
384
+ return statement
385
+
386
+ # Resolve parameter name conflicts
387
+ resolved_names = self._resolve_parameter_conflicts(statement, self._param_names)
388
+
389
+ placeholder_expressions: list[exp.Placeholder] = [
390
+ exp.Placeholder(this=param_name) for param_name in resolved_names
391
+ ]
392
+
393
+ result = statement.where(
394
+ exp.Not(this=exp.In(this=exp.column(self.field_name), expressions=placeholder_expressions))
395
+ )
396
+
397
+ # Add parameters with resolved names
398
+ for resolved_name, value in zip(resolved_names, self.values):
399
+ result = result.add_named_parameter(resolved_name, value)
400
+ return result
401
+
402
+ def get_cache_key(self) -> tuple[Any, ...]:
403
+ """Return cache key for this filter configuration."""
404
+ values_tuple = tuple(self.values) if self.values is not None else None
405
+ return ("NotInCollectionFilter", self.field_name, values_tuple)
406
+
407
+
408
+ class AnyCollectionFilter(InAnyFilter[T]):
409
+ """Data required to construct a ``WHERE column_name = ANY (array_expression)`` clause."""
410
+
411
+ __slots__ = ("_param_names", "field_name", "values")
412
+
413
+ field_name: str
414
+ values: Optional[abc.Collection[T]]
415
+
416
+ def __init__(self, field_name: str, values: Optional[abc.Collection[T]]) -> None:
417
+ """Initialize the AnyCollectionFilter.
418
+
419
+ Args:
420
+ field_name: Name of the model attribute to filter on.
421
+ values: Values for ``= ANY (...)`` clause. An empty list will result in a condition
422
+ that is always false (no rows returned). If ``None``, the filter is not applied
423
+ to the query, and all rows are returned.
424
+ """
425
+ self.field_name = field_name
426
+ self.values = values
427
+
428
+ self._param_names: list[str] = []
429
+ if self.values:
430
+ for i, _ in enumerate(self.values):
431
+ self._param_names.append(f"{self.field_name}_any_{i}")
432
+
433
+ def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
434
+ """Extract filter parameters."""
435
+ named_parameters = {}
436
+ if self.values:
437
+ for i, value in enumerate(self.values):
438
+ named_parameters[self._param_names[i]] = value
439
+ return [], named_parameters
440
+
441
+ def append_to_statement(self, statement: "SQL") -> "SQL":
442
+ if self.values is None:
443
+ return statement
444
+
445
+ if not self.values:
446
+ return statement.where(exp.false())
447
+
448
+ # Resolve parameter name conflicts
449
+ resolved_names = self._resolve_parameter_conflicts(statement, self._param_names)
450
+
451
+ placeholder_expressions: list[exp.Expression] = [
452
+ exp.Placeholder(this=param_name) for param_name in resolved_names
453
+ ]
454
+
455
+ array_expr = exp.Array(expressions=placeholder_expressions)
456
+ result = statement.where(exp.EQ(this=exp.column(self.field_name), expression=exp.Any(this=array_expr)))
457
+
458
+ # Add parameters with resolved names
459
+ for resolved_name, value in zip(resolved_names, self.values):
460
+ result = result.add_named_parameter(resolved_name, value)
461
+ return result
462
+
463
+ def get_cache_key(self) -> tuple[Any, ...]:
464
+ """Return cache key for this filter configuration."""
465
+ values_tuple = tuple(self.values) if self.values is not None else None
466
+ return ("AnyCollectionFilter", self.field_name, values_tuple)
467
+
468
+
469
+ class NotAnyCollectionFilter(InAnyFilter[T]):
470
+ """Data required to construct a ``WHERE NOT (column_name = ANY (array_expression))`` clause."""
471
+
472
+ __slots__ = ("_param_names", "field_name", "values")
473
+
474
+ def __init__(self, field_name: str, values: Optional[abc.Collection[T]]) -> None:
475
+ """Initialize the NotAnyCollectionFilter.
476
+
477
+ Args:
478
+ field_name: Name of the model attribute to filter on.
479
+ values: Values for ``NOT (... = ANY (...))`` clause. An empty list will result in a
480
+ condition that is always true (all rows returned, filter effectively ignored).
481
+ If ``None``, the filter is not applied to the query, and all rows are returned.
482
+ """
483
+ self.field_name = field_name
484
+ self.values = values
485
+
486
+ self._param_names: list[str] = []
487
+ if self.values:
488
+ for i, _ in enumerate(self.values):
489
+ self._param_names.append(f"{self.field_name}_not_any_{i}")
490
+
491
+ def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
492
+ """Extract filter parameters."""
493
+ named_parameters = {}
494
+ if self.values:
495
+ for i, value in enumerate(self.values):
496
+ named_parameters[self._param_names[i]] = value
497
+ return [], named_parameters
498
+
499
+ def append_to_statement(self, statement: "SQL") -> "SQL":
500
+ if self.values is None or not self.values:
501
+ return statement
502
+
503
+ # Resolve parameter name conflicts
504
+ resolved_names = self._resolve_parameter_conflicts(statement, self._param_names)
505
+
506
+ placeholder_expressions: list[exp.Expression] = [
507
+ exp.Placeholder(this=param_name) for param_name in resolved_names
508
+ ]
509
+
510
+ array_expr = exp.Array(expressions=placeholder_expressions)
511
+ condition = exp.EQ(this=exp.column(self.field_name), expression=exp.Any(this=array_expr))
512
+ result = statement.where(exp.Not(this=condition))
513
+
514
+ # Add parameters with resolved names
515
+ for resolved_name, value in zip(resolved_names, self.values):
516
+ result = result.add_named_parameter(resolved_name, value)
517
+ return result
518
+
519
+ def get_cache_key(self) -> tuple[Any, ...]:
520
+ """Return cache key for this filter configuration."""
521
+ values_tuple = tuple(self.values) if self.values is not None else None
522
+ return ("NotAnyCollectionFilter", self.field_name, values_tuple)
523
+
524
+
525
+ class PaginationFilter(StatementFilter, ABC):
526
+ """Subclass for methods that function as a pagination type."""
527
+
528
+ __slots__ = ()
529
+
530
+ @abstractmethod
531
+ def append_to_statement(self, statement: "SQL") -> "SQL":
532
+ raise NotImplementedError
533
+
534
+
535
+ class LimitOffsetFilter(PaginationFilter):
536
+ """Data required to add limit/offset filtering to a query."""
537
+
538
+ __slots__ = ("_limit_param_name", "_offset_param_name", "limit", "offset")
539
+
540
+ limit: int
541
+ offset: int
542
+
543
+ def __init__(self, limit: int, offset: int) -> None:
544
+ """Initialize the LimitOffsetFilter.
545
+
546
+ Args:
547
+ limit: Value for ``LIMIT`` clause of query.
548
+ offset: Value for ``OFFSET`` clause of query.
549
+ """
550
+ self.limit = limit
551
+ self.offset = offset
552
+
553
+ self._limit_param_name = "limit"
554
+ self._offset_param_name = "offset"
555
+
556
+ def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
557
+ """Extract filter parameters."""
558
+ return [], {self._limit_param_name: self.limit, self._offset_param_name: self.offset}
559
+
560
+ def append_to_statement(self, statement: "SQL") -> "SQL":
561
+ import sqlglot
562
+ from sqlglot import exp
563
+
564
+ # Resolve parameter name conflicts
565
+ resolved_names = self._resolve_parameter_conflicts(statement, [self._limit_param_name, self._offset_param_name])
566
+ limit_param_name, offset_param_name = resolved_names
567
+
568
+ limit_placeholder = exp.Placeholder(this=limit_param_name)
569
+ offset_placeholder = exp.Placeholder(this=offset_param_name)
570
+
571
+ # Parse the current SQL to get the statement structure
572
+ try:
573
+ current_statement = sqlglot.parse_one(statement._raw_sql, dialect=getattr(statement, "_dialect", None))
574
+ except Exception:
575
+ # Fallback to wrapping in subquery if parsing fails
576
+ current_statement = exp.Select().from_(f"({statement._raw_sql})")
577
+
578
+ if isinstance(current_statement, exp.Select):
579
+ new_statement = current_statement.limit(limit_placeholder).offset(offset_placeholder)
580
+ else:
581
+ # Wrap non-SELECT statements in a subquery
582
+ new_statement = exp.Select().from_(current_statement).limit(limit_placeholder).offset(offset_placeholder)
583
+
584
+ result = statement.copy(statement=new_statement)
585
+
586
+ result = result.add_named_parameter(limit_param_name, self.limit)
587
+ return result.add_named_parameter(offset_param_name, self.offset)
588
+
589
+ def get_cache_key(self) -> tuple[Any, ...]:
590
+ """Return cache key for this filter configuration."""
591
+ return ("LimitOffsetFilter", self.limit, self.offset)
592
+
593
+
594
+ class OrderByFilter(StatementFilter):
595
+ """Data required to construct a ``ORDER BY ...`` clause."""
596
+
597
+ __slots__ = ("field_name", "sort_order")
598
+
599
+ field_name: str
600
+ sort_order: Literal["asc", "desc"]
601
+
602
+ def __init__(self, field_name: str, sort_order: Literal["asc", "desc"] = "asc") -> None:
603
+ """Initialize the OrderByFilter.
604
+
605
+ Args:
606
+ field_name: Name of the model attribute to sort on.
607
+ sort_order: Sort ascending or descending.
608
+ """
609
+ self.field_name = field_name
610
+ self.sort_order = sort_order
611
+
612
+ def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
613
+ """Extract filter parameters."""
614
+ return [], {}
615
+
616
+ def append_to_statement(self, statement: "SQL") -> "SQL":
617
+ converted_sort_order = self.sort_order.lower()
618
+ if converted_sort_order not in {"asc", "desc"}:
619
+ converted_sort_order = "asc"
620
+
621
+ col_expr = exp.column(self.field_name)
622
+ order_expr = col_expr.desc() if converted_sort_order == "desc" else col_expr.asc()
623
+
624
+ if statement._statement is None:
625
+ new_statement = exp.Select().order_by(order_expr)
626
+ elif isinstance(statement._statement, exp.Select):
627
+ new_statement = statement._statement.order_by(order_expr)
628
+ else:
629
+ new_statement = exp.Select().from_(statement._statement).order_by(order_expr)
630
+
631
+ return statement.copy(statement=new_statement)
632
+
633
+ def get_cache_key(self) -> tuple[Any, ...]:
634
+ """Return cache key for this filter configuration."""
635
+ return ("OrderByFilter", self.field_name, self.sort_order)
636
+
637
+
638
+ class SearchFilter(StatementFilter):
639
+ """Filter for text search queries.
640
+
641
+ Constructs WHERE field_name LIKE '%value%' clauses.
642
+ """
643
+
644
+ __slots__ = ("_param_name", "field_name", "ignore_case", "value")
645
+
646
+ field_name: Union[str, set[str]]
647
+ value: str
648
+ ignore_case: Optional[bool]
649
+
650
+ def __init__(self, field_name: Union[str, set[str]], value: str, ignore_case: Optional[bool] = False) -> None:
651
+ """Initialize the SearchFilter.
652
+
653
+ Args:
654
+ field_name: Name of the model attribute to search on.
655
+ value: Search value.
656
+ ignore_case: Should the search be case insensitive.
657
+ """
658
+ self.field_name = field_name
659
+ self.value = value
660
+ self.ignore_case = ignore_case
661
+
662
+ self._param_name: Optional[str] = None
663
+ if self.value:
664
+ if isinstance(self.field_name, str):
665
+ self._param_name = f"{self.field_name}_search"
666
+ else:
667
+ self._param_name = "search_value"
668
+
669
+ def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
670
+ """Extract filter parameters."""
671
+ named_parameters = {}
672
+ if self.value and self._param_name:
673
+ search_value_with_wildcards = f"%{self.value}%"
674
+ named_parameters[self._param_name] = search_value_with_wildcards
675
+ return [], named_parameters
676
+
677
+ def append_to_statement(self, statement: "SQL") -> "SQL":
678
+ if not self.value or not self._param_name:
679
+ return statement
680
+
681
+ # Resolve parameter name conflicts
682
+ resolved_names = self._resolve_parameter_conflicts(statement, [self._param_name])
683
+ param_name = resolved_names[0]
684
+
685
+ pattern_expr = exp.Placeholder(this=param_name)
686
+ like_op = exp.ILike if self.ignore_case else exp.Like
687
+
688
+ if isinstance(self.field_name, str):
689
+ result = statement.where(like_op(this=exp.column(self.field_name), expression=pattern_expr))
690
+ elif isinstance(self.field_name, set) and self.field_name:
691
+ field_conditions: list[Condition] = [
692
+ like_op(this=exp.column(field), expression=pattern_expr) for field in self.field_name
693
+ ]
694
+ if not field_conditions:
695
+ return statement
696
+
697
+ final_condition: Condition = field_conditions[0]
698
+ for cond in field_conditions[1:]:
699
+ final_condition = exp.Or(this=final_condition, expression=cond)
700
+ result = statement.where(final_condition)
701
+ else:
702
+ result = statement
703
+
704
+ # Add parameter with resolved name
705
+ search_value_with_wildcards = f"%{self.value}%"
706
+ return result.add_named_parameter(param_name, search_value_with_wildcards)
707
+
708
+ def get_cache_key(self) -> tuple[Any, ...]:
709
+ """Return cache key for this filter configuration."""
710
+ field_names = tuple(sorted(self.field_name)) if isinstance(self.field_name, set) else self.field_name
711
+ return ("SearchFilter", field_names, self.value, self.ignore_case)
712
+
713
+
714
+ class NotInSearchFilter(SearchFilter):
715
+ """Data required to construct a ``WHERE field_name NOT LIKE '%' || :value || '%'`` clause."""
716
+
717
+ __slots__ = ()
718
+
719
+ def __init__(self, field_name: Union[str, set[str]], value: str, ignore_case: Optional[bool] = False) -> None:
720
+ """Initialize the NotInSearchFilter.
721
+
722
+ Args:
723
+ field_name: Name of the model attribute to search on.
724
+ value: Search value.
725
+ ignore_case: Should the search be case insensitive.
726
+ """
727
+ super().__init__(field_name, value, ignore_case)
728
+
729
+ self._param_name: Optional[str] = None
730
+ if self.value:
731
+ if isinstance(self.field_name, str):
732
+ self._param_name = f"{self.field_name}_not_search"
733
+ else:
734
+ self._param_name = "not_search_value"
735
+
736
+ def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
737
+ """Extract filter parameters."""
738
+ named_parameters = {}
739
+ if self.value and self._param_name:
740
+ search_value_with_wildcards = f"%{self.value}%"
741
+ named_parameters[self._param_name] = search_value_with_wildcards
742
+ return [], named_parameters
743
+
744
+ def append_to_statement(self, statement: "SQL") -> "SQL":
745
+ if not self.value or not self._param_name:
746
+ return statement
747
+
748
+ # Resolve parameter name conflicts
749
+ resolved_names = self._resolve_parameter_conflicts(statement, [self._param_name])
750
+ param_name = resolved_names[0]
751
+
752
+ pattern_expr = exp.Placeholder(this=param_name)
753
+ like_op = exp.ILike if self.ignore_case else exp.Like
754
+
755
+ result = statement
756
+ if isinstance(self.field_name, str):
757
+ result = statement.where(exp.Not(this=like_op(this=exp.column(self.field_name), expression=pattern_expr)))
758
+ elif isinstance(self.field_name, set) and self.field_name:
759
+ field_conditions: list[Condition] = [
760
+ exp.Not(this=like_op(this=exp.column(field), expression=pattern_expr)) for field in self.field_name
761
+ ]
762
+ if not field_conditions:
763
+ return statement
764
+
765
+ final_condition: Condition = field_conditions[0]
766
+ if len(field_conditions) > 1:
767
+ for cond in field_conditions[1:]:
768
+ final_condition = exp.And(this=final_condition, expression=cond)
769
+ result = statement.where(final_condition)
770
+
771
+ # Add parameter with resolved name
772
+ search_value_with_wildcards = f"%{self.value}%"
773
+ return result.add_named_parameter(param_name, search_value_with_wildcards)
774
+
775
+ def get_cache_key(self) -> tuple[Any, ...]:
776
+ """Return cache key for this filter configuration."""
777
+ field_names = tuple(sorted(self.field_name)) if isinstance(self.field_name, set) else self.field_name
778
+ return ("NotInSearchFilter", field_names, self.value, self.ignore_case)
779
+
780
+
781
+ class OffsetPagination(Generic[T]):
782
+ """Container for data returned using limit/offset pagination."""
783
+
784
+ __slots__ = ("items", "limit", "offset", "total")
785
+
786
+ items: Sequence[T]
787
+ limit: int
788
+ offset: int
789
+ total: int
790
+
791
+ def __init__(self, items: Sequence[T], limit: int, offset: int, total: int) -> None:
792
+ """Initialize OffsetPagination.
793
+
794
+ Args:
795
+ items: List of data being sent as part of the response.
796
+ limit: Maximal number of items to send.
797
+ offset: Offset from the beginning of the query. Identical to an index.
798
+ total: Total number of items.
799
+ """
800
+ self.items = items
801
+ self.limit = limit
802
+ self.offset = offset
803
+ self.total = total
804
+
805
+
806
+ def apply_filter(statement: "SQL", filter_obj: StatementFilter) -> "SQL":
807
+ """Apply a statement filter to a SQL query object.
808
+
809
+ Args:
810
+ statement: The SQL query object to modify.
811
+ filter_obj: The filter to apply.
812
+
813
+ Returns:
814
+ The modified query object.
815
+ """
816
+ return filter_obj.append_to_statement(statement)
817
+
818
+
819
+ FilterTypes: TypeAlias = Union[
820
+ BeforeAfterFilter,
821
+ OnBeforeAfterFilter,
822
+ InCollectionFilter[Any],
823
+ LimitOffsetFilter,
824
+ OrderByFilter,
825
+ SearchFilter,
826
+ NotInCollectionFilter[Any],
827
+ NotInSearchFilter,
828
+ AnyCollectionFilter[Any],
829
+ NotAnyCollectionFilter[Any],
830
+ ]