surrealdb-orm 0.5.2__py3-none-any.whl → 0.5.3__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.
- surreal_orm/model_base.py +76 -17
- surreal_sdk/connection/base.py +17 -0
- surreal_sdk/transaction.py +20 -0
- {surrealdb_orm-0.5.2.dist-info → surrealdb_orm-0.5.3.dist-info}/METADATA +9 -1
- {surrealdb_orm-0.5.2.dist-info → surrealdb_orm-0.5.3.dist-info}/RECORD +8 -8
- {surrealdb_orm-0.5.2.dist-info → surrealdb_orm-0.5.3.dist-info}/WHEEL +0 -0
- {surrealdb_orm-0.5.2.dist-info → surrealdb_orm-0.5.3.dist-info}/entry_points.txt +0 -0
- {surrealdb_orm-0.5.2.dist-info → surrealdb_orm-0.5.3.dist-info}/licenses/LICENSE +0 -0
surreal_orm/model_base.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any, Literal, Self,
|
|
1
|
+
from typing import Any, Literal, Self, TYPE_CHECKING
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, ConfigDict, model_validator
|
|
4
4
|
|
|
@@ -71,6 +71,10 @@ class SurrealConfigDict(ConfigDict):
|
|
|
71
71
|
password_field: Field containing password (USER type, default: "password")
|
|
72
72
|
token_duration: JWT token duration (USER type, default: "15m")
|
|
73
73
|
session_duration: Session duration (USER type, default: "12h")
|
|
74
|
+
server_fields: List of field names that are server-generated and should
|
|
75
|
+
be excluded from save/update operations (e.g., ["created_at", "updated_at"]).
|
|
76
|
+
These fields are populated by SurrealDB's VALUE clause and should not be
|
|
77
|
+
sent back during updates.
|
|
74
78
|
"""
|
|
75
79
|
|
|
76
80
|
primary_key: str | None
|
|
@@ -83,6 +87,7 @@ class SurrealConfigDict(ConfigDict):
|
|
|
83
87
|
password_field: str | None
|
|
84
88
|
token_duration: str | None
|
|
85
89
|
session_duration: str | None
|
|
90
|
+
server_fields: list[str] | None
|
|
86
91
|
|
|
87
92
|
|
|
88
93
|
class BaseSurrealModel(BaseModel):
|
|
@@ -236,6 +241,23 @@ class BaseSurrealModel(BaseModel):
|
|
|
236
241
|
|
|
237
242
|
return None
|
|
238
243
|
|
|
244
|
+
@classmethod
|
|
245
|
+
def get_server_fields(cls) -> set[str]:
|
|
246
|
+
"""
|
|
247
|
+
Get the list of server-generated field names.
|
|
248
|
+
|
|
249
|
+
Server fields are populated by SurrealDB (e.g., via VALUE time::now())
|
|
250
|
+
and should be excluded from save/update operations.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Set of field names to exclude from save/update operations.
|
|
254
|
+
"""
|
|
255
|
+
if hasattr(cls, "model_config"):
|
|
256
|
+
server_fields = cls.model_config.get("server_fields", None)
|
|
257
|
+
if isinstance(server_fields, list):
|
|
258
|
+
return set(server_fields)
|
|
259
|
+
return set()
|
|
260
|
+
|
|
239
261
|
def get_id(self) -> str | None:
|
|
240
262
|
"""
|
|
241
263
|
Get the ID of the model instance.
|
|
@@ -276,6 +298,29 @@ class BaseSurrealModel(BaseModel):
|
|
|
276
298
|
data["id"] = _parse_record_id(data["id"])
|
|
277
299
|
return data
|
|
278
300
|
|
|
301
|
+
def _update_from_db(self, record: dict[str, Any]) -> None:
|
|
302
|
+
"""
|
|
303
|
+
Update instance fields from a database record without marking them as user-set.
|
|
304
|
+
|
|
305
|
+
This preserves the original __pydantic_fields_set__ so that exclude_unset=True
|
|
306
|
+
continues to work correctly on subsequent saves.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
record: Dictionary of field values from the database.
|
|
310
|
+
"""
|
|
311
|
+
# Store original fields_set to preserve user-set tracking
|
|
312
|
+
original_fields_set = self.__pydantic_fields_set__.copy()
|
|
313
|
+
|
|
314
|
+
for key, value in record.items():
|
|
315
|
+
if key == "id":
|
|
316
|
+
value = _parse_record_id(value)
|
|
317
|
+
if hasattr(self, key):
|
|
318
|
+
setattr(self, key, value)
|
|
319
|
+
|
|
320
|
+
# Restore original fields_set - only user-set fields should be marked
|
|
321
|
+
# DB-loaded fields should not be considered as "set" for exclude_unset
|
|
322
|
+
object.__setattr__(self, "__pydantic_fields_set__", original_fields_set)
|
|
323
|
+
|
|
279
324
|
async def refresh(self) -> None:
|
|
280
325
|
"""
|
|
281
326
|
Refresh the model instance from the database.
|
|
@@ -294,12 +339,8 @@ class BaseSurrealModel(BaseModel):
|
|
|
294
339
|
if record is None:
|
|
295
340
|
raise SurrealDbError("Can't refresh data, no record found.") # pragma: no cover
|
|
296
341
|
|
|
297
|
-
# Update instance fields
|
|
298
|
-
|
|
299
|
-
if key == "id":
|
|
300
|
-
value = _parse_record_id(value)
|
|
301
|
-
if hasattr(self, key):
|
|
302
|
-
setattr(self, key, value)
|
|
342
|
+
# Update instance fields without marking them as user-set
|
|
343
|
+
self._update_from_db(record)
|
|
303
344
|
return None
|
|
304
345
|
|
|
305
346
|
async def save(self, tx: "BaseTransaction | None" = None) -> Self:
|
|
@@ -318,15 +359,19 @@ class BaseSurrealModel(BaseModel):
|
|
|
318
359
|
async with SurrealDBConnectionManager.transaction() as tx:
|
|
319
360
|
await user.save(tx=tx)
|
|
320
361
|
"""
|
|
362
|
+
# Build exclude set: always exclude 'id' and any server-generated fields
|
|
363
|
+
exclude_fields = {"id"} | self.get_server_fields()
|
|
364
|
+
|
|
321
365
|
if tx is not None:
|
|
322
366
|
# Use transaction
|
|
323
|
-
data = self.model_dump(exclude=
|
|
367
|
+
data = self.model_dump(exclude=exclude_fields, exclude_unset=True)
|
|
324
368
|
id = self.get_id()
|
|
325
369
|
table = self.get_table_name()
|
|
326
370
|
|
|
327
371
|
if id is not None:
|
|
372
|
+
# Use upsert for idempotent save (create or update)
|
|
328
373
|
thing = f"{table}:{id}"
|
|
329
|
-
await tx.
|
|
374
|
+
await tx.upsert(thing, data)
|
|
330
375
|
return self
|
|
331
376
|
|
|
332
377
|
# Auto-generate ID - create without specific ID
|
|
@@ -335,13 +380,14 @@ class BaseSurrealModel(BaseModel):
|
|
|
335
380
|
|
|
336
381
|
# Original behavior without transaction
|
|
337
382
|
client = await SurrealDBConnectionManager.get_client()
|
|
338
|
-
data = self.model_dump(exclude=
|
|
383
|
+
data = self.model_dump(exclude=exclude_fields, exclude_unset=True)
|
|
339
384
|
id = self.get_id()
|
|
340
385
|
table = self.get_table_name()
|
|
341
386
|
|
|
342
387
|
if id is not None:
|
|
388
|
+
# Use upsert for idempotent save (create or update)
|
|
343
389
|
thing = f"{table}:{id}"
|
|
344
|
-
await client.
|
|
390
|
+
await client.upsert(thing, data)
|
|
345
391
|
return self
|
|
346
392
|
|
|
347
393
|
# Auto-generate the ID
|
|
@@ -351,9 +397,12 @@ class BaseSurrealModel(BaseModel):
|
|
|
351
397
|
if not result.exists:
|
|
352
398
|
raise SurrealDbError("Can't save data, no record returned.") # pragma: no cover
|
|
353
399
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
400
|
+
# Update self's attributes from the database response
|
|
401
|
+
# This includes the auto-generated ID and any server-side fields
|
|
402
|
+
# Use _update_from_db to avoid marking DB fields as user-set
|
|
403
|
+
record = result.record
|
|
404
|
+
if isinstance(record, dict):
|
|
405
|
+
self._update_from_db(record)
|
|
357
406
|
return self
|
|
358
407
|
|
|
359
408
|
raise SurrealDbError("Can't save data, no record returned.") # pragma: no cover
|
|
@@ -365,7 +414,9 @@ class BaseSurrealModel(BaseModel):
|
|
|
365
414
|
Args:
|
|
366
415
|
tx: Optional transaction to use for this operation.
|
|
367
416
|
"""
|
|
368
|
-
|
|
417
|
+
# Build exclude set: always exclude 'id' and any server-generated fields
|
|
418
|
+
exclude_fields = {"id"} | self.get_server_fields()
|
|
419
|
+
data = self.model_dump(exclude=exclude_fields, exclude_unset=True)
|
|
369
420
|
id = self.get_id()
|
|
370
421
|
|
|
371
422
|
if id is None:
|
|
@@ -388,13 +439,16 @@ class BaseSurrealModel(BaseModel):
|
|
|
388
439
|
"""
|
|
389
440
|
return f"{cls.__name__}:{item}"
|
|
390
441
|
|
|
391
|
-
async def merge(self, tx: "BaseTransaction | None" = None, **data: Any) ->
|
|
442
|
+
async def merge(self, tx: "BaseTransaction | None" = None, **data: Any) -> Self:
|
|
392
443
|
"""
|
|
393
444
|
Merge (partial update) the model instance in the database.
|
|
394
445
|
|
|
395
446
|
Args:
|
|
396
447
|
tx: Optional transaction to use for this operation.
|
|
397
448
|
**data: Fields to update.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
Self: The updated model instance.
|
|
398
452
|
"""
|
|
399
453
|
data_set = {key: value for key, value in data.items()}
|
|
400
454
|
|
|
@@ -406,11 +460,16 @@ class BaseSurrealModel(BaseModel):
|
|
|
406
460
|
|
|
407
461
|
if tx is not None:
|
|
408
462
|
await tx.merge(thing, data_set)
|
|
409
|
-
|
|
463
|
+
# Update local instance with merged data
|
|
464
|
+
for key, value in data_set.items():
|
|
465
|
+
if hasattr(self, key):
|
|
466
|
+
setattr(self, key, value)
|
|
467
|
+
return self
|
|
410
468
|
|
|
411
469
|
client = await SurrealDBConnectionManager.get_client()
|
|
412
470
|
await client.merge(thing, data_set)
|
|
413
471
|
await self.refresh()
|
|
472
|
+
return self
|
|
414
473
|
|
|
415
474
|
async def delete(self, tx: "BaseTransaction | None" = None) -> None:
|
|
416
475
|
"""
|
surreal_sdk/connection/base.py
CHANGED
|
@@ -315,6 +315,23 @@ class BaseSurrealConnection(ABC):
|
|
|
315
315
|
result = await self.rpc("update", [thing, data])
|
|
316
316
|
return RecordsResponse.from_rpc_result(result)
|
|
317
317
|
|
|
318
|
+
async def upsert(self, thing: str, data: dict[str, Any]) -> RecordsResponse:
|
|
319
|
+
"""
|
|
320
|
+
Upsert record(s) - create if not exists, update if exists.
|
|
321
|
+
|
|
322
|
+
This is the recommended method for save operations when you have
|
|
323
|
+
a specific ID and want idempotent behavior.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
thing: Table name or record ID
|
|
327
|
+
data: Record data
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
RecordsResponse containing upserted record(s)
|
|
331
|
+
"""
|
|
332
|
+
result = await self.rpc("upsert", [thing, data])
|
|
333
|
+
return RecordsResponse.from_rpc_result(result)
|
|
334
|
+
|
|
318
335
|
async def merge(self, thing: str, data: dict[str, Any]) -> RecordsResponse:
|
|
319
336
|
"""
|
|
320
337
|
Merge data into record(s), updating only specified fields.
|
surreal_sdk/transaction.py
CHANGED
|
@@ -119,6 +119,11 @@ class BaseTransaction(ABC):
|
|
|
119
119
|
"""Update records within the transaction."""
|
|
120
120
|
...
|
|
121
121
|
|
|
122
|
+
@abstractmethod
|
|
123
|
+
async def upsert(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
124
|
+
"""Upsert records within the transaction (create or update)."""
|
|
125
|
+
...
|
|
126
|
+
|
|
122
127
|
@abstractmethod
|
|
123
128
|
async def merge(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
124
129
|
"""Merge data into records within the transaction."""
|
|
@@ -256,6 +261,15 @@ class HTTPTransaction(BaseTransaction):
|
|
|
256
261
|
self._queue_statement(sql, data)
|
|
257
262
|
return RecordsResponse(records=[], raw=[])
|
|
258
263
|
|
|
264
|
+
async def upsert(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
265
|
+
"""Queue an upsert operation (create or update)."""
|
|
266
|
+
from .types import RecordsResponse
|
|
267
|
+
|
|
268
|
+
fields = ", ".join(f"{k} = ${k}" for k in data.keys())
|
|
269
|
+
sql = f"UPSERT {thing} SET {fields};"
|
|
270
|
+
self._queue_statement(sql, data)
|
|
271
|
+
return RecordsResponse(records=[], raw=[])
|
|
272
|
+
|
|
259
273
|
async def merge(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
260
274
|
"""Queue a merge operation."""
|
|
261
275
|
from .types import RecordsResponse
|
|
@@ -361,6 +375,12 @@ class WebSocketTransaction(BaseTransaction):
|
|
|
361
375
|
raise TransactionError("Transaction not active")
|
|
362
376
|
return await self._connection.update(thing, data)
|
|
363
377
|
|
|
378
|
+
async def upsert(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
379
|
+
"""Execute upsert immediately within transaction (create or update)."""
|
|
380
|
+
if not self.is_active:
|
|
381
|
+
raise TransactionError("Transaction not active")
|
|
382
|
+
return await self._connection.upsert(thing, data)
|
|
383
|
+
|
|
364
384
|
async def merge(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
|
|
365
385
|
"""Execute merge immediately within transaction."""
|
|
366
386
|
if not self.is_active:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: surrealdb-orm
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: SurrealDB ORM as 'DJango style' for Python with async support. Works with pydantic validation.
|
|
5
5
|
Project-URL: Homepage, https://github.com/EulogySnowfall/SurrealDB-ORM
|
|
6
6
|
Project-URL: Documentation, https://github.com/EulogySnowfall/SurrealDB-ORM
|
|
@@ -68,6 +68,14 @@ Description-Content-Type: text/markdown
|
|
|
68
68
|
|
|
69
69
|
## What's New in 0.5.x
|
|
70
70
|
|
|
71
|
+
### v0.5.3 - ORM Improvements
|
|
72
|
+
|
|
73
|
+
- **Upsert save behavior** - `save()` now uses `upsert` for existing records (idempotent, Django-like)
|
|
74
|
+
- **`server_fields` config** - Exclude server-generated fields (created_at, updated_at) from saves
|
|
75
|
+
- **`merge()` returns self** - Now returns the updated model instance instead of None
|
|
76
|
+
- **`save()` updates self** - Updates original instance attributes instead of returning new object
|
|
77
|
+
- **NULL values fix** - `exclude_unset=True` now works correctly after loading from DB
|
|
78
|
+
|
|
71
79
|
### v0.5.2 - Bug Fixes & FieldType Improvements
|
|
72
80
|
|
|
73
81
|
- **FieldType enum** - Enhanced migration type system with `generic()` and `from_python_type()` methods
|
|
@@ -3,7 +3,7 @@ surreal_orm/aggregations.py,sha256=5ERMHMWQfaW76OrMNazMjyg7dbf9bJ3GX_8QWz6tfxY,3
|
|
|
3
3
|
surreal_orm/connection_manager.py,sha256=VJVfsuUXK5OEG0rtuiu3DKbyWxkkeFrCEtzjXFd6lAw,10496
|
|
4
4
|
surreal_orm/constants.py,sha256=CLavEca1M6cLJLqVl4l4KoE-cBrgVQNsuGxW9zGJBmg,429
|
|
5
5
|
surreal_orm/enum.py,sha256=kR-vzkHqnqy9YaYOvWTwAHdl2-WCzPcSEch-YTyJv1Y,158
|
|
6
|
-
surreal_orm/model_base.py,sha256=
|
|
6
|
+
surreal_orm/model_base.py,sha256=yXFsCskiCkmqpWtpNpYWy-MIYsrQvovd71jHMgzUNG4,25652
|
|
7
7
|
surreal_orm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
surreal_orm/query_set.py,sha256=3nmQ9A13g-1EJlRZsJQX8z7yFO3pjI_6MscFT3W4Lm0,38890
|
|
9
9
|
surreal_orm/relations.py,sha256=pbsbKz2jw1h9FdFQowidUkx8krYv-l3OreQEViasN5w,21137
|
|
@@ -32,10 +32,10 @@ surreal_sdk/exceptions.py,sha256=qiDA3xJ2VkY8HUhvmDmW8kkNxhZ9NJu-FCUKUzfBrbU,148
|
|
|
32
32
|
surreal_sdk/functions.py,sha256=eTJA6zWlivTEnKJgtR53MAygElhMB1o9p6_FPt8_ErI,20537
|
|
33
33
|
surreal_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
34
|
surreal_sdk/pyproject.toml,sha256=pbcDsXdadtxF2d4aQKlinbEYEIftYfGy-T_lZX_8sRw,1421
|
|
35
|
-
surreal_sdk/transaction.py,sha256=
|
|
35
|
+
surreal_sdk/transaction.py,sha256=AIvEJhGcqmaQ4WiyTCD_knhpGW7zffyd7eSYT32V3Vc,14287
|
|
36
36
|
surreal_sdk/types.py,sha256=y08l9x-ZYgPSAIFztzly474HiZ9slxYqZ0oAA7jJpOI,9841
|
|
37
37
|
surreal_sdk/connection/__init__.py,sha256=zTe2qPLHsl9Upy6NYUr04rr4aWP-D2vCMA2iM5eUcU0,363
|
|
38
|
-
surreal_sdk/connection/base.py,sha256=
|
|
38
|
+
surreal_sdk/connection/base.py,sha256=AN-Ab84Vr5TcI4Wpxm_Px63yDBvBo2vLFUdffgBMtLw,15769
|
|
39
39
|
surreal_sdk/connection/http.py,sha256=bUJ7bwEGKbufFo6NNggOBSimvJZZGmhoY9IR0Jd4YPw,12648
|
|
40
40
|
surreal_sdk/connection/pool.py,sha256=v9FZmpfwWn674iY--xdDtaiYimBgpv3UtGT4k6q0DHE,7849
|
|
41
41
|
surreal_sdk/connection/websocket.py,sha256=sTbYSAi9-C8W3T2gR1bm-XO3vtCirtdO95MYJoxH3z8,18238
|
|
@@ -45,8 +45,8 @@ surreal_sdk/streaming/__init__.py,sha256=TljF9HFN-XOshK_1smmTanF68hcdNsTgdlHtfFo
|
|
|
45
45
|
surreal_sdk/streaming/change_feed.py,sha256=lS6CGNinsMkzslf1y0aV0VgZ2gq01EDIEZrnvvYhv-E,8540
|
|
46
46
|
surreal_sdk/streaming/live_query.py,sha256=QwPXmRIsH0j3jgGQY9ULJU7MB0eTk8dnOVbPzy4SeSo,7561
|
|
47
47
|
surreal_sdk/streaming/live_select.py,sha256=mYg6NKMx3GPShg_hYz4SjjDwhG7baI3t_Gynv8YCBNE,12035
|
|
48
|
-
surrealdb_orm-0.5.
|
|
49
|
-
surrealdb_orm-0.5.
|
|
50
|
-
surrealdb_orm-0.5.
|
|
51
|
-
surrealdb_orm-0.5.
|
|
52
|
-
surrealdb_orm-0.5.
|
|
48
|
+
surrealdb_orm-0.5.3.dist-info/METADATA,sha256=qCZAOpLAFD3y7PY_o8HJ2fgLldLm4wxE8E_G2nANl6k,17102
|
|
49
|
+
surrealdb_orm-0.5.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
50
|
+
surrealdb_orm-0.5.3.dist-info/entry_points.txt,sha256=3zbp1VzVPSxFrxNuvKZAyt9U432QITdBNjp5QnMZ2o8,62
|
|
51
|
+
surrealdb_orm-0.5.3.dist-info/licenses/LICENSE,sha256=OYVJQ7TKsjHAgVndePmKcMWOZkxwJxOHiXOG1TAnCk4,1079
|
|
52
|
+
surrealdb_orm-0.5.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|