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/__init__.py +5 -1
- sqliter/constants.py +18 -1
- sqliter/exceptions.py +39 -12
- sqliter/helpers.py +35 -0
- sqliter/model/__init__.py +3 -2
- sqliter/model/model.py +119 -13
- sqliter/query/__init__.py +5 -1
- sqliter/query/query.py +444 -77
- sqliter/sqliter.py +360 -42
- sqliter_py-0.4.0.dist-info/METADATA +196 -0
- sqliter_py-0.4.0.dist-info/RECORD +13 -0
- sqliter_py-0.4.0.dist-info/licenses/LICENSE.txt +20 -0
- sqliter_py-0.2.0.dist-info/METADATA +0 -351
- sqliter_py-0.2.0.dist-info/RECORD +0 -11
- {sqliter_py-0.2.0.dist-info → sqliter_py-0.4.0.dist-info}/WHEEL +0 -0
sqliter/query/query.py
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
"""
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
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(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
193
|
-
order_by_field
|
|
194
|
-
direction
|
|
195
|
-
|
|
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
|
-
|
|
463
|
+
The QueryBuilder instance for method chaining.
|
|
199
464
|
|
|
200
465
|
Raises:
|
|
201
|
-
InvalidOrderError: If the field or
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
222
|
-
fields = ", ".join(self.model_class.model_fields)
|
|
514
|
+
"""Execute the constructed SQL query.
|
|
223
515
|
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
"""
|
|
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
|
|
273
|
-
"""
|
|
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
|
-
|
|
277
|
-
|
|
596
|
+
Args:
|
|
597
|
+
row: A tuple representing a database row.
|
|
278
598
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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:
|
|
608
|
+
field: row[idx]
|
|
297
609
|
for idx, field in enumerate(self.model_class.model_fields)
|
|
298
610
|
}
|
|
299
611
|
)
|
|
300
612
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
self
|
|
304
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|