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.
Files changed (22) hide show
  1. {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/CHANGELOG.md +41 -0
  2. {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/PKG-INFO +131 -38
  3. {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/README.md +129 -36
  4. {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/pyproject.toml +2 -2
  5. {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/__init__.py +3 -1
  6. {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/constants.py +4 -0
  7. surreal_orm_lite-0.5.0/src/surreal_orm_lite/q.py +112 -0
  8. surreal_orm_lite-0.5.0/src/surreal_orm_lite/query_set.py +688 -0
  9. surreal_orm_lite-0.5.0/src/surreal_orm_lite/utils.py +114 -0
  10. surreal_orm_lite-0.4.0/src/surreal_orm_lite/query_set.py +0 -870
  11. surreal_orm_lite-0.4.0/src/surreal_orm_lite/utils.py +0 -53
  12. {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/.gitignore +0 -0
  13. {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/LICENSE +0 -0
  14. {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/Makefile +0 -0
  15. {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/__init__.py +0 -0
  16. {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/aggregations.py +0 -0
  17. {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/connection_manager.py +0 -0
  18. {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/enum.py +0 -0
  19. {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/exceptions.py +0 -0
  20. {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/model_base.py +0 -0
  21. {surreal_orm_lite-0.4.0 → surreal_orm_lite-0.5.0}/src/surreal_orm_lite/py.typed +0 -0
  22. {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.4.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 :: 4 - Beta
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", OrderBy.DESC).exec()
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 | 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 | ✅ |
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. Aggregations
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
- ### 6. Model Signals
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
- ## Advanced Features?
375
+ ## Roadmap
330
376
 
331
- This project prioritizes **stability and compatibility** with the official SurrealDB Python SDK. Due to current SDK limitations, some advanced features cannot be implemented here.
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
- For a feature-rich ORM with relations, transactions, and more, see:
389
+ See [docs/ROADMAP.md](docs/ROADMAP.md) for full details.
334
390
 
335
- - **GitHub**: [SurrealDB-ORM](https://github.com/EulogySnowfall/SurrealDB-ORM/)
336
- - **PyPI**: [surrealdb-orm](https://pypi.org/project/surrealdb-orm/)
391
+ ---
337
392
 
338
- When the official SDK supports additional features, they will be incorporated into this lite version.
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", OrderBy.DESC).exec()
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 | 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 | ✅ |
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. Aggregations
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
- ### 6. Model Signals
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
- ## Advanced Features?
325
+ ## Roadmap
280
326
 
281
- This project prioritizes **stability and compatibility** with the official SurrealDB Python SDK. Due to current SDK limitations, some advanced features cannot be implemented here.
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
- For a feature-rich ORM with relations, transactions, and more, see:
339
+ See [docs/ROADMAP.md](docs/ROADMAP.md) for full details.
284
340
 
285
- - **GitHub**: [SurrealDB-ORM](https://github.com/EulogySnowfall/SurrealDB-ORM/)
286
- - **PyPI**: [surrealdb-orm](https://pypi.org/project/surrealdb-orm/)
341
+ ---
287
342
 
288
- When the official SDK supports additional features, they will be incorporated into this lite version.
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.4.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 :: 4 - Beta",
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.4.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