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.

Files changed (50) hide show
  1. surreal_orm/__init__.py +72 -3
  2. surreal_orm/aggregations.py +164 -0
  3. surreal_orm/auth/__init__.py +15 -0
  4. surreal_orm/auth/access.py +167 -0
  5. surreal_orm/auth/mixins.py +302 -0
  6. surreal_orm/cli/__init__.py +15 -0
  7. surreal_orm/cli/commands.py +369 -0
  8. surreal_orm/connection_manager.py +58 -18
  9. surreal_orm/fields/__init__.py +36 -0
  10. surreal_orm/fields/encrypted.py +166 -0
  11. surreal_orm/fields/relation.py +465 -0
  12. surreal_orm/migrations/__init__.py +51 -0
  13. surreal_orm/migrations/executor.py +380 -0
  14. surreal_orm/migrations/generator.py +272 -0
  15. surreal_orm/migrations/introspector.py +305 -0
  16. surreal_orm/migrations/migration.py +188 -0
  17. surreal_orm/migrations/operations.py +531 -0
  18. surreal_orm/migrations/state.py +406 -0
  19. surreal_orm/model_base.py +530 -44
  20. surreal_orm/query_set.py +609 -33
  21. surreal_orm/relations.py +645 -0
  22. surreal_orm/surreal_function.py +95 -0
  23. surreal_orm/surreal_ql.py +113 -0
  24. surreal_orm/types.py +86 -0
  25. surreal_sdk/README.md +79 -0
  26. surreal_sdk/__init__.py +151 -0
  27. surreal_sdk/connection/__init__.py +17 -0
  28. surreal_sdk/connection/base.py +516 -0
  29. surreal_sdk/connection/http.py +421 -0
  30. surreal_sdk/connection/pool.py +244 -0
  31. surreal_sdk/connection/websocket.py +519 -0
  32. surreal_sdk/exceptions.py +71 -0
  33. surreal_sdk/functions.py +607 -0
  34. surreal_sdk/protocol/__init__.py +13 -0
  35. surreal_sdk/protocol/rpc.py +218 -0
  36. surreal_sdk/py.typed +0 -0
  37. surreal_sdk/pyproject.toml +49 -0
  38. surreal_sdk/streaming/__init__.py +31 -0
  39. surreal_sdk/streaming/change_feed.py +278 -0
  40. surreal_sdk/streaming/live_query.py +265 -0
  41. surreal_sdk/streaming/live_select.py +369 -0
  42. surreal_sdk/transaction.py +386 -0
  43. surreal_sdk/types.py +346 -0
  44. surrealdb_orm-0.5.1.dist-info/METADATA +465 -0
  45. surrealdb_orm-0.5.1.dist-info/RECORD +52 -0
  46. {surrealdb_orm-0.1.4.dist-info → surrealdb_orm-0.5.1.dist-info}/WHEEL +1 -1
  47. surrealdb_orm-0.5.1.dist-info/entry_points.txt +2 -0
  48. {surrealdb_orm-0.1.4.dist-info → surrealdb_orm-0.5.1.dist-info}/licenses/LICENSE +1 -1
  49. surrealdb_orm-0.1.4.dist-info/METADATA +0 -184
  50. 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