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