surreal-orm-lite 0.2.2__tar.gz → 0.4.0__tar.gz
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.
- {surreal_orm_lite-0.2.2 → surreal_orm_lite-0.4.0}/CHANGELOG.md +58 -0
- {surreal_orm_lite-0.2.2 → surreal_orm_lite-0.4.0}/PKG-INFO +76 -1
- {surreal_orm_lite-0.2.2 → surreal_orm_lite-0.4.0}/README.md +75 -0
- {surreal_orm_lite-0.2.2 → surreal_orm_lite-0.4.0}/pyproject.toml +1 -1
- surreal_orm_lite-0.4.0/src/surreal_orm_lite/__init__.py +64 -0
- surreal_orm_lite-0.4.0/src/surreal_orm_lite/aggregations.py +279 -0
- {surreal_orm_lite-0.2.2 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/model_base.py +161 -14
- {surreal_orm_lite-0.2.2 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/query_set.py +379 -12
- surreal_orm_lite-0.4.0/src/surreal_orm_lite/signals.py +286 -0
- surreal_orm_lite-0.4.0/src/surreal_orm_lite/utils.py +53 -0
- surreal_orm_lite-0.2.2/src/surreal_orm_lite/__init__.py +0 -27
- surreal_orm_lite-0.2.2/src/surreal_orm_lite/utils.py +0 -6
- {surreal_orm_lite-0.2.2 → surreal_orm_lite-0.4.0}/.gitignore +0 -0
- {surreal_orm_lite-0.2.2 → surreal_orm_lite-0.4.0}/LICENSE +0 -0
- {surreal_orm_lite-0.2.2 → surreal_orm_lite-0.4.0}/Makefile +0 -0
- {surreal_orm_lite-0.2.2 → surreal_orm_lite-0.4.0}/src/__init__.py +0 -0
- {surreal_orm_lite-0.2.2 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/connection_manager.py +0 -0
- {surreal_orm_lite-0.2.2 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/constants.py +0 -0
- {surreal_orm_lite-0.2.2 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/enum.py +0 -0
- {surreal_orm_lite-0.2.2 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/exceptions.py +0 -0
- {surreal_orm_lite-0.2.2 → surreal_orm_lite-0.4.0}/src/surreal_orm_lite/py.typed +0 -0
|
@@ -5,6 +5,64 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.0] - 2026-02-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Model Signals**: Django-style event system for model lifecycle
|
|
13
|
+
- `Signal` class for pre/post event handlers
|
|
14
|
+
- `AroundSignal` class for context manager-style wrapping signals with `yield`
|
|
15
|
+
- `pre_save` / `post_save` - Fired before/after `save()` operations
|
|
16
|
+
- `pre_update` / `post_update` - Fired before/after `update()` and `merge()` operations
|
|
17
|
+
- `pre_delete` / `post_delete` - Fired before/after `delete()` operations
|
|
18
|
+
- `around_save` / `around_update` / `around_delete` - Wrap operations for timing, logging, etc.
|
|
19
|
+
- `connect(model_class)` decorator for registering handlers
|
|
20
|
+
- `disconnect(handler, model_class)` for removing handlers
|
|
21
|
+
- `clear()` for removing all handlers
|
|
22
|
+
- `has_handlers()` for checking if handlers are registered
|
|
23
|
+
|
|
24
|
+
- `post_save` signal includes `created` flag to distinguish new records
|
|
25
|
+
- `pre_update` / `post_update` signals include `update_fields` list
|
|
26
|
+
- New test file `tests/test_signals.py` with unit and e2e tests
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `save()`, `update()`, `merge()`, `delete()` now emit signals when handlers are registered
|
|
31
|
+
- Internal `_do_save()` method extracted from `save()` for signal integration
|
|
32
|
+
|
|
33
|
+
## [0.3.0] - 2026-02-05
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **Aggregation Functions**: New aggregation classes for database calculations
|
|
38
|
+
- `Count()` - Count records
|
|
39
|
+
- `Sum(field)` - Sum numeric field values
|
|
40
|
+
- `Avg(field)` - Calculate average of numeric field
|
|
41
|
+
- `Min(field)` - Find minimum value
|
|
42
|
+
- `Max(field)` - Find maximum value
|
|
43
|
+
|
|
44
|
+
- **QuerySet Aggregation Methods**: Shortcut methods for common aggregations
|
|
45
|
+
- `count()` - Returns count as integer directly
|
|
46
|
+
- `sum(field)` - Returns sum as float/int
|
|
47
|
+
- `avg(field)` - Returns average as float
|
|
48
|
+
- `min(field)` - Returns minimum value
|
|
49
|
+
- `max(field)` - Returns maximum value
|
|
50
|
+
|
|
51
|
+
- **GROUP BY Support**: Django-style grouping with annotations
|
|
52
|
+
- `values(*fields)` - Specify fields for GROUP BY
|
|
53
|
+
- `annotate(**aggregations)` - Add aggregation annotations
|
|
54
|
+
|
|
55
|
+
- **exists() Method**: Efficiently check if records exist
|
|
56
|
+
|
|
57
|
+
- **raw_query() Class Method**: Execute arbitrary SurrealQL queries with variables
|
|
58
|
+
|
|
59
|
+
- New test file `tests/test_aggregations.py` with comprehensive unit and e2e tests
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- QuerySet now tracks `_group_by_fields` and `_annotations` for GROUP BY queries
|
|
64
|
+
- `exec()` method now handles GROUP BY queries differently, returning dicts instead of model instances
|
|
65
|
+
|
|
8
66
|
## [0.2.2] - 2026-02-05
|
|
9
67
|
|
|
10
68
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: surreal-orm-lite
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Lightweight Django-style ORM for SurrealDB using the official Python SDK. Async support with Pydantic validation.
|
|
5
5
|
Project-URL: Homepage, https://github.com/EulogySnowfall/SurrealDB-ORM-lite
|
|
6
6
|
Project-URL: Documentation, https://github.com/EulogySnowfall/SurrealDB-ORM-lite
|
|
@@ -191,6 +191,9 @@ results = await User.objects().query(
|
|
|
191
191
|
| Custom primary keys | ✅ |
|
|
192
192
|
| HTTP connections | ✅ |
|
|
193
193
|
| WebSocket connections | ✅ |
|
|
194
|
+
| Aggregations | ✅ |
|
|
195
|
+
| GROUP BY | ✅ |
|
|
196
|
+
| Model Signals | ✅ |
|
|
194
197
|
|
|
195
198
|
### Supported Filter Lookups
|
|
196
199
|
|
|
@@ -201,6 +204,78 @@ results = await User.objects().query(
|
|
|
201
204
|
- `startswith`, `istartswith`
|
|
202
205
|
- `endswith`, `iendswith`
|
|
203
206
|
|
|
207
|
+
### 5. Aggregations
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from surreal_orm_lite import Count, Sum, Avg, Min, Max
|
|
211
|
+
|
|
212
|
+
# Simple aggregations
|
|
213
|
+
count = await User.objects().count()
|
|
214
|
+
total = await Order.objects().sum("amount")
|
|
215
|
+
avg_age = await User.objects().avg("age")
|
|
216
|
+
max_price = await Product.objects().max("price")
|
|
217
|
+
min_price = await Product.objects().min("price")
|
|
218
|
+
|
|
219
|
+
# Check existence
|
|
220
|
+
has_admins = await User.objects().filter(role="admin").exists()
|
|
221
|
+
|
|
222
|
+
# GROUP BY with annotations
|
|
223
|
+
results = await User.objects().values("status").annotate(count=Count()).exec()
|
|
224
|
+
# [{"status": "active", "count": 42}, {"status": "inactive", "count": 8}]
|
|
225
|
+
|
|
226
|
+
# Raw SurrealQL queries
|
|
227
|
+
results = await User.raw_query(
|
|
228
|
+
"SELECT * FROM User WHERE age > $min_age",
|
|
229
|
+
variables={"min_age": 18}
|
|
230
|
+
)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 6. Model Signals
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from surreal_orm_lite import pre_save, post_save, pre_delete, post_delete
|
|
237
|
+
|
|
238
|
+
@post_save.connect(User)
|
|
239
|
+
async def on_user_saved(sender, instance, created, **kwargs):
|
|
240
|
+
"""Called after every User save."""
|
|
241
|
+
if created:
|
|
242
|
+
await send_welcome_email(instance.email)
|
|
243
|
+
await invalidate_cache(f"user:{instance.id}")
|
|
244
|
+
|
|
245
|
+
@pre_delete.connect(User)
|
|
246
|
+
async def on_user_deleting(sender, instance, **kwargs):
|
|
247
|
+
"""Called before User deletion."""
|
|
248
|
+
await archive_user_data(instance.id)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Available signals:**
|
|
252
|
+
|
|
253
|
+
| Signal | When | Extra kwargs |
|
|
254
|
+
| --------------- | --------------------------- | ---------------- |
|
|
255
|
+
| `pre_save` | Before `save()` | |
|
|
256
|
+
| `post_save` | After `save()` | `created` |
|
|
257
|
+
| `pre_update` | Before `update()`/`merge()` | `update_fields` |
|
|
258
|
+
| `post_update` | After `update()`/`merge()` | `update_fields` |
|
|
259
|
+
| `pre_delete` | Before `delete()` | |
|
|
260
|
+
| `post_delete` | After `delete()` | |
|
|
261
|
+
| `around_save` | Wraps `save()` | |
|
|
262
|
+
| `around_update` | Wraps `update()`/`merge()` | `update_fields` |
|
|
263
|
+
| `around_delete` | Wraps `delete()` | |
|
|
264
|
+
|
|
265
|
+
**Around signals** use async generators to wrap operations:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
from surreal_orm_lite import around_save
|
|
269
|
+
|
|
270
|
+
@around_save.connect(User)
|
|
271
|
+
async def time_user_save(sender, instance, **kwargs):
|
|
272
|
+
import time
|
|
273
|
+
start = time.time()
|
|
274
|
+
yield # save() executes here
|
|
275
|
+
duration = time.time() - start
|
|
276
|
+
print(f"Save took {duration:.3f}s")
|
|
277
|
+
```
|
|
278
|
+
|
|
204
279
|
---
|
|
205
280
|
|
|
206
281
|
## Configuration Options
|
|
@@ -141,6 +141,9 @@ results = await User.objects().query(
|
|
|
141
141
|
| Custom primary keys | ✅ |
|
|
142
142
|
| HTTP connections | ✅ |
|
|
143
143
|
| WebSocket connections | ✅ |
|
|
144
|
+
| Aggregations | ✅ |
|
|
145
|
+
| GROUP BY | ✅ |
|
|
146
|
+
| Model Signals | ✅ |
|
|
144
147
|
|
|
145
148
|
### Supported Filter Lookups
|
|
146
149
|
|
|
@@ -151,6 +154,78 @@ results = await User.objects().query(
|
|
|
151
154
|
- `startswith`, `istartswith`
|
|
152
155
|
- `endswith`, `iendswith`
|
|
153
156
|
|
|
157
|
+
### 5. Aggregations
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from surreal_orm_lite import Count, Sum, Avg, Min, Max
|
|
161
|
+
|
|
162
|
+
# Simple aggregations
|
|
163
|
+
count = await User.objects().count()
|
|
164
|
+
total = await Order.objects().sum("amount")
|
|
165
|
+
avg_age = await User.objects().avg("age")
|
|
166
|
+
max_price = await Product.objects().max("price")
|
|
167
|
+
min_price = await Product.objects().min("price")
|
|
168
|
+
|
|
169
|
+
# Check existence
|
|
170
|
+
has_admins = await User.objects().filter(role="admin").exists()
|
|
171
|
+
|
|
172
|
+
# GROUP BY with annotations
|
|
173
|
+
results = await User.objects().values("status").annotate(count=Count()).exec()
|
|
174
|
+
# [{"status": "active", "count": 42}, {"status": "inactive", "count": 8}]
|
|
175
|
+
|
|
176
|
+
# Raw SurrealQL queries
|
|
177
|
+
results = await User.raw_query(
|
|
178
|
+
"SELECT * FROM User WHERE age > $min_age",
|
|
179
|
+
variables={"min_age": 18}
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 6. Model Signals
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from surreal_orm_lite import pre_save, post_save, pre_delete, post_delete
|
|
187
|
+
|
|
188
|
+
@post_save.connect(User)
|
|
189
|
+
async def on_user_saved(sender, instance, created, **kwargs):
|
|
190
|
+
"""Called after every User save."""
|
|
191
|
+
if created:
|
|
192
|
+
await send_welcome_email(instance.email)
|
|
193
|
+
await invalidate_cache(f"user:{instance.id}")
|
|
194
|
+
|
|
195
|
+
@pre_delete.connect(User)
|
|
196
|
+
async def on_user_deleting(sender, instance, **kwargs):
|
|
197
|
+
"""Called before User deletion."""
|
|
198
|
+
await archive_user_data(instance.id)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Available signals:**
|
|
202
|
+
|
|
203
|
+
| Signal | When | Extra kwargs |
|
|
204
|
+
| --------------- | --------------------------- | ---------------- |
|
|
205
|
+
| `pre_save` | Before `save()` | |
|
|
206
|
+
| `post_save` | After `save()` | `created` |
|
|
207
|
+
| `pre_update` | Before `update()`/`merge()` | `update_fields` |
|
|
208
|
+
| `post_update` | After `update()`/`merge()` | `update_fields` |
|
|
209
|
+
| `pre_delete` | Before `delete()` | |
|
|
210
|
+
| `post_delete` | After `delete()` | |
|
|
211
|
+
| `around_save` | Wraps `save()` | |
|
|
212
|
+
| `around_update` | Wraps `update()`/`merge()` | `update_fields` |
|
|
213
|
+
| `around_delete` | Wraps `delete()` | |
|
|
214
|
+
|
|
215
|
+
**Around signals** use async generators to wrap operations:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from surreal_orm_lite import around_save
|
|
219
|
+
|
|
220
|
+
@around_save.connect(User)
|
|
221
|
+
async def time_user_save(sender, instance, **kwargs):
|
|
222
|
+
import time
|
|
223
|
+
start = time.time()
|
|
224
|
+
yield # save() executes here
|
|
225
|
+
duration = time.time() - start
|
|
226
|
+
print(f"Save took {duration:.3f}s")
|
|
227
|
+
```
|
|
228
|
+
|
|
154
229
|
---
|
|
155
230
|
|
|
156
231
|
## Configuration Options
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
2
|
+
|
|
3
|
+
from .aggregations import Aggregation, Avg, Count, Max, Min, Sum
|
|
4
|
+
from .connection_manager import SurrealDBConnectionManager
|
|
5
|
+
from .enum import OrderBy
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
SurrealDbConnectionError,
|
|
8
|
+
SurrealDbError,
|
|
9
|
+
SurrealDbNotFoundError,
|
|
10
|
+
SurrealDbValidationError,
|
|
11
|
+
SurrealORMError,
|
|
12
|
+
)
|
|
13
|
+
from .model_base import BaseSurrealModel, SurrealConfigDict
|
|
14
|
+
from .query_set import QuerySet
|
|
15
|
+
from .signals import (
|
|
16
|
+
AroundSignal,
|
|
17
|
+
Signal,
|
|
18
|
+
around_delete,
|
|
19
|
+
around_save,
|
|
20
|
+
around_update,
|
|
21
|
+
post_delete,
|
|
22
|
+
post_save,
|
|
23
|
+
post_update,
|
|
24
|
+
pre_delete,
|
|
25
|
+
pre_save,
|
|
26
|
+
pre_update,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"__version__",
|
|
31
|
+
# Connection
|
|
32
|
+
"SurrealDBConnectionManager",
|
|
33
|
+
# Model
|
|
34
|
+
"BaseSurrealModel",
|
|
35
|
+
"SurrealConfigDict",
|
|
36
|
+
# QuerySet
|
|
37
|
+
"QuerySet",
|
|
38
|
+
"OrderBy",
|
|
39
|
+
# Aggregations
|
|
40
|
+
"Aggregation",
|
|
41
|
+
"Count",
|
|
42
|
+
"Sum",
|
|
43
|
+
"Avg",
|
|
44
|
+
"Min",
|
|
45
|
+
"Max",
|
|
46
|
+
# Signals
|
|
47
|
+
"Signal",
|
|
48
|
+
"AroundSignal",
|
|
49
|
+
"pre_save",
|
|
50
|
+
"post_save",
|
|
51
|
+
"pre_update",
|
|
52
|
+
"post_update",
|
|
53
|
+
"pre_delete",
|
|
54
|
+
"post_delete",
|
|
55
|
+
"around_save",
|
|
56
|
+
"around_update",
|
|
57
|
+
"around_delete",
|
|
58
|
+
# Exceptions
|
|
59
|
+
"SurrealORMError",
|
|
60
|
+
"SurrealDbError",
|
|
61
|
+
"SurrealDbConnectionError",
|
|
62
|
+
"SurrealDbValidationError",
|
|
63
|
+
"SurrealDbNotFoundError",
|
|
64
|
+
]
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aggregation classes for SurrealDB-ORM-lite.
|
|
3
|
+
|
|
4
|
+
This module provides Django-style aggregation functions that can be used
|
|
5
|
+
with QuerySet to perform aggregate calculations on database fields.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
```python
|
|
9
|
+
from surreal_orm_lite import Count, Sum, Avg, Min, Max
|
|
10
|
+
|
|
11
|
+
# Simple aggregations
|
|
12
|
+
count = await User.objects().count()
|
|
13
|
+
total = await Order.objects().sum("amount")
|
|
14
|
+
|
|
15
|
+
# With GROUP BY
|
|
16
|
+
results = await User.objects().values("status").annotate(count=Count()).exec()
|
|
17
|
+
```
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
|
|
22
|
+
from .utils import validate_field_name
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Aggregation(ABC):
|
|
26
|
+
"""
|
|
27
|
+
Base class for all aggregation functions.
|
|
28
|
+
|
|
29
|
+
Aggregations are used to compute summary values from a set of records.
|
|
30
|
+
Each aggregation must implement the `to_sql()` method that returns
|
|
31
|
+
the SurrealDB SQL expression for the aggregation.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, field: str | None = None, alias: str | None = None) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Initialize an aggregation.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
field: The field name to aggregate. Some aggregations (like Count)
|
|
40
|
+
don't require a field.
|
|
41
|
+
alias: Optional alias for the result. If not provided, a default
|
|
42
|
+
alias will be generated.
|
|
43
|
+
"""
|
|
44
|
+
self.field = field
|
|
45
|
+
self.alias = alias
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def to_sql(self) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Convert the aggregation to a SurrealDB SQL expression.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
str: The SQL expression for this aggregation.
|
|
54
|
+
"""
|
|
55
|
+
pass # pragma: no cover
|
|
56
|
+
|
|
57
|
+
def get_alias(self) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Get the alias for this aggregation result.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
str: The alias name for the aggregation result.
|
|
63
|
+
"""
|
|
64
|
+
if self.alias:
|
|
65
|
+
return self.alias
|
|
66
|
+
if self.field:
|
|
67
|
+
return f"{self.__class__.__name__.lower()}_{self.field}"
|
|
68
|
+
return self.__class__.__name__.lower()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Count(Aggregation):
|
|
72
|
+
"""
|
|
73
|
+
Count aggregation function.
|
|
74
|
+
|
|
75
|
+
Counts the number of records in a query result.
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
```python
|
|
79
|
+
# Count all users
|
|
80
|
+
count = await User.objects().count()
|
|
81
|
+
|
|
82
|
+
# Count with filter
|
|
83
|
+
active_count = await User.objects().filter(status="active").count()
|
|
84
|
+
|
|
85
|
+
# Count with GROUP BY
|
|
86
|
+
results = await User.objects().values("status").annotate(count=Count()).exec()
|
|
87
|
+
```
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, field: str | None = None, alias: str | None = None) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Initialize a Count aggregation.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
field: Optional field name. If not provided, counts all records.
|
|
96
|
+
alias: Optional alias for the result.
|
|
97
|
+
"""
|
|
98
|
+
if field is not None:
|
|
99
|
+
validate_field_name(field, "Count field")
|
|
100
|
+
super().__init__(field, alias)
|
|
101
|
+
|
|
102
|
+
def to_sql(self) -> str:
|
|
103
|
+
"""
|
|
104
|
+
Convert to SurrealDB SQL.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
str: "count()" or "count(field)" expression.
|
|
108
|
+
"""
|
|
109
|
+
if self.field:
|
|
110
|
+
return f"count({self.field})"
|
|
111
|
+
return "count()"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Sum(Aggregation):
|
|
115
|
+
"""
|
|
116
|
+
Sum aggregation function.
|
|
117
|
+
|
|
118
|
+
Calculates the sum of a numeric field.
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
```python
|
|
122
|
+
# Sum of all order amounts
|
|
123
|
+
total = await Order.objects().sum("amount")
|
|
124
|
+
|
|
125
|
+
# Sum with filter
|
|
126
|
+
total_completed = await Order.objects().filter(status="completed").sum("amount")
|
|
127
|
+
|
|
128
|
+
# Sum with GROUP BY
|
|
129
|
+
results = await Order.objects().values("customer_id").annotate(total=Sum("amount")).exec()
|
|
130
|
+
```
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(self, field: str, alias: str | None = None) -> None:
|
|
134
|
+
"""
|
|
135
|
+
Initialize a Sum aggregation.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
field: The numeric field to sum.
|
|
139
|
+
alias: Optional alias for the result.
|
|
140
|
+
"""
|
|
141
|
+
if not field or not field.strip():
|
|
142
|
+
raise ValueError("Sum requires a field name")
|
|
143
|
+
validate_field_name(field, "Sum field")
|
|
144
|
+
super().__init__(field, alias)
|
|
145
|
+
|
|
146
|
+
def to_sql(self) -> str:
|
|
147
|
+
"""
|
|
148
|
+
Convert to SurrealDB SQL.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
str: "math::sum(field)" expression.
|
|
152
|
+
"""
|
|
153
|
+
return f"math::sum({self.field})"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Avg(Aggregation):
|
|
157
|
+
"""
|
|
158
|
+
Average aggregation function.
|
|
159
|
+
|
|
160
|
+
Calculates the average of a numeric field.
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
```python
|
|
164
|
+
# Average age of all users
|
|
165
|
+
avg_age = await User.objects().avg("age")
|
|
166
|
+
|
|
167
|
+
# Average with filter
|
|
168
|
+
avg_active = await User.objects().filter(status="active").avg("age")
|
|
169
|
+
|
|
170
|
+
# Average with GROUP BY
|
|
171
|
+
results = await User.objects().values("department").annotate(avg_salary=Avg("salary")).exec()
|
|
172
|
+
```
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
def __init__(self, field: str, alias: str | None = None) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Initialize an Avg aggregation.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
field: The numeric field to average.
|
|
181
|
+
alias: Optional alias for the result.
|
|
182
|
+
"""
|
|
183
|
+
if not field or not field.strip():
|
|
184
|
+
raise ValueError("Avg requires a field name")
|
|
185
|
+
validate_field_name(field, "Avg field")
|
|
186
|
+
super().__init__(field, alias)
|
|
187
|
+
|
|
188
|
+
def to_sql(self) -> str:
|
|
189
|
+
"""
|
|
190
|
+
Convert to SurrealDB SQL.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
str: "math::mean(field)" expression.
|
|
194
|
+
"""
|
|
195
|
+
return f"math::mean({self.field})"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class Min(Aggregation):
|
|
199
|
+
"""
|
|
200
|
+
Minimum aggregation function.
|
|
201
|
+
|
|
202
|
+
Finds the minimum value of a field.
|
|
203
|
+
|
|
204
|
+
Example:
|
|
205
|
+
```python
|
|
206
|
+
# Minimum price
|
|
207
|
+
min_price = await Product.objects().min("price")
|
|
208
|
+
|
|
209
|
+
# Minimum with filter
|
|
210
|
+
min_active = await Product.objects().filter(active=True).min("price")
|
|
211
|
+
|
|
212
|
+
# Minimum with GROUP BY
|
|
213
|
+
results = await Product.objects().values("category").annotate(min_price=Min("price")).exec()
|
|
214
|
+
```
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
def __init__(self, field: str, alias: str | None = None) -> None:
|
|
218
|
+
"""
|
|
219
|
+
Initialize a Min aggregation.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
field: The field to find the minimum value of.
|
|
223
|
+
alias: Optional alias for the result.
|
|
224
|
+
"""
|
|
225
|
+
if not field or not field.strip():
|
|
226
|
+
raise ValueError("Min requires a field name")
|
|
227
|
+
validate_field_name(field, "Min field")
|
|
228
|
+
super().__init__(field, alias)
|
|
229
|
+
|
|
230
|
+
def to_sql(self) -> str:
|
|
231
|
+
"""
|
|
232
|
+
Convert to SurrealDB SQL.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
str: "math::min(field)" expression.
|
|
236
|
+
"""
|
|
237
|
+
return f"math::min({self.field})"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class Max(Aggregation):
|
|
241
|
+
"""
|
|
242
|
+
Maximum aggregation function.
|
|
243
|
+
|
|
244
|
+
Finds the maximum value of a field.
|
|
245
|
+
|
|
246
|
+
Example:
|
|
247
|
+
```python
|
|
248
|
+
# Maximum price
|
|
249
|
+
max_price = await Product.objects().max("price")
|
|
250
|
+
|
|
251
|
+
# Maximum with filter
|
|
252
|
+
max_active = await Product.objects().filter(active=True).max("price")
|
|
253
|
+
|
|
254
|
+
# Maximum with GROUP BY
|
|
255
|
+
results = await Product.objects().values("category").annotate(max_price=Max("price")).exec()
|
|
256
|
+
```
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
def __init__(self, field: str, alias: str | None = None) -> None:
|
|
260
|
+
"""
|
|
261
|
+
Initialize a Max aggregation.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
field: The field to find the maximum value of.
|
|
265
|
+
alias: Optional alias for the result.
|
|
266
|
+
"""
|
|
267
|
+
if not field or not field.strip():
|
|
268
|
+
raise ValueError("Max requires a field name")
|
|
269
|
+
validate_field_name(field, "Max field")
|
|
270
|
+
super().__init__(field, alias)
|
|
271
|
+
|
|
272
|
+
def to_sql(self) -> str:
|
|
273
|
+
"""
|
|
274
|
+
Convert to SurrealDB SQL.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
str: "math::max(field)" expression.
|
|
278
|
+
"""
|
|
279
|
+
return f"math::max({self.field})"
|