oxyde-postgres 0.1.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.
Files changed (66) hide show
  1. oxyde/__init__.py +151 -0
  2. oxyde/_msgpack.py +3 -0
  3. oxyde/cli/__init__.py +7 -0
  4. oxyde/cli/app.py +107 -0
  5. oxyde/cli/codegen.py +66 -0
  6. oxyde/cli/config.py +84 -0
  7. oxyde/cli/migrations.py +536 -0
  8. oxyde/codegen/__init__.py +15 -0
  9. oxyde/codegen/stub_generator.py +1229 -0
  10. oxyde/core/__init__.py +90 -0
  11. oxyde/core/column_types.py +367 -0
  12. oxyde/core/ir.py +380 -0
  13. oxyde/core/types.py +69 -0
  14. oxyde/core/wrapper.py +242 -0
  15. oxyde/db/__init__.py +230 -0
  16. oxyde/db/pool.py +305 -0
  17. oxyde/db/registry.py +113 -0
  18. oxyde/db/schema.py +51 -0
  19. oxyde/db/transaction.py +392 -0
  20. oxyde/exceptions.py +89 -0
  21. oxyde/migrations/__init__.py +49 -0
  22. oxyde/migrations/config.py +187 -0
  23. oxyde/migrations/context.py +598 -0
  24. oxyde/migrations/executor.py +456 -0
  25. oxyde/migrations/extract.py +316 -0
  26. oxyde/migrations/generator.py +534 -0
  27. oxyde/migrations/replay.py +367 -0
  28. oxyde/migrations/squash.py +109 -0
  29. oxyde/migrations/tracker.py +162 -0
  30. oxyde/migrations/utils.py +145 -0
  31. oxyde/models/__init__.py +83 -0
  32. oxyde/models/base.py +871 -0
  33. oxyde/models/decorators.py +111 -0
  34. oxyde/models/field.py +204 -0
  35. oxyde/models/lookups.py +667 -0
  36. oxyde/models/metadata.py +174 -0
  37. oxyde/models/registry.py +170 -0
  38. oxyde/models/serializers.py +144 -0
  39. oxyde/models/utils.py +116 -0
  40. oxyde/py.typed +0 -0
  41. oxyde/queries/__init__.py +79 -0
  42. oxyde/queries/aggregates.py +145 -0
  43. oxyde/queries/base.py +249 -0
  44. oxyde/queries/conditions.py +92 -0
  45. oxyde/queries/expressions.py +171 -0
  46. oxyde/queries/insert.py +124 -0
  47. oxyde/queries/joins.py +66 -0
  48. oxyde/queries/manager.py +409 -0
  49. oxyde/queries/mixins/__init__.py +68 -0
  50. oxyde/queries/mixins/aggregation.py +335 -0
  51. oxyde/queries/mixins/debug.py +154 -0
  52. oxyde/queries/mixins/execution.py +701 -0
  53. oxyde/queries/mixins/filtering.py +144 -0
  54. oxyde/queries/mixins/joining.py +160 -0
  55. oxyde/queries/mixins/mutation.py +508 -0
  56. oxyde/queries/mixins/pagination.py +145 -0
  57. oxyde/queries/q.py +250 -0
  58. oxyde/queries/raw.py +114 -0
  59. oxyde/queries/select.py +345 -0
  60. oxyde/queries/typed.py +213 -0
  61. oxyde_postgres-0.1.0.dist-info/METADATA +141 -0
  62. oxyde_postgres-0.1.0.dist-info/RECORD +66 -0
  63. oxyde_postgres-0.1.0.dist-info/WHEEL +5 -0
  64. oxyde_postgres-0.1.0.dist-info/entry_points.txt +2 -0
  65. oxyde_postgres-0.1.0.dist-info/licenses/LICENSE +21 -0
  66. oxyde_postgres-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,508 @@
1
+ """Mutation mixin for query building."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from collections.abc import Iterable
7
+ from enum import Enum
8
+ from functools import cache
9
+ from typing import (
10
+ TYPE_CHECKING,
11
+ Any,
12
+ Generic,
13
+ Literal,
14
+ TypeVar,
15
+ get_args,
16
+ get_origin,
17
+ overload,
18
+ )
19
+
20
+ from pydantic import TypeAdapter
21
+
22
+ from oxyde._msgpack import msgpack
23
+ from oxyde.core import ir
24
+ from oxyde.exceptions import ManagerError
25
+ from oxyde.models.serializers import (
26
+ _dump_insert_data,
27
+ _normalize_instance,
28
+ )
29
+ from oxyde.models.utils import _unpack_annotated, _unwrap_optional
30
+ from oxyde.queries.base import (
31
+ SupportsExecute,
32
+ _build_column_db_types,
33
+ _build_column_types,
34
+ _map_values_to_columns,
35
+ _model_key,
36
+ _resolve_execution_client,
37
+ _translate_core_error,
38
+ )
39
+ from oxyde.queries.expressions import F, _Expression, _serialize_value_for_ir
40
+ from oxyde.queries.insert import InsertQuery
41
+
42
+ if TYPE_CHECKING:
43
+ from oxyde.models.base import Model
44
+
45
+
46
+ TModel = TypeVar("TModel", bound="Model")
47
+
48
+
49
+ def _hydrate_models(
50
+ model_class: type[TModel], columns: list[str], rows: list[list[Any]]
51
+ ) -> list[TModel]:
52
+ """Convert raw columns + rows into validated model instances."""
53
+ rmap = model_class._db_meta.reverse_column_map
54
+ field_columns = [rmap.get(c, c) for c in columns] if rmap else columns
55
+ return [model_class.model_validate(dict(zip(field_columns, row))) for row in rows]
56
+
57
+
58
+ def _decode_returning_models(
59
+ model_class: type[TModel], result: dict[str, Any]
60
+ ) -> list[TModel]:
61
+ """Decode mutation-returning result {columns, rows} into model instances."""
62
+ rows = result.get("rows", [])
63
+ if not rows:
64
+ return []
65
+ return _hydrate_models(model_class, result.get("columns", []), rows)
66
+
67
+
68
+ def _decode_columnar_models(
69
+ model_class: type[TModel], result: list[Any]
70
+ ) -> list[TModel]:
71
+ """Decode columnar SELECT result [columns, rows] into model instances."""
72
+ if len(result) < 2 or not result[1]:
73
+ return []
74
+ return _hydrate_models(model_class, result[0], result[1])
75
+
76
+
77
+ def _is_enum_annotation(annotation: Any) -> bool:
78
+ annotation, _ = _unpack_annotated(annotation)
79
+ annotation, _ = _unwrap_optional(annotation)
80
+ origin = get_origin(annotation)
81
+ if origin is list:
82
+ args = get_args(annotation)
83
+ return bool(args) and _is_enum_annotation(args[0])
84
+ if origin is not None:
85
+ return any(
86
+ arg is not type(None) and _is_enum_annotation(arg)
87
+ for arg in get_args(annotation)
88
+ )
89
+ return isinstance(annotation, type) and issubclass(annotation, Enum)
90
+
91
+
92
+ @cache
93
+ def _enum_type_adapter(python_type: Any) -> TypeAdapter:
94
+ return TypeAdapter(python_type)
95
+
96
+
97
+ def _validate_enum_update_values(
98
+ model_class: type[Model],
99
+ values: dict[str, Any],
100
+ ) -> dict[str, Any]:
101
+ validated = dict(values)
102
+ metadata = model_class._db_meta.field_metadata
103
+ for field, value in values.items():
104
+ if isinstance(value, (F, _Expression)):
105
+ continue
106
+ meta = metadata.get(field)
107
+ if meta is not None and _is_enum_annotation(meta.python_type):
108
+ if value is None and meta.nullable:
109
+ validated[field] = None
110
+ else:
111
+ adapter = _enum_type_adapter(meta.python_type)
112
+ validated[field] = adapter.validate_python(value)
113
+ return validated
114
+
115
+
116
+ class MutationMixin(Generic[TModel]):
117
+ """Mixin providing data mutation capabilities."""
118
+
119
+ # These attributes are defined in the base Query class
120
+ model_class: type[TModel]
121
+
122
+ def _build_filter_tree(self) -> ir.FilterNode | None:
123
+ """Must be implemented by FilteringMixin."""
124
+ raise NotImplementedError
125
+
126
+ async def increment(
127
+ self,
128
+ field: str,
129
+ by: int | float = 1,
130
+ *,
131
+ using: str | None = None,
132
+ client: SupportsExecute | None = None,
133
+ ) -> int:
134
+ """
135
+ Atomically increment a field value.
136
+
137
+ Args:
138
+ field: Field name to increment
139
+ by: Amount to increment (default 1)
140
+ using: Database alias
141
+ client: Optional database client
142
+
143
+ Returns:
144
+ Number of affected rows
145
+
146
+ Examples:
147
+ await Post.objects.filter(id=42).increment("views", by=1)
148
+ await User.objects.filter(is_active=True).increment("login_count")
149
+ """
150
+ exec_client = await _resolve_execution_client(using, client)
151
+ column_types = _build_column_types(self.model_class)
152
+ update_ir = ir.build_update_ir(
153
+ table=self.model_class.get_table_name(),
154
+ values={field: _serialize_value_for_ir(F(field) + by)},
155
+ filter_tree=self._build_filter_tree(),
156
+ column_types=column_types,
157
+ column_db_types=_build_column_db_types(self.model_class),
158
+ model=_model_key(self.model_class),
159
+ )
160
+ result_bytes = await exec_client.execute(update_ir)
161
+ result = msgpack.unpackb(result_bytes, raw=False)
162
+ affected: int = result.get("affected", 0)
163
+ return affected
164
+
165
+ @overload
166
+ async def update(
167
+ self,
168
+ *,
169
+ returning: Literal[True],
170
+ using: str | None = ...,
171
+ client: SupportsExecute | None = ...,
172
+ **values: Any,
173
+ ) -> list[TModel]: ...
174
+
175
+ @overload
176
+ async def update(
177
+ self,
178
+ *,
179
+ returning: Literal[False] = ...,
180
+ using: str | None = ...,
181
+ client: SupportsExecute | None = ...,
182
+ **values: Any,
183
+ ) -> int: ...
184
+
185
+ async def update(
186
+ self,
187
+ *,
188
+ returning: bool = False,
189
+ using: str | None = None,
190
+ client: SupportsExecute | None = None,
191
+ **values: Any,
192
+ ) -> int | list[TModel]:
193
+ """
194
+ Update records matching the query.
195
+
196
+ Args:
197
+ returning: If True, return updated model instances.
198
+ Default False — returns number of affected rows (Django-compatible).
199
+ using: Database alias
200
+ client: Optional database client
201
+ **values: Field values to update
202
+
203
+ Returns:
204
+ Number of affected rows (default), or list of updated model instances
205
+ if returning=True.
206
+
207
+ Examples:
208
+ count = await Post.objects.filter(id=42).update(status="published")
209
+ posts = await Post.objects.filter(id=42).update(
210
+ status="published", returning=True
211
+ )
212
+ """
213
+ exec_client = await _resolve_execution_client(using, client)
214
+ column_types = _build_column_types(self.model_class)
215
+ values = _validate_enum_update_values(self.model_class, values)
216
+ mapped_values = _map_values_to_columns(self.model_class, values)
217
+ serialized_values = {
218
+ key: _serialize_value_for_ir(value) for key, value in mapped_values.items()
219
+ }
220
+
221
+ update_ir = ir.build_update_ir(
222
+ table=self.model_class.get_table_name(),
223
+ values=serialized_values,
224
+ filter_tree=self._build_filter_tree(),
225
+ column_types=column_types,
226
+ column_db_types=_build_column_db_types(self.model_class),
227
+ model=_model_key(self.model_class),
228
+ returning=returning,
229
+ )
230
+ result_bytes = await exec_client.execute(update_ir)
231
+ result = msgpack.unpackb(result_bytes, raw=False)
232
+ if returning:
233
+ return _decode_returning_models(self.model_class, result)
234
+ affected: int = result.get("affected", 0)
235
+ return affected
236
+
237
+ async def delete(
238
+ self,
239
+ *,
240
+ using: str | None = None,
241
+ client: SupportsExecute | None = None,
242
+ ) -> int:
243
+ """
244
+ Delete records matching the query.
245
+
246
+ Args:
247
+ using: Database alias
248
+ client: Optional database client
249
+
250
+ Returns:
251
+ Number of affected rows
252
+
253
+ Examples:
254
+ await Post.objects.filter(id=42).delete()
255
+ await User.objects.filter(is_active=False).delete()
256
+ """
257
+ exec_client = await _resolve_execution_client(using, client)
258
+ delete_ir = ir.build_delete_ir(
259
+ table=self.model_class.get_table_name(),
260
+ filter_tree=self._build_filter_tree(),
261
+ column_types=_build_column_types(self.model_class),
262
+ column_db_types=_build_column_db_types(self.model_class),
263
+ model=_model_key(self.model_class),
264
+ )
265
+ result_bytes = await exec_client.execute(delete_ir)
266
+ result = msgpack.unpackb(result_bytes, raw=False)
267
+ affected: int = result.get("affected", 0)
268
+ return affected
269
+
270
+ def _primary_key_field(self) -> str | None:
271
+ """Get primary key field name."""
272
+ return self.model_class._db_meta.pk_field
273
+
274
+ async def _run_mutation(
275
+ self, query: Any, client: SupportsExecute
276
+ ) -> dict[str, Any]:
277
+ """Execute mutation query with error handling."""
278
+ try:
279
+ result = await query.execute(client)
280
+ except ManagerError:
281
+ raise
282
+ except Exception as exc:
283
+ translated = _translate_core_error(exc)
284
+ if translated is not None:
285
+ raise translated from exc
286
+ raise ManagerError(str(exc)) from exc
287
+ if not isinstance(result, dict):
288
+ raise ManagerError("Mutation response must be a dict")
289
+ return result
290
+
291
+ async def create(
292
+ self,
293
+ *,
294
+ instance: TModel | None = None,
295
+ using: str | None = None,
296
+ client: SupportsExecute | None = None,
297
+ _skip_hooks: bool = False,
298
+ **data: Any,
299
+ ) -> TModel:
300
+ """
301
+ Create a new record in the database.
302
+
303
+ Args:
304
+ instance: Model instance to create (alternative to **data)
305
+ using: Database alias
306
+ client: Optional database client
307
+ _skip_hooks: Skip pre_save/post_save hooks
308
+ **data: Field values for the new record
309
+
310
+ Returns:
311
+ Created model instance with populated PK
312
+
313
+ Examples:
314
+ user = await User.objects.create(name="Alice", email="alice@example.com")
315
+ # Or with instance:
316
+ user = User(name="Alice")
317
+ user = await User.objects.create(instance=user)
318
+ """
319
+ if instance is not None and data:
320
+ raise ManagerError(
321
+ "create() accepts either 'instance' or field values, not both"
322
+ )
323
+ if instance is None:
324
+ if not data:
325
+ raise ManagerError("create() requires an instance or field values")
326
+ instance = self.model_class(**data)
327
+
328
+ # Call pre_save hook
329
+ if not _skip_hooks:
330
+ await instance.pre_save(is_create=True, update_fields=None)
331
+
332
+ exec_client = await _resolve_execution_client(using, client)
333
+ payload = _dump_insert_data(instance)
334
+ if not payload:
335
+ raise ManagerError("create() requires at least one value")
336
+ query = InsertQuery(self.model_class).values(**payload)
337
+ result = await self._run_mutation(query, exec_client)
338
+
339
+ # Update instance from RETURNING * result (with Pydantic validation)
340
+ if "rows" in result and result["rows"]:
341
+ rmap = self.model_class._db_meta.reverse_column_map
342
+ columns = result.get("columns", [])
343
+ row = result["rows"][0]
344
+ row_dict = {rmap.get(col, col): value for col, value in zip(columns, row)}
345
+ instance = self.model_class.model_validate(row_dict)
346
+ elif "inserted_ids" in result and result["inserted_ids"]:
347
+ pk_field = self._primary_key_field()
348
+ if pk_field:
349
+ row_dict = instance.model_dump()
350
+ row_dict[pk_field] = result["inserted_ids"][0]
351
+ instance = self.model_class.model_validate(row_dict)
352
+
353
+ # Call post_save hook
354
+ if not _skip_hooks:
355
+ await instance.post_save(is_create=True, update_fields=None)
356
+
357
+ return instance
358
+
359
+ async def bulk_create(
360
+ self,
361
+ objects: Iterable[TModel | dict[str, Any]],
362
+ *,
363
+ using: str | None = None,
364
+ client: SupportsExecute | None = None,
365
+ batch_size: int | None = None,
366
+ ) -> list[TModel]:
367
+ """
368
+ Insert multiple objects efficiently.
369
+
370
+ Args:
371
+ objects: Iterable of model instances or dicts
372
+ using: Database alias
373
+ client: Optional database client
374
+ batch_size: Optional batch size. If None, inserts all in one query.
375
+ Use if hitting PostgreSQL parameter limits (65535).
376
+
377
+ Returns:
378
+ List of created model instances
379
+ """
380
+ instances = [_normalize_instance(self.model_class, obj) for obj in objects]
381
+ if not instances:
382
+ return []
383
+
384
+ exec_client = await _resolve_execution_client(using, client)
385
+
386
+ # Determine batches: all at once or user-specified batch_size
387
+ if batch_size is not None and batch_size <= 0:
388
+ raise ValueError("batch_size must be a positive integer")
389
+ if batch_size is None:
390
+ batches = [instances]
391
+ else:
392
+ batches = [
393
+ instances[i : i + batch_size]
394
+ for i in range(0, len(instances), batch_size)
395
+ ]
396
+
397
+ for batch in batches:
398
+ payloads = []
399
+ for instance in batch:
400
+ payload = _dump_insert_data(instance)
401
+ if not payload:
402
+ raise ManagerError("bulk_create() encountered an empty payload")
403
+ payloads.append(payload)
404
+
405
+ query = InsertQuery(self.model_class).bulk_values(payloads)
406
+ result = await self._run_mutation(query, exec_client)
407
+
408
+ # Assign auto-generated PKs to instances
409
+ inserted_ids = result.get("inserted_ids", [])
410
+ pk_field = self._primary_key_field()
411
+
412
+ if inserted_ids and len(inserted_ids) != len(batch):
413
+ warnings.warn(
414
+ f"bulk_create: received {len(inserted_ids)} IDs for {len(batch)} rows. "
415
+ "Check conflict handling and the returned primary keys. "
416
+ "non-sequential auto_increment, or non-integer primary keys. "
417
+ "Assigned IDs may be incorrect.",
418
+ RuntimeWarning,
419
+ stacklevel=2,
420
+ )
421
+
422
+ if pk_field and inserted_ids:
423
+ for instance, pk_value in zip(batch, inserted_ids):
424
+ setattr(instance, pk_field, pk_value)
425
+
426
+ return instances
427
+
428
+ async def bulk_update(
429
+ self,
430
+ objects: Iterable[TModel],
431
+ fields: Iterable[str],
432
+ *,
433
+ using: str | None = None,
434
+ client: SupportsExecute | None = None,
435
+ ) -> int:
436
+ """
437
+ Bulk update multiple objects with CASE WHEN.
438
+
439
+ Args:
440
+ objects: Instances to update
441
+ fields: Field names to update
442
+ using: Database alias
443
+ client: Optional database client
444
+
445
+ Returns:
446
+ Number of affected rows
447
+
448
+ Examples:
449
+ users = await User.objects.filter(is_active=True).all()
450
+ for user in users:
451
+ user.login_count += 1
452
+ await User.objects.bulk_update(users, ["login_count"])
453
+ """
454
+ objects_list = list(objects)
455
+ if not objects_list:
456
+ return 0
457
+
458
+ fields_list = list(fields)
459
+ if not fields_list:
460
+ raise ManagerError("bulk_update() requires at least one field")
461
+
462
+ pk_field = self._primary_key_field()
463
+ if not pk_field:
464
+ raise ManagerError("bulk_update() requires a primary key")
465
+
466
+ pk_column = self.model_class._db_meta.field_metadata[pk_field].db_column
467
+
468
+ exec_client = await _resolve_execution_client(using, client)
469
+
470
+ # Build bulk_update payload
471
+ bulk_entries: list[dict[str, Any]] = []
472
+ for obj in objects_list:
473
+ pk_value = getattr(obj, pk_field)
474
+ if pk_value is None:
475
+ continue
476
+
477
+ all_values = obj.model_dump(mode="python", exclude_none=False)
478
+
479
+ # Extract requested fields and map to db_column names
480
+ values = {
481
+ field_name: _serialize_value_for_ir(all_values[field_name])
482
+ for field_name in fields_list
483
+ if field_name in all_values
484
+ }
485
+ mapped_values = _map_values_to_columns(self.model_class, values)
486
+
487
+ if mapped_values:
488
+ bulk_entries.append(
489
+ {
490
+ "filters": {pk_column: pk_value},
491
+ "values": mapped_values,
492
+ }
493
+ )
494
+
495
+ if not bulk_entries:
496
+ return 0
497
+
498
+ column_types = _build_column_types(self.model_class)
499
+ update_ir = ir.build_update_ir(
500
+ table=self.model_class.get_table_name(),
501
+ bulk_update=bulk_entries,
502
+ column_types=column_types,
503
+ model=_model_key(self.model_class),
504
+ )
505
+
506
+ result_bytes = await exec_client.execute(update_ir)
507
+ result = msgpack.unpackb(result_bytes, raw=False)
508
+ return int(result.get("affected", 0))
@@ -0,0 +1,145 @@
1
+ """Pagination mixin for query building."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
6
+
7
+ from typing_extensions import Self
8
+
9
+ if TYPE_CHECKING:
10
+ from typing import Literal, overload
11
+
12
+ from oxyde.models.base import Model
13
+ from oxyde.queries.typed import (
14
+ FlatValuesListQuery,
15
+ ValuesListQuery,
16
+ ValuesQuery,
17
+ )
18
+
19
+
20
+ TModel = TypeVar("TModel", bound="Model")
21
+
22
+
23
+ class PaginationMixin(Generic[TModel]):
24
+ """Mixin providing pagination and ordering capabilities."""
25
+
26
+ # These attributes are defined in the base Query class
27
+ _limit_value: int | None
28
+ _offset_value: int | None
29
+ _order_by_fields: list[tuple[str, str]]
30
+ _distinct: bool
31
+ _result_mode: str | None
32
+ _values_flat: bool
33
+ _selected_fields: list[str] | None
34
+
35
+ def _clone(self) -> Self:
36
+ """Must be implemented by the main Query class."""
37
+ raise NotImplementedError
38
+
39
+ def select(self, *fields: str) -> Self:
40
+ """Must be implemented by the main Query class."""
41
+ raise NotImplementedError
42
+
43
+ def limit(self, value: int) -> Self:
44
+ """Set LIMIT."""
45
+ if value < 0:
46
+ raise ValueError(f"limit() requires a non-negative value, got {value}")
47
+ clone = self._clone()
48
+ clone._limit_value = value
49
+ return clone
50
+
51
+ def offset(self, value: int) -> Self:
52
+ """Set OFFSET."""
53
+ if value < 0:
54
+ raise ValueError(f"offset() requires a non-negative value, got {value}")
55
+ clone = self._clone()
56
+ clone._offset_value = value
57
+ return clone
58
+
59
+ def order_by(self, *fields: str) -> Self:
60
+ """Set ORDER BY fields. Use "?" for random ordering (ORDER BY RANDOM())."""
61
+ clone = self._clone()
62
+ for field in fields:
63
+ if field == "?":
64
+ clone._order_by_fields.append(("?", "RANDOM"))
65
+ elif field.startswith("-"):
66
+ clone._order_by_fields.append((field[1:], "DESC"))
67
+ else:
68
+ clone._order_by_fields.append((field, "ASC"))
69
+ return clone
70
+
71
+ def distinct(self, distinct: bool = True) -> Self:
72
+ """Set DISTINCT."""
73
+ clone = self._clone()
74
+ clone._distinct = bool(distinct)
75
+ return clone
76
+
77
+ # values()/values_list() flip a result-mode flag on a clone at runtime;
78
+ # statically they are declared to return the typing facades from
79
+ # oxyde.queries.typed so terminal methods reflect the actual row shape
80
+ # (dicts / tuples / scalars) instead of model instances.
81
+ if TYPE_CHECKING:
82
+
83
+ def values(self, *fields: str) -> ValuesQuery[TModel]:
84
+ """Return results as dictionaries."""
85
+ ...
86
+
87
+ @overload
88
+ def values_list(
89
+ self, field: str, /, *, flat: Literal[True]
90
+ ) -> FlatValuesListQuery[TModel]: ...
91
+
92
+ @overload
93
+ def values_list(
94
+ self, *fields: str, flat: Literal[False] = ...
95
+ ) -> ValuesListQuery[TModel]: ...
96
+
97
+ @overload
98
+ def values_list(
99
+ self, *fields: str, flat: bool
100
+ ) -> ValuesListQuery[TModel] | FlatValuesListQuery[TModel]: ...
101
+
102
+ def values_list(self, *fields: str, flat: bool = False) -> Any:
103
+ """Return results as tuples (or flat list if flat=True)."""
104
+ ...
105
+
106
+ else:
107
+
108
+ def values(self, *fields):
109
+ """Return results as dictionaries."""
110
+ clone = self._clone()
111
+ if fields:
112
+ clone = clone.select(*fields)
113
+ clone._result_mode = "dict"
114
+ return clone
115
+
116
+ def values_list(self, *fields, flat=False):
117
+ """Return results as tuples (or flat list if flat=True)."""
118
+ clone = self._clone()
119
+ if fields:
120
+ clone = clone.select(*fields)
121
+ if flat and fields and len(fields) != 1:
122
+ raise ValueError(
123
+ "flat=True is only valid when a single field is selected"
124
+ )
125
+ clone._result_mode = "list"
126
+ clone._values_flat = flat
127
+ return clone
128
+
129
+ def __getitem__(self, key: slice | int) -> Self:
130
+ """Support slicing: query[0:10] or query[5]."""
131
+ if isinstance(key, slice):
132
+ start = key.start or 0
133
+ stop = key.stop
134
+ if stop is None:
135
+ raise ValueError("Slicing a Query requires an end index")
136
+ if start < 0 or (stop is not None and stop < 0):
137
+ raise ValueError("Negative slicing is not supported")
138
+ clone = self.offset(start)
139
+ length = max(0, stop - start)
140
+ return clone.limit(length)
141
+ if isinstance(key, int):
142
+ if key < 0:
143
+ raise ValueError("Negative indexing is not supported")
144
+ return self.offset(key).limit(1)
145
+ raise TypeError("Invalid argument type for slicing Query")