surrealdb-orm 0.1.4__py3-none-any.whl → 0.5.1__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 +72 -3
- surreal_orm/aggregations.py +164 -0
- surreal_orm/auth/__init__.py +15 -0
- surreal_orm/auth/access.py +167 -0
- surreal_orm/auth/mixins.py +302 -0
- surreal_orm/cli/__init__.py +15 -0
- surreal_orm/cli/commands.py +369 -0
- surreal_orm/connection_manager.py +58 -18
- surreal_orm/fields/__init__.py +36 -0
- surreal_orm/fields/encrypted.py +166 -0
- surreal_orm/fields/relation.py +465 -0
- surreal_orm/migrations/__init__.py +51 -0
- surreal_orm/migrations/executor.py +380 -0
- surreal_orm/migrations/generator.py +272 -0
- surreal_orm/migrations/introspector.py +305 -0
- surreal_orm/migrations/migration.py +188 -0
- surreal_orm/migrations/operations.py +531 -0
- surreal_orm/migrations/state.py +406 -0
- surreal_orm/model_base.py +530 -44
- surreal_orm/query_set.py +609 -33
- surreal_orm/relations.py +645 -0
- surreal_orm/surreal_function.py +95 -0
- surreal_orm/surreal_ql.py +113 -0
- surreal_orm/types.py +86 -0
- surreal_sdk/README.md +79 -0
- surreal_sdk/__init__.py +151 -0
- surreal_sdk/connection/__init__.py +17 -0
- surreal_sdk/connection/base.py +516 -0
- surreal_sdk/connection/http.py +421 -0
- surreal_sdk/connection/pool.py +244 -0
- surreal_sdk/connection/websocket.py +519 -0
- surreal_sdk/exceptions.py +71 -0
- surreal_sdk/functions.py +607 -0
- surreal_sdk/protocol/__init__.py +13 -0
- surreal_sdk/protocol/rpc.py +218 -0
- surreal_sdk/py.typed +0 -0
- surreal_sdk/pyproject.toml +49 -0
- surreal_sdk/streaming/__init__.py +31 -0
- surreal_sdk/streaming/change_feed.py +278 -0
- surreal_sdk/streaming/live_query.py +265 -0
- surreal_sdk/streaming/live_select.py +369 -0
- surreal_sdk/transaction.py +386 -0
- surreal_sdk/types.py +346 -0
- surrealdb_orm-0.5.1.dist-info/METADATA +465 -0
- surrealdb_orm-0.5.1.dist-info/RECORD +52 -0
- {surrealdb_orm-0.1.4.dist-info → surrealdb_orm-0.5.1.dist-info}/WHEEL +1 -1
- surrealdb_orm-0.5.1.dist-info/entry_points.txt +2 -0
- {surrealdb_orm-0.1.4.dist-info → surrealdb_orm-0.5.1.dist-info}/licenses/LICENSE +1 -1
- surrealdb_orm-0.1.4.dist-info/METADATA +0 -184
- surrealdb_orm-0.1.4.dist-info/RECORD +0 -12
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Migration operations for SurrealDB schema changes.
|
|
3
|
+
|
|
4
|
+
Each operation represents a single schema modification that can be
|
|
5
|
+
applied (forwards) or reverted (backwards).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Callable, Coroutine
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Operation(ABC):
|
|
15
|
+
"""
|
|
16
|
+
Base class for all migration operations.
|
|
17
|
+
|
|
18
|
+
Operations must implement forwards() and backwards() methods
|
|
19
|
+
that return SurrealQL statements.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
reversible: bool = field(default=True, init=False)
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def forwards(self) -> str:
|
|
26
|
+
"""Generate forward SurrealQL statement."""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def backwards(self) -> str:
|
|
31
|
+
"""Generate rollback SurrealQL statement."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
def describe(self) -> str:
|
|
35
|
+
"""Human-readable description of the operation."""
|
|
36
|
+
return f"{self.__class__.__name__}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class CreateTable(Operation):
|
|
41
|
+
"""
|
|
42
|
+
Create a new table with optional schema mode and changefeed.
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
CreateTable(name="users", schema_mode="SCHEMAFULL", changefeed="7d")
|
|
46
|
+
|
|
47
|
+
Generates:
|
|
48
|
+
DEFINE TABLE users SCHEMAFULL CHANGEFEED 7d;
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
name: str
|
|
52
|
+
schema_mode: str = "SCHEMAFULL"
|
|
53
|
+
table_type: str | None = None
|
|
54
|
+
changefeed: str | None = None
|
|
55
|
+
permissions: dict[str, str] | None = None
|
|
56
|
+
comment: str | None = None
|
|
57
|
+
|
|
58
|
+
def forwards(self) -> str:
|
|
59
|
+
parts = [f"DEFINE TABLE {self.name}"]
|
|
60
|
+
|
|
61
|
+
if self.schema_mode:
|
|
62
|
+
parts.append(self.schema_mode)
|
|
63
|
+
|
|
64
|
+
if self.changefeed:
|
|
65
|
+
parts.append(f"CHANGEFEED {self.changefeed}")
|
|
66
|
+
|
|
67
|
+
if self.comment:
|
|
68
|
+
parts.append(f"COMMENT '{self.comment}'")
|
|
69
|
+
|
|
70
|
+
sql = " ".join(parts) + ";"
|
|
71
|
+
|
|
72
|
+
# Add permissions if specified
|
|
73
|
+
if self.permissions:
|
|
74
|
+
perm_parts = []
|
|
75
|
+
for action, condition in self.permissions.items():
|
|
76
|
+
perm_parts.append(f"FOR {action} WHERE {condition}")
|
|
77
|
+
if perm_parts:
|
|
78
|
+
sql += f"\nDEFINE TABLE {self.name} PERMISSIONS {' '.join(perm_parts)};"
|
|
79
|
+
|
|
80
|
+
return sql
|
|
81
|
+
|
|
82
|
+
def backwards(self) -> str:
|
|
83
|
+
return f"REMOVE TABLE {self.name};"
|
|
84
|
+
|
|
85
|
+
def describe(self) -> str:
|
|
86
|
+
return f"Create table {self.name}"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class DropTable(Operation):
|
|
91
|
+
"""
|
|
92
|
+
Drop an existing table.
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
DropTable(name="users")
|
|
96
|
+
|
|
97
|
+
Generates:
|
|
98
|
+
REMOVE TABLE users;
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
name: str
|
|
102
|
+
|
|
103
|
+
def __post_init__(self) -> None:
|
|
104
|
+
self.reversible = False
|
|
105
|
+
|
|
106
|
+
def forwards(self) -> str:
|
|
107
|
+
return f"REMOVE TABLE {self.name};"
|
|
108
|
+
|
|
109
|
+
def backwards(self) -> str:
|
|
110
|
+
# Cannot reverse without knowing the original schema
|
|
111
|
+
return ""
|
|
112
|
+
|
|
113
|
+
def describe(self) -> str:
|
|
114
|
+
return f"Drop table {self.name}"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class AddField(Operation):
|
|
119
|
+
"""
|
|
120
|
+
Add a field to a table.
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
AddField(
|
|
124
|
+
table="users",
|
|
125
|
+
name="email",
|
|
126
|
+
field_type="string",
|
|
127
|
+
assertion="is::email($value)"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
Generates:
|
|
131
|
+
DEFINE FIELD email ON users TYPE string ASSERT is::email($value);
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
table: str
|
|
135
|
+
name: str
|
|
136
|
+
field_type: str
|
|
137
|
+
default: Any = None
|
|
138
|
+
assertion: str | None = None
|
|
139
|
+
encrypted: bool = False
|
|
140
|
+
flexible: bool = False
|
|
141
|
+
readonly: bool = False
|
|
142
|
+
value: str | None = None
|
|
143
|
+
comment: str | None = None
|
|
144
|
+
|
|
145
|
+
def forwards(self) -> str:
|
|
146
|
+
parts = [f"DEFINE FIELD {self.name} ON {self.table}"]
|
|
147
|
+
|
|
148
|
+
if self.flexible:
|
|
149
|
+
parts.append("FLEXIBLE")
|
|
150
|
+
|
|
151
|
+
parts.append(f"TYPE {self.field_type}")
|
|
152
|
+
|
|
153
|
+
# For encrypted fields, use VALUE clause with crypto function
|
|
154
|
+
if self.encrypted:
|
|
155
|
+
parts.append("VALUE crypto::argon2::generate($value)")
|
|
156
|
+
elif self.value:
|
|
157
|
+
parts.append(f"VALUE {self.value}")
|
|
158
|
+
|
|
159
|
+
if self.default is not None:
|
|
160
|
+
if isinstance(self.default, str):
|
|
161
|
+
# Check if it's a function call or literal
|
|
162
|
+
if self.default.startswith("time::") or self.default.startswith("rand::"):
|
|
163
|
+
parts.append(f"DEFAULT {self.default}")
|
|
164
|
+
else:
|
|
165
|
+
parts.append(f"DEFAULT '{self.default}'")
|
|
166
|
+
elif isinstance(self.default, bool):
|
|
167
|
+
parts.append(f"DEFAULT {str(self.default).lower()}")
|
|
168
|
+
else:
|
|
169
|
+
parts.append(f"DEFAULT {self.default}")
|
|
170
|
+
|
|
171
|
+
if self.assertion:
|
|
172
|
+
parts.append(f"ASSERT {self.assertion}")
|
|
173
|
+
|
|
174
|
+
if self.readonly:
|
|
175
|
+
parts.append("READONLY")
|
|
176
|
+
|
|
177
|
+
if self.comment:
|
|
178
|
+
parts.append(f"COMMENT '{self.comment}'")
|
|
179
|
+
|
|
180
|
+
return " ".join(parts) + ";"
|
|
181
|
+
|
|
182
|
+
def backwards(self) -> str:
|
|
183
|
+
return f"REMOVE FIELD {self.name} ON {self.table};"
|
|
184
|
+
|
|
185
|
+
def describe(self) -> str:
|
|
186
|
+
return f"Add field {self.name} to {self.table}"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclass
|
|
190
|
+
class DropField(Operation):
|
|
191
|
+
"""
|
|
192
|
+
Remove a field from a table.
|
|
193
|
+
|
|
194
|
+
Example:
|
|
195
|
+
DropField(table="users", name="old_field")
|
|
196
|
+
|
|
197
|
+
Generates:
|
|
198
|
+
REMOVE FIELD old_field ON users;
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
table: str
|
|
202
|
+
name: str
|
|
203
|
+
|
|
204
|
+
def __post_init__(self) -> None:
|
|
205
|
+
self.reversible = False
|
|
206
|
+
|
|
207
|
+
def forwards(self) -> str:
|
|
208
|
+
return f"REMOVE FIELD {self.name} ON {self.table};"
|
|
209
|
+
|
|
210
|
+
def backwards(self) -> str:
|
|
211
|
+
# Cannot reverse without knowing the original field definition
|
|
212
|
+
return ""
|
|
213
|
+
|
|
214
|
+
def describe(self) -> str:
|
|
215
|
+
return f"Drop field {self.name} from {self.table}"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclass
|
|
219
|
+
class AlterField(Operation):
|
|
220
|
+
"""
|
|
221
|
+
Alter an existing field's definition.
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
AlterField(
|
|
225
|
+
table="users",
|
|
226
|
+
name="email",
|
|
227
|
+
field_type="string",
|
|
228
|
+
assertion="is::email($value)"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
Generates:
|
|
232
|
+
DEFINE FIELD email ON users TYPE string ASSERT is::email($value);
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
table: str
|
|
236
|
+
name: str
|
|
237
|
+
field_type: str | None = None
|
|
238
|
+
default: Any = None
|
|
239
|
+
assertion: str | None = None
|
|
240
|
+
encrypted: bool = False
|
|
241
|
+
flexible: bool = False
|
|
242
|
+
readonly: bool = False
|
|
243
|
+
value: str | None = None
|
|
244
|
+
# Store previous definition for rollback
|
|
245
|
+
previous_type: str | None = None
|
|
246
|
+
previous_default: Any = None
|
|
247
|
+
previous_assertion: str | None = None
|
|
248
|
+
|
|
249
|
+
def forwards(self) -> str:
|
|
250
|
+
# DEFINE FIELD is idempotent - it creates or updates
|
|
251
|
+
parts = [f"DEFINE FIELD {self.name} ON {self.table}"]
|
|
252
|
+
|
|
253
|
+
if self.flexible:
|
|
254
|
+
parts.append("FLEXIBLE")
|
|
255
|
+
|
|
256
|
+
if self.field_type:
|
|
257
|
+
parts.append(f"TYPE {self.field_type}")
|
|
258
|
+
|
|
259
|
+
if self.encrypted:
|
|
260
|
+
parts.append("VALUE crypto::argon2::generate($value)")
|
|
261
|
+
elif self.value:
|
|
262
|
+
parts.append(f"VALUE {self.value}")
|
|
263
|
+
|
|
264
|
+
if self.default is not None:
|
|
265
|
+
if isinstance(self.default, str):
|
|
266
|
+
if self.default.startswith("time::") or self.default.startswith("rand::"):
|
|
267
|
+
parts.append(f"DEFAULT {self.default}")
|
|
268
|
+
else:
|
|
269
|
+
parts.append(f"DEFAULT '{self.default}'")
|
|
270
|
+
elif isinstance(self.default, bool):
|
|
271
|
+
parts.append(f"DEFAULT {str(self.default).lower()}")
|
|
272
|
+
else:
|
|
273
|
+
parts.append(f"DEFAULT {self.default}")
|
|
274
|
+
|
|
275
|
+
if self.assertion:
|
|
276
|
+
parts.append(f"ASSERT {self.assertion}")
|
|
277
|
+
|
|
278
|
+
if self.readonly:
|
|
279
|
+
parts.append("READONLY")
|
|
280
|
+
|
|
281
|
+
return " ".join(parts) + ";"
|
|
282
|
+
|
|
283
|
+
def backwards(self) -> str:
|
|
284
|
+
if not self.previous_type:
|
|
285
|
+
return ""
|
|
286
|
+
|
|
287
|
+
parts = [f"DEFINE FIELD {self.name} ON {self.table} TYPE {self.previous_type}"]
|
|
288
|
+
|
|
289
|
+
if self.previous_default is not None:
|
|
290
|
+
if isinstance(self.previous_default, str):
|
|
291
|
+
parts.append(f"DEFAULT '{self.previous_default}'")
|
|
292
|
+
else:
|
|
293
|
+
parts.append(f"DEFAULT {self.previous_default}")
|
|
294
|
+
|
|
295
|
+
if self.previous_assertion:
|
|
296
|
+
parts.append(f"ASSERT {self.previous_assertion}")
|
|
297
|
+
|
|
298
|
+
return " ".join(parts) + ";"
|
|
299
|
+
|
|
300
|
+
def __post_init__(self) -> None:
|
|
301
|
+
"""Set reversible based on whether previous state is stored."""
|
|
302
|
+
object.__setattr__(self, "reversible", self.previous_type is not None)
|
|
303
|
+
|
|
304
|
+
def describe(self) -> str:
|
|
305
|
+
return f"Alter field {self.name} on {self.table}"
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@dataclass
|
|
309
|
+
class CreateIndex(Operation):
|
|
310
|
+
"""
|
|
311
|
+
Create an index on a table.
|
|
312
|
+
|
|
313
|
+
Example:
|
|
314
|
+
CreateIndex(
|
|
315
|
+
table="users",
|
|
316
|
+
name="email_idx",
|
|
317
|
+
fields=["email"],
|
|
318
|
+
unique=True
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
Generates:
|
|
322
|
+
DEFINE INDEX email_idx ON users FIELDS email UNIQUE;
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
table: str
|
|
326
|
+
name: str
|
|
327
|
+
fields: list[str]
|
|
328
|
+
unique: bool = False
|
|
329
|
+
search_analyzer: str | None = None
|
|
330
|
+
comment: str | None = None
|
|
331
|
+
|
|
332
|
+
def forwards(self) -> str:
|
|
333
|
+
fields_str = ", ".join(self.fields)
|
|
334
|
+
parts = [f"DEFINE INDEX {self.name} ON {self.table} FIELDS {fields_str}"]
|
|
335
|
+
|
|
336
|
+
if self.unique:
|
|
337
|
+
parts.append("UNIQUE")
|
|
338
|
+
|
|
339
|
+
if self.search_analyzer:
|
|
340
|
+
parts.append(f"SEARCH ANALYZER {self.search_analyzer}")
|
|
341
|
+
|
|
342
|
+
if self.comment:
|
|
343
|
+
parts.append(f"COMMENT '{self.comment}'")
|
|
344
|
+
|
|
345
|
+
return " ".join(parts) + ";"
|
|
346
|
+
|
|
347
|
+
def backwards(self) -> str:
|
|
348
|
+
return f"REMOVE INDEX {self.name} ON {self.table};"
|
|
349
|
+
|
|
350
|
+
def describe(self) -> str:
|
|
351
|
+
return f"Create index {self.name} on {self.table}"
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@dataclass
|
|
355
|
+
class DropIndex(Operation):
|
|
356
|
+
"""
|
|
357
|
+
Remove an index from a table.
|
|
358
|
+
|
|
359
|
+
Example:
|
|
360
|
+
DropIndex(table="users", name="email_idx")
|
|
361
|
+
|
|
362
|
+
Generates:
|
|
363
|
+
REMOVE INDEX email_idx ON users;
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
table: str
|
|
367
|
+
name: str
|
|
368
|
+
|
|
369
|
+
def __post_init__(self) -> None:
|
|
370
|
+
self.reversible = False
|
|
371
|
+
|
|
372
|
+
def forwards(self) -> str:
|
|
373
|
+
return f"REMOVE INDEX {self.name} ON {self.table};"
|
|
374
|
+
|
|
375
|
+
def backwards(self) -> str:
|
|
376
|
+
return ""
|
|
377
|
+
|
|
378
|
+
def describe(self) -> str:
|
|
379
|
+
return f"Drop index {self.name} from {self.table}"
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@dataclass
|
|
383
|
+
class DefineAccess(Operation):
|
|
384
|
+
"""
|
|
385
|
+
Define access control for authentication (DEFINE ACCESS ... TYPE RECORD).
|
|
386
|
+
|
|
387
|
+
Example:
|
|
388
|
+
DefineAccess(
|
|
389
|
+
name="user_auth",
|
|
390
|
+
table="User",
|
|
391
|
+
signup_fields={"email": "$email", "password": "crypto::argon2::generate($password)"},
|
|
392
|
+
signin_where="email = $email AND crypto::argon2::compare(password, $password)"
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
Generates:
|
|
396
|
+
DEFINE ACCESS user_auth ON DATABASE TYPE RECORD
|
|
397
|
+
SIGNUP (CREATE User SET email = $email, password = crypto::argon2::generate($password))
|
|
398
|
+
SIGNIN (SELECT * FROM User WHERE email = $email AND crypto::argon2::compare(password, $password))
|
|
399
|
+
DURATION FOR TOKEN 15m, FOR SESSION 12h;
|
|
400
|
+
"""
|
|
401
|
+
|
|
402
|
+
name: str
|
|
403
|
+
table: str
|
|
404
|
+
signup_fields: dict[str, str]
|
|
405
|
+
signin_where: str
|
|
406
|
+
duration_token: str = "15m"
|
|
407
|
+
duration_session: str = "12h"
|
|
408
|
+
comment: str | None = None
|
|
409
|
+
|
|
410
|
+
def forwards(self) -> str:
|
|
411
|
+
signup_sets = ", ".join(f"{field} = {expr}" for field, expr in self.signup_fields.items())
|
|
412
|
+
|
|
413
|
+
sql = f"""DEFINE ACCESS {self.name} ON DATABASE TYPE RECORD
|
|
414
|
+
SIGNUP (CREATE {self.table} SET {signup_sets})
|
|
415
|
+
SIGNIN (SELECT * FROM {self.table} WHERE {self.signin_where})
|
|
416
|
+
DURATION FOR TOKEN {self.duration_token}, FOR SESSION {self.duration_session}"""
|
|
417
|
+
|
|
418
|
+
if self.comment:
|
|
419
|
+
sql += f"\n COMMENT '{self.comment}'"
|
|
420
|
+
|
|
421
|
+
return sql + ";"
|
|
422
|
+
|
|
423
|
+
def backwards(self) -> str:
|
|
424
|
+
return f"REMOVE ACCESS {self.name} ON DATABASE;"
|
|
425
|
+
|
|
426
|
+
def describe(self) -> str:
|
|
427
|
+
return f"Define access {self.name} for {self.table}"
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@dataclass
|
|
431
|
+
class RemoveAccess(Operation):
|
|
432
|
+
"""
|
|
433
|
+
Remove an access definition.
|
|
434
|
+
|
|
435
|
+
Example:
|
|
436
|
+
RemoveAccess(name="user_auth")
|
|
437
|
+
|
|
438
|
+
Generates:
|
|
439
|
+
REMOVE ACCESS user_auth ON DATABASE;
|
|
440
|
+
"""
|
|
441
|
+
|
|
442
|
+
name: str
|
|
443
|
+
|
|
444
|
+
def __post_init__(self) -> None:
|
|
445
|
+
self.reversible = False
|
|
446
|
+
|
|
447
|
+
def forwards(self) -> str:
|
|
448
|
+
return f"REMOVE ACCESS {self.name} ON DATABASE;"
|
|
449
|
+
|
|
450
|
+
def backwards(self) -> str:
|
|
451
|
+
return ""
|
|
452
|
+
|
|
453
|
+
def describe(self) -> str:
|
|
454
|
+
return f"Remove access {self.name}"
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
@dataclass
|
|
458
|
+
class DataMigration(Operation):
|
|
459
|
+
"""
|
|
460
|
+
Execute data transformations (UPDATE, DELETE operations on records).
|
|
461
|
+
|
|
462
|
+
Used for the 'upgrade' command to transform existing data.
|
|
463
|
+
|
|
464
|
+
Example:
|
|
465
|
+
DataMigration(
|
|
466
|
+
forwards_sql="UPDATE User SET status = 'active' WHERE status IS NULL;",
|
|
467
|
+
backwards_sql="UPDATE User SET status = NULL WHERE status = 'active';"
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
Or with async functions:
|
|
471
|
+
DataMigration(
|
|
472
|
+
forwards_func=async_migrate_passwords,
|
|
473
|
+
backwards_func=None # Irreversible
|
|
474
|
+
)
|
|
475
|
+
"""
|
|
476
|
+
|
|
477
|
+
forwards_sql: str | None = None
|
|
478
|
+
backwards_sql: str | None = None
|
|
479
|
+
forwards_func: Callable[[], Coroutine[Any, Any, None]] | None = None
|
|
480
|
+
backwards_func: Callable[[], Coroutine[Any, Any, None]] | None = None
|
|
481
|
+
description: str = "Data migration"
|
|
482
|
+
|
|
483
|
+
def __post_init__(self) -> None:
|
|
484
|
+
if not self.forwards_sql and not self.forwards_func:
|
|
485
|
+
raise ValueError("DataMigration requires either forwards_sql or forwards_func")
|
|
486
|
+
self.reversible = bool(self.backwards_sql or self.backwards_func)
|
|
487
|
+
|
|
488
|
+
def forwards(self) -> str:
|
|
489
|
+
return self.forwards_sql or ""
|
|
490
|
+
|
|
491
|
+
def backwards(self) -> str:
|
|
492
|
+
return self.backwards_sql or ""
|
|
493
|
+
|
|
494
|
+
@property
|
|
495
|
+
def has_func(self) -> bool:
|
|
496
|
+
"""Check if this migration uses async functions."""
|
|
497
|
+
return self.forwards_func is not None
|
|
498
|
+
|
|
499
|
+
def describe(self) -> str:
|
|
500
|
+
return self.description
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
@dataclass
|
|
504
|
+
class RawSQL(Operation):
|
|
505
|
+
"""
|
|
506
|
+
Execute raw SurrealQL statements.
|
|
507
|
+
|
|
508
|
+
Use with caution - prefer structured operations when possible.
|
|
509
|
+
|
|
510
|
+
Example:
|
|
511
|
+
RawSQL(
|
|
512
|
+
sql="DEFINE EVENT user_created ON TABLE User WHEN $event = 'CREATE' THEN (CREATE log SET action = 'user_created');",
|
|
513
|
+
reverse_sql="REMOVE EVENT user_created ON TABLE User;"
|
|
514
|
+
)
|
|
515
|
+
"""
|
|
516
|
+
|
|
517
|
+
sql: str
|
|
518
|
+
reverse_sql: str = ""
|
|
519
|
+
description: str = "Raw SQL"
|
|
520
|
+
|
|
521
|
+
def __post_init__(self) -> None:
|
|
522
|
+
self.reversible = bool(self.reverse_sql)
|
|
523
|
+
|
|
524
|
+
def forwards(self) -> str:
|
|
525
|
+
return self.sql
|
|
526
|
+
|
|
527
|
+
def backwards(self) -> str:
|
|
528
|
+
return self.reverse_sql
|
|
529
|
+
|
|
530
|
+
def describe(self) -> str:
|
|
531
|
+
return self.description
|