surreal-orm-lite 0.32.1__tar.gz → 0.34.0__tar.gz
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_lite-0.32.1 → surreal_orm_lite-0.34.0}/CHANGELOG.md +319 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/PKG-INFO +914 -92
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/README.md +913 -91
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/pyproject.toml +1 -1
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/__init__.py +4 -2
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/edge.py +241 -12
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/model_auth.py +129 -15
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/model_base.py +570 -20
- surreal_orm_lite-0.34.0/src/surreal_orm_lite/model_ddl.py +827 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/model_generator.py +206 -68
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/schema.py +759 -16
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/testing/__init__.py +1 -1
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/types.py +34 -1
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/utils.py +23 -1
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/.gitignore +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/LICENSE +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/Makefile +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/_sdk.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/aggregations.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/auth.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/cache.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/changefeed.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/concurrency.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/connection_config.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/connection_manager.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/constants.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/decoding.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/define_parser.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/embedded.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/encoding.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/enum.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/exceptions.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/functions.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/geo.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/introspection.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/live.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/py.typed +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/q.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/query_set.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/routing.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/schema_state.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/signals.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/subquery.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/surql_literal.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/testing/database.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/testing/fixtures.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/time_travel.py +0 -0
- {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/transaction.py +0 -0
|
@@ -5,6 +5,325 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.34.0] - 2026-10-21
|
|
9
|
+
|
|
10
|
+
Model table DDL: a model declares its table's schema mode, change feed, permissions and comment
|
|
11
|
+
with the full SurrealDB-ORM's keys, and `define_table()` writes the table's `DEFINE TABLE` and one
|
|
12
|
+
`DEFINE FIELD` per column from the model's annotations and applies them, keeping what the model
|
|
13
|
+
does not declare. The same code runs on SurrealDB 2.x, 3.x and the embedded engine. The last minor
|
|
14
|
+
of Tier 3's schema and DDL phase; this release also completes the roadmap renumbering below.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`SchemaMode`** (`SCHEMAFULL`, `SCHEMALESS`), the full SurrealDB-ORM's enum, exported from the
|
|
19
|
+
package root, and the full ORM's five table keys on `SurrealConfigDict`: `schema_mode`,
|
|
20
|
+
`changefeed` (a duration, optionally followed by `INCLUDE ORIGINAL`), `permissions` (a mapping of
|
|
21
|
+
`select`/`create`/`update`/`delete` to `FULL`, `NONE`, `WHERE <condition>` or a bare condition;
|
|
22
|
+
an operation left out is `NONE`), `comment` and `flexible_fields`. They are checked when the
|
|
23
|
+
class is defined; a subclass inherits them and `None` resets one. `get_schema_mode()` (`None`
|
|
24
|
+
when not declared), `get_changefeed()` (the normalised text, `"1h INCLUDE ORIGINAL"`) and
|
|
25
|
+
`get_permissions()` (the declared mapping, a copy, as the full ORM's) read them back.
|
|
26
|
+
- **`Model.table_ddl()` and `await Model.define_table()`**: `table_ddl()` renders the model's
|
|
27
|
+
`DEFINE TABLE OVERWRITE` and one `DEFINE FIELD OVERWRITE` per column as a `list[str]`, without a
|
|
28
|
+
connection. `define_table()` reads the table's definition (one `INFO FOR DB`, and one
|
|
29
|
+
`INFO FOR TABLE` for a table that exists), sends the statements one by one, table first and
|
|
30
|
+
parents before children, and returns those applied joined with `"\n"` — the full ORM's return.
|
|
31
|
+
It takes `overwrite=` (`False` writes `IF NOT EXISTS`) and `tx=`, runs on the model's connection,
|
|
32
|
+
is idempotent (run it at start-up) and removes nothing. Each column's kind comes from its
|
|
33
|
+
annotation (strings, numbers, `Decimal`, datetimes, durations, `UUID`, `RecordID`, geometries,
|
|
34
|
+
ranges, enums, literals, lists, tuples, sets, dictionaries and sub-models, one column per
|
|
35
|
+
sub-model field), `option<…>` for a nullable column; a constant default is written `DEFAULT` on a
|
|
36
|
+
column that is not nullable, a computed field `VALUE`, and `FLEXIBLE` on the free-form object
|
|
37
|
+
columns of a `SCHEMAFULL` table. A view model's `define_table()` applies `define_view()`; an edge
|
|
38
|
+
model's applies the relation statement `define_relation()` carries, then the edge's data fields.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
class Person(BaseSurrealModel):
|
|
42
|
+
model_config = SurrealConfigDict(
|
|
43
|
+
table_name="person",
|
|
44
|
+
schema_mode=SchemaMode.SCHEMAFULL,
|
|
45
|
+
changefeed="7d",
|
|
46
|
+
permissions={"select": "FULL", "create": "$auth.id != NONE"},
|
|
47
|
+
comment="People",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
id: str | None = None
|
|
51
|
+
name: str
|
|
52
|
+
age: int = 18
|
|
53
|
+
nickname: str | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
print(*Person.table_ddl(), sep="\n")
|
|
57
|
+
# DEFINE TABLE OVERWRITE `person` TYPE NORMAL SCHEMAFULL COMMENT 'People' CHANGEFEED 7d PERMISSIONS FOR select FULL, FOR create WHERE $auth.id != NONE, FOR update, delete NONE;
|
|
58
|
+
# DEFINE FIELD OVERWRITE `name` ON `person` TYPE string;
|
|
59
|
+
# DEFINE FIELD OVERWRITE `age` ON `person` TYPE int DEFAULT 18;
|
|
60
|
+
# DEFINE FIELD OVERWRITE `nickname` ON `person` TYPE option<string>;
|
|
61
|
+
sql = await Person.define_table() # the same statements, applied; idempotent
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- **`TableType`'s other members take effect** (v0.33.0 recorded them): `TYPE NORMAL` for
|
|
65
|
+
`NORMAL`, `USER`, `STREAM` and `HASH` and for a model without `table_type`, `TYPE ANY` for `ANY`
|
|
66
|
+
(a table that holds records and edges). `USER` makes the table `SCHEMAFULL` and `HASH`
|
|
67
|
+
`SCHEMALESS` when `schema_mode` is left out; `STREAM` adds nothing (declare `changefeed`).
|
|
68
|
+
- README section 35, "Model table DDL (`define_table()`, `schema_mode`, `permissions`)" (declaring
|
|
69
|
+
the table, field types, applying it, views and edges, user models, what SurrealDB checks,
|
|
70
|
+
SurrealDB 2.x and 3.x, transactions, connections and the cache, security, a note for users of the
|
|
71
|
+
full SurrealDB-ORM), updates to sections 16, 19, 31, 33 and 34, a features row, four rows in the
|
|
72
|
+
2.x/3.x behaviour table and one in the embedded-engine table, the comparison and roadmap rows,
|
|
73
|
+
and the example notebook `examples/v0.34.0_model_table_ddl.ipynb`.
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- **`schema_mode`, `changefeed`, `permissions`, `comment` and `flexible_fields` are checked at the
|
|
78
|
+
`class` statement** (they were accepted and ignored before 0.34.0): a value of the wrong type
|
|
79
|
+
raises `TypeError` and one of the wrong form `ValueError` when the module is imported — a
|
|
80
|
+
plain-string `schema_mode` such as `"SCHEMAFULL"` included: use `SchemaMode.SCHEMAFULL`.
|
|
81
|
+
`schema_mode=SchemaMode.SCHEMAFULL` with `extra="allow"`, and `TableType.USER` with
|
|
82
|
+
`SchemaMode.SCHEMALESS`, raise `TypeError`.
|
|
83
|
+
- **A materialized view model with `table_type`** (accepted and ignored in 0.33.0),
|
|
84
|
+
`schema_mode` or `flexible_fields` raises `TypeError` at the `class` statement. A view takes
|
|
85
|
+
`permissions`, `changefeed` and `comment`.
|
|
86
|
+
- **`define_view()` keeps the permissions, comment, change feed, `DROP` and `SCHEMAFULL` set by
|
|
87
|
+
hand on a stored view** unless the model declares them (it reset them). When its statement
|
|
88
|
+
writes permissions, a refusal that quotes the statement is reported without it.
|
|
89
|
+
- **`view_ddl()`, `Edge.relation_ddl()` and `Edge.define_relation()` write the clauses the model
|
|
90
|
+
declares** (unchanged for a model without the keys). `define_relation()` of an edge model that
|
|
91
|
+
declares `schema_mode=SchemaMode.SCHEMAFULL` and has data fields raises `SurrealDbError` before
|
|
92
|
+
any request: it defines no field, so apply that edge with `define_table()`.
|
|
93
|
+
- **`access_ddl()` and `define_access()` follow the model's table keys**: the declared clauses
|
|
94
|
+
replace v0.17.0's defaults, and a model with `table_type` gets `TYPE NORMAL` (or `TYPE ANY`) in
|
|
95
|
+
its table statement. A model whose schema mode is `SCHEMAFULL` — declared, or `TableType.USER` as
|
|
96
|
+
in the full ORM — gets a `SCHEMAFULL` table with one `DEFINE FIELD` per field, so signup and
|
|
97
|
+
signin work on SurrealDB 2.x and 3.x and `define_table()` gives the same table in any call order;
|
|
98
|
+
`TableType.USER` with `extra="allow"` raises `TypeError` there (with `with_table=True`). A 0.33.0
|
|
99
|
+
`TableType.USER` model's table therefore turns `SCHEMAFULL` at its next `define_access()`: a
|
|
100
|
+
record stored before is checked at its next write, where a field the model does not declare is
|
|
101
|
+
refused (SurrealDB 3.x) or dropped (2.x). A model with `AuthenticatedUserMixin` and none of the
|
|
102
|
+
new keys renders v0.17.0's statements unchanged.
|
|
103
|
+
- **`generate_models_from_db()` writes the table's clauses as keys**: `table_type=TableType.ANY`
|
|
104
|
+
for a `TYPE ANY` table, `schema_mode=SchemaMode.SCHEMAFULL`, `changefeed=`, `permissions={…}`
|
|
105
|
+
and `comment=` instead of comments (a comment stays where the `class` statement would refuse the
|
|
106
|
+
key), and the generated module's docstring says `define_table()` may be called on its models.
|
|
107
|
+
README section 31 says so; the v0.30.0, v0.32.0 and v0.33.0 notebooks are re-executed for it.
|
|
108
|
+
- **Docstrings that named v0.37.0 for the table DDL and v0.39.0 for `ModelFactory`** name v0.34.0
|
|
109
|
+
and v0.40.0.
|
|
110
|
+
- **Roadmap: the model table DDL is v0.34.0.** `Model.define_table()`, as close as possible to
|
|
111
|
+
the full SurrealDB-ORM's, was planned with the migrations; it now comes before the search
|
|
112
|
+
versions, whose index and analyzer DDL build on it. The later versions move up one: full-text
|
|
113
|
+
search v0.35.0, vector search v0.36.0, hybrid search v0.37.0, migrations v0.38.0, the CLI
|
|
114
|
+
v0.39.0, test factories v0.40.0 and the Beta phase v0.41.0. `docs/ROADMAP.md` records the
|
|
115
|
+
constraints the earlier versions left v0.34.0, and the README uses the new numbers.
|
|
116
|
+
- **Reading the released entries below**: they keep the numbers they were written with. Full-text
|
|
117
|
+
search v0.34.0 is now v0.35.0; `define_table()` and `TableType`'s other members v0.37.0 are now
|
|
118
|
+
v0.34.0; migrations, `schema_diff()`/`SchemaState.diff()` and the removal of events v0.37.0 are
|
|
119
|
+
now v0.38.0; the `inspectdb` command v0.38.0 is now v0.39.0; `ModelFactory` and `Faker` v0.39.0
|
|
120
|
+
are now v0.40.0; retrying arbitrary queries v0.40.0 is now v0.41.0 (Beta).
|
|
121
|
+
- **README section 33 links the upstream SurrealDB issues** behind its two view workarounds on
|
|
122
|
+
3.x: `id IN` on a view table answers no record
|
|
123
|
+
([surrealdb/surrealdb#7608](https://github.com/surrealdb/surrealdb/issues/7608)), and a view's
|
|
124
|
+
live notifications carry no `id`
|
|
125
|
+
([surrealdb/surrealdb#7607](https://github.com/surrealdb/surrealdb/issues/7607)). The v0.32.0
|
|
126
|
+
row "Live queries and events on a view" of "ORM behaviour: SurrealDB 2.x vs 3.x" links #7607
|
|
127
|
+
too, next to its 3.x workaround (filter on the view's fields, not on `id`).
|
|
128
|
+
|
|
129
|
+
### Fixed
|
|
130
|
+
|
|
131
|
+
- **The 0.33.0 notes: `relation_ddl()` and `define_relation()` are edge-model methods.** The
|
|
132
|
+
[0.33.0] entry below writes `Model.relation_ddl()` and `await Model.define_relation()`. Both are
|
|
133
|
+
class methods of `BaseSurrealEdge` only, `Edge.relation_ddl()` and
|
|
134
|
+
`await Edge.define_relation()`; a node model has neither (`AttributeError`).
|
|
135
|
+
`Model.get_table_type()` is on every model, as written. The ROADMAP's 0.33.0 entry now says so.
|
|
136
|
+
|
|
137
|
+
### Notes
|
|
138
|
+
|
|
139
|
+
- **What the model declares wins; the rest is kept from the database**: the table's schema mode,
|
|
140
|
+
comment, change feed, permissions and `DROP`, and each field's `ASSERT`, `READONLY`,
|
|
141
|
+
`REFERENCE`, permissions, comment and a stored `DEFAULT`, `VALUE` or `COMPUTED` the model does
|
|
142
|
+
not write. Nothing is removed: records, other fields, indexes, events, the views that read the
|
|
143
|
+
table and its live queries stay; a key left out never removes a stored clause.
|
|
144
|
+
`schema_mode=SchemaMode.SCHEMALESS` turns a `SCHEMAFULL` table back and `permissions={}` writes
|
|
145
|
+
`PERMISSIONS NONE`. Before any DDL, `define_table()` refuses a materialized view, a relation
|
|
146
|
+
table (derive the model from `BaseSurrealEdge`) and a definition it cannot read back or carry.
|
|
147
|
+
- **SurrealDB 2.x and the embedded engine empty the objects and arrays of an `any` column and of
|
|
148
|
+
an untyped array or set on a `SCHEMAFULL` table**, optional or not; `FLEXIBLE` keeps them, and
|
|
149
|
+
3.x keeps them without it but refuses `FLEXIBLE` there. They also drop the `dict`, `Any` or
|
|
150
|
+
untyped `list` of a sub-model in a list of sub-models (`list[M]`, at any depth: `items.*.meta`)
|
|
151
|
+
from every item, then refuse the record's next write (`Found NONE for field`), unless the list is
|
|
152
|
+
`FLEXIBLE` too; 3.x keeps those values without it. `define_table()` reads the line
|
|
153
|
+
(`client.version()`, once, only for a `SCHEMAFULL` table with such a column or such a list) and
|
|
154
|
+
writes `FLEXIBLE` on those columns and lists before 3.x, the items' typed fields still checked;
|
|
155
|
+
`table_ddl()`, which needs no connection, writes the 3.x form. An untyped `list` gets an item
|
|
156
|
+
definition `TYPE any` (`bag.*`) on every line, without which 3.x refuses an object item of a
|
|
157
|
+
required untyped array. A union holding a list or a set together with an object or another
|
|
158
|
+
container writes its lists, sets and tuples as an untyped `array` with that item definition,
|
|
159
|
+
since SurrealDB derives one item type for every member of such a union.
|
|
160
|
+
- **A write naming a field a `SCHEMAFULL` table does not define** (a `merge()` key the model does
|
|
161
|
+
not declare, `bulk_update()`, a raw query, a record stored before the table became `SCHEMAFULL`)
|
|
162
|
+
is refused on SurrealDB 3.x (`no such field exists`) and the field dropped, silently, on 2.x and
|
|
163
|
+
the embedded engine: declare the field on the model. SurrealDB checks the records already in the
|
|
164
|
+
table at their next write, not when `define_table()` changes a kind or the schema mode.
|
|
165
|
+
- **`COMPUTED` fields are SurrealDB 3.x's**: `define_table()` keeps a stored `COMPUTED` field
|
|
166
|
+
`COMPUTED` (replacing it by `VALUE` would leave the stored records without the field until their
|
|
167
|
+
next write) and writes a new computed field `VALUE` on both lines; `define_computed_fields()`
|
|
168
|
+
still writes `VALUE` alone. A computed field's expression is checked before any request.
|
|
169
|
+
- **No `DEFAULT` on a nullable column**: SurrealDB applies `DEFAULT` to a `NONE` on every create,
|
|
170
|
+
so the `None` the ORM writes would become the default. A `server_fields` column keeps the
|
|
171
|
+
database's `DEFAULT`, `VALUE` or `COMPUTED`, and the model's constant default is written only
|
|
172
|
+
where the database has none; one that is not nullable and has no constant default is written
|
|
173
|
+
without `DEFAULT`, so every create leaves it out and SurrealDB refuses it until the database
|
|
174
|
+
gives the column a `DEFAULT` or `VALUE` (or annotate it `T | None`).
|
|
175
|
+
- **Each call on a table with a change feed adds one `define_table` entry** to it; `changes()`
|
|
176
|
+
skips it, a raw `SHOW CHANGES` shows it.
|
|
177
|
+
- **Transactions**: with `tx=`, the reads run on committed state before anything is queued and
|
|
178
|
+
the statements join the transaction (a refusal in a buffered one is the transaction's
|
|
179
|
+
`SurrealDbError`). A definition of the same table queued earlier in the block is not seen: define
|
|
180
|
+
it before the block. The reads and the definitions are a check, then the DDL, not a lock.
|
|
181
|
+
- **Security**: names are written in backquotes, kinds built from Python types and `DEFAULT`
|
|
182
|
+
values through the ORM's closed SurrealQL encoder. The declared permission rules, the comment and
|
|
183
|
+
each computed field's expression are checked for mistakes (a comment marker, which would swallow
|
|
184
|
+
the clauses after it, is refused); they are trusted application code, and the checks are not an
|
|
185
|
+
injection defence. A clause the model does not declare is kept from the database, re-checked, so
|
|
186
|
+
applying a model never resets a table's or a field's permissions; a refusal names the table or
|
|
187
|
+
the field, never the statement. Applying needs a privileged session (root, or a namespace or
|
|
188
|
+
database `OWNER` or `EDITOR`).
|
|
189
|
+
- **Differences from the full SurrealDB-ORM** (README section 35 lists them): a model that declares
|
|
190
|
+
no schema mode keeps the stored one and a new table is `SCHEMALESS`; clauses the model does not
|
|
191
|
+
declare are kept; one statement per call, a refusal raising `SurrealDbError` naming the table or
|
|
192
|
+
the field (the full ORM raises `MigrationStatementError` quoting the SQL); `TYPE NORMAL` written
|
|
193
|
+
for a model without `table_type`; `FLEXIBLE` written after `TYPE`; kinds for the rich field
|
|
194
|
+
types; no `DEFAULT` on a nullable field; a view model's `define_table()` defines no field.
|
|
195
|
+
- **SurrealDB 3.1.5 (informative)** behaves as 3.3.4 except that it refuses a Python `set`
|
|
196
|
+
written to a `set` field and an object item of an optional untyped `list`, and that a field
|
|
197
|
+
named after a SurrealQL keyword (`` `select` ``) breaks every later statement on its table — 3.1
|
|
198
|
+
server defects.
|
|
199
|
+
- **Deferred**: declaring field clauses on the model (`ASSERT`, `READONLY`, field permissions and
|
|
200
|
+
comment), typed record links (`record<table>`; `RecordID` is written `record`), refusing
|
|
201
|
+
undeclared field names before a write, and `define_table(tx=…)` seeing a definition queued
|
|
202
|
+
earlier in the block. Migrations (v0.38.0) bring the removal of fields and clauses a model no
|
|
203
|
+
longer declares, and decide whether the lite gains `MigrationStatementError`.
|
|
204
|
+
- **Server pins unchanged**: tested against SurrealDB 3.3.4 and 2.7.0 and on the embedded engine,
|
|
205
|
+
plus 3.1.5 for backward compatibility.
|
|
206
|
+
|
|
207
|
+
## [0.33.0] - 2026-10-20
|
|
208
|
+
|
|
209
|
+
`TYPE RELATION` enforcement: an edge model declares which tables its edges may join and whether
|
|
210
|
+
both ends must exist, with the full SurrealDB-ORM's keys, and `define_relation()` applies the
|
|
211
|
+
constraint to its table while keeping the table's other clauses. SurrealDB enforces it on every
|
|
212
|
+
write, and every edge creation path raises the same ORM errors on SurrealDB 2.x, 3.x and the
|
|
213
|
+
embedded engine. The last minor of Tier 3's schema and DDL phase.
|
|
214
|
+
|
|
215
|
+
### Added
|
|
216
|
+
|
|
217
|
+
- **`SurrealConfigDict(relation_in=…, relation_out=…, enforced=…)` on `BaseSurrealEdge`**, the
|
|
218
|
+
full SurrealDB-ORM's keys for SurrealDB's `TYPE RELATION IN … OUT … ENFORCED`: `relation_in`
|
|
219
|
+
names the tables an edge's `in` may come from, `relation_out` those its `out` may point to (one
|
|
220
|
+
name, several separated by `|`, or a list or tuple; `None` or an empty value means any table),
|
|
221
|
+
and `enforced=True` makes SurrealDB refuse an edge whose endpoint record does not exist. The keys
|
|
222
|
+
are checked when the class is defined; a subclass inherits them and `None` resets one.
|
|
223
|
+
`get_relation_in()` and `get_relation_out()` return tuples, in the shape of introspection's
|
|
224
|
+
`TableState.relation_in`/`relation_out`, and `is_enforced()` a bool.
|
|
225
|
+
- **`TableType`**, the full SurrealDB-ORM's enum (`NORMAL`, `USER`, `STREAM`, `HASH`, `RELATION`,
|
|
226
|
+
`ANY`, same values), exported from the package root, and `Model.get_table_type()` on every
|
|
227
|
+
model: `TableType.RELATION` for an edge model, else the configured `table_type`, else
|
|
228
|
+
`TableType.NORMAL`. `table_type=TableType.RELATION` is optional on an edge model; the other
|
|
229
|
+
members are recorded for v0.37.0 and have no effect before it.
|
|
230
|
+
- **`Model.relation_ddl()` and `await Model.define_relation()`**: `relation_ddl()` renders
|
|
231
|
+
``DEFINE TABLE OVERWRITE `<table>` TYPE RELATION IN … OUT … ENFORCED;`` without a connection
|
|
232
|
+
(pure, deterministic; `overwrite=False` writes `IF NOT EXISTS`). `define_relation()` reads the
|
|
233
|
+
table's definition once (`INFO FOR DB`), then sends one `DEFINE TABLE OVERWRITE` that writes the
|
|
234
|
+
model's `TYPE RELATION` clause and keeps the table's schema mode, `DROP`, comment, change feed
|
|
235
|
+
and permissions, as well as its records, fields, indexes, events, the views that read it and
|
|
236
|
+
its live queries. It returns the statement applied, is idempotent (run it at start-up), takes
|
|
237
|
+
`tx=`, runs on the model's connection and is enforced at once on every connection. A model
|
|
238
|
+
without the keys removes the constraint the same way.
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
class Likes(BaseSurrealEdge):
|
|
242
|
+
model_config = SurrealConfigDict(
|
|
243
|
+
table_name="likes", relation_in="person", relation_out=["post", "book"], enforced=True
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
await Likes.define_relation()
|
|
248
|
+
await ada.relate(Likes, graphs) # accepted
|
|
249
|
+
await acme.relate(Likes, graphs) # SurrealDbError: a company is not a person
|
|
250
|
+
await ada.relate(Likes, RecordID("book", "missing")) # SurrealDbNotFoundError (ENFORCED)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
- README section 34, "Relation constraints (`relation_in`, `define_relation()`)" (declaring the
|
|
254
|
+
constraint, applying it, what SurrealDB refuses, records already in the table, removing it,
|
|
255
|
+
transactions, connections and the query cache, SurrealDB 2.x and 3.x, security, a note for users
|
|
256
|
+
of the full SurrealDB-ORM), a features row, three rows in the 2.x/3.x behaviour table and one in
|
|
257
|
+
the embedded-engine table, the comparison and roadmap rows, and the example notebook
|
|
258
|
+
`examples/v0.33.0_type_relation_enforcement.ipynb`.
|
|
259
|
+
|
|
260
|
+
### Changed
|
|
261
|
+
|
|
262
|
+
- **`relation_in`, `relation_out`, `enforced` and `table_type` are checked at the `class`
|
|
263
|
+
statement** (they were accepted and ignored before 0.33.0). On a node model
|
|
264
|
+
(`BaseSurrealModel`), `relation_in`, `relation_out` and `enforced` (even empty or `False`) and
|
|
265
|
+
`table_type=TableType.RELATION` raise `TypeError`: derive the model from `BaseSurrealEdge`. A
|
|
266
|
+
`table_type` that is not a `TableType` (a plain `"relation"` string included) or an `enforced`
|
|
267
|
+
that is not a bool raises `TypeError`; a table name that is not an identifier raises
|
|
268
|
+
`ValueError`.
|
|
269
|
+
- **The string form `relate("<edge table>", …)` translates server errors**, as the typed form does
|
|
270
|
+
since v0.22.0: an endpoint the table's constraint refuses raises `SurrealDbError`, a missing
|
|
271
|
+
endpoint of an `ENFORCED` table `SurrealDbNotFoundError`, a duplicate pair on a `UNIQUE` index
|
|
272
|
+
`SurrealDbError`, instead of the SDK's `ServerError` (kept as the `__cause__`). Inside a
|
|
273
|
+
transaction nothing changes.
|
|
274
|
+
- **`generate_models_from_db()` writes a relation table's ends as keys**: `relation_in=`,
|
|
275
|
+
`relation_out=` (a string for one name, a list for several, in the server's order) and
|
|
276
|
+
`enforced=True` right after `table_name=`, instead of the `# IN … OUT … ENFORCED` comment, so a
|
|
277
|
+
relation table round-trips (define → introspect → generate → define gives the same
|
|
278
|
+
`TableState`). A table name that is not an identifier keeps the comment. README section 31 says
|
|
279
|
+
so; the v0.30.0 introspection notebook is re-executed for it.
|
|
280
|
+
|
|
281
|
+
### Notes
|
|
282
|
+
|
|
283
|
+
- **Enforcement is SurrealDB's**: the ORM adds no check of its own before a write. An edge refused
|
|
284
|
+
by the constraint, through any creation path (`relate()` typed or by table name, an edge's
|
|
285
|
+
`save()`, `objects().bulk_create()`), raises `SurrealDbError` for an endpoint from a table
|
|
286
|
+
outside `IN`/`OUT` and `SurrealDbNotFoundError` (a `SurrealDbError`) for a missing endpoint with
|
|
287
|
+
`ENFORCED`; a batch with one refused edge writes nothing; inside a transaction the refusal is the
|
|
288
|
+
transaction's `SurrealDbError`. A plain record written into the relation table, and an update of
|
|
289
|
+
an edge stored before the constraint that it refuses, raise the SDK's `InternalError`.
|
|
290
|
+
- **Records already in the table are not checked**: a nonconforming edge stays (its updates are
|
|
291
|
+
refused), a dangling edge stays and can be updated (`ENFORCED` is checked at creation), and
|
|
292
|
+
`delete()` always works. README section 34 gives the two queries that find them.
|
|
293
|
+
- **`define_relation()` refuses before any DDL** a materialized view (the `OVERWRITE` would drop
|
|
294
|
+
its query), a definition the ORM cannot read back, and `overwrite=False` on a table that exists
|
|
295
|
+
and is not a relation table (`IF NOT EXISTS` would leave the constraint unapplied). The read and
|
|
296
|
+
the definition are a check, then the DDL, not a lock. In a transaction the read runs on committed
|
|
297
|
+
state before anything is queued: define the table's other clauses before the block.
|
|
298
|
+
- **SurrealDB rewrites the `in` and `out` field definitions** from `IN` and `OUT`: an `ASSERT`
|
|
299
|
+
written on them by hand is lost. Turning the table back into `TYPE ANY` is not offered and does
|
|
300
|
+
not lift the constraint on any line; a keyless `define_relation()` does.
|
|
301
|
+
- **SurrealDB 2.x and 3.x**: `define_relation()` is the same on every line and on the embedded
|
|
302
|
+
engine. The refusal's wording differs (`Found … but expected a record<…>` on 2.x and the
|
|
303
|
+
embedded engine, `Couldn't coerce value for field …` on 3.x), and an endpoint both outside
|
|
304
|
+
`IN`/`OUT` and missing, on an `ENFORCED` table, raises `SurrealDbNotFoundError` on 2.x and the
|
|
305
|
+
embedded engine and `SurrealDbError` on 3.x; `except SurrealDbError` handles both. A raw
|
|
306
|
+
`ALTER TABLE … TYPE RELATION …` of a table already written to is not enforced on 2.x and the
|
|
307
|
+
embedded engine until a `DEFINE TABLE OVERWRITE` of the table; the ORM never sends
|
|
308
|
+
`ALTER TABLE`.
|
|
309
|
+
- **Security**: the table name and every `relation_in`/`relation_out` name are checked as
|
|
310
|
+
identifiers and written in backquotes; no other application value reaches the statement. The
|
|
311
|
+
clauses `define_relation()` keeps are the database's own definition, the comment re-quoted and
|
|
312
|
+
each permission rule re-checked; a value that fails is not written. A refusal names the table,
|
|
313
|
+
never the statement. The constraint is not an access control, and applying it needs a privileged
|
|
314
|
+
system user (root, or a namespace or database `OWNER` or `EDITOR`).
|
|
315
|
+
- **Differences from the full SurrealDB-ORM**: the same keys and `TableType`, on `BaseSurrealEdge`
|
|
316
|
+
(a `BaseSurrealModel` with `table_type=TableType.RELATION` raises `TypeError`);
|
|
317
|
+
`relation_ddl()` and `define_relation()` where the full ORM applies the DDL through migrations or
|
|
318
|
+
`define_table()`; names are identifiers in backquotes and a pipe-joined string is parsed;
|
|
319
|
+
`get_relation_in()`, `get_relation_out()` and `is_enforced()` are lite additions.
|
|
320
|
+
- **Deferred**: migrations (`CreateTable`/`AlterTable` with the relation keys) and
|
|
321
|
+
`define_table()`, which will apply an edge model through `define_relation()`, come with v0.37.0;
|
|
322
|
+
`TableType`'s other members have no effect until then. The lite has no schema-mode, permissions,
|
|
323
|
+
change-feed or comment keys yet, so `define_relation()` keeps those clauses from the database.
|
|
324
|
+
- **Server pins unchanged**: tested against SurrealDB 3.3.4 and 2.7.0 and on the embedded engine,
|
|
325
|
+
plus 3.1.5 for backward compatibility.
|
|
326
|
+
|
|
8
327
|
## [0.32.1] - 2026-10-20
|
|
9
328
|
|
|
10
329
|
Two follow-ups to the materialized views of v0.32.0 (#286): a function SurrealDB lists as unusable
|