sqlspec 0.14.1__py3-none-any.whl → 0.16.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.

Potentially problematic release.


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

Files changed (159) hide show
  1. sqlspec/__init__.py +50 -25
  2. sqlspec/__main__.py +1 -1
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +480 -121
  6. sqlspec/_typing.py +278 -142
  7. sqlspec/adapters/adbc/__init__.py +4 -3
  8. sqlspec/adapters/adbc/_types.py +12 -0
  9. sqlspec/adapters/adbc/config.py +115 -260
  10. sqlspec/adapters/adbc/driver.py +462 -367
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +199 -129
  14. sqlspec/adapters/aiosqlite/driver.py +230 -269
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -168
  18. sqlspec/adapters/asyncmy/driver.py +260 -225
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +82 -181
  22. sqlspec/adapters/asyncpg/driver.py +285 -383
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -258
  26. sqlspec/adapters/bigquery/driver.py +474 -646
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +415 -351
  30. sqlspec/adapters/duckdb/driver.py +343 -413
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -379
  34. sqlspec/adapters/oracledb/driver.py +507 -560
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -254
  38. sqlspec/adapters/psqlpy/driver.py +505 -234
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -403
  42. sqlspec/adapters/psycopg/driver.py +706 -872
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +202 -118
  46. sqlspec/adapters/sqlite/driver.py +264 -303
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder → builder}/_base.py +120 -55
  50. sqlspec/{statement/builder → builder}/_column.py +17 -6
  51. sqlspec/{statement/builder → builder}/_ddl.py +46 -79
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +5 -10
  53. sqlspec/{statement/builder → builder}/_delete.py +6 -25
  54. sqlspec/{statement/builder → builder}/_insert.py +18 -65
  55. sqlspec/builder/_merge.py +56 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +8 -11
  57. sqlspec/{statement/builder → builder}/_select.py +11 -56
  58. sqlspec/{statement/builder → builder}/_update.py +12 -18
  59. sqlspec/{statement/builder → builder}/mixins/__init__.py +10 -14
  60. sqlspec/{statement/builder → builder}/mixins/_cte_and_set_ops.py +48 -59
  61. sqlspec/{statement/builder → builder}/mixins/_insert_operations.py +34 -18
  62. sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
  63. sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +19 -9
  64. sqlspec/{statement/builder → builder}/mixins/_order_limit_operations.py +3 -3
  65. sqlspec/{statement/builder → builder}/mixins/_pivot_operations.py +4 -8
  66. sqlspec/{statement/builder → builder}/mixins/_select_operations.py +25 -38
  67. sqlspec/{statement/builder → builder}/mixins/_update_operations.py +15 -16
  68. sqlspec/{statement/builder → builder}/mixins/_where_clause.py +210 -137
  69. sqlspec/cli.py +4 -5
  70. sqlspec/config.py +180 -133
  71. sqlspec/core/__init__.py +63 -0
  72. sqlspec/core/cache.py +873 -0
  73. sqlspec/core/compiler.py +396 -0
  74. sqlspec/core/filters.py +830 -0
  75. sqlspec/core/hashing.py +310 -0
  76. sqlspec/core/parameters.py +1209 -0
  77. sqlspec/core/result.py +664 -0
  78. sqlspec/{statement → core}/splitter.py +321 -191
  79. sqlspec/core/statement.py +666 -0
  80. sqlspec/driver/__init__.py +7 -10
  81. sqlspec/driver/_async.py +387 -176
  82. sqlspec/driver/_common.py +527 -289
  83. sqlspec/driver/_sync.py +390 -172
  84. sqlspec/driver/mixins/__init__.py +2 -19
  85. sqlspec/driver/mixins/_result_tools.py +164 -0
  86. sqlspec/driver/mixins/_sql_translator.py +6 -3
  87. sqlspec/exceptions.py +5 -252
  88. sqlspec/extensions/aiosql/adapter.py +93 -96
  89. sqlspec/extensions/litestar/cli.py +1 -1
  90. sqlspec/extensions/litestar/config.py +0 -1
  91. sqlspec/extensions/litestar/handlers.py +15 -26
  92. sqlspec/extensions/litestar/plugin.py +18 -16
  93. sqlspec/extensions/litestar/providers.py +17 -52
  94. sqlspec/loader.py +424 -105
  95. sqlspec/migrations/__init__.py +12 -0
  96. sqlspec/migrations/base.py +92 -68
  97. sqlspec/migrations/commands.py +24 -106
  98. sqlspec/migrations/loaders.py +402 -0
  99. sqlspec/migrations/runner.py +49 -51
  100. sqlspec/migrations/tracker.py +31 -44
  101. sqlspec/migrations/utils.py +64 -24
  102. sqlspec/protocols.py +7 -183
  103. sqlspec/storage/__init__.py +1 -1
  104. sqlspec/storage/backends/base.py +37 -40
  105. sqlspec/storage/backends/fsspec.py +136 -112
  106. sqlspec/storage/backends/obstore.py +138 -160
  107. sqlspec/storage/capabilities.py +5 -4
  108. sqlspec/storage/registry.py +57 -106
  109. sqlspec/typing.py +136 -115
  110. sqlspec/utils/__init__.py +2 -3
  111. sqlspec/utils/correlation.py +0 -3
  112. sqlspec/utils/deprecation.py +6 -6
  113. sqlspec/utils/fixtures.py +6 -6
  114. sqlspec/utils/logging.py +0 -2
  115. sqlspec/utils/module_loader.py +7 -12
  116. sqlspec/utils/singleton.py +0 -1
  117. sqlspec/utils/sync_tools.py +17 -38
  118. sqlspec/utils/text.py +12 -51
  119. sqlspec/utils/type_guards.py +443 -232
  120. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/METADATA +7 -2
  121. sqlspec-0.16.0.dist-info/RECORD +134 -0
  122. sqlspec/adapters/adbc/transformers.py +0 -108
  123. sqlspec/driver/connection.py +0 -207
  124. sqlspec/driver/mixins/_cache.py +0 -114
  125. sqlspec/driver/mixins/_csv_writer.py +0 -91
  126. sqlspec/driver/mixins/_pipeline.py +0 -508
  127. sqlspec/driver/mixins/_query_tools.py +0 -796
  128. sqlspec/driver/mixins/_result_utils.py +0 -138
  129. sqlspec/driver/mixins/_storage.py +0 -912
  130. sqlspec/driver/mixins/_type_coercion.py +0 -128
  131. sqlspec/driver/parameters.py +0 -138
  132. sqlspec/statement/__init__.py +0 -21
  133. sqlspec/statement/builder/_merge.py +0 -95
  134. sqlspec/statement/cache.py +0 -50
  135. sqlspec/statement/filters.py +0 -625
  136. sqlspec/statement/parameters.py +0 -956
  137. sqlspec/statement/pipelines/__init__.py +0 -210
  138. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  139. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  140. sqlspec/statement/pipelines/context.py +0 -109
  141. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  142. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  143. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  144. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  145. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  146. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  147. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  148. sqlspec/statement/pipelines/validators/_performance.py +0 -714
  149. sqlspec/statement/pipelines/validators/_security.py +0 -967
  150. sqlspec/statement/result.py +0 -435
  151. sqlspec/statement/sql.py +0 -1774
  152. sqlspec/utils/cached_property.py +0 -25
  153. sqlspec/utils/statement_hashing.py +0 -203
  154. sqlspec-0.14.1.dist-info/RECORD +0 -145
  155. /sqlspec/{statement/builder → builder}/mixins/_delete_operations.py +0 -0
  156. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/WHEEL +0 -0
  157. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/entry_points.txt +0 -0
  158. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/LICENSE +0 -0
  159. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,625 +0,0 @@
1
- """Collection filter datastructures."""
2
-
3
- from abc import ABC, abstractmethod
4
- from collections import abc
5
- from collections.abc import Sequence
6
- from dataclasses import dataclass
7
- from datetime import datetime
8
- from typing import TYPE_CHECKING, Any, Generic, Literal, Optional, Protocol, Union, runtime_checkable
9
-
10
- from sqlglot import exp
11
- from typing_extensions import TypeAlias, TypeVar
12
-
13
- if TYPE_CHECKING:
14
- from sqlglot.expressions import Condition
15
-
16
- from sqlspec.statement import SQL
17
-
18
- __all__ = (
19
- "AnyCollectionFilter",
20
- "BeforeAfterFilter",
21
- "FilterTypeT",
22
- "FilterTypes",
23
- "InAnyFilter",
24
- "InCollectionFilter",
25
- "LimitOffsetFilter",
26
- "NotAnyCollectionFilter",
27
- "NotInCollectionFilter",
28
- "NotInSearchFilter",
29
- "OffsetPagination",
30
- "OnBeforeAfterFilter",
31
- "OrderByFilter",
32
- "PaginationFilter",
33
- "SearchFilter",
34
- "StatementFilter",
35
- "apply_filter",
36
- )
37
-
38
- T = TypeVar("T")
39
- FilterTypeT = TypeVar("FilterTypeT", bound="StatementFilter")
40
- """Type variable for filter types.
41
-
42
- :class:`~advanced_alchemy.filters.StatementFilter`
43
- """
44
-
45
-
46
- @runtime_checkable
47
- class StatementFilter(Protocol):
48
- """Protocol for filters that can be appended to a statement."""
49
-
50
- @abstractmethod
51
- def append_to_statement(self, statement: "SQL") -> "SQL":
52
- """Append the filter to the statement.
53
-
54
- This method should modify the SQL expression only, not the parameters.
55
- Parameters should be provided via extract_parameters().
56
- """
57
- ...
58
-
59
- def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
60
- """Extract parameters that this filter contributes.
61
-
62
- Returns:
63
- Tuple of (positional_params, named_params) where:
64
- - positional_params: List of positional parameter values
65
- - named_params: Dict of parameter name to value
66
- """
67
- return [], {}
68
-
69
-
70
- @dataclass
71
- class BeforeAfterFilter(StatementFilter):
72
- """Data required to filter a query on a ``datetime`` column.
73
-
74
- Note:
75
- After applying this filter, only the filter's parameters (e.g., before/after) will be present in the resulting SQL statement's parameters. Original parameters from the statement are not preserved in the result.
76
- """
77
-
78
- field_name: str
79
- """Name of the model attribute to filter on."""
80
- before: Optional[datetime] = None
81
- """Filter results where field earlier than this."""
82
- after: Optional[datetime] = None
83
- """Filter results where field later than this."""
84
-
85
- def __post_init__(self) -> None:
86
- """Initialize parameter names."""
87
- self._param_name_before: Optional[str] = None
88
- self._param_name_after: Optional[str] = None
89
-
90
- if self.before:
91
- self._param_name_before = f"{self.field_name}_before"
92
- if self.after:
93
- self._param_name_after = f"{self.field_name}_after"
94
-
95
- def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
96
- """Extract filter parameters."""
97
- named_params = {}
98
- if self.before and self._param_name_before:
99
- named_params[self._param_name_before] = self.before
100
- if self.after and self._param_name_after:
101
- named_params[self._param_name_after] = self.after
102
- return [], named_params
103
-
104
- def append_to_statement(self, statement: "SQL") -> "SQL":
105
- """Apply filter to SQL expression only."""
106
- conditions: list[Condition] = []
107
- col_expr = exp.column(self.field_name)
108
-
109
- if self.before and self._param_name_before:
110
- conditions.append(exp.LT(this=col_expr, expression=exp.Placeholder(this=self._param_name_before)))
111
- if self.after and self._param_name_after:
112
- conditions.append(exp.GT(this=col_expr, expression=exp.Placeholder(this=self._param_name_after)))
113
-
114
- if conditions:
115
- final_condition = conditions[0]
116
- for cond in conditions[1:]:
117
- final_condition = exp.And(this=final_condition, expression=cond)
118
- result = statement.where(final_condition)
119
- _, named_params = self.extract_parameters()
120
- for name, value in named_params.items():
121
- result = result.add_named_parameter(name, value)
122
- return result
123
- return statement
124
-
125
-
126
- @dataclass
127
- class OnBeforeAfterFilter(StatementFilter):
128
- """Data required to filter a query on a ``datetime`` column."""
129
-
130
- field_name: str
131
- """Name of the model attribute to filter on."""
132
- on_or_before: Optional[datetime] = None
133
- """Filter results where field is on or earlier than this."""
134
- on_or_after: Optional[datetime] = None
135
- """Filter results where field on or later than this."""
136
-
137
- def __post_init__(self) -> None:
138
- """Initialize parameter names."""
139
- self._param_name_on_or_before: Optional[str] = None
140
- self._param_name_on_or_after: Optional[str] = None
141
-
142
- if self.on_or_before:
143
- self._param_name_on_or_before = f"{self.field_name}_on_or_before"
144
- if self.on_or_after:
145
- self._param_name_on_or_after = f"{self.field_name}_on_or_after"
146
-
147
- def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
148
- """Extract filter parameters."""
149
- named_params = {}
150
- if self.on_or_before and self._param_name_on_or_before:
151
- named_params[self._param_name_on_or_before] = self.on_or_before
152
- if self.on_or_after and self._param_name_on_or_after:
153
- named_params[self._param_name_on_or_after] = self.on_or_after
154
- return [], named_params
155
-
156
- def append_to_statement(self, statement: "SQL") -> "SQL":
157
- conditions: list[Condition] = []
158
-
159
- if self.on_or_before and self._param_name_on_or_before:
160
- conditions.append(
161
- exp.LTE(
162
- this=exp.column(self.field_name), expression=exp.Placeholder(this=self._param_name_on_or_before)
163
- )
164
- )
165
- if self.on_or_after and self._param_name_on_or_after:
166
- conditions.append(
167
- exp.GTE(this=exp.column(self.field_name), expression=exp.Placeholder(this=self._param_name_on_or_after))
168
- )
169
-
170
- if conditions:
171
- final_condition = conditions[0]
172
- for cond in conditions[1:]:
173
- final_condition = exp.And(this=final_condition, expression=cond)
174
- result = statement.where(final_condition)
175
- _, named_params = self.extract_parameters()
176
- for name, value in named_params.items():
177
- result = result.add_named_parameter(name, value)
178
- return result
179
- return statement
180
-
181
-
182
- class InAnyFilter(StatementFilter, ABC, Generic[T]):
183
- """Subclass for methods that have a `prefer_any` attribute."""
184
-
185
- @abstractmethod
186
- def append_to_statement(self, statement: "SQL") -> "SQL":
187
- raise NotImplementedError
188
-
189
-
190
- @dataclass
191
- class InCollectionFilter(InAnyFilter[T]):
192
- """Data required to construct a ``WHERE ... IN (...)`` clause.
193
-
194
- Note:
195
- After applying this filter, only the filter's parameters (e.g., the generated IN parameters) will be present in the resulting SQL statement's parameters. Original parameters from the statement are not preserved in the result.
196
- """
197
-
198
- field_name: str
199
- """Name of the model attribute to filter on."""
200
- values: Optional[abc.Collection[T]]
201
- """Values for ``IN`` clause.
202
-
203
- An empty list will return an empty result set, however, if ``None``, the filter is not applied to the query, and all rows are returned. """
204
-
205
- def __post_init__(self) -> None:
206
- """Initialize parameter names."""
207
- self._param_names: list[str] = []
208
- if self.values:
209
- for i, _ in enumerate(self.values):
210
- self._param_names.append(f"{self.field_name}_in_{i}")
211
-
212
- def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
213
- """Extract filter parameters."""
214
- named_params = {}
215
- if self.values:
216
- for i, value in enumerate(self.values):
217
- named_params[self._param_names[i]] = value
218
- return [], named_params
219
-
220
- def append_to_statement(self, statement: "SQL") -> "SQL":
221
- if self.values is None:
222
- return statement
223
-
224
- if not self.values:
225
- return statement.where(exp.false())
226
-
227
- placeholder_expressions: list[exp.Placeholder] = [
228
- exp.Placeholder(this=param_name) for param_name in self._param_names
229
- ]
230
-
231
- result = statement.where(exp.In(this=exp.column(self.field_name), expressions=placeholder_expressions))
232
- _, named_params = self.extract_parameters()
233
- for name, value in named_params.items():
234
- result = result.add_named_parameter(name, value)
235
- return result
236
-
237
-
238
- @dataclass
239
- class NotInCollectionFilter(InAnyFilter[T]):
240
- """Data required to construct a ``WHERE ... NOT IN (...)`` clause."""
241
-
242
- field_name: str
243
- """Name of the model attribute to filter on."""
244
- values: Optional[abc.Collection[T]]
245
- """Values for ``NOT IN`` clause.
246
-
247
- An empty list or ``None`` will return all rows."""
248
-
249
- def __post_init__(self) -> None:
250
- """Initialize parameter names."""
251
- self._param_names: list[str] = []
252
- if self.values:
253
- for i, _ in enumerate(self.values):
254
- self._param_names.append(f"{self.field_name}_notin_{i}_{id(self)}")
255
-
256
- def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
257
- """Extract filter parameters."""
258
- named_params = {}
259
- if self.values:
260
- for i, value in enumerate(self.values):
261
- named_params[self._param_names[i]] = value
262
- return [], named_params
263
-
264
- def append_to_statement(self, statement: "SQL") -> "SQL":
265
- if self.values is None or not self.values:
266
- return statement
267
-
268
- placeholder_expressions: list[exp.Placeholder] = [
269
- exp.Placeholder(this=param_name) for param_name in self._param_names
270
- ]
271
-
272
- result = statement.where(
273
- exp.Not(this=exp.In(this=exp.column(self.field_name), expressions=placeholder_expressions))
274
- )
275
- _, named_params = self.extract_parameters()
276
- for name, value in named_params.items():
277
- result = result.add_named_parameter(name, value)
278
- return result
279
-
280
-
281
- @dataclass
282
- class AnyCollectionFilter(InAnyFilter[T]):
283
- """Data required to construct a ``WHERE column_name = ANY (array_expression)`` clause."""
284
-
285
- field_name: str
286
- """Name of the model attribute to filter on."""
287
- values: Optional[abc.Collection[T]]
288
- """Values for ``= ANY (...)`` clause.
289
-
290
- An empty list will result in a condition that is always false (no rows returned).
291
- If ``None``, the filter is not applied to the query, and all rows are returned.
292
- """
293
-
294
- def __post_init__(self) -> None:
295
- """Initialize parameter names."""
296
- self._param_names: list[str] = []
297
- if self.values:
298
- for i, _ in enumerate(self.values):
299
- self._param_names.append(f"{self.field_name}_any_{i}")
300
-
301
- def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
302
- """Extract filter parameters."""
303
- named_params = {}
304
- if self.values:
305
- for i, value in enumerate(self.values):
306
- named_params[self._param_names[i]] = value
307
- return [], named_params
308
-
309
- def append_to_statement(self, statement: "SQL") -> "SQL":
310
- if self.values is None:
311
- return statement
312
-
313
- if not self.values:
314
- # column = ANY (empty_array) is generally false
315
- return statement.where(exp.false())
316
-
317
- placeholder_expressions: list[exp.Expression] = [
318
- exp.Placeholder(this=param_name) for param_name in self._param_names
319
- ]
320
-
321
- array_expr = exp.Array(expressions=placeholder_expressions)
322
- # Generates SQL like: self.field_name = ANY(ARRAY[?, ?, ...])
323
- result = statement.where(exp.EQ(this=exp.column(self.field_name), expression=exp.Any(this=array_expr)))
324
- _, named_params = self.extract_parameters()
325
- for name, value in named_params.items():
326
- result = result.add_named_parameter(name, value)
327
- return result
328
-
329
-
330
- @dataclass
331
- class NotAnyCollectionFilter(InAnyFilter[T]):
332
- """Data required to construct a ``WHERE NOT (column_name = ANY (array_expression))`` clause."""
333
-
334
- field_name: str
335
- """Name of the model attribute to filter on."""
336
- values: Optional[abc.Collection[T]]
337
- """Values for ``NOT (... = ANY (...))`` clause.
338
-
339
- An empty list will result in a condition that is always true (all rows returned, filter effectively ignored).
340
- If ``None``, the filter is not applied to the query, and all rows are returned.
341
- """
342
-
343
- def __post_init__(self) -> None:
344
- """Initialize parameter names."""
345
- self._param_names: list[str] = []
346
- if self.values:
347
- for i, _ in enumerate(self.values):
348
- self._param_names.append(f"{self.field_name}_notany_{i}")
349
-
350
- def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
351
- """Extract filter parameters."""
352
- named_params = {}
353
- if self.values:
354
- for i, value in enumerate(self.values):
355
- named_params[self._param_names[i]] = value
356
- return [], named_params
357
-
358
- def append_to_statement(self, statement: "SQL") -> "SQL":
359
- if self.values is None or not self.values:
360
- # NOT (column = ANY (empty_array)) is generally true
361
- # So, if values is empty or None, this filter should not restrict results.
362
- return statement
363
-
364
- placeholder_expressions: list[exp.Expression] = [
365
- exp.Placeholder(this=param_name) for param_name in self._param_names
366
- ]
367
-
368
- array_expr = exp.Array(expressions=placeholder_expressions)
369
- # Generates SQL like: NOT (self.field_name = ANY(ARRAY[?, ?, ...]))
370
- condition = exp.EQ(this=exp.column(self.field_name), expression=exp.Any(this=array_expr))
371
- result = statement.where(exp.Not(this=condition))
372
- _, named_params = self.extract_parameters()
373
- for name, value in named_params.items():
374
- result = result.add_named_parameter(name, value)
375
- return result
376
-
377
-
378
- class PaginationFilter(StatementFilter, ABC):
379
- """Subclass for methods that function as a pagination type."""
380
-
381
- @abstractmethod
382
- def append_to_statement(self, statement: "SQL") -> "SQL":
383
- raise NotImplementedError
384
-
385
-
386
- @dataclass
387
- class LimitOffsetFilter(PaginationFilter):
388
- """Data required to add limit/offset filtering to a query."""
389
-
390
- limit: int
391
- """Value for ``LIMIT`` clause of query."""
392
- offset: int
393
- """Value for ``OFFSET`` clause of query."""
394
-
395
- def __post_init__(self) -> None:
396
- """Initialize parameter names."""
397
- # Generate unique parameter names to avoid conflicts
398
- import uuid
399
-
400
- unique_suffix = str(uuid.uuid4()).replace("-", "")[:8]
401
- self._limit_param_name = f"limit_{unique_suffix}"
402
- self._offset_param_name = f"offset_{unique_suffix}"
403
-
404
- def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
405
- """Extract filter parameters."""
406
- return [], {self._limit_param_name: self.limit, self._offset_param_name: self.offset}
407
-
408
- def append_to_statement(self, statement: "SQL") -> "SQL":
409
- # Create limit and offset expressions using our pre-generated parameter names
410
- from sqlglot import exp
411
-
412
- limit_placeholder = exp.Placeholder(this=self._limit_param_name)
413
- offset_placeholder = exp.Placeholder(this=self._offset_param_name)
414
-
415
- # Apply LIMIT and OFFSET to the statement
416
- result = statement
417
-
418
- # Check if the statement supports LIMIT directly
419
- if isinstance(result._statement, exp.Select):
420
- new_statement = result._statement.limit(limit_placeholder)
421
- else:
422
- # Wrap in a SELECT if the statement doesn't support LIMIT directly
423
- new_statement = exp.Select().from_(result._statement).limit(limit_placeholder)
424
-
425
- # Add OFFSET
426
- if isinstance(new_statement, exp.Select):
427
- new_statement = new_statement.offset(offset_placeholder)
428
-
429
- result = result.copy(statement=new_statement)
430
-
431
- # Add the parameters to the result
432
- _, named_params = self.extract_parameters()
433
- for name, value in named_params.items():
434
- result = result.add_named_parameter(name, value)
435
- return result.filter(self)
436
-
437
-
438
- @dataclass
439
- class OrderByFilter(StatementFilter):
440
- """Data required to construct a ``ORDER BY ...`` clause."""
441
-
442
- field_name: str
443
- """Name of the model attribute to sort on."""
444
- sort_order: Literal["asc", "desc"] = "asc"
445
- """Sort ascending or descending"""
446
-
447
- def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
448
- """Extract filter parameters."""
449
- # ORDER BY doesn't use parameters, only column names and sort direction
450
- return [], {}
451
-
452
- def append_to_statement(self, statement: "SQL") -> "SQL":
453
- converted_sort_order = self.sort_order.lower()
454
- if converted_sort_order not in {"asc", "desc"}:
455
- converted_sort_order = "asc"
456
-
457
- col_expr = exp.column(self.field_name)
458
- order_expr = col_expr.desc() if converted_sort_order == "desc" else col_expr.asc()
459
-
460
- # Check if the statement supports ORDER BY directly
461
- if isinstance(statement._statement, exp.Select):
462
- new_statement = statement._statement.order_by(order_expr)
463
- else:
464
- # Wrap in a SELECT if the statement doesn't support ORDER BY directly
465
- new_statement = exp.Select().from_(statement._statement).order_by(order_expr)
466
-
467
- return statement.copy(statement=new_statement)
468
-
469
-
470
- @dataclass
471
- class SearchFilter(StatementFilter):
472
- """Data required to construct a ``WHERE field_name LIKE '%' || :value || '%'`` clause.
473
-
474
- Note:
475
- After applying this filter, only the filter's parameters (e.g., the generated search parameter) will be present in the resulting SQL statement's parameters. Original parameters from the statement are not preserved in the result.
476
- """
477
-
478
- field_name: Union[str, set[str]]
479
- """Name of the model attribute to search on."""
480
- value: str
481
- """Search value."""
482
- ignore_case: Optional[bool] = False
483
- """Should the search be case insensitive."""
484
-
485
- def __post_init__(self) -> None:
486
- """Initialize parameter names."""
487
- self._param_name: Optional[str] = None
488
- if self.value:
489
- if isinstance(self.field_name, str):
490
- self._param_name = f"{self.field_name}_search"
491
- else:
492
- self._param_name = "search_value"
493
-
494
- def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
495
- """Extract filter parameters."""
496
- named_params = {}
497
- if self.value and self._param_name:
498
- search_value_with_wildcards = f"%{self.value}%"
499
- named_params[self._param_name] = search_value_with_wildcards
500
- return [], named_params
501
-
502
- def append_to_statement(self, statement: "SQL") -> "SQL":
503
- if not self.value or not self._param_name:
504
- return statement
505
-
506
- pattern_expr = exp.Placeholder(this=self._param_name)
507
- like_op = exp.ILike if self.ignore_case else exp.Like
508
-
509
- result = statement
510
- if isinstance(self.field_name, str):
511
- result = statement.where(like_op(this=exp.column(self.field_name), expression=pattern_expr))
512
- elif isinstance(self.field_name, set) and self.field_name:
513
- field_conditions: list[Condition] = [
514
- like_op(this=exp.column(field), expression=pattern_expr) for field in self.field_name
515
- ]
516
- if not field_conditions:
517
- return statement
518
-
519
- final_condition: Condition = field_conditions[0]
520
- if len(field_conditions) > 1:
521
- for cond in field_conditions[1:]:
522
- final_condition = exp.Or(this=final_condition, expression=cond)
523
- result = statement.where(final_condition)
524
-
525
- _, named_params = self.extract_parameters()
526
- for name, value in named_params.items():
527
- result = result.add_named_parameter(name, value)
528
- return result
529
-
530
-
531
- @dataclass
532
- class NotInSearchFilter(SearchFilter):
533
- """Data required to construct a ``WHERE field_name NOT LIKE '%' || :value || '%'`` clause."""
534
-
535
- def __post_init__(self) -> None:
536
- """Initialize parameter names."""
537
- self._param_name: Optional[str] = None
538
- if self.value:
539
- if isinstance(self.field_name, str):
540
- self._param_name = f"{self.field_name}_not_search"
541
- else:
542
- self._param_name = "not_search_value"
543
-
544
- def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
545
- """Extract filter parameters."""
546
- named_params = {}
547
- if self.value and self._param_name:
548
- search_value_with_wildcards = f"%{self.value}%"
549
- named_params[self._param_name] = search_value_with_wildcards
550
- return [], named_params
551
-
552
- def append_to_statement(self, statement: "SQL") -> "SQL":
553
- if not self.value or not self._param_name:
554
- return statement
555
-
556
- pattern_expr = exp.Placeholder(this=self._param_name)
557
- like_op = exp.ILike if self.ignore_case else exp.Like
558
-
559
- result = statement
560
- if isinstance(self.field_name, str):
561
- result = statement.where(exp.Not(this=like_op(this=exp.column(self.field_name), expression=pattern_expr)))
562
- elif isinstance(self.field_name, set) and self.field_name:
563
- field_conditions: list[Condition] = [
564
- exp.Not(this=like_op(this=exp.column(field), expression=pattern_expr)) for field in self.field_name
565
- ]
566
- if not field_conditions:
567
- return statement
568
-
569
- final_condition: Condition = field_conditions[0]
570
- if len(field_conditions) > 1:
571
- for cond in field_conditions[1:]:
572
- final_condition = exp.And(this=final_condition, expression=cond)
573
- result = statement.where(final_condition)
574
-
575
- _, named_params = self.extract_parameters()
576
- for name, value in named_params.items():
577
- result = result.add_named_parameter(name, value)
578
- return result
579
-
580
-
581
- @dataclass
582
- class OffsetPagination(Generic[T]):
583
- """Container for data returned using limit/offset pagination."""
584
-
585
- __slots__ = ("items", "limit", "offset", "total")
586
-
587
- items: Sequence[T]
588
- """List of data being sent as part of the response."""
589
- limit: int
590
- """Maximal number of items to send."""
591
- offset: int
592
- """Offset from the beginning of the query.
593
-
594
- Identical to an index.
595
- """
596
- total: int
597
- """Total number of items."""
598
-
599
-
600
- def apply_filter(statement: "SQL", filter_obj: StatementFilter) -> "SQL":
601
- """Apply a statement filter to a SQL query object.
602
-
603
- Args:
604
- statement: The SQL query object to modify.
605
- filter_obj: The filter to apply.
606
-
607
- Returns:
608
- The modified query object.
609
- """
610
- return filter_obj.append_to_statement(statement)
611
-
612
-
613
- FilterTypes: TypeAlias = Union[
614
- BeforeAfterFilter,
615
- OnBeforeAfterFilter,
616
- InCollectionFilter[Any],
617
- LimitOffsetFilter,
618
- OrderByFilter,
619
- SearchFilter,
620
- NotInCollectionFilter[Any],
621
- NotInSearchFilter,
622
- AnyCollectionFilter[Any],
623
- NotAnyCollectionFilter[Any],
624
- ]
625
- """Aggregate type alias of the types supported for collection filtering."""