surreal-orm-lite 0.4.0__tar.gz → 0.5.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.4.0 → surreal_orm_lite-0.5.0}/CHANGELOG.md +41 -0
- {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/PKG-INFO +131 -38
- {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/README.md +129 -36
- {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/pyproject.toml +2 -2
- {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/__init__.py +3 -1
- {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/constants.py +4 -0
- surreal_orm_lite-0.5.0/src/surreal_orm_lite/q.py +112 -0
- surreal_orm_lite-0.5.0/src/surreal_orm_lite/query_set.py +688 -0
- surreal_orm_lite-0.5.0/src/surreal_orm_lite/utils.py +114 -0
- surreal_orm_lite-0.4.0/src/surreal_orm_lite/query_set.py +0 -870
- surreal_orm_lite-0.4.0/src/surreal_orm_lite/utils.py +0 -53
- {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/.gitignore +0 -0
- {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/LICENSE +0 -0
- {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/Makefile +0 -0
- {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/__init__.py +0 -0
- {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/aggregations.py +0 -0
- {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/connection_manager.py +0 -0
- {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/enum.py +0 -0
- {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/exceptions.py +0 -0
- {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/model_base.py +0 -0
- {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/py.typed +0 -0
- {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/signals.py +0 -0
|
@@ -5,6 +5,47 @@ 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.5.0] - 2026-02-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Q Objects**: Django-style composable query expressions for complex filters
|
|
13
|
+
- `Q(field=value)` for basic conditions
|
|
14
|
+
- `Q(...) | Q(...)` for OR queries
|
|
15
|
+
- `Q(...) & Q(...)` for AND queries
|
|
16
|
+
- `~Q(...)` for NOT queries
|
|
17
|
+
- Nested combinations: `Q(age__gte=18) & (Q(role="admin") | Q(role="mod"))`
|
|
18
|
+
|
|
19
|
+
- **Parameterized Filters**: All filter values now use parameterized variables (`$_fN`) instead of string interpolation, preventing SQL injection
|
|
20
|
+
|
|
21
|
+
- **New Lookup Operators**:
|
|
22
|
+
- `not_in` - NOT IN operator
|
|
23
|
+
- `not_contains` - CONTAINSNOT operator
|
|
24
|
+
- `containsall` - CONTAINSALL operator
|
|
25
|
+
- `containsany` - CONTAINSANY operator
|
|
26
|
+
|
|
27
|
+
- **`-field` Ordering Shorthand**: Prefix field with `-` for descending order
|
|
28
|
+
- `order_by("-created_at")` instead of `order_by("created_at", OrderBy.DESC)`
|
|
29
|
+
- Multi-field ordering: `order_by("-age", "name")`
|
|
30
|
+
|
|
31
|
+
- **Bulk Operations**:
|
|
32
|
+
- `bulk_create(models)` - Create multiple records via SDK's `insert()`
|
|
33
|
+
- `bulk_update(**kwargs)` - Update all matching records with parameterized SET clause
|
|
34
|
+
- `bulk_delete()` - Delete all matching records, returns count
|
|
35
|
+
|
|
36
|
+
- New `Q` class exported from `surreal_orm_lite`
|
|
37
|
+
- New test file `tests/test_v050.py` for all v0.5.0 features
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- `_compile_query()`, `_compile_aggregation_query()`, `_compile_group_by_query()` now return `tuple[str, dict]` with parameterized variables
|
|
42
|
+
- `_execute_query()` now accepts optional `variables` parameter
|
|
43
|
+
- `filter()` now accepts `*args: Q` positional arguments alongside keyword filters
|
|
44
|
+
- `order_by()` now accepts multiple fields with `-field` prefix support
|
|
45
|
+
- `_build_where()` replaces `_build_where_clauses()` and returns parameterized WHERE clause
|
|
46
|
+
- Shared `parse_lookup()` and `build_filter_condition()` functions moved to `utils.py`
|
|
47
|
+
- Fixed `isnull` lookup: now correctly generates `IS NULL`/`IS NOT NULL`
|
|
48
|
+
|
|
8
49
|
## [0.4.0] - 2026-02-07
|
|
9
50
|
|
|
10
51
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: surreal-orm-lite
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -31,7 +31,7 @@ License: # MIT License
|
|
|
31
31
|
SOFTWARE.
|
|
32
32
|
License-File: LICENSE
|
|
33
33
|
Keywords: async,database,orm,pydantic,surrealdb
|
|
34
|
-
Classifier: Development Status ::
|
|
34
|
+
Classifier: Development Status :: 3 - Alpha
|
|
35
35
|
Classifier: Framework :: Pydantic :: 2
|
|
36
36
|
Classifier: License :: OSI Approved :: MIT License
|
|
37
37
|
Classifier: Operating System :: OS Independent
|
|
@@ -154,9 +154,9 @@ users = await User.objects().filter(
|
|
|
154
154
|
name__startswith="A"
|
|
155
155
|
).exec()
|
|
156
156
|
|
|
157
|
-
# Ordering
|
|
157
|
+
# Ordering (with -field shorthand for DESC)
|
|
158
158
|
users = await User.objects().order_by("name").exec()
|
|
159
|
-
users = await User.objects().order_by("age",
|
|
159
|
+
users = await User.objects().order_by("-age", "name").exec()
|
|
160
160
|
|
|
161
161
|
# Pagination
|
|
162
162
|
users = await User.objects().limit(10).offset(20).exec()
|
|
@@ -181,30 +181,76 @@ results = await User.objects().query(
|
|
|
181
181
|
|
|
182
182
|
## Features
|
|
183
183
|
|
|
184
|
-
| Feature
|
|
185
|
-
|
|
|
186
|
-
| Async/await support
|
|
187
|
-
| Pydantic validation
|
|
188
|
-
| CRUD operations
|
|
189
|
-
| QuerySet with filters
|
|
190
|
-
| Django-style lookups
|
|
191
|
-
| Custom primary keys
|
|
192
|
-
| HTTP connections
|
|
193
|
-
| WebSocket connections
|
|
194
|
-
| Aggregations
|
|
195
|
-
| GROUP BY
|
|
196
|
-
| Model Signals
|
|
184
|
+
| Feature | Status |
|
|
185
|
+
| ---------------------- | ------ |
|
|
186
|
+
| Async/await support | ✅ |
|
|
187
|
+
| Pydantic validation | ✅ |
|
|
188
|
+
| CRUD operations | ✅ |
|
|
189
|
+
| QuerySet with filters | ✅ |
|
|
190
|
+
| Django-style lookups | ✅ |
|
|
191
|
+
| Custom primary keys | ✅ |
|
|
192
|
+
| HTTP connections | ✅ |
|
|
193
|
+
| WebSocket connections | ✅ |
|
|
194
|
+
| Aggregations | ✅ |
|
|
195
|
+
| GROUP BY | ✅ |
|
|
196
|
+
| Model Signals | ✅ |
|
|
197
|
+
| Raw SurrealQL queries | ✅ |
|
|
198
|
+
| Q Objects (OR/AND/NOT) | ✅ |
|
|
199
|
+
| Parameterized filters | ✅ |
|
|
200
|
+
| Bulk operations | ✅ |
|
|
201
|
+
| `-field` ordering | ✅ |
|
|
197
202
|
|
|
198
203
|
### Supported Filter Lookups
|
|
199
204
|
|
|
200
205
|
- `exact` (default)
|
|
201
206
|
- `gt`, `gte`, `lt`, `lte`
|
|
202
|
-
- `in`
|
|
203
|
-
- `contains`, `icontains`
|
|
207
|
+
- `in`, `not_in`
|
|
208
|
+
- `contains`, `icontains`, `not_contains`
|
|
209
|
+
- `containsall`, `containsany`
|
|
204
210
|
- `startswith`, `istartswith`
|
|
205
211
|
- `endswith`, `iendswith`
|
|
212
|
+
- `like`, `ilike`
|
|
213
|
+
- `match`, `regex`, `iregex`
|
|
214
|
+
- `isnull`
|
|
206
215
|
|
|
207
|
-
### 5.
|
|
216
|
+
### 5. Q Objects (Complex Queries)
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from surreal_orm_lite import Q
|
|
220
|
+
|
|
221
|
+
# OR queries
|
|
222
|
+
users = await User.objects().filter(Q(name="Alice") | Q(name="Bob")).exec()
|
|
223
|
+
|
|
224
|
+
# NOT queries
|
|
225
|
+
active = await User.objects().filter(~Q(status="banned")).exec()
|
|
226
|
+
|
|
227
|
+
# Complex combinations
|
|
228
|
+
results = await User.objects().filter(
|
|
229
|
+
Q(age__gte=18) & (Q(role="admin") | Q(role="mod"))
|
|
230
|
+
).exec()
|
|
231
|
+
|
|
232
|
+
# Mix Q objects with keyword filters
|
|
233
|
+
results = await User.objects().filter(
|
|
234
|
+
Q(role="admin") | Q(role="mod"),
|
|
235
|
+
age__gte=25
|
|
236
|
+
).exec()
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 6. Bulk Operations
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
# Bulk create
|
|
243
|
+
users = [User(name="Alice", age=30), User(name="Bob", age=25)]
|
|
244
|
+
created = await User.objects().bulk_create(users)
|
|
245
|
+
|
|
246
|
+
# Bulk update (returns count of updated records)
|
|
247
|
+
count = await User.objects().filter(status="pending").bulk_update(status="active")
|
|
248
|
+
|
|
249
|
+
# Bulk delete (returns count of deleted records)
|
|
250
|
+
count = await User.objects().filter(status="inactive").bulk_delete()
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### 7. Aggregations
|
|
208
254
|
|
|
209
255
|
```python
|
|
210
256
|
from surreal_orm_lite import Count, Sum, Avg, Min, Max
|
|
@@ -230,7 +276,7 @@ results = await User.raw_query(
|
|
|
230
276
|
)
|
|
231
277
|
```
|
|
232
278
|
|
|
233
|
-
###
|
|
279
|
+
### 8. Model Signals
|
|
234
280
|
|
|
235
281
|
```python
|
|
236
282
|
from surreal_orm_lite import pre_save, post_save, pre_delete, post_delete
|
|
@@ -250,17 +296,17 @@ async def on_user_deleting(sender, instance, **kwargs):
|
|
|
250
296
|
|
|
251
297
|
**Available signals:**
|
|
252
298
|
|
|
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()` |
|
|
299
|
+
| Signal | When | Extra kwargs |
|
|
300
|
+
| --------------- | --------------------------- | --------------- |
|
|
301
|
+
| `pre_save` | Before `save()` | |
|
|
302
|
+
| `post_save` | After `save()` | `created` |
|
|
303
|
+
| `pre_update` | Before `update()`/`merge()` | `update_fields` |
|
|
304
|
+
| `post_update` | After `update()`/`merge()` | `update_fields` |
|
|
305
|
+
| `pre_delete` | Before `delete()` | |
|
|
306
|
+
| `post_delete` | After `delete()` | |
|
|
307
|
+
| `around_save` | Wraps `save()` | |
|
|
308
|
+
| `around_update` | Wraps `update()`/`merge()` | `update_fields` |
|
|
309
|
+
| `around_delete` | Wraps `delete()` | |
|
|
264
310
|
|
|
265
311
|
**Around signals** use async generators to wrap operations:
|
|
266
312
|
|
|
@@ -326,16 +372,62 @@ Contributions are welcome! Please:
|
|
|
326
372
|
|
|
327
373
|
---
|
|
328
374
|
|
|
329
|
-
##
|
|
375
|
+
## Roadmap
|
|
330
376
|
|
|
331
|
-
|
|
377
|
+
| Version | Theme | Status |
|
|
378
|
+
| ------- | ----------------------------- | ----------- |
|
|
379
|
+
| v0.2.x | Core ORM (CRUD, QuerySet) | ✅ Released |
|
|
380
|
+
| v0.3.0 | Aggregations & Utilities | ✅ Released |
|
|
381
|
+
| v0.4.0 | Model Signals | ✅ Released |
|
|
382
|
+
| v0.5.0 | Bulk Operations & Q Objects | ✅ Released |
|
|
383
|
+
| v0.6.0 | Relations & Graph | 📋 Next |
|
|
384
|
+
| v0.7.0 | Transactions ORM | 📋 Planned |
|
|
385
|
+
| v0.8.0 | SurrealFunc & Computed Fields | 📋 Planned |
|
|
386
|
+
| v0.9.0 | Field Aliases & DX | 📋 Planned |
|
|
387
|
+
| v1.0.0 | Production Ready | 📋 Planned |
|
|
332
388
|
|
|
333
|
-
|
|
389
|
+
See [docs/ROADMAP.md](docs/ROADMAP.md) for full details.
|
|
334
390
|
|
|
335
|
-
|
|
336
|
-
- **PyPI**: [surrealdb-orm](https://pypi.org/project/surrealdb-orm/)
|
|
391
|
+
---
|
|
337
392
|
|
|
338
|
-
|
|
393
|
+
## SurrealDB-ORM-lite vs SurrealDB-ORM
|
|
394
|
+
|
|
395
|
+
This project prioritizes **stability and compatibility** with the official SurrealDB Python SDK. The full [SurrealDB-ORM](https://github.com/EulogySnowfall/SurrealDB-ORM/) uses a custom SDK for advanced features.
|
|
396
|
+
|
|
397
|
+
| Feature | ORM-lite (official SDK) | ORM (custom SDK) |
|
|
398
|
+
| ------------------------- | ----------------------- | ---------------- |
|
|
399
|
+
| CRUD & QuerySet | ✅ | ✅ |
|
|
400
|
+
| Aggregations & GROUP BY | ✅ | ✅ |
|
|
401
|
+
| Model Signals | ✅ | ✅ |
|
|
402
|
+
| Bulk Operations | ✅ | ✅ |
|
|
403
|
+
| Q Objects (OR/AND/NOT) | ✅ | ✅ |
|
|
404
|
+
| Parameterized Filters | ✅ | ✅ |
|
|
405
|
+
| Relations & Graph | v0.6.0 | ✅ |
|
|
406
|
+
| FETCH clause | v0.6.0 | ✅ |
|
|
407
|
+
| Transactions (tx=) | v0.7.0 | ✅ |
|
|
408
|
+
| SurrealFunc & Computed | v0.8.0 | ✅ |
|
|
409
|
+
| Field Aliases | v0.9.0 | ✅ |
|
|
410
|
+
| Retry, Logging, Metrics | v0.10.0 | ✅ |
|
|
411
|
+
| Live Models / CDC | ❌ | ✅ |
|
|
412
|
+
| Vector / Full-Text Search | ❌ | ✅ |
|
|
413
|
+
| Hybrid Search (RRF) | ❌ | ✅ |
|
|
414
|
+
| Migrations & CLI | ❌ | ✅ |
|
|
415
|
+
| JWT Authentication | ❌ | ✅ |
|
|
416
|
+
| Schema Introspection | ❌ | ✅ |
|
|
417
|
+
| Connection Pool | ❌ | ✅ |
|
|
418
|
+
| CBOR Protocol | ❌ | ✅ |
|
|
419
|
+
| Subqueries & Query Cache | ❌ | ✅ |
|
|
420
|
+
| Geospatial Fields | ❌ | ✅ |
|
|
421
|
+
| DEFINE EVENT | ❌ | ✅ |
|
|
422
|
+
| Test Fixtures & Factories | ❌ | ✅ |
|
|
423
|
+
| Atomic Array Operations | ❌ | ✅ |
|
|
424
|
+
|
|
425
|
+
**Choose ORM-lite** if you want the official SDK, minimal dependencies, and core ORM features.
|
|
426
|
+
|
|
427
|
+
**Choose ORM** if you need live queries, migrations, authentication, vector search, or advanced features.
|
|
428
|
+
|
|
429
|
+
- **SurrealDB-ORM GitHub**: [github.com/EulogySnowfall/SurrealDB-ORM](https://github.com/EulogySnowfall/SurrealDB-ORM/)
|
|
430
|
+
- **SurrealDB-ORM PyPI**: [surrealdb-orm](https://pypi.org/project/surrealdb-orm/)
|
|
339
431
|
|
|
340
432
|
---
|
|
341
433
|
|
|
@@ -356,3 +448,4 @@ GitHub: [@EulogySnowfall](https://github.com/EulogySnowfall)
|
|
|
356
448
|
|
|
357
449
|
- [SurrealDB](https://surrealdb.com/) - The database
|
|
358
450
|
- [surrealdb.py](https://github.com/surrealdb/surrealdb.py) - Official Python SDK
|
|
451
|
+
- [SurrealDB-ORM](https://github.com/EulogySnowfall/SurrealDB-ORM/) - Full-featured ORM with custom SDK
|
|
@@ -104,9 +104,9 @@ users = await User.objects().filter(
|
|
|
104
104
|
name__startswith="A"
|
|
105
105
|
).exec()
|
|
106
106
|
|
|
107
|
-
# Ordering
|
|
107
|
+
# Ordering (with -field shorthand for DESC)
|
|
108
108
|
users = await User.objects().order_by("name").exec()
|
|
109
|
-
users = await User.objects().order_by("age",
|
|
109
|
+
users = await User.objects().order_by("-age", "name").exec()
|
|
110
110
|
|
|
111
111
|
# Pagination
|
|
112
112
|
users = await User.objects().limit(10).offset(20).exec()
|
|
@@ -131,30 +131,76 @@ results = await User.objects().query(
|
|
|
131
131
|
|
|
132
132
|
## Features
|
|
133
133
|
|
|
134
|
-
| Feature
|
|
135
|
-
|
|
|
136
|
-
| Async/await support
|
|
137
|
-
| Pydantic validation
|
|
138
|
-
| CRUD operations
|
|
139
|
-
| QuerySet with filters
|
|
140
|
-
| Django-style lookups
|
|
141
|
-
| Custom primary keys
|
|
142
|
-
| HTTP connections
|
|
143
|
-
| WebSocket connections
|
|
144
|
-
| Aggregations
|
|
145
|
-
| GROUP BY
|
|
146
|
-
| Model Signals
|
|
134
|
+
| Feature | Status |
|
|
135
|
+
| ---------------------- | ------ |
|
|
136
|
+
| Async/await support | ✅ |
|
|
137
|
+
| Pydantic validation | ✅ |
|
|
138
|
+
| CRUD operations | ✅ |
|
|
139
|
+
| QuerySet with filters | ✅ |
|
|
140
|
+
| Django-style lookups | ✅ |
|
|
141
|
+
| Custom primary keys | ✅ |
|
|
142
|
+
| HTTP connections | ✅ |
|
|
143
|
+
| WebSocket connections | ✅ |
|
|
144
|
+
| Aggregations | ✅ |
|
|
145
|
+
| GROUP BY | ✅ |
|
|
146
|
+
| Model Signals | ✅ |
|
|
147
|
+
| Raw SurrealQL queries | ✅ |
|
|
148
|
+
| Q Objects (OR/AND/NOT) | ✅ |
|
|
149
|
+
| Parameterized filters | ✅ |
|
|
150
|
+
| Bulk operations | ✅ |
|
|
151
|
+
| `-field` ordering | ✅ |
|
|
147
152
|
|
|
148
153
|
### Supported Filter Lookups
|
|
149
154
|
|
|
150
155
|
- `exact` (default)
|
|
151
156
|
- `gt`, `gte`, `lt`, `lte`
|
|
152
|
-
- `in`
|
|
153
|
-
- `contains`, `icontains`
|
|
157
|
+
- `in`, `not_in`
|
|
158
|
+
- `contains`, `icontains`, `not_contains`
|
|
159
|
+
- `containsall`, `containsany`
|
|
154
160
|
- `startswith`, `istartswith`
|
|
155
161
|
- `endswith`, `iendswith`
|
|
162
|
+
- `like`, `ilike`
|
|
163
|
+
- `match`, `regex`, `iregex`
|
|
164
|
+
- `isnull`
|
|
156
165
|
|
|
157
|
-
### 5.
|
|
166
|
+
### 5. Q Objects (Complex Queries)
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from surreal_orm_lite import Q
|
|
170
|
+
|
|
171
|
+
# OR queries
|
|
172
|
+
users = await User.objects().filter(Q(name="Alice") | Q(name="Bob")).exec()
|
|
173
|
+
|
|
174
|
+
# NOT queries
|
|
175
|
+
active = await User.objects().filter(~Q(status="banned")).exec()
|
|
176
|
+
|
|
177
|
+
# Complex combinations
|
|
178
|
+
results = await User.objects().filter(
|
|
179
|
+
Q(age__gte=18) & (Q(role="admin") | Q(role="mod"))
|
|
180
|
+
).exec()
|
|
181
|
+
|
|
182
|
+
# Mix Q objects with keyword filters
|
|
183
|
+
results = await User.objects().filter(
|
|
184
|
+
Q(role="admin") | Q(role="mod"),
|
|
185
|
+
age__gte=25
|
|
186
|
+
).exec()
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 6. Bulk Operations
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
# Bulk create
|
|
193
|
+
users = [User(name="Alice", age=30), User(name="Bob", age=25)]
|
|
194
|
+
created = await User.objects().bulk_create(users)
|
|
195
|
+
|
|
196
|
+
# Bulk update (returns count of updated records)
|
|
197
|
+
count = await User.objects().filter(status="pending").bulk_update(status="active")
|
|
198
|
+
|
|
199
|
+
# Bulk delete (returns count of deleted records)
|
|
200
|
+
count = await User.objects().filter(status="inactive").bulk_delete()
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### 7. Aggregations
|
|
158
204
|
|
|
159
205
|
```python
|
|
160
206
|
from surreal_orm_lite import Count, Sum, Avg, Min, Max
|
|
@@ -180,7 +226,7 @@ results = await User.raw_query(
|
|
|
180
226
|
)
|
|
181
227
|
```
|
|
182
228
|
|
|
183
|
-
###
|
|
229
|
+
### 8. Model Signals
|
|
184
230
|
|
|
185
231
|
```python
|
|
186
232
|
from surreal_orm_lite import pre_save, post_save, pre_delete, post_delete
|
|
@@ -200,17 +246,17 @@ async def on_user_deleting(sender, instance, **kwargs):
|
|
|
200
246
|
|
|
201
247
|
**Available signals:**
|
|
202
248
|
|
|
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()` |
|
|
249
|
+
| Signal | When | Extra kwargs |
|
|
250
|
+
| --------------- | --------------------------- | --------------- |
|
|
251
|
+
| `pre_save` | Before `save()` | |
|
|
252
|
+
| `post_save` | After `save()` | `created` |
|
|
253
|
+
| `pre_update` | Before `update()`/`merge()` | `update_fields` |
|
|
254
|
+
| `post_update` | After `update()`/`merge()` | `update_fields` |
|
|
255
|
+
| `pre_delete` | Before `delete()` | |
|
|
256
|
+
| `post_delete` | After `delete()` | |
|
|
257
|
+
| `around_save` | Wraps `save()` | |
|
|
258
|
+
| `around_update` | Wraps `update()`/`merge()` | `update_fields` |
|
|
259
|
+
| `around_delete` | Wraps `delete()` | |
|
|
214
260
|
|
|
215
261
|
**Around signals** use async generators to wrap operations:
|
|
216
262
|
|
|
@@ -276,16 +322,62 @@ Contributions are welcome! Please:
|
|
|
276
322
|
|
|
277
323
|
---
|
|
278
324
|
|
|
279
|
-
##
|
|
325
|
+
## Roadmap
|
|
280
326
|
|
|
281
|
-
|
|
327
|
+
| Version | Theme | Status |
|
|
328
|
+
| ------- | ----------------------------- | ----------- |
|
|
329
|
+
| v0.2.x | Core ORM (CRUD, QuerySet) | ✅ Released |
|
|
330
|
+
| v0.3.0 | Aggregations & Utilities | ✅ Released |
|
|
331
|
+
| v0.4.0 | Model Signals | ✅ Released |
|
|
332
|
+
| v0.5.0 | Bulk Operations & Q Objects | ✅ Released |
|
|
333
|
+
| v0.6.0 | Relations & Graph | 📋 Next |
|
|
334
|
+
| v0.7.0 | Transactions ORM | 📋 Planned |
|
|
335
|
+
| v0.8.0 | SurrealFunc & Computed Fields | 📋 Planned |
|
|
336
|
+
| v0.9.0 | Field Aliases & DX | 📋 Planned |
|
|
337
|
+
| v1.0.0 | Production Ready | 📋 Planned |
|
|
282
338
|
|
|
283
|
-
|
|
339
|
+
See [docs/ROADMAP.md](docs/ROADMAP.md) for full details.
|
|
284
340
|
|
|
285
|
-
|
|
286
|
-
- **PyPI**: [surrealdb-orm](https://pypi.org/project/surrealdb-orm/)
|
|
341
|
+
---
|
|
287
342
|
|
|
288
|
-
|
|
343
|
+
## SurrealDB-ORM-lite vs SurrealDB-ORM
|
|
344
|
+
|
|
345
|
+
This project prioritizes **stability and compatibility** with the official SurrealDB Python SDK. The full [SurrealDB-ORM](https://github.com/EulogySnowfall/SurrealDB-ORM/) uses a custom SDK for advanced features.
|
|
346
|
+
|
|
347
|
+
| Feature | ORM-lite (official SDK) | ORM (custom SDK) |
|
|
348
|
+
| ------------------------- | ----------------------- | ---------------- |
|
|
349
|
+
| CRUD & QuerySet | ✅ | ✅ |
|
|
350
|
+
| Aggregations & GROUP BY | ✅ | ✅ |
|
|
351
|
+
| Model Signals | ✅ | ✅ |
|
|
352
|
+
| Bulk Operations | ✅ | ✅ |
|
|
353
|
+
| Q Objects (OR/AND/NOT) | ✅ | ✅ |
|
|
354
|
+
| Parameterized Filters | ✅ | ✅ |
|
|
355
|
+
| Relations & Graph | v0.6.0 | ✅ |
|
|
356
|
+
| FETCH clause | v0.6.0 | ✅ |
|
|
357
|
+
| Transactions (tx=) | v0.7.0 | ✅ |
|
|
358
|
+
| SurrealFunc & Computed | v0.8.0 | ✅ |
|
|
359
|
+
| Field Aliases | v0.9.0 | ✅ |
|
|
360
|
+
| Retry, Logging, Metrics | v0.10.0 | ✅ |
|
|
361
|
+
| Live Models / CDC | ❌ | ✅ |
|
|
362
|
+
| Vector / Full-Text Search | ❌ | ✅ |
|
|
363
|
+
| Hybrid Search (RRF) | ❌ | ✅ |
|
|
364
|
+
| Migrations & CLI | ❌ | ✅ |
|
|
365
|
+
| JWT Authentication | ❌ | ✅ |
|
|
366
|
+
| Schema Introspection | ❌ | ✅ |
|
|
367
|
+
| Connection Pool | ❌ | ✅ |
|
|
368
|
+
| CBOR Protocol | ❌ | ✅ |
|
|
369
|
+
| Subqueries & Query Cache | ❌ | ✅ |
|
|
370
|
+
| Geospatial Fields | ❌ | ✅ |
|
|
371
|
+
| DEFINE EVENT | ❌ | ✅ |
|
|
372
|
+
| Test Fixtures & Factories | ❌ | ✅ |
|
|
373
|
+
| Atomic Array Operations | ❌ | ✅ |
|
|
374
|
+
|
|
375
|
+
**Choose ORM-lite** if you want the official SDK, minimal dependencies, and core ORM features.
|
|
376
|
+
|
|
377
|
+
**Choose ORM** if you need live queries, migrations, authentication, vector search, or advanced features.
|
|
378
|
+
|
|
379
|
+
- **SurrealDB-ORM GitHub**: [github.com/EulogySnowfall/SurrealDB-ORM](https://github.com/EulogySnowfall/SurrealDB-ORM/)
|
|
380
|
+
- **SurrealDB-ORM PyPI**: [surrealdb-orm](https://pypi.org/project/surrealdb-orm/)
|
|
289
381
|
|
|
290
382
|
---
|
|
291
383
|
|
|
@@ -306,3 +398,4 @@ GitHub: [@EulogySnowfall](https://github.com/EulogySnowfall)
|
|
|
306
398
|
|
|
307
399
|
- [SurrealDB](https://surrealdb.com/) - The database
|
|
308
400
|
- [surrealdb.py](https://github.com/surrealdb/surrealdb.py) - Official Python SDK
|
|
401
|
+
- [SurrealDB-ORM](https://github.com/EulogySnowfall/SurrealDB-ORM/) - Full-featured ORM with custom SDK
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "surreal-orm-lite"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "Lightweight Django-style ORM for SurrealDB using the official Python SDK. Async support with Pydantic validation."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -10,7 +10,7 @@ authors = [
|
|
|
10
10
|
]
|
|
11
11
|
keywords = ["surrealdb", "orm", "database", "async", "pydantic"]
|
|
12
12
|
classifiers = [
|
|
13
|
-
"Development Status ::
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
14
|
"Programming Language :: Python :: 3",
|
|
15
15
|
"Programming Language :: Python :: 3.11",
|
|
16
16
|
"Programming Language :: Python :: 3.12",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.5.0"
|
|
2
2
|
|
|
3
3
|
from .aggregations import Aggregation, Avg, Count, Max, Min, Sum
|
|
4
4
|
from .connection_manager import SurrealDBConnectionManager
|
|
@@ -11,6 +11,7 @@ from .exceptions import (
|
|
|
11
11
|
SurrealORMError,
|
|
12
12
|
)
|
|
13
13
|
from .model_base import BaseSurrealModel, SurrealConfigDict
|
|
14
|
+
from .q import Q
|
|
14
15
|
from .query_set import QuerySet
|
|
15
16
|
from .signals import (
|
|
16
17
|
AroundSignal,
|
|
@@ -35,6 +36,7 @@ __all__ = [
|
|
|
35
36
|
"SurrealConfigDict",
|
|
36
37
|
# QuerySet
|
|
37
38
|
"QuerySet",
|
|
39
|
+
"Q",
|
|
38
40
|
"OrderBy",
|
|
39
41
|
# Aggregations
|
|
40
42
|
"Aggregation",
|
|
@@ -5,10 +5,14 @@ LOOKUP_OPERATORS = {
|
|
|
5
5
|
"lt": "<",
|
|
6
6
|
"lte": "<=",
|
|
7
7
|
"in": "IN",
|
|
8
|
+
"not_in": "NOT IN",
|
|
8
9
|
"like": "LIKE",
|
|
9
10
|
"ilike": "ILIKE",
|
|
10
11
|
"contains": "CONTAINS",
|
|
11
12
|
"icontains": "CONTAINS",
|
|
13
|
+
"not_contains": "CONTAINSNOT",
|
|
14
|
+
"containsall": "CONTAINSALL",
|
|
15
|
+
"containsany": "CONTAINSANY",
|
|
12
16
|
"startswith": "STARTSWITH",
|
|
13
17
|
"istartswith": "STARTSWITH",
|
|
14
18
|
"endswith": "ENDSWITH",
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .utils import build_filter_condition, parse_lookup
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Q:
|
|
9
|
+
"""
|
|
10
|
+
Composable query expression for building complex filters with OR, AND, and NOT operators.
|
|
11
|
+
|
|
12
|
+
Q objects can be combined using ``|`` (OR), ``&`` (AND), and ``~`` (NOT) operators
|
|
13
|
+
to create complex query filters that go beyond simple AND-joined kwargs.
|
|
14
|
+
|
|
15
|
+
Example::
|
|
16
|
+
|
|
17
|
+
# OR query
|
|
18
|
+
Q(name="alice") | Q(email__contains="alice")
|
|
19
|
+
|
|
20
|
+
# NOT query
|
|
21
|
+
~Q(status="banned")
|
|
22
|
+
|
|
23
|
+
# Complex combination
|
|
24
|
+
Q(age__gte=18) & Q(age__lte=65) & (Q(role="admin") | Q(role="mod"))
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
AND = "AND"
|
|
28
|
+
OR = "OR"
|
|
29
|
+
|
|
30
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
31
|
+
self.filters: dict[str, Any] = kwargs
|
|
32
|
+
self.children: list[Q] = []
|
|
33
|
+
self.connector: str = self.AND
|
|
34
|
+
self.negated: bool = False
|
|
35
|
+
|
|
36
|
+
def __or__(self, other: Q) -> Q:
|
|
37
|
+
if not isinstance(other, Q):
|
|
38
|
+
return NotImplemented
|
|
39
|
+
node = Q()
|
|
40
|
+
node.children = [self, other]
|
|
41
|
+
node.connector = self.OR
|
|
42
|
+
return node
|
|
43
|
+
|
|
44
|
+
def __and__(self, other: Q) -> Q:
|
|
45
|
+
if not isinstance(other, Q):
|
|
46
|
+
return NotImplemented
|
|
47
|
+
node = Q()
|
|
48
|
+
node.children = [self, other]
|
|
49
|
+
node.connector = self.AND
|
|
50
|
+
return node
|
|
51
|
+
|
|
52
|
+
def __invert__(self) -> Q:
|
|
53
|
+
clone = Q(**self.filters)
|
|
54
|
+
clone.children = list(self.children)
|
|
55
|
+
clone.connector = self.connector
|
|
56
|
+
clone.negated = not self.negated
|
|
57
|
+
return clone
|
|
58
|
+
|
|
59
|
+
def __repr__(self) -> str:
|
|
60
|
+
parts = []
|
|
61
|
+
if self.filters:
|
|
62
|
+
parts.append(f"filters={self.filters}")
|
|
63
|
+
if self.children:
|
|
64
|
+
parts.append(f"children={self.children}")
|
|
65
|
+
if self.connector != self.AND:
|
|
66
|
+
parts.append(f"connector={self.connector}")
|
|
67
|
+
if self.negated:
|
|
68
|
+
parts.append("negated=True")
|
|
69
|
+
return f"Q({', '.join(parts)})"
|
|
70
|
+
|
|
71
|
+
def to_sql(self, counter: int = 0) -> tuple[str, dict[str, Any], int]:
|
|
72
|
+
"""
|
|
73
|
+
Generate a parameterized SQL WHERE fragment.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
counter: The starting variable counter for unique naming.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
A tuple of (sql_fragment, variables_dict, next_counter).
|
|
80
|
+
"""
|
|
81
|
+
all_parts: list[str] = []
|
|
82
|
+
all_variables: dict[str, Any] = {}
|
|
83
|
+
|
|
84
|
+
# Own filters (always AND-joined within a single Q)
|
|
85
|
+
if self.filters:
|
|
86
|
+
filter_parts: list[str] = []
|
|
87
|
+
for key, value in self.filters.items():
|
|
88
|
+
field, lookup = parse_lookup(key)
|
|
89
|
+
sql, vars_, counter = build_filter_condition(field, lookup, value, counter)
|
|
90
|
+
filter_parts.append(sql)
|
|
91
|
+
all_variables.update(vars_)
|
|
92
|
+
if len(filter_parts) == 1:
|
|
93
|
+
all_parts.append(filter_parts[0])
|
|
94
|
+
else:
|
|
95
|
+
all_parts.append(f"({' AND '.join(filter_parts)})")
|
|
96
|
+
|
|
97
|
+
# Child Q objects
|
|
98
|
+
for child in self.children:
|
|
99
|
+
sql, vars_, counter = child.to_sql(counter)
|
|
100
|
+
if sql:
|
|
101
|
+
all_parts.append(sql)
|
|
102
|
+
all_variables.update(vars_)
|
|
103
|
+
|
|
104
|
+
if not all_parts:
|
|
105
|
+
return "", {}, counter
|
|
106
|
+
|
|
107
|
+
result = all_parts[0] if len(all_parts) == 1 else f"({' {conn} '.join(all_parts)})".format(conn=self.connector)
|
|
108
|
+
|
|
109
|
+
if self.negated:
|
|
110
|
+
result = f"NOT ({result})"
|
|
111
|
+
|
|
112
|
+
return result, all_variables, counter
|