sqliter-py 0.2.0__py3-none-any.whl → 0.4.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 sqliter-py might be problematic. Click here for more details.

sqliter/query/query.py CHANGED
@@ -1,9 +1,25 @@
1
- """Define the 'QueryBuilder' class for building SQL queries."""
1
+ """Implements the query building and execution logic for SQLiter.
2
+
3
+ This module defines the QueryBuilder class, which provides a fluent
4
+ interface for constructing SQL queries. It supports operations such
5
+ as filtering, ordering, limiting, and various data retrieval methods,
6
+ allowing for flexible and expressive database queries without writing
7
+ raw SQL.
8
+ """
2
9
 
3
10
  from __future__ import annotations
4
11
 
5
12
  import sqlite3
6
- from typing import TYPE_CHECKING, Any, Callable, Optional, Union
13
+ import warnings
14
+ from typing import (
15
+ TYPE_CHECKING,
16
+ Any,
17
+ Callable,
18
+ Literal,
19
+ Optional,
20
+ Union,
21
+ overload,
22
+ )
7
23
 
8
24
  from typing_extensions import LiteralString, Self
9
25
 
@@ -28,10 +44,36 @@ FilterValue = Union[
28
44
 
29
45
 
30
46
  class QueryBuilder:
31
- """Functions to build and execute queries for a given model."""
47
+ """Builds and executes database queries for a specific model.
48
+
49
+ This class provides methods to construct SQL queries, apply filters,
50
+ set ordering, and execute the queries against the database.
51
+
52
+ Attributes:
53
+ db (SqliterDB): The database connection object.
54
+ model_class (type[BaseDBModel]): The Pydantic model class.
55
+ table_name (str): The name of the database table.
56
+ filters (list): List of applied filter conditions.
57
+ _limit (Optional[int]): The LIMIT clause value, if any.
58
+ _offset (Optional[int]): The OFFSET clause value, if any.
59
+ _order_by (Optional[str]): The ORDER BY clause, if any.
60
+ _fields (Optional[list[str]]): List of fields to select, if specified.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ db: SqliterDB,
66
+ model_class: type[BaseDBModel],
67
+ fields: Optional[list[str]] = None,
68
+ ) -> None:
69
+ """Initialize a new QueryBuilder instance.
32
70
 
33
- def __init__(self, db: SqliterDB, model_class: type[BaseDBModel]) -> None:
34
- """Initialize the query builder with the database, model class, etc."""
71
+ Args:
72
+ db: The database connection object.
73
+ model_class: The Pydantic model class for the table.
74
+ fields: Optional list of field names to select. If None, all fields
75
+ are selected.
76
+ """
35
77
  self.db = db
36
78
  self.model_class = model_class
37
79
  self.table_name = model_class.get_table_name() # Use model_class method
@@ -39,9 +81,48 @@ class QueryBuilder:
39
81
  self._limit: Optional[int] = None
40
82
  self._offset: Optional[int] = None
41
83
  self._order_by: Optional[str] = None
84
+ self._fields: Optional[list[str]] = fields
85
+
86
+ if self._fields:
87
+ self._validate_fields()
88
+
89
+ def _validate_fields(self) -> None:
90
+ """Validate that the specified fields exist in the model.
91
+
92
+ Raises:
93
+ ValueError: If any specified field is not in the model.
94
+ """
95
+ if self._fields is None:
96
+ return
97
+ valid_fields = set(self.model_class.model_fields.keys())
98
+ invalid_fields = set(self._fields) - valid_fields
99
+ if invalid_fields:
100
+ err_message = (
101
+ f"Invalid fields specified: {', '.join(invalid_fields)}"
102
+ )
103
+ raise ValueError(err_message)
42
104
 
43
105
  def filter(self, **conditions: str | float | None) -> QueryBuilder:
44
- """Add filter conditions to the query."""
106
+ """Apply filter conditions to the query.
107
+
108
+ This method allows adding one or more filter conditions to the query.
109
+ Each condition is specified as a keyword argument, where the key is
110
+ the field name and the value is the condition to apply.
111
+
112
+ Args:
113
+ **conditions: Arbitrary keyword arguments representing filter
114
+ conditions. The key is the field name, and the value is the
115
+ condition to apply. Supported operators include equality,
116
+ comparison, and special operators like __in, __isnull, etc.
117
+
118
+ Returns:
119
+ QueryBuilder: The current QueryBuilder instance for method
120
+ chaining.
121
+
122
+ Examples:
123
+ >>> query.filter(name="John", age__gt=30)
124
+ >>> query.filter(status__in=["active", "pending"])
125
+ """
45
126
  valid_fields = self.model_class.model_fields
46
127
 
47
128
  for field, value in conditions.items():
@@ -53,9 +134,94 @@ class QueryBuilder:
53
134
 
54
135
  return self
55
136
 
137
+ def fields(self, fields: Optional[list[str]] = None) -> QueryBuilder:
138
+ """Specify which fields to select in the query.
139
+
140
+ Args:
141
+ fields: List of field names to select. If None, all fields are
142
+ selected.
143
+
144
+ Returns:
145
+ The QueryBuilder instance for method chaining.
146
+ """
147
+ if fields:
148
+ self._fields = fields
149
+ self._validate_fields()
150
+ return self
151
+
152
+ def exclude(self, fields: Optional[list[str]] = None) -> QueryBuilder:
153
+ """Specify which fields to exclude from the query results.
154
+
155
+ Args:
156
+ fields: List of field names to exclude. If None, no fields are
157
+ excluded.
158
+
159
+ Returns:
160
+ The QueryBuilder instance for method chaining.
161
+
162
+ Raises:
163
+ ValueError: If exclusion results in no fields being selected or if
164
+ invalid fields are specified.
165
+ """
166
+ if fields:
167
+ all_fields = set(self.model_class.model_fields.keys())
168
+
169
+ # Check for invalid fields before subtraction
170
+ invalid_fields = set(fields) - all_fields
171
+ if invalid_fields:
172
+ err = (
173
+ "Invalid fields specified for exclusion: "
174
+ f"{', '.join(invalid_fields)}"
175
+ )
176
+ raise ValueError(err)
177
+
178
+ # Subtract the fields specified for exclusion
179
+ self._fields = list(all_fields - set(fields))
180
+
181
+ # Explicit check: raise an error if no fields remain
182
+ if not self._fields:
183
+ err = "Exclusion results in no fields being selected."
184
+ raise ValueError(err)
185
+
186
+ # Now validate the remaining fields to ensure they are all valid
187
+ self._validate_fields()
188
+
189
+ return self
190
+
191
+ def only(self, field: str) -> QueryBuilder:
192
+ """Specify a single field to select in the query.
193
+
194
+ Args:
195
+ field: The name of the field to select.
196
+
197
+ Returns:
198
+ The QueryBuilder instance for method chaining.
199
+
200
+ Raises:
201
+ ValueError: If the specified field is invalid.
202
+ """
203
+ all_fields = set(self.model_class.model_fields.keys())
204
+
205
+ # Validate that the field exists
206
+ if field not in all_fields:
207
+ err = f"Invalid field specified: {field}"
208
+ raise ValueError(err)
209
+
210
+ # Set self._fields to just the single field
211
+ self._fields = [field]
212
+ return self
213
+
56
214
  def _get_operator_handler(
57
215
  self, operator: str
58
216
  ) -> Callable[[str, Any, str], None]:
217
+ """Get the appropriate handler function for the given operator.
218
+
219
+ Args:
220
+ operator: The filter operator string.
221
+
222
+ Returns:
223
+ A callable that handles the specific operator type.
224
+ """
59
225
  handlers = {
60
226
  "__isnull": self._handle_null,
61
227
  "__notnull": self._handle_null,
@@ -78,12 +244,31 @@ class QueryBuilder:
78
244
  def _validate_field(
79
245
  self, field_name: str, valid_fields: dict[str, FieldInfo]
80
246
  ) -> None:
247
+ """Validate that a field exists in the model.
248
+
249
+ Args:
250
+ field_name: The name of the field to validate.
251
+ valid_fields: Dictionary of valid fields from the model.
252
+
253
+ Raises:
254
+ InvalidFilterError: If the field is not in the model.
255
+ """
81
256
  if field_name not in valid_fields:
82
257
  raise InvalidFilterError(field_name)
83
258
 
84
259
  def _handle_equality(
85
260
  self, field_name: str, value: FilterValue, operator: str
86
261
  ) -> None:
262
+ """Handle equality filter conditions.
263
+
264
+ Args:
265
+ field_name: The name of the field to filter on.
266
+ value: The value to compare against.
267
+ operator: The operator string (usually '__eq').
268
+
269
+ This method adds an equality condition to the filters list, handling
270
+ NULL values separately.
271
+ """
87
272
  if value is None:
88
273
  self.filters.append((f"{field_name} IS NULL", None, "__isnull"))
89
274
  else:
@@ -92,6 +277,16 @@ class QueryBuilder:
92
277
  def _handle_null(
93
278
  self, field_name: str, _: FilterValue, operator: str
94
279
  ) -> None:
280
+ """Handle IS NULL and IS NOT NULL filter conditions.
281
+
282
+ Args:
283
+ field_name: The name of the field to filter on. _: Placeholder for
284
+ unused value parameter.
285
+ operator: The operator string ('__isnull' or '__notnull').
286
+
287
+ This method adds an IS NULL or IS NOT NULL condition to the filters
288
+ list.
289
+ """
95
290
  condition = (
96
291
  f"{field_name} IS NOT NULL"
97
292
  if operator == "__notnull"
@@ -102,6 +297,18 @@ class QueryBuilder:
102
297
  def _handle_in(
103
298
  self, field_name: str, value: FilterValue, operator: str
104
299
  ) -> None:
300
+ """Handle IN and NOT IN filter conditions.
301
+
302
+ Args:
303
+ field_name: The name of the field to filter on.
304
+ value: A list of values to check against.
305
+ operator: The operator string ('__in' or '__not_in').
306
+
307
+ Raises:
308
+ TypeError: If the value is not a list.
309
+
310
+ This method adds an IN or NOT IN condition to the filters list.
311
+ """
105
312
  if not isinstance(value, list):
106
313
  err = f"{field_name} requires a list for '{operator}'"
107
314
  raise TypeError(err)
@@ -118,6 +325,19 @@ class QueryBuilder:
118
325
  def _handle_like(
119
326
  self, field_name: str, value: FilterValue, operator: str
120
327
  ) -> None:
328
+ """Handle LIKE and GLOB filter conditions.
329
+
330
+ Args:
331
+ field_name: The name of the field to filter on.
332
+ value: The pattern to match against.
333
+ operator: The operator string (e.g., '__startswith', '__contains').
334
+
335
+ Raises:
336
+ TypeError: If the value is not a string.
337
+
338
+ This method adds a LIKE or GLOB condition to the filters list, depending
339
+ on whether the operation is case-sensitive or not.
340
+ """
121
341
  if not isinstance(value, str):
122
342
  err = f"{field_name} requires a string value for '{operator}'"
123
343
  raise TypeError(err)
@@ -142,11 +362,29 @@ class QueryBuilder:
142
362
  def _handle_comparison(
143
363
  self, field_name: str, value: FilterValue, operator: str
144
364
  ) -> None:
365
+ """Handle comparison filter conditions.
366
+
367
+ Args:
368
+ field_name: The name of the field to filter on.
369
+ value: The value to compare against.
370
+ operator: The comparison operator string (e.g., '__lt', '__gte').
371
+
372
+ This method adds a comparison condition to the filters list.
373
+ """
145
374
  sql_operator = OPERATOR_MAPPING[operator]
146
375
  self.filters.append((f"{field_name} {sql_operator} ?", value, operator))
147
376
 
148
377
  # Helper method for parsing field and operator
149
378
  def _parse_field_operator(self, field: str) -> tuple[str, str]:
379
+ """Parse a field string to separate the field name and operator.
380
+
381
+ Args:
382
+ field: The field string, potentially including an operator.
383
+
384
+ Returns:
385
+ A tuple containing the field name and the operator (or '__eq' if
386
+ no operator was specified).
387
+ """
150
388
  for operator in OPERATOR_MAPPING:
151
389
  if field.endswith(operator):
152
390
  return field[: -len(operator)], operator
@@ -154,7 +392,15 @@ class QueryBuilder:
154
392
 
155
393
  # Helper method for formatting string operators (like startswith)
156
394
  def _format_string_for_operator(self, operator: str, value: str) -> str:
157
- # Mapping operators to their corresponding string format
395
+ """Format a string value based on the specified operator.
396
+
397
+ Args:
398
+ operator: The operator string (e.g., '__startswith', '__contains').
399
+ value: The original string value.
400
+
401
+ Returns:
402
+ The formatted string value suitable for the given operator.
403
+ """
158
404
  format_map = {
159
405
  "__startswith": f"{value}*",
160
406
  "__endswith": f"*{value}",
@@ -168,12 +414,29 @@ class QueryBuilder:
168
414
  return format_map.get(operator, value)
169
415
 
170
416
  def limit(self, limit_value: int) -> Self:
171
- """Limit the number of results returned by the query."""
417
+ """Limit the number of results returned by the query.
418
+
419
+ Args:
420
+ limit_value: The maximum number of records to return.
421
+
422
+ Returns:
423
+ The QueryBuilder instance for method chaining.
424
+ """
172
425
  self._limit = limit_value
173
426
  return self
174
427
 
175
428
  def offset(self, offset_value: int) -> Self:
176
- """Set an offset value for the query."""
429
+ """Set an offset value for the query.
430
+
431
+ Args:
432
+ offset_value: The number of records to skip.
433
+
434
+ Returns:
435
+ The QueryBuilder instance for method chaining.
436
+
437
+ Raises:
438
+ InvalidOffsetError: If the offset value is negative.
439
+ """
177
440
  if offset_value < 0:
178
441
  raise InvalidOffsetError(offset_value)
179
442
  self._offset = offset_value
@@ -182,34 +445,64 @@ class QueryBuilder:
182
445
  self._limit = -1
183
446
  return self
184
447
 
185
- def order(self, order_by_field: str, direction: str = "ASC") -> Self:
186
- """Order the results by a specific field and optionally direction.
187
-
188
- Currently only supports ordering by a single field, though this will be
189
- expanded in the future. You can chain this method to order by multiple
190
- fields.
448
+ def order(
449
+ self,
450
+ order_by_field: Optional[str] = None,
451
+ direction: Optional[str] = None,
452
+ *,
453
+ reverse: bool = False,
454
+ ) -> Self:
455
+ """Order the query results by the specified field.
191
456
 
192
- Parameters:
193
- order_by_field (str): The field to order by.
194
- direction (str, optional): The sorting direction, either 'ASC' or
195
- 'DESC'. Defaults to 'ASC'.
457
+ Args:
458
+ order_by_field: The field to order by [optional].
459
+ direction: Deprecated. Use 'reverse' instead.
460
+ reverse: If True, sort in descending order.
196
461
 
197
462
  Returns:
198
- Self: Returns the query object for chaining.
463
+ The QueryBuilder instance for method chaining.
199
464
 
200
465
  Raises:
201
- InvalidOrderError: If the field or direction is invalid.
466
+ InvalidOrderError: If the field doesn't exist or if both 'direction'
467
+ and 'reverse' are specified.
468
+
469
+ Warns:
470
+ DeprecationWarning: If 'direction' is used instead of 'reverse'.
202
471
  """
472
+ if direction:
473
+ warnings.warn(
474
+ "'direction' argument is deprecated and will be removed in a "
475
+ "future version. Use 'reverse' instead.",
476
+ DeprecationWarning,
477
+ stacklevel=2,
478
+ )
479
+
480
+ if order_by_field is None:
481
+ order_by_field = self.model_class.get_primary_key()
482
+
203
483
  if order_by_field not in self.model_class.model_fields:
204
484
  err = f"'{order_by_field}' does not exist in the model fields."
205
485
  raise InvalidOrderError(err)
206
-
207
- valid_directions = {"ASC", "DESC"}
208
- if direction.upper() not in valid_directions:
209
- err = f"'{direction}' is not a valid sorting direction."
486
+ # Raise an exception if both 'direction' and 'reverse' are specified
487
+ if direction and reverse:
488
+ err = (
489
+ "Cannot specify both 'direction' and 'reverse' as it "
490
+ "is ambiguous."
491
+ )
210
492
  raise InvalidOrderError(err)
211
493
 
212
- self._order_by = f'"{order_by_field}" {direction.upper()}'
494
+ # Determine the sorting direction
495
+ if reverse:
496
+ sort_order = "DESC"
497
+ elif direction:
498
+ sort_order = direction.upper()
499
+ if sort_order not in {"ASC", "DESC"}:
500
+ err = f"'{direction}' is not a valid sorting direction."
501
+ raise InvalidOrderError(err)
502
+ else:
503
+ sort_order = "ASC"
504
+
505
+ self._order_by = f'"{order_by_field}" {sort_order}'
213
506
  return self
214
507
 
215
508
  def _execute_query(
@@ -218,15 +511,32 @@ class QueryBuilder:
218
511
  fetch_one: bool = False,
219
512
  count_only: bool = False,
220
513
  ) -> list[tuple[Any, ...]] | Optional[tuple[Any, ...]]:
221
- """Helper function to execute the query with filters."""
222
- fields = ", ".join(self.model_class.model_fields)
514
+ """Execute the constructed SQL query.
223
515
 
224
- # Build the WHERE clause with special handling for None (NULL in SQL)
225
- values, where_clause = self._parse_filter()
516
+ Args:
517
+ fetch_one: If True, fetch only one result.
518
+ count_only: If True, return only the count of results.
519
+
520
+ Returns:
521
+ A list of tuples (all results), a single tuple (one result),
522
+ or None if no results are found.
226
523
 
227
- select_fields = fields if not count_only else "COUNT(*)"
524
+ Raises:
525
+ RecordFetchError: If there's an error executing the query.
526
+ """
527
+ if count_only:
528
+ fields = "COUNT(*)"
529
+ elif self._fields:
530
+ fields = ", ".join(f'"{field}"' for field in self._fields)
531
+ else:
532
+ fields = ", ".join(
533
+ f'"{field}"' for field in self.model_class.model_fields
534
+ )
228
535
 
229
- sql = f'SELECT {select_fields} FROM "{self.table_name}"' # noqa: S608 # nosec
536
+ sql = f'SELECT {fields} FROM "{self.table_name}"' # noqa: S608 # nosec
537
+
538
+ # Build the WHERE clause with special handling for None (NULL in SQL)
539
+ values, where_clause = self._parse_filter()
230
540
 
231
541
  if self.filters:
232
542
  sql += f" WHERE {where_clause}"
@@ -242,6 +552,11 @@ class QueryBuilder:
242
552
  sql += " OFFSET ?"
243
553
  values.append(self._offset)
244
554
 
555
+ # Print the raw SQL and values if debug is enabled
556
+ # Log the SQL if debug is enabled
557
+ if self.db.debug:
558
+ self.db._log_sql(sql, values) # noqa: SLF001
559
+
245
560
  try:
246
561
  with self.db.connect() as conn:
247
562
  cursor = conn.cursor()
@@ -251,7 +566,13 @@ class QueryBuilder:
251
566
  raise RecordFetchError(self.table_name) from exc
252
567
 
253
568
  def _parse_filter(self) -> tuple[list[Any], LiteralString]:
254
- """Actually parse the filters."""
569
+ """Parse the filter conditions into SQL clauses and values.
570
+
571
+ Returns:
572
+ A tuple containing:
573
+ - A list of values to be used in the SQL query.
574
+ - A string representing the WHERE clause of the SQL query.
575
+ """
255
576
  where_clauses = []
256
577
  values = []
257
578
  for field, value, operator in self.filters:
@@ -269,68 +590,114 @@ class QueryBuilder:
269
590
  where_clause = " AND ".join(where_clauses)
270
591
  return values, where_clause
271
592
 
272
- def fetch_all(self) -> list[BaseDBModel]:
273
- """Fetch all results matching the filters."""
274
- results = self._execute_query()
593
+ def _convert_row_to_model(self, row: tuple[Any, ...]) -> BaseDBModel:
594
+ """Convert a database row to a model instance.
275
595
 
276
- if not results:
277
- return []
596
+ Args:
597
+ row: A tuple representing a database row.
278
598
 
279
- return [
280
- self.model_class(
281
- **{
282
- field: row[idx]
283
- for idx, field in enumerate(self.model_class.model_fields)
284
- }
599
+ Returns:
600
+ An instance of the model class populated with the row data.
601
+ """
602
+ if self._fields:
603
+ return self.model_class.model_validate_partial(
604
+ {field: row[idx] for idx, field in enumerate(self._fields)}
285
605
  )
286
- for row in results
287
- ]
288
-
289
- def fetch_one(self) -> BaseDBModel | None:
290
- """Fetch exactly one result."""
291
- result = self._execute_query(fetch_one=True)
292
- if not result:
293
- return None
294
606
  return self.model_class(
295
607
  **{
296
- field: result[idx]
608
+ field: row[idx]
297
609
  for idx, field in enumerate(self.model_class.model_fields)
298
610
  }
299
611
  )
300
612
 
301
- def fetch_first(self) -> BaseDBModel | None:
302
- """Fetch the first result of the query."""
303
- self._limit = 1
304
- result = self._execute_query()
613
+ @overload
614
+ def _fetch_result(
615
+ self, *, fetch_one: Literal[True]
616
+ ) -> Optional[BaseDBModel]: ...
617
+
618
+ @overload
619
+ def _fetch_result(
620
+ self, *, fetch_one: Literal[False]
621
+ ) -> list[BaseDBModel]: ...
622
+
623
+ def _fetch_result(
624
+ self, *, fetch_one: bool = False
625
+ ) -> Union[list[BaseDBModel], Optional[BaseDBModel]]:
626
+ """Fetch and convert query results to model instances.
627
+
628
+ Args:
629
+ fetch_one: If True, fetch only one result.
630
+
631
+ Returns:
632
+ A list of model instances, a single model instance, or None if no
633
+ results are found.
634
+ """
635
+ result = self._execute_query(fetch_one=fetch_one)
636
+
305
637
  if not result:
306
- return None
307
- return self.model_class(
308
- **{
309
- field: result[0][idx]
310
- for idx, field in enumerate(self.model_class.model_fields)
311
- }
312
- )
638
+ if fetch_one:
639
+ return None
640
+ return []
641
+
642
+ if fetch_one:
643
+ # Ensure we pass a tuple, not a list, to _convert_row_to_model
644
+ if isinstance(result, list):
645
+ result = result[
646
+ 0
647
+ ] # Get the first (and only) result if it's wrapped in a list.
648
+ return self._convert_row_to_model(result)
649
+
650
+ return [self._convert_row_to_model(row) for row in result]
651
+
652
+ def fetch_all(self) -> list[BaseDBModel]:
653
+ """Fetch all results of the query.
654
+
655
+ Returns:
656
+ A list of model instances representing all query results.
657
+ """
658
+ return self._fetch_result(fetch_one=False)
659
+
660
+ def fetch_one(self) -> Optional[BaseDBModel]:
661
+ """Fetch a single result of the query.
662
+
663
+ Returns:
664
+ A single model instance or None if no result is found.
665
+ """
666
+ return self._fetch_result(fetch_one=True)
667
+
668
+ def fetch_first(self) -> Optional[BaseDBModel]:
669
+ """Fetch the first result of the query.
313
670
 
314
- def fetch_last(self) -> BaseDBModel | None:
315
- """Fetch the last result of the query (based on the insertion order)."""
671
+ Returns:
672
+ The first model instance or None if no result is found.
673
+ """
674
+ self._limit = 1
675
+ return self._fetch_result(fetch_one=True)
676
+
677
+ def fetch_last(self) -> Optional[BaseDBModel]:
678
+ """Fetch the last result of the query.
679
+
680
+ Returns:
681
+ The last model instance or None if no result is found.
682
+ """
316
683
  self._limit = 1
317
684
  self._order_by = "rowid DESC"
318
- result = self._execute_query()
319
- if not result:
320
- return None
321
- return self.model_class(
322
- **{
323
- field: result[0][idx]
324
- for idx, field in enumerate(self.model_class.model_fields)
325
- }
326
- )
685
+ return self._fetch_result(fetch_one=True)
327
686
 
328
687
  def count(self) -> int:
329
- """Return the count of records matching the filters."""
688
+ """Count the number of results for the current query.
689
+
690
+ Returns:
691
+ The number of results that match the current query conditions.
692
+ """
330
693
  result = self._execute_query(count_only=True)
331
694
 
332
695
  return int(result[0][0]) if result else 0
333
696
 
334
697
  def exists(self) -> bool:
335
- """Return True if any record matches the filters."""
698
+ """Check if any results exist for the current query.
699
+
700
+ Returns:
701
+ True if at least one result exists, False otherwise.
702
+ """
336
703
  return self.count() > 0