surrealdb-orm 0.1.3__py3-none-any.whl → 0.5.0__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/__init__.py +78 -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 +594 -135
- surreal_orm/py.typed +0 -0
- surreal_orm/query_set.py +609 -34
- 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.0.dist-info/METADATA +465 -0
- surrealdb_orm-0.5.0.dist-info/RECORD +52 -0
- {surrealdb_orm-0.1.3.dist-info → surrealdb_orm-0.5.0.dist-info}/WHEEL +1 -1
- surrealdb_orm-0.5.0.dist-info/entry_points.txt +2 -0
- {surrealdb_orm-0.1.3.dist-info → surrealdb_orm-0.5.0.dist-info}/licenses/LICENSE +1 -1
- surrealdb_orm-0.1.3.dist-info/METADATA +0 -184
- surrealdb_orm-0.1.3.dist-info/RECORD +0 -11
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Schema state tracking and diffing for migrations.
|
|
3
|
+
|
|
4
|
+
This module provides classes to represent the current schema state
|
|
5
|
+
and compute the differences between two states to generate migrations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .operations import Operation
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class FieldState:
|
|
17
|
+
"""
|
|
18
|
+
Represents the state of a single field in a table.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
name: Field name
|
|
22
|
+
field_type: SurrealDB type (string, int, etc.)
|
|
23
|
+
nullable: Whether the field can be null
|
|
24
|
+
default: Default value if any
|
|
25
|
+
assertion: Validation assertion
|
|
26
|
+
encrypted: Whether the field is encrypted
|
|
27
|
+
flexible: Whether the field accepts additional types
|
|
28
|
+
readonly: Whether the field is read-only
|
|
29
|
+
value: VALUE clause for computed fields
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
name: str
|
|
33
|
+
field_type: str
|
|
34
|
+
nullable: bool = True
|
|
35
|
+
default: Any = None
|
|
36
|
+
assertion: str | None = None
|
|
37
|
+
encrypted: bool = False
|
|
38
|
+
flexible: bool = False
|
|
39
|
+
readonly: bool = False
|
|
40
|
+
value: str | None = None
|
|
41
|
+
|
|
42
|
+
def __eq__(self, other: object) -> bool:
|
|
43
|
+
if not isinstance(other, FieldState):
|
|
44
|
+
return False
|
|
45
|
+
return (
|
|
46
|
+
self.name == other.name
|
|
47
|
+
and self.field_type == other.field_type
|
|
48
|
+
and self.nullable == other.nullable
|
|
49
|
+
and self.default == other.default
|
|
50
|
+
and self.assertion == other.assertion
|
|
51
|
+
and self.encrypted == other.encrypted
|
|
52
|
+
and self.flexible == other.flexible
|
|
53
|
+
and self.readonly == other.readonly
|
|
54
|
+
and self.value == other.value
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def has_changed(self, other: "FieldState") -> bool:
|
|
58
|
+
"""Check if field definition has changed from other."""
|
|
59
|
+
return self != other
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class IndexState:
|
|
64
|
+
"""
|
|
65
|
+
Represents the state of an index on a table.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
name: Index name
|
|
69
|
+
fields: List of field names in the index
|
|
70
|
+
unique: Whether the index enforces uniqueness
|
|
71
|
+
search_analyzer: Full-text search analyzer if any
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
name: str
|
|
75
|
+
fields: list[str]
|
|
76
|
+
unique: bool = False
|
|
77
|
+
search_analyzer: str | None = None
|
|
78
|
+
|
|
79
|
+
def __eq__(self, other: object) -> bool:
|
|
80
|
+
if not isinstance(other, IndexState):
|
|
81
|
+
return False
|
|
82
|
+
return (
|
|
83
|
+
self.name == other.name
|
|
84
|
+
and self.fields == other.fields
|
|
85
|
+
and self.unique == other.unique
|
|
86
|
+
and self.search_analyzer == other.search_analyzer
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class AccessState:
|
|
92
|
+
"""
|
|
93
|
+
Represents the state of an access definition for authentication.
|
|
94
|
+
|
|
95
|
+
Attributes:
|
|
96
|
+
name: Access name
|
|
97
|
+
table: Associated table
|
|
98
|
+
signup_fields: Fields set during signup
|
|
99
|
+
signin_where: WHERE clause for signin
|
|
100
|
+
duration_token: Token duration
|
|
101
|
+
duration_session: Session duration
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
name: str
|
|
105
|
+
table: str
|
|
106
|
+
signup_fields: dict[str, str]
|
|
107
|
+
signin_where: str
|
|
108
|
+
duration_token: str = "15m"
|
|
109
|
+
duration_session: str = "12h"
|
|
110
|
+
|
|
111
|
+
def __eq__(self, other: object) -> bool:
|
|
112
|
+
if not isinstance(other, AccessState):
|
|
113
|
+
return False
|
|
114
|
+
return (
|
|
115
|
+
self.name == other.name
|
|
116
|
+
and self.table == other.table
|
|
117
|
+
and self.signup_fields == other.signup_fields
|
|
118
|
+
and self.signin_where == other.signin_where
|
|
119
|
+
and self.duration_token == other.duration_token
|
|
120
|
+
and self.duration_session == other.duration_session
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class TableState:
|
|
126
|
+
"""
|
|
127
|
+
Represents the complete state of a table.
|
|
128
|
+
|
|
129
|
+
Attributes:
|
|
130
|
+
name: Table name
|
|
131
|
+
schema_mode: SCHEMAFULL or SCHEMALESS
|
|
132
|
+
table_type: Table type classification (normal, user, stream, hash)
|
|
133
|
+
fields: Dict of field name to FieldState
|
|
134
|
+
indexes: Dict of index name to IndexState
|
|
135
|
+
changefeed: Changefeed duration if enabled
|
|
136
|
+
permissions: Dict of action to WHERE condition
|
|
137
|
+
access: Access definition if this is a USER table
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
name: str
|
|
141
|
+
schema_mode: str = "SCHEMAFULL"
|
|
142
|
+
table_type: str = "normal"
|
|
143
|
+
fields: dict[str, FieldState] = field(default_factory=dict)
|
|
144
|
+
indexes: dict[str, IndexState] = field(default_factory=dict)
|
|
145
|
+
changefeed: str | None = None
|
|
146
|
+
permissions: dict[str, str] = field(default_factory=dict)
|
|
147
|
+
access: AccessState | None = None
|
|
148
|
+
|
|
149
|
+
def __eq__(self, other: object) -> bool:
|
|
150
|
+
if not isinstance(other, TableState):
|
|
151
|
+
return False
|
|
152
|
+
return (
|
|
153
|
+
self.name == other.name
|
|
154
|
+
and self.schema_mode == other.schema_mode
|
|
155
|
+
and self.table_type == other.table_type
|
|
156
|
+
and self.fields == other.fields
|
|
157
|
+
and self.indexes == other.indexes
|
|
158
|
+
and self.changefeed == other.changefeed
|
|
159
|
+
and self.permissions == other.permissions
|
|
160
|
+
and self.access == other.access
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class SchemaState:
|
|
166
|
+
"""
|
|
167
|
+
Represents the complete database schema state.
|
|
168
|
+
|
|
169
|
+
This class tracks all tables and their definitions, and can compute
|
|
170
|
+
the operations needed to transform one state into another.
|
|
171
|
+
|
|
172
|
+
Attributes:
|
|
173
|
+
tables: Dict of table name to TableState
|
|
174
|
+
applied_migrations: List of already applied migration names
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
tables: dict[str, TableState] = field(default_factory=dict)
|
|
178
|
+
applied_migrations: list[str] = field(default_factory=list)
|
|
179
|
+
|
|
180
|
+
def diff(self, target: "SchemaState") -> list["Operation"]:
|
|
181
|
+
"""
|
|
182
|
+
Compute operations needed to transform this state into target state.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
target: The desired schema state
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
List of operations to apply (CreateTable, AddField, etc.)
|
|
189
|
+
"""
|
|
190
|
+
from .operations import (
|
|
191
|
+
AddField,
|
|
192
|
+
AlterField,
|
|
193
|
+
CreateIndex,
|
|
194
|
+
CreateTable,
|
|
195
|
+
DefineAccess,
|
|
196
|
+
DropField,
|
|
197
|
+
DropIndex,
|
|
198
|
+
DropTable,
|
|
199
|
+
RemoveAccess,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
operations: list[Operation] = []
|
|
203
|
+
|
|
204
|
+
# Tables to create (in target but not in self)
|
|
205
|
+
for table_name, target_table in target.tables.items():
|
|
206
|
+
if table_name not in self.tables:
|
|
207
|
+
# Create the table
|
|
208
|
+
operations.append(
|
|
209
|
+
CreateTable(
|
|
210
|
+
name=table_name,
|
|
211
|
+
schema_mode=target_table.schema_mode,
|
|
212
|
+
table_type=target_table.table_type,
|
|
213
|
+
changefeed=target_table.changefeed,
|
|
214
|
+
permissions=target_table.permissions or None,
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
# Add all fields
|
|
218
|
+
for field_name, field_state in target_table.fields.items():
|
|
219
|
+
operations.append(
|
|
220
|
+
AddField(
|
|
221
|
+
table=table_name,
|
|
222
|
+
name=field_name,
|
|
223
|
+
field_type=field_state.field_type,
|
|
224
|
+
default=field_state.default,
|
|
225
|
+
assertion=field_state.assertion,
|
|
226
|
+
encrypted=field_state.encrypted,
|
|
227
|
+
flexible=field_state.flexible,
|
|
228
|
+
readonly=field_state.readonly,
|
|
229
|
+
value=field_state.value,
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
# Add all indexes
|
|
233
|
+
for index_name, index_state in target_table.indexes.items():
|
|
234
|
+
operations.append(
|
|
235
|
+
CreateIndex(
|
|
236
|
+
table=table_name,
|
|
237
|
+
name=index_name,
|
|
238
|
+
fields=index_state.fields,
|
|
239
|
+
unique=index_state.unique,
|
|
240
|
+
search_analyzer=index_state.search_analyzer,
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
# Add access definition if present
|
|
244
|
+
if target_table.access:
|
|
245
|
+
operations.append(
|
|
246
|
+
DefineAccess(
|
|
247
|
+
name=target_table.access.name,
|
|
248
|
+
table=table_name,
|
|
249
|
+
signup_fields=target_table.access.signup_fields,
|
|
250
|
+
signin_where=target_table.access.signin_where,
|
|
251
|
+
duration_token=target_table.access.duration_token,
|
|
252
|
+
duration_session=target_table.access.duration_session,
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Tables to drop (in self but not in target)
|
|
257
|
+
for table_name in self.tables:
|
|
258
|
+
if table_name not in target.tables:
|
|
259
|
+
operations.append(DropTable(name=table_name))
|
|
260
|
+
|
|
261
|
+
# Tables to modify (in both)
|
|
262
|
+
for table_name, target_table in target.tables.items():
|
|
263
|
+
if table_name in self.tables:
|
|
264
|
+
current_table = self.tables[table_name]
|
|
265
|
+
|
|
266
|
+
# Check if table definition changed
|
|
267
|
+
if (
|
|
268
|
+
current_table.schema_mode != target_table.schema_mode
|
|
269
|
+
or current_table.changefeed != target_table.changefeed
|
|
270
|
+
or current_table.permissions != target_table.permissions
|
|
271
|
+
):
|
|
272
|
+
# Recreate table definition (DEFINE TABLE is idempotent)
|
|
273
|
+
operations.append(
|
|
274
|
+
CreateTable(
|
|
275
|
+
name=table_name,
|
|
276
|
+
schema_mode=target_table.schema_mode,
|
|
277
|
+
table_type=target_table.table_type,
|
|
278
|
+
changefeed=target_table.changefeed,
|
|
279
|
+
permissions=target_table.permissions or None,
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Fields to add
|
|
284
|
+
for field_name, field_state in target_table.fields.items():
|
|
285
|
+
if field_name not in current_table.fields:
|
|
286
|
+
operations.append(
|
|
287
|
+
AddField(
|
|
288
|
+
table=table_name,
|
|
289
|
+
name=field_name,
|
|
290
|
+
field_type=field_state.field_type,
|
|
291
|
+
default=field_state.default,
|
|
292
|
+
assertion=field_state.assertion,
|
|
293
|
+
encrypted=field_state.encrypted,
|
|
294
|
+
flexible=field_state.flexible,
|
|
295
|
+
readonly=field_state.readonly,
|
|
296
|
+
value=field_state.value,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
elif current_table.fields[field_name] != field_state:
|
|
300
|
+
# Field changed
|
|
301
|
+
current_field = current_table.fields[field_name]
|
|
302
|
+
operations.append(
|
|
303
|
+
AlterField(
|
|
304
|
+
table=table_name,
|
|
305
|
+
name=field_name,
|
|
306
|
+
field_type=field_state.field_type,
|
|
307
|
+
default=field_state.default,
|
|
308
|
+
assertion=field_state.assertion,
|
|
309
|
+
encrypted=field_state.encrypted,
|
|
310
|
+
flexible=field_state.flexible,
|
|
311
|
+
readonly=field_state.readonly,
|
|
312
|
+
value=field_state.value,
|
|
313
|
+
previous_type=current_field.field_type,
|
|
314
|
+
previous_default=current_field.default,
|
|
315
|
+
previous_assertion=current_field.assertion,
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Fields to drop
|
|
320
|
+
for field_name in current_table.fields:
|
|
321
|
+
if field_name not in target_table.fields:
|
|
322
|
+
operations.append(DropField(table=table_name, name=field_name))
|
|
323
|
+
|
|
324
|
+
# Indexes to add
|
|
325
|
+
for index_name, index_state in target_table.indexes.items():
|
|
326
|
+
if index_name not in current_table.indexes:
|
|
327
|
+
operations.append(
|
|
328
|
+
CreateIndex(
|
|
329
|
+
table=table_name,
|
|
330
|
+
name=index_name,
|
|
331
|
+
fields=index_state.fields,
|
|
332
|
+
unique=index_state.unique,
|
|
333
|
+
search_analyzer=index_state.search_analyzer,
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
elif current_table.indexes[index_name] != index_state:
|
|
337
|
+
# Index changed - drop and recreate
|
|
338
|
+
operations.append(DropIndex(table=table_name, name=index_name))
|
|
339
|
+
operations.append(
|
|
340
|
+
CreateIndex(
|
|
341
|
+
table=table_name,
|
|
342
|
+
name=index_name,
|
|
343
|
+
fields=index_state.fields,
|
|
344
|
+
unique=index_state.unique,
|
|
345
|
+
search_analyzer=index_state.search_analyzer,
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Indexes to drop
|
|
350
|
+
for index_name in current_table.indexes:
|
|
351
|
+
if index_name not in target_table.indexes:
|
|
352
|
+
operations.append(DropIndex(table=table_name, name=index_name))
|
|
353
|
+
|
|
354
|
+
# Access definition changes
|
|
355
|
+
if target_table.access and not current_table.access:
|
|
356
|
+
# Add access
|
|
357
|
+
operations.append(
|
|
358
|
+
DefineAccess(
|
|
359
|
+
name=target_table.access.name,
|
|
360
|
+
table=table_name,
|
|
361
|
+
signup_fields=target_table.access.signup_fields,
|
|
362
|
+
signin_where=target_table.access.signin_where,
|
|
363
|
+
duration_token=target_table.access.duration_token,
|
|
364
|
+
duration_session=target_table.access.duration_session,
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
elif current_table.access and not target_table.access:
|
|
368
|
+
# Remove access
|
|
369
|
+
operations.append(RemoveAccess(name=current_table.access.name))
|
|
370
|
+
elif target_table.access and current_table.access and target_table.access != current_table.access:
|
|
371
|
+
# Update access (remove old, add new)
|
|
372
|
+
operations.append(RemoveAccess(name=current_table.access.name))
|
|
373
|
+
operations.append(
|
|
374
|
+
DefineAccess(
|
|
375
|
+
name=target_table.access.name,
|
|
376
|
+
table=table_name,
|
|
377
|
+
signup_fields=target_table.access.signup_fields,
|
|
378
|
+
signin_where=target_table.access.signin_where,
|
|
379
|
+
duration_token=target_table.access.duration_token,
|
|
380
|
+
duration_session=target_table.access.duration_session,
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return operations
|
|
385
|
+
|
|
386
|
+
def clone(self) -> "SchemaState":
|
|
387
|
+
"""Create a deep copy of this schema state."""
|
|
388
|
+
import copy
|
|
389
|
+
|
|
390
|
+
return copy.deepcopy(self)
|
|
391
|
+
|
|
392
|
+
def add_table(self, table: TableState) -> None:
|
|
393
|
+
"""Add a table to the schema."""
|
|
394
|
+
self.tables[table.name] = table
|
|
395
|
+
|
|
396
|
+
def remove_table(self, name: str) -> None:
|
|
397
|
+
"""Remove a table from the schema."""
|
|
398
|
+
if name in self.tables:
|
|
399
|
+
del self.tables[name]
|
|
400
|
+
|
|
401
|
+
def get_table(self, name: str) -> TableState | None:
|
|
402
|
+
"""Get a table by name."""
|
|
403
|
+
return self.tables.get(name)
|
|
404
|
+
|
|
405
|
+
def __repr__(self) -> str:
|
|
406
|
+
return f"SchemaState(tables={list(self.tables.keys())}, migrations={len(self.applied_migrations)})"
|