surrealdb-orm 0.1.2__py3-none-any.whl → 0.1.4__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.
Potentially problematic release.
This version of surrealdb-orm might be problematic. Click here for more details.
- surreal_orm/__init__.py +12 -0
- surreal_orm/connection_manager.py +301 -0
- surreal_orm/constants.py +20 -0
- surreal_orm/enum.py +12 -0
- surreal_orm/model_base.py +202 -0
- surreal_orm/py.typed +0 -0
- surreal_orm/query_set.py +491 -0
- surreal_orm/utils.py +6 -0
- {surrealdb_orm-0.1.2.dist-info → surrealdb_orm-0.1.4.dist-info}/METADATA +2 -2
- surrealdb_orm-0.1.4.dist-info/RECORD +12 -0
- surrealdb_orm-0.1.2.dist-info/RECORD +0 -4
- {surrealdb_orm-0.1.2.dist-info → surrealdb_orm-0.1.4.dist-info}/WHEEL +0 -0
- {surrealdb_orm-0.1.2.dist-info → surrealdb_orm-0.1.4.dist-info}/licenses/LICENSE +0 -0
surreal_orm/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .model_base import BaseSurrealModel, SurrealConfigDict
|
|
2
|
+
from .connection_manager import SurrealDBConnectionManager
|
|
3
|
+
from .query_set import QuerySet
|
|
4
|
+
from .enum import OrderBy
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"SurrealDBConnectionManager",
|
|
8
|
+
"BaseSurrealModel",
|
|
9
|
+
"QuerySet",
|
|
10
|
+
"OrderBy",
|
|
11
|
+
"SurrealConfigDict",
|
|
12
|
+
]
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from surrealdb import AsyncSurrealDB
|
|
3
|
+
from surrealdb.errors import SurrealDbConnectionError
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SurrealDBConnectionManager:
|
|
10
|
+
__url: str | None = None
|
|
11
|
+
__user: str | None = None
|
|
12
|
+
__password: str | None = None
|
|
13
|
+
__namespace: str | None = None
|
|
14
|
+
__database: str | None = None
|
|
15
|
+
__client: AsyncSurrealDB | None = None
|
|
16
|
+
|
|
17
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
18
|
+
await SurrealDBConnectionManager.close_connection()
|
|
19
|
+
|
|
20
|
+
async def __aenter__(self) -> AsyncSurrealDB:
|
|
21
|
+
return await SurrealDBConnectionManager.get_client()
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def set_connection(cls, url: str, user: str, password: str, namespace: str, database: str) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Set the connection kwargs for the SurrealDB instance.
|
|
27
|
+
|
|
28
|
+
:param kwargs: The connection kwargs for the SurrealDB instance.
|
|
29
|
+
"""
|
|
30
|
+
cls.__url = url
|
|
31
|
+
cls.__user = user
|
|
32
|
+
cls.__password = password
|
|
33
|
+
cls.__namespace = namespace
|
|
34
|
+
cls.__database = database
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
async def unset_connection(cls) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Set the connection kwargs for the SurrealDB instance.
|
|
40
|
+
|
|
41
|
+
:param kwargs: The connection kwargs for the SurrealDB instance.
|
|
42
|
+
"""
|
|
43
|
+
cls.__url = None
|
|
44
|
+
cls.__user = None
|
|
45
|
+
cls.__password = None
|
|
46
|
+
cls.__namespace = None
|
|
47
|
+
cls.__database = None
|
|
48
|
+
await cls.close_connection()
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def is_connection_set(cls) -> bool:
|
|
52
|
+
"""
|
|
53
|
+
Check if the connection kwargs are set.
|
|
54
|
+
|
|
55
|
+
:return: True if the connection kwargs are set, False otherwise.
|
|
56
|
+
"""
|
|
57
|
+
return all([cls.__url, cls.__user, cls.__password, cls.__namespace, cls.__database])
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
async def get_client(cls) -> AsyncSurrealDB:
|
|
61
|
+
"""
|
|
62
|
+
Connect to the SurrealDB instance.
|
|
63
|
+
|
|
64
|
+
:return: The SurrealDB instance.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
if cls.__client is not None:
|
|
68
|
+
return cls.__client
|
|
69
|
+
|
|
70
|
+
if not cls.is_connection_set():
|
|
71
|
+
raise ValueError("Connection not been set.")
|
|
72
|
+
|
|
73
|
+
# Établir la connexion
|
|
74
|
+
try:
|
|
75
|
+
_client = AsyncSurrealDB(cls.get_connection_string())
|
|
76
|
+
await _client.connect() # type: ignore
|
|
77
|
+
await _client.use(cls.__namespace, cls.__database) # type: ignore
|
|
78
|
+
await _client.sign_in(cls.__user, cls.__password) # type: ignore
|
|
79
|
+
|
|
80
|
+
cls.__client = _client
|
|
81
|
+
return cls.__client
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.warning(f"Can't get connection: {e}")
|
|
84
|
+
if isinstance(cls.__client, AsyncSurrealDB): # pragma: no cover
|
|
85
|
+
await cls.__client.close()
|
|
86
|
+
cls.__client = None
|
|
87
|
+
raise SurrealDbConnectionError("Can't connect to the database.")
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
async def close_connection(cls) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Close the connection to the SurrealDB instance.
|
|
93
|
+
"""
|
|
94
|
+
# Fermer la connexion
|
|
95
|
+
|
|
96
|
+
if cls.__client is None:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
await cls.__client.close()
|
|
100
|
+
cls.__client = None
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
async def reconnect(cls) -> AsyncSurrealDB | None:
|
|
104
|
+
"""
|
|
105
|
+
Reconnect to the SurrealDB instance.
|
|
106
|
+
"""
|
|
107
|
+
# Fermer la connexion
|
|
108
|
+
await cls.close_connection()
|
|
109
|
+
# Établir la connexion
|
|
110
|
+
return await cls.get_client()
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
async def validate_connection(cls) -> bool:
|
|
114
|
+
"""
|
|
115
|
+
Validate the connection to the SurrealDB instance.
|
|
116
|
+
|
|
117
|
+
:return: True if the connection is valid, False otherwise.
|
|
118
|
+
"""
|
|
119
|
+
# Valider la connexion
|
|
120
|
+
try:
|
|
121
|
+
await cls.reconnect()
|
|
122
|
+
return True
|
|
123
|
+
except SurrealDbConnectionError:
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def get_connection_string(cls) -> str | None:
|
|
128
|
+
"""
|
|
129
|
+
Get the connection string for the SurrealDB instance.
|
|
130
|
+
|
|
131
|
+
:return: The connection string for the SurrealDB instance.
|
|
132
|
+
"""
|
|
133
|
+
return cls.__url
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def get_connection_kwargs(cls) -> dict[str, str | None]:
|
|
137
|
+
"""
|
|
138
|
+
Get the connection kwargs for the SurrealDB instance.
|
|
139
|
+
|
|
140
|
+
:return: The connection kwargs for the SurrealDB instance.
|
|
141
|
+
"""
|
|
142
|
+
return {
|
|
143
|
+
"url": cls.__url,
|
|
144
|
+
"user": cls.__user,
|
|
145
|
+
"namespace": cls.__namespace,
|
|
146
|
+
"database": cls.__database,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
async def set_url(cls, url: str, reconnect: bool = False) -> bool:
|
|
151
|
+
"""
|
|
152
|
+
Set the URL for the SurrealDB instance.
|
|
153
|
+
|
|
154
|
+
:param url: The URL of the SurrealDB instance.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
if not cls.is_connection_set():
|
|
158
|
+
raise ValueError("You can't change the URL when the others setting are not already set.")
|
|
159
|
+
|
|
160
|
+
cls.__url = url
|
|
161
|
+
|
|
162
|
+
if reconnect:
|
|
163
|
+
if not await cls.validate_connection(): # pragma: no cover
|
|
164
|
+
cls.__url = None
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
async def set_user(cls, user: str, reconnect: bool = False) -> bool:
|
|
171
|
+
"""
|
|
172
|
+
Set the username for authentication.
|
|
173
|
+
|
|
174
|
+
:param user: The username for authentication.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
if not cls.is_connection_set():
|
|
178
|
+
raise ValueError("You can't change the User when the others setting are not already set.")
|
|
179
|
+
|
|
180
|
+
cls.__user = user
|
|
181
|
+
|
|
182
|
+
if reconnect:
|
|
183
|
+
if not await cls.validate_connection(): # pragma: no cover
|
|
184
|
+
cls.__user = None
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
async def set_password(cls, password: str, reconnect: bool = False) -> bool:
|
|
191
|
+
"""
|
|
192
|
+
Set the password for authentication.
|
|
193
|
+
|
|
194
|
+
:param password: The password for authentication.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
if not cls.is_connection_set():
|
|
198
|
+
raise ValueError("You can't change the password when the others setting are not already set.")
|
|
199
|
+
|
|
200
|
+
cls.__password = password
|
|
201
|
+
|
|
202
|
+
if reconnect:
|
|
203
|
+
if not await cls.validate_connection(): # pragma: no cover
|
|
204
|
+
cls.__password = None
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
async def set_namespace(cls, namespace: str, reconnect: bool = False) -> bool:
|
|
211
|
+
"""
|
|
212
|
+
Set the namespace to use.
|
|
213
|
+
|
|
214
|
+
:param namespace: The namespace to use.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
if not cls.is_connection_set():
|
|
218
|
+
raise ValueError("You can't change the namespace when the others setting are not already set.")
|
|
219
|
+
|
|
220
|
+
cls.__namespace = namespace
|
|
221
|
+
|
|
222
|
+
if reconnect:
|
|
223
|
+
if not await cls.validate_connection(): # pragma: no cover
|
|
224
|
+
cls.__namespace = None
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
@classmethod
|
|
230
|
+
async def set_database(cls, database: str, reconnect: bool = False) -> bool:
|
|
231
|
+
"""
|
|
232
|
+
Set the database to use.
|
|
233
|
+
|
|
234
|
+
:param database: The database to use.
|
|
235
|
+
"""
|
|
236
|
+
if not cls.is_connection_set():
|
|
237
|
+
raise ValueError("You can't change the database when the others setting are not already set.")
|
|
238
|
+
|
|
239
|
+
cls.__database = database
|
|
240
|
+
|
|
241
|
+
if reconnect:
|
|
242
|
+
if not await cls.validate_connection(): # pragma: no cover
|
|
243
|
+
cls.__database = None
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
@classmethod
|
|
249
|
+
def get_url(cls) -> str | None:
|
|
250
|
+
"""
|
|
251
|
+
Get the URL of the SurrealDB instance.
|
|
252
|
+
|
|
253
|
+
:return: The URL of the SurrealDB instance.
|
|
254
|
+
"""
|
|
255
|
+
return cls.__url
|
|
256
|
+
|
|
257
|
+
@classmethod
|
|
258
|
+
def get_user(cls) -> str | None:
|
|
259
|
+
"""
|
|
260
|
+
Get the username for authentication.
|
|
261
|
+
|
|
262
|
+
:return: The username for authentication.
|
|
263
|
+
"""
|
|
264
|
+
return cls.__user
|
|
265
|
+
|
|
266
|
+
@classmethod
|
|
267
|
+
def get_namespace(cls) -> str | None:
|
|
268
|
+
"""
|
|
269
|
+
Get the namespace to use.
|
|
270
|
+
|
|
271
|
+
:return: The namespace to use.
|
|
272
|
+
"""
|
|
273
|
+
return cls.__namespace
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def get_database(cls) -> str | None:
|
|
277
|
+
"""
|
|
278
|
+
Get the database to use.
|
|
279
|
+
|
|
280
|
+
:return: The database to use.
|
|
281
|
+
"""
|
|
282
|
+
return cls.__database
|
|
283
|
+
|
|
284
|
+
@classmethod
|
|
285
|
+
def is_password_set(cls) -> bool:
|
|
286
|
+
"""
|
|
287
|
+
Get the database to use.
|
|
288
|
+
|
|
289
|
+
:return: The database to use.
|
|
290
|
+
"""
|
|
291
|
+
return cls.__password is not None
|
|
292
|
+
|
|
293
|
+
@classmethod
|
|
294
|
+
def is_connected(cls) -> bool:
|
|
295
|
+
"""
|
|
296
|
+
Check if the connection to the SurrealDB instance is established.
|
|
297
|
+
|
|
298
|
+
:return: True if the connection is established, False otherwise.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
return cls.__client is not None
|
surreal_orm/constants.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LOOKUP_OPERATORS = {
|
|
2
|
+
"exact": "=",
|
|
3
|
+
"gt": ">",
|
|
4
|
+
"gte": ">=",
|
|
5
|
+
"lt": "<",
|
|
6
|
+
"lte": "<=",
|
|
7
|
+
"in": "IN",
|
|
8
|
+
"like": "LIKE",
|
|
9
|
+
"ilike": "ILIKE",
|
|
10
|
+
"contains": "CONTAINS",
|
|
11
|
+
"icontains": "CONTAINS",
|
|
12
|
+
"startswith": "STARTSWITH",
|
|
13
|
+
"istartswith": "STARTSWITH",
|
|
14
|
+
"endswith": "ENDSWITH",
|
|
15
|
+
"iendswith": "ENDSWITH",
|
|
16
|
+
"match": "MATCH",
|
|
17
|
+
"regex": "REGEX",
|
|
18
|
+
"iregex": "REGEX",
|
|
19
|
+
"isnull": "IS",
|
|
20
|
+
}
|
surreal_orm/enum.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
from typing import Any, Self
|
|
2
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
3
|
+
from .connection_manager import SurrealDBConnectionManager
|
|
4
|
+
from surrealdb import RecordID, SurrealDbError
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SurrealConfigDict(ConfigDict):
|
|
13
|
+
"""
|
|
14
|
+
SurrealConfigDict is a configuration dictionary for SurrealDB models.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
primary_key (str | None): The primary key field name for the model.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
primary_key: str | None
|
|
21
|
+
" The primary key field name for the model. "
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BaseSurrealModel(BaseModel):
|
|
25
|
+
"""
|
|
26
|
+
Base class for models interacting with SurrealDB.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get_table_name(cls) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Get the table name for the model.
|
|
33
|
+
"""
|
|
34
|
+
return cls.__name__
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def get_index_primary_key(cls) -> str | None:
|
|
38
|
+
"""
|
|
39
|
+
Get the primary key field name for the model.
|
|
40
|
+
"""
|
|
41
|
+
if hasattr(cls, "model_config"): # pragma: no cover
|
|
42
|
+
primary_key = cls.model_config.get("primary_key", None)
|
|
43
|
+
if isinstance(primary_key, str):
|
|
44
|
+
return primary_key
|
|
45
|
+
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def get_id(self) -> None | str | RecordID:
|
|
49
|
+
"""
|
|
50
|
+
Get the ID of the model instance.
|
|
51
|
+
"""
|
|
52
|
+
if hasattr(self, "id"):
|
|
53
|
+
id_value = getattr(self, "id")
|
|
54
|
+
return str(id_value) if id_value is not None else None
|
|
55
|
+
|
|
56
|
+
if hasattr(self, "model_config"):
|
|
57
|
+
primary_key = self.model_config.get("primary_key", None)
|
|
58
|
+
if isinstance(primary_key, str) and hasattr(self, primary_key):
|
|
59
|
+
primary_key_value = getattr(self, primary_key)
|
|
60
|
+
return str(primary_key_value) if primary_key_value is not None else None
|
|
61
|
+
|
|
62
|
+
return None # pragma: no cover
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_db(cls, record: dict | list) -> Self | list[Self]:
|
|
66
|
+
"""
|
|
67
|
+
Create an instance from a SurrealDB record.
|
|
68
|
+
"""
|
|
69
|
+
if isinstance(record, list):
|
|
70
|
+
return [cls.from_db(rs) for rs in record] # type: ignore
|
|
71
|
+
|
|
72
|
+
return cls(**record)
|
|
73
|
+
|
|
74
|
+
@model_validator(mode="before")
|
|
75
|
+
@classmethod
|
|
76
|
+
def set_data(cls, data: Any) -> Any:
|
|
77
|
+
"""
|
|
78
|
+
Set the ID of the model instance.
|
|
79
|
+
"""
|
|
80
|
+
if isinstance(data, dict): # pragma: no cover
|
|
81
|
+
if "id" in data and isinstance(data["id"], RecordID):
|
|
82
|
+
data["id"] = str(data["id"]).split(":")[1]
|
|
83
|
+
return data
|
|
84
|
+
|
|
85
|
+
async def refresh(self) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Refresh the model instance from the database.
|
|
88
|
+
"""
|
|
89
|
+
if not self.get_id():
|
|
90
|
+
raise SurrealDbError("Can't refresh data, not recorded yet.") # pragma: no cover
|
|
91
|
+
|
|
92
|
+
client = await SurrealDBConnectionManager.get_client()
|
|
93
|
+
record = await client.select(f"{self.get_table_name()}:{self.get_id()}")
|
|
94
|
+
|
|
95
|
+
if record is None:
|
|
96
|
+
raise SurrealDbError("Can't refresh data, no record found.") # pragma: no cover
|
|
97
|
+
|
|
98
|
+
self.from_db(record)
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
async def save(self) -> Self:
|
|
102
|
+
"""
|
|
103
|
+
Save the model instance to the database.
|
|
104
|
+
"""
|
|
105
|
+
client = await SurrealDBConnectionManager.get_client()
|
|
106
|
+
data = self.model_dump(exclude={"id"})
|
|
107
|
+
id = self.get_id()
|
|
108
|
+
table = self.get_table_name()
|
|
109
|
+
|
|
110
|
+
if id is not None:
|
|
111
|
+
thing = f"{table}:{id}"
|
|
112
|
+
await client.create(thing, data)
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
# Auto-generate the ID
|
|
116
|
+
record = await client.create(table, data) # pragma: no cover
|
|
117
|
+
|
|
118
|
+
if isinstance(record, list):
|
|
119
|
+
raise SurrealDbError("Can't save data, multiple records returned.") # pragma: no cover
|
|
120
|
+
|
|
121
|
+
if record is None:
|
|
122
|
+
raise SurrealDbError("Can't save data, no record returned.") # pragma: no cover
|
|
123
|
+
|
|
124
|
+
obj = self.from_db(record)
|
|
125
|
+
if isinstance(obj, type(self)):
|
|
126
|
+
self = obj
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
raise SurrealDbError("Can't save data, no record returned.") # pragma: no cover
|
|
130
|
+
|
|
131
|
+
async def update(self) -> Any:
|
|
132
|
+
"""
|
|
133
|
+
Update the model instance to the database.
|
|
134
|
+
"""
|
|
135
|
+
client = await SurrealDBConnectionManager.get_client()
|
|
136
|
+
|
|
137
|
+
data = self.model_dump(exclude={"id"})
|
|
138
|
+
id = self.get_id()
|
|
139
|
+
if id is not None:
|
|
140
|
+
thing = f"{self.__class__.__name__}:{id}"
|
|
141
|
+
test = await client.update(thing, data)
|
|
142
|
+
return test
|
|
143
|
+
raise SurrealDbError("Can't update data, no id found.")
|
|
144
|
+
|
|
145
|
+
async def merge(self, **data: Any) -> Any:
|
|
146
|
+
"""
|
|
147
|
+
Update the model instance to the database.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
client = await SurrealDBConnectionManager.get_client()
|
|
151
|
+
data_set = {key: value for key, value in data.items()}
|
|
152
|
+
|
|
153
|
+
id = self.get_id()
|
|
154
|
+
if id:
|
|
155
|
+
thing = f"{self.get_table_name()}:{id}"
|
|
156
|
+
|
|
157
|
+
await client.merge(thing, data_set)
|
|
158
|
+
await self.refresh()
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
raise SurrealDbError(f"No Id for the data to merge: {data}")
|
|
162
|
+
|
|
163
|
+
async def delete(self) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Delete the model instance from the database.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
client = await SurrealDBConnectionManager.get_client()
|
|
169
|
+
|
|
170
|
+
id = self.get_id()
|
|
171
|
+
|
|
172
|
+
thing = f"{self.get_table_name()}:{id}"
|
|
173
|
+
|
|
174
|
+
deleted = await client.delete(thing)
|
|
175
|
+
|
|
176
|
+
if not deleted:
|
|
177
|
+
raise SurrealDbError(f"Can't delete Record id -> '{id}' not found!")
|
|
178
|
+
|
|
179
|
+
logger.info(f"Record deleted -> {deleted}.")
|
|
180
|
+
del self
|
|
181
|
+
|
|
182
|
+
@model_validator(mode="after")
|
|
183
|
+
def check_config(self) -> Self:
|
|
184
|
+
"""
|
|
185
|
+
Check the model configuration.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
if not self.get_index_primary_key() and not hasattr(self, "id"):
|
|
189
|
+
raise SurrealDbError( # pragma: no cover
|
|
190
|
+
"Can't create model, the model need either 'id' field or primirary_key in 'model_config'."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def objects(cls) -> Any:
|
|
197
|
+
"""
|
|
198
|
+
Return a QuerySet for the model class.
|
|
199
|
+
"""
|
|
200
|
+
from .query_set import QuerySet
|
|
201
|
+
|
|
202
|
+
return QuerySet(cls)
|
surreal_orm/py.typed
ADDED
|
File without changes
|
surreal_orm/query_set.py
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
from .constants import LOOKUP_OPERATORS
|
|
2
|
+
from .enum import OrderBy
|
|
3
|
+
from .utils import remove_quotes_for_variables
|
|
4
|
+
from surrealdb import QueryResponse, Table, AsyncSurrealDB
|
|
5
|
+
from surrealdb.errors import SurrealDbError
|
|
6
|
+
from . import BaseSurrealModel, SurrealDBConnectionManager
|
|
7
|
+
from typing import Self, Any, cast
|
|
8
|
+
from pydantic_core import ValidationError
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class QuerySet:
|
|
16
|
+
"""
|
|
17
|
+
A class used to build, execute, and manage queries on a SurrealDB table associated with a specific model.
|
|
18
|
+
|
|
19
|
+
The `QuerySet` class provides a fluent interface to construct complex queries using method chaining.
|
|
20
|
+
It supports selecting specific fields, filtering results, ordering, limiting, and offsetting the results.
|
|
21
|
+
Additionally, it allows executing custom queries and managing table-level operations such as deletion.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
```python
|
|
25
|
+
queryset = QuerySet(UserModel)
|
|
26
|
+
users = await queryset.filter(age__gt=21).order_by('name').limit(10).all()
|
|
27
|
+
```
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, model: type[BaseSurrealModel]) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Initialize the QuerySet with a specific model.
|
|
33
|
+
|
|
34
|
+
This constructor sets up the initial state of the QuerySet, including the model it operates on,
|
|
35
|
+
default filters, selected fields, and other query parameters.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
model (type[BaseSurrealModel]): The model class associated with the table. This model should
|
|
39
|
+
inherit from `BaseSurrealModel` and define the table name either via a `_table_name` attribute
|
|
40
|
+
or by defaulting to the class name.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
model (type[BaseSurrealModel]): The model class associated with the table.
|
|
44
|
+
_filters (list[tuple[str, str, Any]]): A list of filter conditions as tuples of (field, lookup, value).
|
|
45
|
+
select_item (list[str]): A list of field names to be selected in the query.
|
|
46
|
+
_limit (int | None): The maximum number of records to retrieve.
|
|
47
|
+
_offset (int | None): The number of records to skip before starting to return records.
|
|
48
|
+
_order_by (str | None): The field and direction to order the results by.
|
|
49
|
+
_model_table (str): The name of the table in SurrealDB.
|
|
50
|
+
_variables (dict): A dictionary of variables to be used in the query.
|
|
51
|
+
"""
|
|
52
|
+
self.model = model
|
|
53
|
+
self._filters: list[tuple[str, str, Any]] = []
|
|
54
|
+
self.select_item: list[str] = []
|
|
55
|
+
self._limit: int | None = None
|
|
56
|
+
self._offset: int | None = None
|
|
57
|
+
self._order_by: str | None = None
|
|
58
|
+
self._model_table: str = getattr(model, "_table_name", model.__name__)
|
|
59
|
+
self._variables: dict = {}
|
|
60
|
+
|
|
61
|
+
def select(self, *fields: str) -> Self:
|
|
62
|
+
"""
|
|
63
|
+
Specify the fields to retrieve in the query.
|
|
64
|
+
|
|
65
|
+
By default, all fields are selected (`SELECT *`). This method allows you to specify
|
|
66
|
+
a subset of fields to be retrieved, which can improve performance by fetching only necessary data.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
*fields (str): Variable length argument list of field names to select.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Self: The current instance of QuerySet to allow method chaining.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
```python
|
|
76
|
+
queryset.select('id', 'name', 'email')
|
|
77
|
+
```
|
|
78
|
+
"""
|
|
79
|
+
# Store the list of fields to retrieve
|
|
80
|
+
self.select_item = list(fields)
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def variables(self, **kwargs: Any) -> Self:
|
|
84
|
+
"""
|
|
85
|
+
Set variables for the query.
|
|
86
|
+
|
|
87
|
+
Variables can be used in parameterized queries to safely inject values without risking SQL injection.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
**kwargs (Any): Arbitrary keyword arguments representing variable names and their corresponding values.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Self: The current instance of QuerySet to allow method chaining.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
```python
|
|
97
|
+
queryset.variables(status='active', role='admin')
|
|
98
|
+
```
|
|
99
|
+
"""
|
|
100
|
+
self._variables = {key: value for key, value in kwargs.items()}
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def filter(self, **kwargs: Any) -> Self:
|
|
104
|
+
"""
|
|
105
|
+
Add filter conditions to the query.
|
|
106
|
+
|
|
107
|
+
This method allows adding one or multiple filter conditions to narrow down the query results.
|
|
108
|
+
Filters are added using keyword arguments where the key represents the field and the lookup type,
|
|
109
|
+
and the value represents the value to filter by.
|
|
110
|
+
|
|
111
|
+
Supported lookup types include:
|
|
112
|
+
- exact
|
|
113
|
+
- contains
|
|
114
|
+
- gt (greater than)
|
|
115
|
+
- lt (less than)
|
|
116
|
+
- gte (greater than or equal)
|
|
117
|
+
- lte (less than or equal)
|
|
118
|
+
- in (within a list of values)
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
**kwargs (Any): Arbitrary keyword arguments representing filter conditions. The key should be in the format
|
|
122
|
+
`field__lookup` (e.g., `age__gt=30`). If no lookup is provided, `exact` is assumed.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Self: The current instance of QuerySet to allow method chaining.
|
|
126
|
+
|
|
127
|
+
Example:
|
|
128
|
+
```python
|
|
129
|
+
queryset.filter(age__gt=21, status='active')
|
|
130
|
+
```
|
|
131
|
+
"""
|
|
132
|
+
for key, value in kwargs.items():
|
|
133
|
+
field_name, lookup = self._parse_lookup(key)
|
|
134
|
+
self._filters.append((field_name, lookup, value))
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
def _parse_lookup(self, key: str) -> tuple[str, str]:
|
|
138
|
+
"""
|
|
139
|
+
Parse the lookup type from the filter key.
|
|
140
|
+
|
|
141
|
+
This helper method splits the filter key into the field name and the lookup type.
|
|
142
|
+
If no lookup type is specified, it defaults to `exact`.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
key (str): The filter key in the format `field__lookup` or just `field`.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
tuple[str, str]: A tuple containing the field name and the lookup type.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
```python
|
|
152
|
+
_parse_lookup('age__gt') # Returns ('age', 'gt')
|
|
153
|
+
_parse_lookup('status') # Returns ('status', 'exact')
|
|
154
|
+
```
|
|
155
|
+
"""
|
|
156
|
+
if "__" in key:
|
|
157
|
+
field_name, lookup_name = key.split("__", 1)
|
|
158
|
+
else:
|
|
159
|
+
field_name, lookup_name = key, "exact"
|
|
160
|
+
return field_name, lookup_name
|
|
161
|
+
|
|
162
|
+
def limit(self, value: int) -> Self:
|
|
163
|
+
"""
|
|
164
|
+
Set a limit on the number of results to retrieve.
|
|
165
|
+
|
|
166
|
+
This method restricts the number of records returned by the query, which is useful for pagination
|
|
167
|
+
or when only a subset of results is needed.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
value (int): The maximum number of records to retrieve.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Self: The current instance of QuerySet to allow method chaining.
|
|
174
|
+
|
|
175
|
+
Example:
|
|
176
|
+
```python
|
|
177
|
+
queryset.limit(10)
|
|
178
|
+
```
|
|
179
|
+
"""
|
|
180
|
+
self._limit = value
|
|
181
|
+
return self
|
|
182
|
+
|
|
183
|
+
def offset(self, value: int) -> Self:
|
|
184
|
+
"""
|
|
185
|
+
Set an offset for the results.
|
|
186
|
+
|
|
187
|
+
This method skips a specified number of records before starting to return records.
|
|
188
|
+
It is commonly used in conjunction with `limit` for pagination purposes.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
value (int): The number of records to skip.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Self: The current instance of QuerySet to allow method chaining.
|
|
195
|
+
|
|
196
|
+
Example:
|
|
197
|
+
```python
|
|
198
|
+
queryset.offset(20)
|
|
199
|
+
```
|
|
200
|
+
"""
|
|
201
|
+
self._offset = value
|
|
202
|
+
return self
|
|
203
|
+
|
|
204
|
+
def order_by(self, field_name: str, type: OrderBy = OrderBy.ASC) -> Self:
|
|
205
|
+
"""
|
|
206
|
+
Set the field and direction to order the results by.
|
|
207
|
+
|
|
208
|
+
This method allows sorting the query results based on a specified field and direction
|
|
209
|
+
(ascending or descending).
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
field_name (str): The name of the field to sort by.
|
|
213
|
+
type (OrderBy, optional): The direction to sort by. Defaults to `OrderBy.ASC`.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Self: The current instance of QuerySet to allow method chaining.
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
```python
|
|
220
|
+
queryset.order_by('name', OrderBy.DESC)
|
|
221
|
+
```
|
|
222
|
+
"""
|
|
223
|
+
self._order_by = f"{field_name} {type}"
|
|
224
|
+
return self
|
|
225
|
+
|
|
226
|
+
def _compile_query(self) -> str:
|
|
227
|
+
"""
|
|
228
|
+
Compile the QuerySet parameters into a SQL query string.
|
|
229
|
+
|
|
230
|
+
This method constructs the final SQL query by combining the selected fields, filters,
|
|
231
|
+
ordering, limit, and offset parameters.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
str: The compiled SQL query string.
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
```python
|
|
238
|
+
query = queryset._compile_query()
|
|
239
|
+
# Returns something like:
|
|
240
|
+
# "SELECT id, name FROM users WHERE age > 21 AND status = 'active' ORDER BY name ASC LIMIT 10 START 20;"
|
|
241
|
+
```
|
|
242
|
+
"""
|
|
243
|
+
where_clauses = []
|
|
244
|
+
for field_name, lookup_name, value in self._filters:
|
|
245
|
+
op = LOOKUP_OPERATORS.get(lookup_name, "=")
|
|
246
|
+
if lookup_name == "in":
|
|
247
|
+
# Assuming value is iterable for 'IN' operations
|
|
248
|
+
formatted_values = ", ".join(repr(v) for v in value)
|
|
249
|
+
where_clauses.append(f"{field_name} {op} [{formatted_values}]")
|
|
250
|
+
else:
|
|
251
|
+
where_clauses.append(f"{field_name} {op} {repr(value)}")
|
|
252
|
+
|
|
253
|
+
# Construct the SELECT clause
|
|
254
|
+
if self.select_item:
|
|
255
|
+
fields = ", ".join(self.select_item)
|
|
256
|
+
query = f"SELECT {fields} FROM {self._model_table}"
|
|
257
|
+
else:
|
|
258
|
+
query = f"SELECT * FROM {self._model_table}"
|
|
259
|
+
|
|
260
|
+
# Append WHERE clauses if any
|
|
261
|
+
if where_clauses:
|
|
262
|
+
query += " WHERE " + " AND ".join(where_clauses)
|
|
263
|
+
|
|
264
|
+
# Append LIMIT if set
|
|
265
|
+
if self._limit is not None:
|
|
266
|
+
query += f" LIMIT {self._limit}"
|
|
267
|
+
|
|
268
|
+
# Append OFFSET (START) if set
|
|
269
|
+
if self._offset is not None:
|
|
270
|
+
query += f" START {self._offset}"
|
|
271
|
+
|
|
272
|
+
# Append ORDER BY if set
|
|
273
|
+
if self._order_by:
|
|
274
|
+
query += f" ORDER BY {self._order_by}"
|
|
275
|
+
|
|
276
|
+
query += ";"
|
|
277
|
+
return query
|
|
278
|
+
|
|
279
|
+
async def exec(self) -> Any:
|
|
280
|
+
"""
|
|
281
|
+
Execute the compiled query and return the results.
|
|
282
|
+
|
|
283
|
+
This method runs the constructed SQL query against the SurrealDB database and processes
|
|
284
|
+
the results. If the data conforms to the model schema, it returns a list of model instances;
|
|
285
|
+
otherwise, it returns a list of dictionaries.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
list[BaseSurrealModel] | list[dict]: A list of model instances if validation is successful,
|
|
289
|
+
otherwise a list of dictionaries representing the raw data.
|
|
290
|
+
|
|
291
|
+
Raises:
|
|
292
|
+
SurrealDbError: If there is an issue executing the query.
|
|
293
|
+
|
|
294
|
+
Example:
|
|
295
|
+
```python
|
|
296
|
+
results = await queryset.exec()
|
|
297
|
+
```
|
|
298
|
+
"""
|
|
299
|
+
data: dict[str, Any] = {"result": []}
|
|
300
|
+
query = self._compile_query()
|
|
301
|
+
results = await self._execute_query(query)
|
|
302
|
+
try:
|
|
303
|
+
data = cast(dict, results[0])
|
|
304
|
+
return self.model.from_db(data["result"])
|
|
305
|
+
except ValidationError as e:
|
|
306
|
+
logger.info(f"Pydantic invalid format for the class, returning dict value: {e}")
|
|
307
|
+
return data["result"]
|
|
308
|
+
|
|
309
|
+
async def first(self) -> Any:
|
|
310
|
+
"""
|
|
311
|
+
Execute the query and return the first result.
|
|
312
|
+
|
|
313
|
+
This method modifies the QuerySet to limit the results to one and retrieves the first record.
|
|
314
|
+
If no records are found, it returns `None`.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
BaseSurrealModel | dict | None: The first model instance if available, a dictionary if
|
|
318
|
+
model validation fails, or `None` if no results are found.
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
SurrealDbError: If there is an issue executing the query.
|
|
322
|
+
|
|
323
|
+
Example:
|
|
324
|
+
```python
|
|
325
|
+
first_user = await queryset.filter(name='Alice').first()
|
|
326
|
+
```
|
|
327
|
+
"""
|
|
328
|
+
self._limit = 1
|
|
329
|
+
results = await self.exec()
|
|
330
|
+
if results:
|
|
331
|
+
return results[0]
|
|
332
|
+
|
|
333
|
+
raise SurrealDbError("No result found.")
|
|
334
|
+
|
|
335
|
+
async def get(self, id_item: Any = None) -> Any:
|
|
336
|
+
"""
|
|
337
|
+
Retrieve a single record by its unique identifier or based on the current QuerySet filters.
|
|
338
|
+
|
|
339
|
+
This method fetches a specific record by its ID if provided. If no ID is provided, it attempts
|
|
340
|
+
to retrieve a single record based on the existing filters. It raises an error if multiple or
|
|
341
|
+
no records are found when no ID is specified.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
id_item (str | None, optional): The unique identifier of the item to retrieve. Defaults to `None`.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
BaseSurrealModel | dict[str, Any]: The retrieved model instance or a dictionary representing the raw data.
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
SurrealDbError: If multiple records are found when `id_item` is not provided or if no records are found.
|
|
351
|
+
|
|
352
|
+
Example:
|
|
353
|
+
```python
|
|
354
|
+
user = await queryset.get(id_item='user:123')
|
|
355
|
+
```
|
|
356
|
+
"""
|
|
357
|
+
if id_item:
|
|
358
|
+
client = await SurrealDBConnectionManager.get_client()
|
|
359
|
+
data = await client.select(f"{self._model_table}:{id_item}")
|
|
360
|
+
return self.model.from_db(data)
|
|
361
|
+
else:
|
|
362
|
+
result = await self.exec()
|
|
363
|
+
if len(result) > 1:
|
|
364
|
+
raise SurrealDbError("More than one result found.")
|
|
365
|
+
|
|
366
|
+
if len(result) == 0:
|
|
367
|
+
raise SurrealDbError("No result found.")
|
|
368
|
+
return result[0]
|
|
369
|
+
|
|
370
|
+
async def all(self) -> Any:
|
|
371
|
+
"""
|
|
372
|
+
Fetch all records from the associated table.
|
|
373
|
+
|
|
374
|
+
This method retrieves every record from the table without applying any filters, limits, or ordering.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
list[BaseSurrealModel]: A list of model instances representing all records in the table.
|
|
378
|
+
|
|
379
|
+
Raises:
|
|
380
|
+
SurrealDbError: If there is an issue executing the query.
|
|
381
|
+
|
|
382
|
+
Example:
|
|
383
|
+
```python
|
|
384
|
+
all_users = await queryset.all()
|
|
385
|
+
```
|
|
386
|
+
"""
|
|
387
|
+
client = await SurrealDBConnectionManager.get_client()
|
|
388
|
+
results = await client.select(Table(self._model_table))
|
|
389
|
+
return self.model.from_db(results)
|
|
390
|
+
|
|
391
|
+
async def _execute_query(self, query: str) -> list[QueryResponse]:
|
|
392
|
+
"""
|
|
393
|
+
Execute the given SQL query using the SurrealDB client.
|
|
394
|
+
|
|
395
|
+
This internal method handles the execution of the compiled SQL query and returns the raw results
|
|
396
|
+
from the database.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
query (str): The SQL query string to execute.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
list[QueryResponse]: A list of `QueryResponse` objects containing the query results.
|
|
403
|
+
|
|
404
|
+
Raises:
|
|
405
|
+
SurrealDbError: If there is an issue executing the query.
|
|
406
|
+
|
|
407
|
+
Example:
|
|
408
|
+
```python
|
|
409
|
+
results = await self._execute_query("SELECT * FROM users;")
|
|
410
|
+
```
|
|
411
|
+
"""
|
|
412
|
+
client = await SurrealDBConnectionManager.get_client()
|
|
413
|
+
return await self._run_query_on_client(client, query)
|
|
414
|
+
|
|
415
|
+
async def _run_query_on_client(self, client: AsyncSurrealDB, query: str) -> list[QueryResponse]:
|
|
416
|
+
"""
|
|
417
|
+
Run the SQL query on the provided SurrealDB client.
|
|
418
|
+
|
|
419
|
+
This internal method sends the query to the SurrealDB client along with any predefined variables
|
|
420
|
+
and returns the raw query responses.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
client (AsyncSurrealDB): The active SurrealDB client instance.
|
|
424
|
+
query (str): The SQL query string to execute.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
list[QueryResponse]: A list of `QueryResponse` objects containing the query results.
|
|
428
|
+
|
|
429
|
+
Raises:
|
|
430
|
+
SurrealDbError: If there is an issue executing the query.
|
|
431
|
+
|
|
432
|
+
Example:
|
|
433
|
+
```python
|
|
434
|
+
results = await self._run_query_on_client(client, "SELECT * FROM users;")
|
|
435
|
+
```
|
|
436
|
+
"""
|
|
437
|
+
return await client.query(remove_quotes_for_variables(query), self._variables) # type: ignore
|
|
438
|
+
|
|
439
|
+
async def delete_table(self) -> bool:
|
|
440
|
+
"""
|
|
441
|
+
Delete the associated table from the SurrealDB database.
|
|
442
|
+
|
|
443
|
+
This method performs a destructive operation by removing the entire table from the database.
|
|
444
|
+
Use with caution, especially in production environments.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
bool: `True` if the table was successfully deleted.
|
|
448
|
+
|
|
449
|
+
Raises:
|
|
450
|
+
SurrealDbError: If there is an issue deleting the table.
|
|
451
|
+
|
|
452
|
+
Example:
|
|
453
|
+
```python
|
|
454
|
+
success = await queryset.delete_table()
|
|
455
|
+
```
|
|
456
|
+
"""
|
|
457
|
+
client = await SurrealDBConnectionManager.get_client()
|
|
458
|
+
await client.delete(Table(self._model_table))
|
|
459
|
+
return True
|
|
460
|
+
|
|
461
|
+
async def query(self, query: str, variables: dict[str, Any] = {}) -> Any:
|
|
462
|
+
"""
|
|
463
|
+
Execute a custom SQL query on the SurrealDB database.
|
|
464
|
+
|
|
465
|
+
This method allows running arbitrary SQL queries, provided they operate on the correct table
|
|
466
|
+
associated with the current model. It ensures that the query includes the `FROM` clause referencing
|
|
467
|
+
the correct table to maintain consistency and security.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
query (str): The custom SQL query string to execute.
|
|
471
|
+
variables (dict[str, Any], optional): A dictionary of variables to substitute into the query.
|
|
472
|
+
Defaults to an empty dictionary.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
Any: The result of the query, typically a model instance or a list of model instances.
|
|
476
|
+
|
|
477
|
+
Raises:
|
|
478
|
+
SurrealDbError: If the query does not include the correct `FROM` clause or if there is an issue executing the query.
|
|
479
|
+
|
|
480
|
+
Example:
|
|
481
|
+
```python
|
|
482
|
+
custom_query = "SELECT name, email FROM UserModel WHERE status = $status;"
|
|
483
|
+
results = await queryset.query(custom_query, variables={'status': 'active'})
|
|
484
|
+
```
|
|
485
|
+
"""
|
|
486
|
+
if f"FROM {self.model.__name__}" not in query:
|
|
487
|
+
raise SurrealDbError(f"The query must include 'FROM {self.model.__name__}' to reference the correct table.")
|
|
488
|
+
client = await SurrealDBConnectionManager.get_client()
|
|
489
|
+
results = await client.query(remove_quotes_for_variables(query), variables)
|
|
490
|
+
data = cast(dict, results[0])
|
|
491
|
+
return self.model.from_db(data["result"])
|
surreal_orm/utils.py
ADDED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: surrealdb-orm
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
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
|
|
@@ -70,7 +70,7 @@ Description-Content-Type: text/markdown
|
|
|
70
70
|
|
|
71
71
|
## ✅ Version
|
|
72
72
|
|
|
73
|
-
Alpha 0.1.
|
|
73
|
+
Alpha 0.1.4
|
|
74
74
|
|
|
75
75
|
---
|
|
76
76
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
surreal_orm/__init__.py,sha256=p2dnNi1Ar0FNrLlC7oHtYiUrrdYwQKt--72_-omCElk,306
|
|
2
|
+
surreal_orm/connection_manager.py,sha256=kRAK6qhkKRVbhTjWZV6LaOGiH_jClwQ3zzmjca1A5Ro,8561
|
|
3
|
+
surreal_orm/constants.py,sha256=CLavEca1M6cLJLqVl4l4KoE-cBrgVQNsuGxW9zGJBmg,429
|
|
4
|
+
surreal_orm/enum.py,sha256=kR-vzkHqnqy9YaYOvWTwAHdl2-WCzPcSEch-YTyJv1Y,158
|
|
5
|
+
surreal_orm/model_base.py,sha256=zz6y8TQavk0XvLhunzQflJpPG85FizeCR-mAy-LHlK8,6082
|
|
6
|
+
surreal_orm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
surreal_orm/query_set.py,sha256=VD8F2vKv5_uIXRCV_0HCx6lnarF-Qx3x8MYJq2LH78U,18155
|
|
8
|
+
surreal_orm/utils.py,sha256=mni_dTtb4VGTdge8eWSZpBw5xoWci2m-XThKFHYPKTo,171
|
|
9
|
+
surrealdb_orm-0.1.4.dist-info/METADATA,sha256=AFDw-vOKASG0mGa73Zo9CcaQKe-E-ocia3tOUJdcELY,5980
|
|
10
|
+
surrealdb_orm-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
surrealdb_orm-0.1.4.dist-info/licenses/LICENSE,sha256=TO3Ub0nPPx5NxwjsDuBAu3RBdBLmdDybqC5dem4oGts,1074
|
|
12
|
+
surrealdb_orm-0.1.4.dist-info/RECORD,,
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
surrealdb_orm-0.1.2.dist-info/METADATA,sha256=Il4HDCn2wZO06wKqRZhQS605FRM7SZkqAv3sAOpwTAI,5980
|
|
2
|
-
surrealdb_orm-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
3
|
-
surrealdb_orm-0.1.2.dist-info/licenses/LICENSE,sha256=TO3Ub0nPPx5NxwjsDuBAu3RBdBLmdDybqC5dem4oGts,1074
|
|
4
|
-
surrealdb_orm-0.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|