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.
Files changed (48) hide show
  1. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/CHANGELOG.md +319 -0
  2. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/PKG-INFO +914 -92
  3. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/README.md +913 -91
  4. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/pyproject.toml +1 -1
  5. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/__init__.py +4 -2
  6. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/edge.py +241 -12
  7. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/model_auth.py +129 -15
  8. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/model_base.py +570 -20
  9. surreal_orm_lite-0.34.0/src/surreal_orm_lite/model_ddl.py +827 -0
  10. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/model_generator.py +206 -68
  11. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/schema.py +759 -16
  12. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/testing/__init__.py +1 -1
  13. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/types.py +34 -1
  14. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/utils.py +23 -1
  15. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/.gitignore +0 -0
  16. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/LICENSE +0 -0
  17. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/Makefile +0 -0
  18. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/_sdk.py +0 -0
  19. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/aggregations.py +0 -0
  20. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/auth.py +0 -0
  21. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/cache.py +0 -0
  22. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/changefeed.py +0 -0
  23. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/concurrency.py +0 -0
  24. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/connection_config.py +0 -0
  25. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/connection_manager.py +0 -0
  26. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/constants.py +0 -0
  27. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/decoding.py +0 -0
  28. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/define_parser.py +0 -0
  29. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/embedded.py +0 -0
  30. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/encoding.py +0 -0
  31. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/enum.py +0 -0
  32. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/exceptions.py +0 -0
  33. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/functions.py +0 -0
  34. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/geo.py +0 -0
  35. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/introspection.py +0 -0
  36. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/live.py +0 -0
  37. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/py.typed +0 -0
  38. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/q.py +0 -0
  39. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/query_set.py +0 -0
  40. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/routing.py +0 -0
  41. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/schema_state.py +0 -0
  42. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/signals.py +0 -0
  43. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/subquery.py +0 -0
  44. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/surql_literal.py +0 -0
  45. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/testing/database.py +0 -0
  46. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/testing/fixtures.py +0 -0
  47. {surreal_orm_lite-0.32.1 → surreal_orm_lite-0.34.0}/src/surreal_orm_lite/time_travel.py +0 -0
  48. {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