sqlspec 0.16.1__cp310-cp310-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.
- 51ff5a9eadfdefd49f98__mypyc.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/__init__.py +92 -0
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +14 -0
- sqlspec/_serialization.py +77 -0
- sqlspec/_sql.py +1780 -0
- sqlspec/_typing.py +680 -0
- sqlspec/adapters/__init__.py +0 -0
- sqlspec/adapters/adbc/__init__.py +5 -0
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +361 -0
- sqlspec/adapters/adbc/driver.py +512 -0
- sqlspec/adapters/aiosqlite/__init__.py +19 -0
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +253 -0
- sqlspec/adapters/aiosqlite/driver.py +248 -0
- sqlspec/adapters/asyncmy/__init__.py +19 -0
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +180 -0
- sqlspec/adapters/asyncmy/driver.py +274 -0
- sqlspec/adapters/asyncpg/__init__.py +21 -0
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +229 -0
- sqlspec/adapters/asyncpg/driver.py +344 -0
- sqlspec/adapters/bigquery/__init__.py +18 -0
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +298 -0
- sqlspec/adapters/bigquery/driver.py +558 -0
- sqlspec/adapters/duckdb/__init__.py +22 -0
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +504 -0
- sqlspec/adapters/duckdb/driver.py +368 -0
- sqlspec/adapters/oracledb/__init__.py +32 -0
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +317 -0
- sqlspec/adapters/oracledb/driver.py +538 -0
- sqlspec/adapters/psqlpy/__init__.py +16 -0
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +214 -0
- sqlspec/adapters/psqlpy/driver.py +530 -0
- sqlspec/adapters/psycopg/__init__.py +32 -0
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +426 -0
- sqlspec/adapters/psycopg/driver.py +796 -0
- sqlspec/adapters/sqlite/__init__.py +15 -0
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +240 -0
- sqlspec/adapters/sqlite/driver.py +294 -0
- sqlspec/base.py +571 -0
- sqlspec/builder/__init__.py +62 -0
- sqlspec/builder/_base.py +473 -0
- sqlspec/builder/_column.py +320 -0
- sqlspec/builder/_ddl.py +1346 -0
- sqlspec/builder/_ddl_utils.py +103 -0
- sqlspec/builder/_delete.py +76 -0
- sqlspec/builder/_insert.py +256 -0
- sqlspec/builder/_merge.py +71 -0
- sqlspec/builder/_parsing_utils.py +140 -0
- sqlspec/builder/_select.py +170 -0
- sqlspec/builder/_update.py +188 -0
- sqlspec/builder/mixins/__init__.py +55 -0
- sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
- sqlspec/builder/mixins/_delete_operations.py +41 -0
- sqlspec/builder/mixins/_insert_operations.py +244 -0
- sqlspec/builder/mixins/_join_operations.py +122 -0
- sqlspec/builder/mixins/_merge_operations.py +476 -0
- sqlspec/builder/mixins/_order_limit_operations.py +135 -0
- sqlspec/builder/mixins/_pivot_operations.py +153 -0
- sqlspec/builder/mixins/_select_operations.py +603 -0
- sqlspec/builder/mixins/_update_operations.py +187 -0
- sqlspec/builder/mixins/_where_clause.py +621 -0
- sqlspec/cli.py +247 -0
- sqlspec/config.py +395 -0
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/cache.py +871 -0
- sqlspec/core/compiler.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/compiler.py +417 -0
- sqlspec/core/filters.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters.py +1237 -0
- sqlspec/core/result.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/result.py +677 -0
- sqlspec/core/splitter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/splitter.py +819 -0
- sqlspec/core/statement.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/statement.py +676 -0
- sqlspec/driver/__init__.py +19 -0
- sqlspec/driver/_async.py +502 -0
- sqlspec/driver/_common.py +631 -0
- sqlspec/driver/_sync.py +503 -0
- sqlspec/driver/mixins/__init__.py +6 -0
- sqlspec/driver/mixins/_result_tools.py +193 -0
- sqlspec/driver/mixins/_sql_translator.py +86 -0
- sqlspec/exceptions.py +193 -0
- sqlspec/extensions/__init__.py +0 -0
- sqlspec/extensions/aiosql/__init__.py +10 -0
- sqlspec/extensions/aiosql/adapter.py +461 -0
- sqlspec/extensions/litestar/__init__.py +6 -0
- sqlspec/extensions/litestar/_utils.py +52 -0
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/config.py +92 -0
- sqlspec/extensions/litestar/handlers.py +260 -0
- sqlspec/extensions/litestar/plugin.py +145 -0
- sqlspec/extensions/litestar/providers.py +454 -0
- sqlspec/loader.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/loader.py +760 -0
- sqlspec/migrations/__init__.py +35 -0
- sqlspec/migrations/base.py +414 -0
- sqlspec/migrations/commands.py +443 -0
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +213 -0
- sqlspec/migrations/tracker.py +140 -0
- sqlspec/migrations/utils.py +129 -0
- sqlspec/protocols.py +407 -0
- sqlspec/py.typed +0 -0
- sqlspec/storage/__init__.py +23 -0
- sqlspec/storage/backends/__init__.py +0 -0
- sqlspec/storage/backends/base.py +163 -0
- sqlspec/storage/backends/fsspec.py +386 -0
- sqlspec/storage/backends/obstore.py +459 -0
- sqlspec/storage/capabilities.py +102 -0
- sqlspec/storage/registry.py +239 -0
- sqlspec/typing.py +299 -0
- sqlspec/utils/__init__.py +3 -0
- sqlspec/utils/correlation.py +150 -0
- sqlspec/utils/deprecation.py +106 -0
- sqlspec/utils/fixtures.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/fixtures.py +58 -0
- sqlspec/utils/logging.py +127 -0
- sqlspec/utils/module_loader.py +89 -0
- sqlspec/utils/serializers.py +4 -0
- sqlspec/utils/singleton.py +32 -0
- sqlspec/utils/sync_tools.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/sync_tools.py +237 -0
- sqlspec/utils/text.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/text.py +96 -0
- sqlspec/utils/type_guards.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/type_guards.py +1139 -0
- sqlspec-0.16.1.dist-info/METADATA +365 -0
- sqlspec-0.16.1.dist-info/RECORD +148 -0
- sqlspec-0.16.1.dist-info/WHEEL +7 -0
- sqlspec-0.16.1.dist-info/entry_points.txt +2 -0
- sqlspec-0.16.1.dist-info/licenses/LICENSE +21 -0
- sqlspec-0.16.1.dist-info/licenses/NOTICE +29 -0
sqlspec/core/filters.py
ADDED
|
@@ -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
|
+
]
|