sqlspec 0.24.1__py3-none-any.whl → 0.26.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 (95) hide show
  1. sqlspec/_serialization.py +223 -21
  2. sqlspec/_sql.py +20 -62
  3. sqlspec/_typing.py +11 -0
  4. sqlspec/adapters/adbc/config.py +8 -1
  5. sqlspec/adapters/adbc/data_dictionary.py +290 -0
  6. sqlspec/adapters/adbc/driver.py +129 -20
  7. sqlspec/adapters/adbc/type_converter.py +159 -0
  8. sqlspec/adapters/aiosqlite/config.py +3 -0
  9. sqlspec/adapters/aiosqlite/data_dictionary.py +117 -0
  10. sqlspec/adapters/aiosqlite/driver.py +17 -3
  11. sqlspec/adapters/asyncmy/_types.py +1 -1
  12. sqlspec/adapters/asyncmy/config.py +11 -8
  13. sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
  14. sqlspec/adapters/asyncmy/driver.py +31 -7
  15. sqlspec/adapters/asyncpg/config.py +3 -0
  16. sqlspec/adapters/asyncpg/data_dictionary.py +134 -0
  17. sqlspec/adapters/asyncpg/driver.py +19 -4
  18. sqlspec/adapters/bigquery/config.py +3 -0
  19. sqlspec/adapters/bigquery/data_dictionary.py +109 -0
  20. sqlspec/adapters/bigquery/driver.py +21 -3
  21. sqlspec/adapters/bigquery/type_converter.py +93 -0
  22. sqlspec/adapters/duckdb/_types.py +1 -1
  23. sqlspec/adapters/duckdb/config.py +2 -0
  24. sqlspec/adapters/duckdb/data_dictionary.py +124 -0
  25. sqlspec/adapters/duckdb/driver.py +32 -5
  26. sqlspec/adapters/duckdb/pool.py +1 -1
  27. sqlspec/adapters/duckdb/type_converter.py +103 -0
  28. sqlspec/adapters/oracledb/config.py +6 -0
  29. sqlspec/adapters/oracledb/data_dictionary.py +442 -0
  30. sqlspec/adapters/oracledb/driver.py +68 -9
  31. sqlspec/adapters/oracledb/migrations.py +51 -67
  32. sqlspec/adapters/oracledb/type_converter.py +132 -0
  33. sqlspec/adapters/psqlpy/config.py +3 -0
  34. sqlspec/adapters/psqlpy/data_dictionary.py +133 -0
  35. sqlspec/adapters/psqlpy/driver.py +23 -179
  36. sqlspec/adapters/psqlpy/type_converter.py +73 -0
  37. sqlspec/adapters/psycopg/config.py +8 -4
  38. sqlspec/adapters/psycopg/data_dictionary.py +257 -0
  39. sqlspec/adapters/psycopg/driver.py +40 -5
  40. sqlspec/adapters/sqlite/config.py +3 -0
  41. sqlspec/adapters/sqlite/data_dictionary.py +117 -0
  42. sqlspec/adapters/sqlite/driver.py +18 -3
  43. sqlspec/adapters/sqlite/pool.py +13 -4
  44. sqlspec/base.py +3 -4
  45. sqlspec/builder/_base.py +130 -48
  46. sqlspec/builder/_column.py +66 -24
  47. sqlspec/builder/_ddl.py +91 -41
  48. sqlspec/builder/_insert.py +40 -58
  49. sqlspec/builder/_parsing_utils.py +127 -12
  50. sqlspec/builder/_select.py +147 -2
  51. sqlspec/builder/_update.py +1 -1
  52. sqlspec/builder/mixins/_cte_and_set_ops.py +31 -23
  53. sqlspec/builder/mixins/_delete_operations.py +12 -7
  54. sqlspec/builder/mixins/_insert_operations.py +50 -36
  55. sqlspec/builder/mixins/_join_operations.py +15 -30
  56. sqlspec/builder/mixins/_merge_operations.py +210 -78
  57. sqlspec/builder/mixins/_order_limit_operations.py +4 -10
  58. sqlspec/builder/mixins/_pivot_operations.py +1 -0
  59. sqlspec/builder/mixins/_select_operations.py +44 -22
  60. sqlspec/builder/mixins/_update_operations.py +30 -37
  61. sqlspec/builder/mixins/_where_clause.py +52 -70
  62. sqlspec/cli.py +246 -140
  63. sqlspec/config.py +33 -19
  64. sqlspec/core/__init__.py +3 -2
  65. sqlspec/core/cache.py +298 -352
  66. sqlspec/core/compiler.py +61 -4
  67. sqlspec/core/filters.py +246 -213
  68. sqlspec/core/hashing.py +9 -11
  69. sqlspec/core/parameters.py +27 -10
  70. sqlspec/core/statement.py +72 -12
  71. sqlspec/core/type_conversion.py +234 -0
  72. sqlspec/driver/__init__.py +6 -3
  73. sqlspec/driver/_async.py +108 -5
  74. sqlspec/driver/_common.py +186 -17
  75. sqlspec/driver/_sync.py +108 -5
  76. sqlspec/driver/mixins/_result_tools.py +60 -7
  77. sqlspec/exceptions.py +5 -0
  78. sqlspec/loader.py +8 -9
  79. sqlspec/migrations/__init__.py +4 -3
  80. sqlspec/migrations/base.py +153 -14
  81. sqlspec/migrations/commands.py +34 -96
  82. sqlspec/migrations/context.py +145 -0
  83. sqlspec/migrations/loaders.py +25 -8
  84. sqlspec/migrations/runner.py +352 -82
  85. sqlspec/storage/backends/fsspec.py +1 -0
  86. sqlspec/typing.py +4 -0
  87. sqlspec/utils/config_resolver.py +153 -0
  88. sqlspec/utils/serializers.py +50 -2
  89. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
  90. sqlspec-0.26.0.dist-info/RECORD +157 -0
  91. sqlspec-0.24.1.dist-info/RECORD +0 -139
  92. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
  93. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
  94. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
  95. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/NOTICE +0 -0
sqlspec/core/filters.py CHANGED
@@ -52,6 +52,8 @@ __all__ = (
52
52
  "SearchFilter",
53
53
  "StatementFilter",
54
54
  "apply_filter",
55
+ "canonicalize_filters",
56
+ "create_filters",
55
57
  )
56
58
 
57
59
  T = TypeVar("T")
@@ -71,7 +73,7 @@ class StatementFilter(ABC):
71
73
  Parameters should be provided via extract_parameters().
72
74
  """
73
75
 
74
- def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
76
+ def extract_parameters(self) -> "tuple[list[Any], dict[str, Any]]":
75
77
  """Extract parameters that this filter contributes.
76
78
 
77
79
  Returns:
@@ -91,7 +93,7 @@ class StatementFilter(ABC):
91
93
  Returns:
92
94
  List of resolved parameter names (same length as proposed_names)
93
95
  """
94
- existing_params = set(statement._named_parameters.keys())
96
+ existing_params = set(statement.named_parameters.keys())
95
97
  existing_params.update(statement.parameters.keys() if isinstance(statement.parameters, dict) else [])
96
98
 
97
99
  resolved_names = []
@@ -121,39 +123,44 @@ class BeforeAfterFilter(StatementFilter):
121
123
  Applies WHERE clauses for before/after datetime filtering.
122
124
  """
123
125
 
124
- __slots__ = ("_param_name_after", "_param_name_before", "after", "before", "field_name")
125
-
126
- field_name: str
127
- before: Optional[datetime]
128
- after: Optional[datetime]
126
+ __slots__ = ("_after", "_before", "_field_name")
129
127
 
130
128
  def __init__(self, field_name: str, before: Optional[datetime] = None, after: Optional[datetime] = None) -> None:
131
- """Initialize the BeforeAfterFilter.
129
+ self._field_name = field_name
130
+ self._before = before
131
+ self._after = after
132
132
 
133
- Args:
134
- field_name: Name of the model attribute to filter on.
135
- before: Filter results where field earlier than this.
136
- after: Filter results where field later than this.
137
- """
138
- self.field_name = field_name
139
- self.before = before
140
- self.after = after
133
+ @property
134
+ def field_name(self) -> str:
135
+ return self._field_name
136
+
137
+ @property
138
+ def before(self) -> Optional[datetime]:
139
+ return self._before
141
140
 
142
- self._param_name_before: Optional[str] = None
143
- self._param_name_after: Optional[str] = None
141
+ @property
142
+ def after(self) -> Optional[datetime]:
143
+ return self._after
144
144
 
145
+ def get_param_names(self) -> list[str]:
146
+ """Get parameter names without storing them."""
147
+ names = []
145
148
  if self.before:
146
- self._param_name_before = f"{self.field_name}_before"
149
+ names.append(f"{self.field_name}_before")
147
150
  if self.after:
148
- self._param_name_after = f"{self.field_name}_after"
151
+ names.append(f"{self.field_name}_after")
152
+ return names
149
153
 
150
154
  def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
151
155
  """Extract filter parameters."""
152
156
  named_parameters = {}
153
- if self.before and self._param_name_before:
154
- named_parameters[self._param_name_before] = self.before
155
- if self.after and self._param_name_after:
156
- named_parameters[self._param_name_after] = self.after
157
+ param_names = self.get_param_names()
158
+ param_idx = 0
159
+ if self.before:
160
+ named_parameters[param_names[param_idx]] = self.before
161
+ param_idx += 1
162
+ if self.after:
163
+ named_parameters[param_names[param_idx]] = self.after
157
164
  return [], named_parameters
158
165
 
159
166
  def append_to_statement(self, statement: "SQL") -> "SQL":
@@ -161,12 +168,7 @@ class BeforeAfterFilter(StatementFilter):
161
168
  conditions: list[Condition] = []
162
169
  col_expr = exp.column(self.field_name)
163
170
 
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
-
171
+ proposed_names = self.get_param_names()
170
172
  if not proposed_names:
171
173
  return statement
172
174
 
@@ -174,13 +176,13 @@ class BeforeAfterFilter(StatementFilter):
174
176
 
175
177
  param_idx = 0
176
178
  result = statement
177
- if self.before and self._param_name_before:
179
+ if self.before:
178
180
  before_param_name = resolved_names[param_idx]
179
181
  param_idx += 1
180
182
  conditions.append(exp.LT(this=col_expr, expression=exp.Placeholder(this=before_param_name)))
181
183
  result = result.add_named_parameter(before_param_name, self.before)
182
184
 
183
- if self.after and self._param_name_after:
185
+ if self.after:
184
186
  after_param_name = resolved_names[param_idx]
185
187
  conditions.append(exp.GT(this=col_expr, expression=exp.Placeholder(this=after_param_name)))
186
188
  result = result.add_named_parameter(after_param_name, self.after)
@@ -201,52 +203,52 @@ class OnBeforeAfterFilter(StatementFilter):
201
203
  Applies WHERE clauses for on-or-before/on-or-after datetime filtering.
202
204
  """
203
205
 
204
- __slots__ = ("_param_name_on_or_after", "_param_name_on_or_before", "field_name", "on_or_after", "on_or_before")
205
-
206
- field_name: str
207
- on_or_before: Optional[datetime]
208
- on_or_after: Optional[datetime]
206
+ __slots__ = ("_field_name", "_on_or_after", "_on_or_before")
209
207
 
210
208
  def __init__(
211
209
  self, field_name: str, on_or_before: Optional[datetime] = None, on_or_after: Optional[datetime] = None
212
210
  ) -> None:
213
- """Initialize the OnBeforeAfterFilter.
211
+ self._field_name = field_name
212
+ self._on_or_before = on_or_before
213
+ self._on_or_after = on_or_after
214
214
 
215
- Args:
216
- field_name: Name of the model attribute to filter on.
217
- on_or_before: Filter results where field is on or earlier than this.
218
- on_or_after: Filter results where field on or later than this.
219
- """
220
- self.field_name = field_name
221
- self.on_or_before = on_or_before
222
- self.on_or_after = on_or_after
215
+ @property
216
+ def field_name(self) -> str:
217
+ return self._field_name
218
+
219
+ @property
220
+ def on_or_before(self) -> Optional[datetime]:
221
+ return self._on_or_before
223
222
 
224
- self._param_name_on_or_before: Optional[str] = None
225
- self._param_name_on_or_after: Optional[str] = None
223
+ @property
224
+ def on_or_after(self) -> Optional[datetime]:
225
+ return self._on_or_after
226
226
 
227
+ def get_param_names(self) -> list[str]:
228
+ """Get parameter names without storing them."""
229
+ names = []
227
230
  if self.on_or_before:
228
- self._param_name_on_or_before = f"{self.field_name}_on_or_before"
231
+ names.append(f"{self.field_name}_on_or_before")
229
232
  if self.on_or_after:
230
- self._param_name_on_or_after = f"{self.field_name}_on_or_after"
233
+ names.append(f"{self.field_name}_on_or_after")
234
+ return names
231
235
 
232
236
  def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
233
237
  """Extract filter parameters."""
234
238
  named_parameters = {}
235
- if self.on_or_before and self._param_name_on_or_before:
236
- named_parameters[self._param_name_on_or_before] = self.on_or_before
237
- if self.on_or_after and self._param_name_on_or_after:
238
- named_parameters[self._param_name_on_or_after] = self.on_or_after
239
+ param_names = self.get_param_names()
240
+ param_idx = 0
241
+ if self.on_or_before:
242
+ named_parameters[param_names[param_idx]] = self.on_or_before
243
+ param_idx += 1
244
+ if self.on_or_after:
245
+ named_parameters[param_names[param_idx]] = self.on_or_after
239
246
  return [], named_parameters
240
247
 
241
248
  def append_to_statement(self, statement: "SQL") -> "SQL":
242
249
  conditions: list[Condition] = []
243
250
 
244
- proposed_names = []
245
- if self.on_or_before and self._param_name_on_or_before:
246
- proposed_names.append(self._param_name_on_or_before)
247
- if self.on_or_after and self._param_name_on_or_after:
248
- proposed_names.append(self._param_name_on_or_after)
249
-
251
+ proposed_names = self.get_param_names()
250
252
  if not proposed_names:
251
253
  return statement
252
254
 
@@ -254,7 +256,7 @@ class OnBeforeAfterFilter(StatementFilter):
254
256
 
255
257
  param_idx = 0
256
258
  result = statement
257
- if self.on_or_before and self._param_name_on_or_before:
259
+ if self.on_or_before:
258
260
  before_param_name = resolved_names[param_idx]
259
261
  param_idx += 1
260
262
  conditions.append(
@@ -262,7 +264,7 @@ class OnBeforeAfterFilter(StatementFilter):
262
264
  )
263
265
  result = result.add_named_parameter(before_param_name, self.on_or_before)
264
266
 
265
- if self.on_or_after and self._param_name_on_or_after:
267
+ if self.on_or_after:
266
268
  after_param_name = resolved_names[param_idx]
267
269
  conditions.append(
268
270
  exp.GTE(this=exp.column(self.field_name), expression=exp.Placeholder(this=after_param_name))
@@ -294,33 +296,33 @@ class InCollectionFilter(InAnyFilter[T]):
294
296
  Constructs WHERE ... IN (...) clauses.
295
297
  """
296
298
 
297
- __slots__ = ("_param_names", "field_name", "values")
299
+ __slots__ = ("_field_name", "_values")
298
300
 
299
- field_name: str
300
- values: Optional[abc.Collection[T]]
301
+ def __init__(self, field_name: str, values: Optional[abc.Collection[T]] = None) -> None:
302
+ self._field_name = field_name
303
+ self._values = values
301
304
 
302
- def __init__(self, field_name: str, values: Optional[abc.Collection[T]]) -> None:
303
- """Initialize the InCollectionFilter.
305
+ @property
306
+ def field_name(self) -> str:
307
+ return self._field_name
304
308
 
305
- Args:
306
- field_name: Name of the model attribute to filter on.
307
- values: Values for ``IN`` clause. An empty list will return an empty result set,
308
- however, if ``None``, the filter is not applied to the query, and all rows are returned.
309
- """
310
- self.field_name = field_name
311
- self.values = values
309
+ @property
310
+ def values(self) -> Optional[abc.Collection[T]]:
311
+ return self._values
312
312
 
313
- self._param_names: list[str] = []
314
- if self.values:
315
- for i, _ in enumerate(self.values):
316
- self._param_names.append(f"{self.field_name}_in_{i}")
313
+ def get_param_names(self) -> list[str]:
314
+ """Get parameter names without storing them."""
315
+ if not self.values:
316
+ return []
317
+ return [f"{self.field_name}_in_{i}" for i, _ in enumerate(self.values)]
317
318
 
318
319
  def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
319
320
  """Extract filter parameters."""
320
321
  named_parameters = {}
321
322
  if self.values:
323
+ param_names = self.get_param_names()
322
324
  for i, value in enumerate(self.values):
323
- named_parameters[self._param_names[i]] = value
325
+ named_parameters[param_names[i]] = value
324
326
  return [], named_parameters
325
327
 
326
328
  def append_to_statement(self, statement: "SQL") -> "SQL":
@@ -330,7 +332,7 @@ class InCollectionFilter(InAnyFilter[T]):
330
332
  if not self.values:
331
333
  return statement.where(exp.false())
332
334
 
333
- resolved_names = self._resolve_parameter_conflicts(statement, self._param_names)
335
+ resolved_names = self._resolve_parameter_conflicts(statement, self.get_param_names())
334
336
 
335
337
  placeholder_expressions: list[exp.Placeholder] = [
336
338
  exp.Placeholder(this=param_name) for param_name in resolved_names
@@ -354,39 +356,41 @@ class NotInCollectionFilter(InAnyFilter[T]):
354
356
  Constructs WHERE ... NOT IN (...) clauses.
355
357
  """
356
358
 
357
- __slots__ = ("_param_names", "field_name", "values")
359
+ __slots__ = ("_field_name", "_values")
358
360
 
359
- field_name: str
360
- values: Optional[abc.Collection[T]]
361
+ def __init__(self, field_name: str, values: Optional[abc.Collection[T]] = None) -> None:
362
+ self._field_name = field_name
363
+ self._values = values
361
364
 
362
- def __init__(self, field_name: str, values: Optional[abc.Collection[T]]) -> None:
363
- """Initialize the NotInCollectionFilter.
365
+ @property
366
+ def field_name(self) -> str:
367
+ return self._field_name
364
368
 
365
- Args:
366
- field_name: Name of the model attribute to filter on.
367
- values: Values for ``NOT IN`` clause. An empty list or ``None`` will return all rows.
368
- """
369
- self.field_name = field_name
370
- self.values = values
369
+ @property
370
+ def values(self) -> Optional[abc.Collection[T]]:
371
+ return self._values
371
372
 
372
- self._param_names: list[str] = []
373
- if self.values:
374
- for i, _ in enumerate(self.values):
375
- self._param_names.append(f"{self.field_name}_notin_{i}_{id(self)}")
373
+ def get_param_names(self) -> list[str]:
374
+ """Get parameter names without storing them."""
375
+ if not self.values:
376
+ return []
377
+ # Use object id to ensure uniqueness between instances
378
+ return [f"{self.field_name}_notin_{i}_{id(self)}" for i, _ in enumerate(self.values)]
376
379
 
377
380
  def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
378
381
  """Extract filter parameters."""
379
382
  named_parameters = {}
380
383
  if self.values:
384
+ param_names = self.get_param_names()
381
385
  for i, value in enumerate(self.values):
382
- named_parameters[self._param_names[i]] = value
386
+ named_parameters[param_names[i]] = value
383
387
  return [], named_parameters
384
388
 
385
389
  def append_to_statement(self, statement: "SQL") -> "SQL":
386
390
  if self.values is None or not self.values:
387
391
  return statement
388
392
 
389
- resolved_names = self._resolve_parameter_conflicts(statement, self._param_names)
393
+ resolved_names = self._resolve_parameter_conflicts(statement, self.get_param_names())
390
394
 
391
395
  placeholder_expressions: list[exp.Placeholder] = [
392
396
  exp.Placeholder(this=param_name) for param_name in resolved_names
@@ -412,34 +416,33 @@ class AnyCollectionFilter(InAnyFilter[T]):
412
416
  Constructs WHERE column_name = ANY (array_expression) clauses.
413
417
  """
414
418
 
415
- __slots__ = ("_param_names", "field_name", "values")
419
+ __slots__ = ("_field_name", "_values")
416
420
 
417
- field_name: str
418
- values: Optional[abc.Collection[T]]
421
+ def __init__(self, field_name: str, values: Optional[abc.Collection[T]] = None) -> None:
422
+ self._field_name = field_name
423
+ self._values = values
419
424
 
420
- def __init__(self, field_name: str, values: Optional[abc.Collection[T]]) -> None:
421
- """Initialize the AnyCollectionFilter.
425
+ @property
426
+ def field_name(self) -> str:
427
+ return self._field_name
422
428
 
423
- Args:
424
- field_name: Name of the model attribute to filter on.
425
- values: Values for ``= ANY (...)`` clause. An empty list will result in a condition
426
- that is always false (no rows returned). If ``None``, the filter is not applied
427
- to the query, and all rows are returned.
428
- """
429
- self.field_name = field_name
430
- self.values = values
429
+ @property
430
+ def values(self) -> Optional[abc.Collection[T]]:
431
+ return self._values
431
432
 
432
- self._param_names: list[str] = []
433
- if self.values:
434
- for i, _ in enumerate(self.values):
435
- self._param_names.append(f"{self.field_name}_any_{i}")
433
+ def get_param_names(self) -> list[str]:
434
+ """Get parameter names without storing them."""
435
+ if not self.values:
436
+ return []
437
+ return [f"{self.field_name}_any_{i}" for i, _ in enumerate(self.values)]
436
438
 
437
439
  def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
438
440
  """Extract filter parameters."""
439
441
  named_parameters = {}
440
442
  if self.values:
443
+ param_names = self.get_param_names()
441
444
  for i, value in enumerate(self.values):
442
- named_parameters[self._param_names[i]] = value
445
+ named_parameters[param_names[i]] = value
443
446
  return [], named_parameters
444
447
 
445
448
  def append_to_statement(self, statement: "SQL") -> "SQL":
@@ -449,7 +452,7 @@ class AnyCollectionFilter(InAnyFilter[T]):
449
452
  if not self.values:
450
453
  return statement.where(exp.false())
451
454
 
452
- resolved_names = self._resolve_parameter_conflicts(statement, self._param_names)
455
+ resolved_names = self._resolve_parameter_conflicts(statement, self.get_param_names())
453
456
 
454
457
  placeholder_expressions: list[exp.Expression] = [
455
458
  exp.Placeholder(this=param_name) for param_name in resolved_names
@@ -474,38 +477,40 @@ class NotAnyCollectionFilter(InAnyFilter[T]):
474
477
  Constructs WHERE NOT (column_name = ANY (array_expression)) clauses.
475
478
  """
476
479
 
477
- __slots__ = ("_param_names", "field_name", "values")
480
+ __slots__ = ("_field_name", "_values")
478
481
 
479
- def __init__(self, field_name: str, values: Optional[abc.Collection[T]]) -> None:
480
- """Initialize the NotAnyCollectionFilter.
482
+ def __init__(self, field_name: str, values: Optional[abc.Collection[T]] = None) -> None:
483
+ self._field_name = field_name
484
+ self._values = values
481
485
 
482
- Args:
483
- field_name: Name of the model attribute to filter on.
484
- values: Values for ``NOT (... = ANY (...))`` clause. An empty list will result in a
485
- condition that is always true (all rows returned, filter effectively ignored).
486
- If ``None``, the filter is not applied to the query, and all rows are returned.
487
- """
488
- self.field_name = field_name
489
- self.values = values
486
+ @property
487
+ def field_name(self) -> str:
488
+ return self._field_name
490
489
 
491
- self._param_names: list[str] = []
492
- if self.values:
493
- for i, _ in enumerate(self.values):
494
- self._param_names.append(f"{self.field_name}_not_any_{i}")
490
+ @property
491
+ def values(self) -> Optional[abc.Collection[T]]:
492
+ return self._values
493
+
494
+ def get_param_names(self) -> list[str]:
495
+ """Get parameter names without storing them."""
496
+ if not self.values:
497
+ return []
498
+ return [f"{self.field_name}_not_any_{i}" for i, _ in enumerate(self.values)]
495
499
 
496
500
  def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
497
501
  """Extract filter parameters."""
498
502
  named_parameters = {}
499
503
  if self.values:
504
+ param_names = self.get_param_names()
500
505
  for i, value in enumerate(self.values):
501
- named_parameters[self._param_names[i]] = value
506
+ named_parameters[param_names[i]] = value
502
507
  return [], named_parameters
503
508
 
504
509
  def append_to_statement(self, statement: "SQL") -> "SQL":
505
510
  if self.values is None or not self.values:
506
511
  return statement
507
512
 
508
- resolved_names = self._resolve_parameter_conflicts(statement, self._param_names)
513
+ resolved_names = self._resolve_parameter_conflicts(statement, self.get_param_names())
509
514
 
510
515
  placeholder_expressions: list[exp.Expression] = [
511
516
  exp.Placeholder(this=param_name) for param_name in resolved_names
@@ -541,39 +546,40 @@ class LimitOffsetFilter(PaginationFilter):
541
546
  Adds pagination support through LIMIT/OFFSET SQL clauses.
542
547
  """
543
548
 
544
- __slots__ = ("_limit_param_name", "_offset_param_name", "limit", "offset")
545
-
546
- limit: int
547
- offset: int
549
+ __slots__ = ("_limit", "_offset")
548
550
 
549
551
  def __init__(self, limit: int, offset: int) -> None:
550
- """Initialize the LimitOffsetFilter.
552
+ self._limit = limit
553
+ self._offset = offset
551
554
 
552
- Args:
553
- limit: Value for ``LIMIT`` clause of query.
554
- offset: Value for ``OFFSET`` clause of query.
555
- """
556
- self.limit = limit
557
- self.offset = offset
555
+ @property
556
+ def limit(self) -> int:
557
+ return self._limit
558
+
559
+ @property
560
+ def offset(self) -> int:
561
+ return self._offset
558
562
 
559
- self._limit_param_name = "limit"
560
- self._offset_param_name = "offset"
563
+ def get_param_names(self) -> list[str]:
564
+ """Get parameter names without storing them."""
565
+ return ["limit", "offset"]
561
566
 
562
567
  def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
563
568
  """Extract filter parameters."""
564
- return [], {self._limit_param_name: self.limit, self._offset_param_name: self.offset}
569
+ param_names = self.get_param_names()
570
+ return [], {param_names[0]: self.limit, param_names[1]: self.offset}
565
571
 
566
572
  def append_to_statement(self, statement: "SQL") -> "SQL":
567
- resolved_names = self._resolve_parameter_conflicts(statement, [self._limit_param_name, self._offset_param_name])
573
+ resolved_names = self._resolve_parameter_conflicts(statement, self.get_param_names())
568
574
  limit_param_name, offset_param_name = resolved_names
569
575
 
570
576
  limit_placeholder = exp.Placeholder(this=limit_param_name)
571
577
  offset_placeholder = exp.Placeholder(this=offset_param_name)
572
578
 
573
579
  try:
574
- current_statement = sqlglot.parse_one(statement._raw_sql, dialect=getattr(statement, "_dialect", None))
580
+ current_statement = sqlglot.parse_one(statement.raw_sql, dialect=statement.dialect)
575
581
  except Exception:
576
- current_statement = exp.Select().from_(f"({statement._raw_sql})")
582
+ current_statement = exp.Select().from_(f"({statement.raw_sql})")
577
583
 
578
584
  if isinstance(current_statement, exp.Select):
579
585
  new_statement = current_statement.limit(limit_placeholder).offset(offset_placeholder)
@@ -596,20 +602,19 @@ class OrderByFilter(StatementFilter):
596
602
  Adds sorting capability to SQL queries.
597
603
  """
598
604
 
599
- __slots__ = ("field_name", "sort_order")
600
-
601
- field_name: str
602
- sort_order: Literal["asc", "desc"]
605
+ __slots__ = ("_field_name", "_sort_order")
603
606
 
604
607
  def __init__(self, field_name: str, sort_order: Literal["asc", "desc"] = "asc") -> None:
605
- """Initialize the OrderByFilter.
608
+ self._field_name = field_name
609
+ self._sort_order = sort_order
606
610
 
607
- Args:
608
- field_name: Name of the model attribute to sort on.
609
- sort_order: Sort ascending or descending.
610
- """
611
- self.field_name = field_name
612
- self.sort_order = sort_order
611
+ @property
612
+ def field_name(self) -> str:
613
+ return self._field_name
614
+
615
+ @property
616
+ def sort_order(self) -> Literal["asc", "desc"]:
617
+ return self._sort_order # pyright: ignore
613
618
 
614
619
  def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
615
620
  """Extract filter parameters."""
@@ -623,12 +628,12 @@ class OrderByFilter(StatementFilter):
623
628
  col_expr = exp.column(self.field_name)
624
629
  order_expr = col_expr.desc() if converted_sort_order == "desc" else col_expr.asc()
625
630
 
626
- if statement._statement is None:
631
+ if statement.statement_expression is None:
627
632
  new_statement = exp.Select().order_by(order_expr)
628
- elif isinstance(statement._statement, exp.Select):
629
- new_statement = statement._statement.order_by(order_expr)
633
+ elif isinstance(statement.statement_expression, exp.Select):
634
+ new_statement = statement.statement_expression.order_by(order_expr)
630
635
  else:
631
- new_statement = exp.Select().from_(statement._statement).order_by(order_expr)
636
+ new_statement = exp.Select().from_(statement.statement_expression).order_by(order_expr)
632
637
 
633
638
  return statement.copy(statement=new_statement)
634
639
 
@@ -643,44 +648,48 @@ class SearchFilter(StatementFilter):
643
648
  Constructs WHERE field_name LIKE '%value%' clauses.
644
649
  """
645
650
 
646
- __slots__ = ("_param_name", "field_name", "ignore_case", "value")
647
-
648
- field_name: Union[str, set[str]]
649
- value: str
650
- ignore_case: Optional[bool]
651
+ __slots__ = ("_field_name", "_ignore_case", "_value")
651
652
 
652
653
  def __init__(self, field_name: Union[str, set[str]], value: str, ignore_case: Optional[bool] = False) -> None:
653
- """Initialize the SearchFilter.
654
-
655
- Args:
656
- field_name: Name of the model attribute to search on.
657
- value: Search value.
658
- ignore_case: Should the search be case insensitive.
659
- """
660
- self.field_name = field_name
661
- self.value = value
662
- self.ignore_case = ignore_case
663
-
664
- self._param_name: Optional[str] = None
665
- if self.value:
666
- if isinstance(self.field_name, str):
667
- self._param_name = f"{self.field_name}_search"
668
- else:
669
- self._param_name = "search_value"
654
+ self._field_name = field_name
655
+ self._value = value
656
+ self._ignore_case = ignore_case
657
+
658
+ @property
659
+ def field_name(self) -> Union[str, set[str]]:
660
+ return self._field_name
661
+
662
+ @property
663
+ def value(self) -> str:
664
+ return self._value
665
+
666
+ @property
667
+ def ignore_case(self) -> Optional[bool]:
668
+ return self._ignore_case
669
+
670
+ def get_param_name(self) -> Optional[str]:
671
+ """Get parameter name without storing it."""
672
+ if not self.value:
673
+ return None
674
+ if isinstance(self.field_name, str):
675
+ return f"{self.field_name}_search"
676
+ return "search_value"
670
677
 
671
678
  def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
672
679
  """Extract filter parameters."""
673
680
  named_parameters = {}
674
- if self.value and self._param_name:
681
+ param_name = self.get_param_name()
682
+ if self.value and param_name:
675
683
  search_value_with_wildcards = f"%{self.value}%"
676
- named_parameters[self._param_name] = search_value_with_wildcards
684
+ named_parameters[param_name] = search_value_with_wildcards
677
685
  return [], named_parameters
678
686
 
679
687
  def append_to_statement(self, statement: "SQL") -> "SQL":
680
- if not self.value or not self._param_name:
688
+ param_name = self.get_param_name()
689
+ if not self.value or not param_name:
681
690
  return statement
682
691
 
683
- resolved_names = self._resolve_parameter_conflicts(statement, [self._param_name])
692
+ resolved_names = self._resolve_parameter_conflicts(statement, [param_name])
684
693
  param_name = resolved_names[0]
685
694
 
686
695
  pattern_expr = exp.Placeholder(this=param_name)
@@ -717,38 +726,29 @@ class NotInSearchFilter(SearchFilter):
717
726
  Constructs WHERE field_name NOT LIKE '%value%' clauses.
718
727
  """
719
728
 
720
- __slots__ = ()
721
-
722
- def __init__(self, field_name: Union[str, set[str]], value: str, ignore_case: Optional[bool] = False) -> None:
723
- """Initialize the NotInSearchFilter.
724
-
725
- Args:
726
- field_name: Name of the model attribute to search on.
727
- value: Search value.
728
- ignore_case: Should the search be case insensitive.
729
- """
730
- super().__init__(field_name, value, ignore_case)
731
-
732
- self._param_name: Optional[str] = None
733
- if self.value:
734
- if isinstance(self.field_name, str):
735
- self._param_name = f"{self.field_name}_not_search"
736
- else:
737
- self._param_name = "not_search_value"
729
+ def get_param_name(self) -> Optional[str]:
730
+ """Get parameter name without storing it."""
731
+ if not self.value:
732
+ return None
733
+ if isinstance(self.field_name, str):
734
+ return f"{self.field_name}_not_search"
735
+ return "not_search_value"
738
736
 
739
737
  def extract_parameters(self) -> tuple[list[Any], dict[str, Any]]:
740
738
  """Extract filter parameters."""
741
739
  named_parameters = {}
742
- if self.value and self._param_name:
740
+ param_name = self.get_param_name()
741
+ if self.value and param_name:
743
742
  search_value_with_wildcards = f"%{self.value}%"
744
- named_parameters[self._param_name] = search_value_with_wildcards
743
+ named_parameters[param_name] = search_value_with_wildcards
745
744
  return [], named_parameters
746
745
 
747
746
  def append_to_statement(self, statement: "SQL") -> "SQL":
748
- if not self.value or not self._param_name:
747
+ param_name = self.get_param_name()
748
+ if not self.value or not param_name:
749
749
  return statement
750
750
 
751
- resolved_names = self._resolve_parameter_conflicts(statement, [self._param_name])
751
+ resolved_names = self._resolve_parameter_conflicts(statement, [param_name])
752
752
  param_name = resolved_names[0]
753
753
 
754
754
  pattern_expr = exp.Placeholder(this=param_name)
@@ -829,3 +829,36 @@ FilterTypes: TypeAlias = Union[
829
829
  AnyCollectionFilter[Any],
830
830
  NotAnyCollectionFilter[Any],
831
831
  ]
832
+
833
+
834
+ def create_filters(filters: "list[StatementFilter]") -> tuple["StatementFilter", ...]:
835
+ """Convert mutable filters to immutable tuple.
836
+
837
+ Since StatementFilter classes are now immutable (with read-only properties),
838
+ we just need to convert to a tuple for consistent sharing.
839
+
840
+ Args:
841
+ filters: List of StatementFilter objects (already immutable)
842
+
843
+ Returns:
844
+ Tuple of StatementFilter objects
845
+ """
846
+ return tuple(filters)
847
+
848
+
849
+ def canonicalize_filters(filters: "list[StatementFilter]") -> tuple["StatementFilter", ...]:
850
+ """Sort filters by type and field_name for consistent hashing.
851
+
852
+ Args:
853
+ filters: List of StatementFilter objects
854
+
855
+ Returns:
856
+ Canonically sorted tuple of filters
857
+ """
858
+
859
+ def sort_key(f: "StatementFilter") -> tuple[str, str]:
860
+ class_name = type(f).__name__
861
+ field_name = getattr(f, "field_name", "")
862
+ return (class_name, str(field_name))
863
+
864
+ return tuple(sorted(filters, key=sort_key))