surreal-orm-lite 0.31.2__tar.gz → 0.32.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.31.2 → surreal_orm_lite-0.32.0}/CHANGELOG.md +145 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/PKG-INFO +264 -15
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/README.md +263 -14
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/pyproject.toml +1 -1
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/__init__.py +1 -1
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/define_parser.py +38 -1
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/live.py +5 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/model_base.py +282 -5
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/model_generator.py +49 -15
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/q.py +18 -2
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/query_set.py +32 -11
- surreal_orm_lite-0.32.0/src/surreal_orm_lite/schema.py +1147 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/subquery.py +18 -1
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/utils.py +28 -6
- surreal_orm_lite-0.31.2/src/surreal_orm_lite/schema.py +0 -437
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/.gitignore +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/LICENSE +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/Makefile +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/_sdk.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/aggregations.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/auth.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/cache.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/changefeed.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/concurrency.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/connection_config.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/connection_manager.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/constants.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/decoding.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/edge.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/embedded.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/encoding.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/enum.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/exceptions.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/functions.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/geo.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/introspection.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/model_auth.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/py.typed +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/routing.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/schema_state.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/signals.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/surql_literal.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/testing/__init__.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/testing/database.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/testing/fixtures.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/time_travel.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/transaction.py +0 -0
- {surreal_orm_lite-0.31.2 → surreal_orm_lite-0.32.0}/src/surreal_orm_lite/types.py +0 -0
|
@@ -5,6 +5,151 @@ 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.32.0] - 2026-10-20
|
|
9
|
+
|
|
10
|
+
Materialized views as read-only models: a model declares the `SELECT` that SurrealDB computes a
|
|
11
|
+
table from (`DEFINE TABLE … AS SELECT`), applies it idempotently with `define_view()` and reads it
|
|
12
|
+
like any model, while every ORM write of it is refused. The same code runs on SurrealDB 2.x, 3.x
|
|
13
|
+
and the embedded engine; a grouped view that SurrealDB 2.x would compute wrongly is refused before
|
|
14
|
+
any DDL there. The third minor of Tier 3.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`SurrealConfigDict(view_query="SELECT …")`**: the model reads a materialized view, a table
|
|
19
|
+
SurrealDB computes from that query and keeps up to date as the tables it selects from change.
|
|
20
|
+
The query is one `SELECT … FROM <table> [WHERE …] [GROUP BY … | GROUP ALL]` (a final `;` is
|
|
21
|
+
dropped), checked when the class is defined: a mistake raises `TypeError` or `ValueError` at the
|
|
22
|
+
`class` statement. `Model.get_view_query()` returns it as checked. `Model.view_ddl()` renders
|
|
23
|
+
``DEFINE TABLE OVERWRITE `<table>` AS (<query>);`` without a connection,
|
|
24
|
+
`await Model.define_view()` applies it on the model's connection (idempotent: run it at
|
|
25
|
+
start-up) and returns it, and `await Model.remove_view()` writes `REMOVE TABLE IF EXISTS`.
|
|
26
|
+
`overwrite=False` writes `IF NOT EXISTS`, `missing_ok=False` raises `SurrealDbNotFoundError` for
|
|
27
|
+
a missing view, and both appliers take `tx=`. A subclass inherits `view_query`;
|
|
28
|
+
`view_query=None` makes it an ordinary model.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
class OrderStats(BaseSurrealModel):
|
|
32
|
+
model_config = SurrealConfigDict(
|
|
33
|
+
table_name="order_stats",
|
|
34
|
+
view_query="SELECT status, count() AS total, math::sum(amount) AS revenue FROM orders GROUP BY status",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
id: RecordID | None = None
|
|
38
|
+
status: str
|
|
39
|
+
total: int
|
|
40
|
+
revenue: float
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
await OrderStats.define_view() # idempotent (OVERWRITE)
|
|
44
|
+
paid = await OrderStats.objects().get(["paid"]) # a grouped view's id: its group values
|
|
45
|
+
await paid.merge(total=0) # TypeError: Cannot modify materialized view …
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- **Every write of a view model raises `TypeError`** before any signal, transaction or request:
|
|
49
|
+
`save()`, `upsert()`, `update()`, `merge()`, `patch()`, `delete()`, the atomic helpers
|
|
50
|
+
(`atomic_append()` … `atomic_remove_many()`), and the QuerySet's `bulk_create()`,
|
|
51
|
+
`bulk_update()`, `bulk_delete()`, `patch()`, `update_or_create()`, `get_or_create()` (even when
|
|
52
|
+
the record exists) and `delete_table()`. The message starts with the full SurrealDB-ORM's
|
|
53
|
+
`Cannot modify materialized view '<table>'` and names the call: write to the tables the view
|
|
54
|
+
selects from instead. Reads, `refresh()`, the relation helpers, `define_events()` and
|
|
55
|
+
`remove_event()` stay allowed, since they do not write the view's table.
|
|
56
|
+
- **Array record ids**: `get(["paid"])` and `filter(id=["paid"])` address a grouped view's record
|
|
57
|
+
(`order_stats:['paid']`, the array of its group values); a `GROUP ALL` view's id is `[]`, read
|
|
58
|
+
with `first()`.
|
|
59
|
+
- README section 33, "Materialized views (`view_query`, `define_view()`)" (declaring a view,
|
|
60
|
+
applying and removing, reading, refused writes, transactions, connections and the query cache,
|
|
61
|
+
SurrealDB 2.x and 3.x, security, a note for users of the full SurrealDB-ORM), a features row,
|
|
62
|
+
eight rows in the 2.x/3.x behaviour table and one in the embedded-engine table, the comparison
|
|
63
|
+
and roadmap rows, and the example notebook `examples/v0.32.0_materialized_views.ipynb`.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- **`generate_models_from_db()` writes a view model**: a view table becomes a class with
|
|
68
|
+
`view_query=…` in its `SurrealConfigDict` and `id: RecordID | None = None`, marked
|
|
69
|
+
`# Read-only materialized view`, instead of a class carrying a `# View: AS …` comment. A view the
|
|
70
|
+
lite cannot declare (an edge, a computed field, a query `view_query` refuses) keeps that comment.
|
|
71
|
+
README section 31 says so.
|
|
72
|
+
- **Cached reads of a view model are dropped by every ORM write** (README section 29): reads of a
|
|
73
|
+
view model, and reads whose subqueries read one, since a write to a source table changes the view
|
|
74
|
+
and the ORM does not know which tables a view reads. `define_view()` and `remove_view()` drop
|
|
75
|
+
cached reads like every write. Writes by raw SurrealQL or by another process are not seen, as for
|
|
76
|
+
every table.
|
|
77
|
+
- A list or tuple given as a record id (`get()`, `filter(id=...)`, `id__in` items,
|
|
78
|
+
`get_or_create()`/`update_or_create()` id criteria) is now an array record id; it used to be the
|
|
79
|
+
string of its `repr()`.
|
|
80
|
+
|
|
81
|
+
### Notes
|
|
82
|
+
|
|
83
|
+
- **A view's query is trusted application code, never user input.** It is written into `AS (…)`
|
|
84
|
+
as given, since SurrealDB takes no bound parameter in a definition. Its structural check
|
|
85
|
+
(literals and quoted identifiers terminated, brackets balanced, no NUL, no SurrealQL comment
|
|
86
|
+
marker outside a literal, one `SELECT` and no `;` outside brackets) catches mistakes —
|
|
87
|
+
unbalanced brackets, a comment that would swallow the parenthesis the ORM appends, a second
|
|
88
|
+
statement where one SELECT is expected — and is not an injection defence. To filter on a value,
|
|
89
|
+
store it in a record or a `DEFINE PARAM` and read it from the query; never format it into the
|
|
90
|
+
text. The table name is checked as an identifier and quoted. A refusal names the view's table,
|
|
91
|
+
never the statement, and keeps the server's reason whole, which names a missing source table.
|
|
92
|
+
- **Grouped views on SurrealDB 2.x and the embedded engine**: those engines keep only whole
|
|
93
|
+
aggregates (`count()`, `math::sum()`, `math::mean()`, `math::min()`, `math::max()`,
|
|
94
|
+
`time::min()`, `time::max()`) and `GROUP BY` fields up to date, and compute anything else once
|
|
95
|
+
per source record. So `define_view()` raises `SurrealDbError` before any DDL there for
|
|
96
|
+
`math::stddev()` and `math::variance()`, an expression around an aggregate
|
|
97
|
+
(`math::sum(amount) / count()`), a field outside `GROUP BY` and outside an aggregate (3.x refuses
|
|
98
|
+
that one too) and a `GROUP BY` on an alias. SurrealDB 3.x keeps them all up to date.
|
|
99
|
+
- **Removing a view that another view reads**: SurrealDB 3.x refuses it, and `remove_view()`
|
|
100
|
+
passes the refusal on as `SurrealDbError`; on 2.x and the embedded engine `remove_view()` raises
|
|
101
|
+
`SurrealDbError` before any DDL, since those engines would turn each view reading it into an
|
|
102
|
+
ordinary table that keeps its last records and no longer updates. Remove the outer views first.
|
|
103
|
+
- **The table guard**: `define_view()` never turns a table that holds records into a view, since
|
|
104
|
+
SurrealDB would delete them, and `define_view(overwrite=False)` refuses any ordinary table, which
|
|
105
|
+
`IF NOT EXISTS` would silently keep. An ordinary table without records, as `define_events()` or,
|
|
106
|
+
on 2.x, a `watch()` run before `define_view()` leaves it, becomes the view. `remove_view()`
|
|
107
|
+
refuses a table that is not a view. The guard is a check-then-act on committed state, not a
|
|
108
|
+
lock.
|
|
109
|
+
- **SurrealDB 2.x and 3.x**: raw writes to a view (raw SurrealQL, not the ORM) are accepted by 2.x
|
|
110
|
+
and 3.1, which change the view's records, and refused by 3.2 and later; a sum of integers is an
|
|
111
|
+
`int` and a mean a `Decimal` on 2.x, both `float` on 3.x (declare them `float`); `count()` of an
|
|
112
|
+
array field adds the arrays' lengths on 2.x and counts the records whose array is not empty on
|
|
113
|
+
3.x; live queries and events see an emptied group as an `UPDATE` then a `DELETE` on 2.x, a
|
|
114
|
+
`DELETE` on 3.x. The embedded engine behaves as a 2.x server, without live queries.
|
|
115
|
+
- **A source write missing an aggregated field is refused** by SurrealDB on every line
|
|
116
|
+
([surrealdb/surrealdb#7447](https://github.com/surrealdb/surrealdb/issues/7447)): write
|
|
117
|
+
`math::sum(amount ?? 0)`.
|
|
118
|
+
- **`define_view()` recomputes the view** (`OVERWRITE`): each call costs one pass over the source
|
|
119
|
+
and drops the events, fields and indexes defined on the view, so run `define_events()` after
|
|
120
|
+
`define_view()`. A view is defined without a `PERMISSIONS` clause, so SurrealDB applies its
|
|
121
|
+
default `PERMISSIONS NONE`: a record user reads nothing from it, and `define_view()` resets
|
|
122
|
+
permissions set by hand. Applying needs a privileged system user (root, or a namespace or
|
|
123
|
+
database `OWNER` or `EDITOR`): run `define_view()` at start-up or deployment.
|
|
124
|
+
- **The ORM's write refusal guards against mistakes and is not an access control**: on 2.x and
|
|
125
|
+
3.1 the server accepts writes to a view from any session allowed to write the table.
|
|
126
|
+
- On a view model, `filter(id__in=...)` is sent as `$ids CONTAINS id`: SurrealDB 3.x answers no
|
|
127
|
+
record to `id IN` on a view table, while `CONTAINS` is right on every line. Write raw SurrealQL
|
|
128
|
+
over a view the same way.
|
|
129
|
+
- On SurrealDB 3.x, `live()` takes a view instance's id from the notification's record, since the
|
|
130
|
+
server's view notifications carry none; `watch()` yields the envelopes as received (read
|
|
131
|
+
`record`). A live query on a view filtered on `id` never notifies on 3.x: filter on the view's
|
|
132
|
+
fields.
|
|
133
|
+
- On SurrealDB 3.1, tested for backward compatibility only, the server refuses to redefine a view
|
|
134
|
+
that holds records: `define_view()` raises `SurrealDbError` ("… The key being inserted already
|
|
135
|
+
exists") and the view stays as it was, where 3.2 and later, 2.x and the embedded engine
|
|
136
|
+
recompute it. On 3.1, call `remove_view()` before `define_view()`.
|
|
137
|
+
- **Differences from the full SurrealDB-ORM**: the same `view_query` key and `_is_view()`, plus
|
|
138
|
+
`get_view_query()`; the same `TypeError` first words on every write path (full ORM: `save()`,
|
|
139
|
+
`update()`, `merge()`, `delete()` only); `define_view()`, `remove_view()` and `view_ddl()` where
|
|
140
|
+
the full ORM applies a view through migrations or `define_table()`. The documented
|
|
141
|
+
`MaterializedView` base class with `source_query` is not shipped by the full ORM either: declare
|
|
142
|
+
`view_query` on a `BaseSurrealModel`.
|
|
143
|
+
- **Deferred**: applying a view through migrations (`CreateTable`/`AlterTable`) and
|
|
144
|
+
`define_table()`, which will accept a view model and apply it through `view_ddl()`, come with
|
|
145
|
+
v0.37.0; table permissions for record users and a view query built from a QuerySet come later.
|
|
146
|
+
- The 0.31.0 notes placed the `SurrealConfigDict` keys that generated classes stand in for with
|
|
147
|
+
comments in v0.32.0–v0.34.0: `view_query` arrives with this release, and README section 31 now
|
|
148
|
+
places the others (`table_type`, `permissions`, `changefeed`, `relation_in`, `relation_out`,
|
|
149
|
+
`enforced`) in later minors.
|
|
150
|
+
- **Server pins unchanged**: tested against SurrealDB 3.3.4 and 2.7.0 and on the embedded engine,
|
|
151
|
+
plus 3.1.5 for backward compatibility.
|
|
152
|
+
|
|
8
153
|
## [0.31.2] - 2026-10-19
|
|
9
154
|
|
|
10
155
|
Transaction errors give the server's reason and the failed statement's position, without the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: surreal-orm-lite
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.32.0
|
|
4
4
|
Summary: Lightweight Django-style ORM for SurrealDB using the official Python SDK. Async support with Pydantic validation.
|
|
5
5
|
Project-URL: Homepage, https://github.com/EulogySnowfall/SurrealDB-ORM-lite
|
|
6
6
|
Project-URL: Documentation, https://github.com/EulogySnowfall/SurrealDB-ORM-lite
|
|
@@ -266,6 +266,7 @@ embedded engine refuse one above `4294967295` (`2**32 - 1`), SurrealDB 3.x reads
|
|
|
266
266
|
| Multiple databases (named connections, `using()`) | ✅ |
|
|
267
267
|
| Schema introspection (`DatabaseIntrospector`, `generate_models_from_db()`) | ✅ |
|
|
268
268
|
| Server-side events & function DDL (`Event`, `define_function()`) | ✅ |
|
|
269
|
+
| Materialized views (`view_query`, `define_view()`, read-only) | ✅ |
|
|
269
270
|
|
|
270
271
|
### Supported Filter Lookups
|
|
271
272
|
|
|
@@ -4299,15 +4300,18 @@ class Person(BaseSurrealModel):
|
|
|
4299
4300
|
temporary file, and a symbolic link is replaced by the file, its target never written. `overwrite=False` refuses an
|
|
4300
4301
|
existing path with `FileExistsError` before reading anything, and creates the file exclusively.
|
|
4301
4302
|
- One class per table: a `BaseSurrealModel`, or a `BaseSurrealEdge` for a `TYPE RELATION` table (which declares no
|
|
4302
|
-
`id`, `in_` or `out`: the base class provides them). Its `model_config` holds the `table_name`, `
|
|
4303
|
-
|
|
4304
|
-
|
|
4303
|
+
`id`, `in_` or `out`: the base class provides them). Its `model_config` holds the `table_name`, `view_query` for a
|
|
4304
|
+
view table (since v0.32.0: a materialized view model, [section 33](#33-materialized-views-view_query-define_view)),
|
|
4305
|
+
`connection=` when the schema was read from another connection than `"default"`, then `server_fields` and
|
|
4306
|
+
`extra="allow"` (below). A model declares `id: str | None = None` first (`id: RecordID | None = None` for a view: a
|
|
4307
|
+
grouped view's ids are arrays), then one line per field, in codepoint order of the columns.
|
|
4305
4308
|
- A class is named after its table in PascalCase, without singularisation (`game_tables` → `GameTables`); a name
|
|
4306
4309
|
that is taken — a Python keyword, a name the module imports, another table's class — gets `Model` appended (`range`
|
|
4307
|
-
→ `RangeModel`). The table's comment, relation ends,
|
|
4310
|
+
→ `RangeModel`). The table's comment, relation ends, change feed (`# Changefeed: 1d`), permissions
|
|
4308
4311
|
(`# Permissions: FOR select WHERE …, FOR create, update FULL, FOR delete NONE`, left out when all four are `NONE`,
|
|
4309
4312
|
a table's default), indexes and events are comments of its class, and the module's header lists every table or
|
|
4310
|
-
field left out, with the reason.
|
|
4313
|
+
field left out, with the reason. A view model's class says `# Read-only materialized view`; a view the lite cannot
|
|
4314
|
+
declare — an edge, a computed field, a query `view_query` refuses — keeps its query as a `# View: AS …` comment.
|
|
4311
4315
|
- The source is safe to import: names, comments and expressions read from the database reach it only as escaped
|
|
4312
4316
|
string literals, single-line ASCII comments and identifiers the generator builds, so a schema cannot inject code
|
|
4313
4317
|
into it. It holds no timestamp, server address, namespace or database name, and no
|
|
@@ -4402,8 +4406,8 @@ class Person(BaseSurrealModel):
|
|
|
4402
4406
|
`DEFINE FIELD OVERWRITE … VALUE …`, without their `TYPE`, `ASSERT` or `PERMISSIONS` (the module's docstring says so).
|
|
4403
4407
|
- Long lines: the `model_config` line is wrapped the way `ruff format` keeps it — one keyword per line once it exceeds
|
|
4404
4408
|
128 columns, and the `server_fields` names one per line once their own line does. Other long lines — long names, a
|
|
4405
|
-
long `Literal` union — are left on one line for your formatter. When every
|
|
4406
|
-
passes `ruff check` and `ruff format --check` at that line length.
|
|
4409
|
+
long `Literal` union, a long view query (`view_query="…"`) — are left on one line for your formatter. When every
|
|
4410
|
+
line fits in 128 columns, the module passes `ruff check` and `ruff format --check` at that line length.
|
|
4407
4411
|
|
|
4408
4412
|
#### SurrealDB 2.x and 3.x
|
|
4409
4413
|
|
|
@@ -4483,10 +4487,11 @@ reads, stores, logs or writes them.
|
|
|
4483
4487
|
ORM's connection name, which routing, reconnects and the generated models' `connection=` key need (full ORM: an
|
|
4484
4488
|
`HTTPConnection`).
|
|
4485
4489
|
- **Generated models use comments** where the full ORM writes
|
|
4486
|
-
`SurrealConfigDict(permissions=…, changefeed=…,
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
v0.32.0
|
|
4490
|
+
`SurrealConfigDict(permissions=…, changefeed=…, relation_in=…, relation_out=…, enforced=…)` (`# Permissions: …`,
|
|
4491
|
+
`# Changefeed: …`, `# IN … OUT … ENFORCED`), and the base class where it writes `table_type=` (`BaseSurrealEdge`
|
|
4492
|
+
for a relation table): those keys arrive in the lite's `SurrealConfigDict` with later minors. A view is generated
|
|
4493
|
+
with `view_query=…` since v0.32.0, as in the full ORM (a `# View: AS …` comment when the lite cannot declare it: an
|
|
4494
|
+
edge, a computed field, a query it refuses). `READONLY` is a comment too (full ORM: `Field(readonly=True)`).
|
|
4490
4495
|
- **`RecordID` instead of `ForeignKey`/`ReferencesField`**; no `VectorField` or `Encrypted`; `computed()` instead
|
|
4491
4496
|
of `Computed()`; `Duration`, `UUID`, `Decimal` and the geometry classes instead of `str`, `dict` and `float`;
|
|
4492
4497
|
nested fields become sub-models (full ORM: dotted attribute lines).
|
|
@@ -4757,6 +4762,240 @@ system user: run `define_events()` and `define_function()` at start-up or deploy
|
|
|
4757
4762
|
- **The migration operations** (`DefineEvent`, `RemoveEvent`, `RawSQL`) and the removal of the events a model no
|
|
4758
4763
|
longer declares **come with migrations (v0.37.0)**.
|
|
4759
4764
|
|
|
4765
|
+
### 33. Materialized views (`view_query`, `define_view()`)
|
|
4766
|
+
|
|
4767
|
+
Since v0.32.0 a model can read a **materialized view**: a table SurrealDB computes from a `SELECT` and keeps up to
|
|
4768
|
+
date as its source table changes — counts, sums, filtered copies. The view is declared on the model with
|
|
4769
|
+
`view_query`, applied idempotently with `define_view()` and read like any model, and every write of the model is
|
|
4770
|
+
refused: SurrealDB computes the view's records itself. The same code runs on SurrealDB 2.x, 3.x and the embedded
|
|
4771
|
+
engine, except where the [2.x/3.x table](#orm-behaviour-surrealdb-2x-vs-3x) says otherwise. The examples assume a
|
|
4772
|
+
configured connection: they run as written on the embedded engine (`mem://`).
|
|
4773
|
+
|
|
4774
|
+
```python
|
|
4775
|
+
from surrealdb import RecordID
|
|
4776
|
+
|
|
4777
|
+
from surreal_orm_lite import BaseSurrealModel, SurrealConfigDict
|
|
4778
|
+
|
|
4779
|
+
|
|
4780
|
+
class Order(BaseSurrealModel):
|
|
4781
|
+
model_config = SurrealConfigDict(table_name="orders")
|
|
4782
|
+
|
|
4783
|
+
id: str | None = None
|
|
4784
|
+
status: str
|
|
4785
|
+
amount: float
|
|
4786
|
+
customer: str | None = None
|
|
4787
|
+
|
|
4788
|
+
|
|
4789
|
+
class OrderStats(BaseSurrealModel):
|
|
4790
|
+
model_config = SurrealConfigDict(
|
|
4791
|
+
table_name="order_stats",
|
|
4792
|
+
view_query="SELECT status, count() AS total, math::sum(amount) AS revenue FROM orders GROUP BY status",
|
|
4793
|
+
)
|
|
4794
|
+
|
|
4795
|
+
id: RecordID | None = None
|
|
4796
|
+
status: str
|
|
4797
|
+
total: int
|
|
4798
|
+
revenue: float
|
|
4799
|
+
|
|
4800
|
+
|
|
4801
|
+
print(OrderStats.view_ddl())
|
|
4802
|
+
# DEFINE TABLE OVERWRITE `order_stats` AS (SELECT status, count() AS total, math::sum(amount) AS revenue FROM orders GROUP BY status);
|
|
4803
|
+
|
|
4804
|
+
await Order(id="o1", status="paid", amount=10.0, customer="ada").save() # the source table exists first
|
|
4805
|
+
await OrderStats.define_view() # idempotent (OVERWRITE): run it at start-up
|
|
4806
|
+
|
|
4807
|
+
await Order(id="o2", status="paid", amount=5.5, customer="bob").save()
|
|
4808
|
+
await Order(id="o3", status="new", amount=1.0, customer="ada").save()
|
|
4809
|
+
for stats in await OrderStats.objects().order_by("status").exec():
|
|
4810
|
+
print(stats.status, stats.total, stats.revenue)
|
|
4811
|
+
# new 1 1.0
|
|
4812
|
+
# paid 2 15.5
|
|
4813
|
+
|
|
4814
|
+
paid = await OrderStats.objects().get(["paid"])
|
|
4815
|
+
print(paid.id, paid.total)
|
|
4816
|
+
# order_stats:['paid'] 2
|
|
4817
|
+
|
|
4818
|
+
try:
|
|
4819
|
+
await paid.merge(total=0)
|
|
4820
|
+
except TypeError as error:
|
|
4821
|
+
print(error)
|
|
4822
|
+
# Cannot modify materialized view 'order_stats': OrderStats.merge() writes to it, and SurrealDB computes a view's records from its view_query. Write to the tables it selects from instead.
|
|
4823
|
+
```
|
|
4824
|
+
|
|
4825
|
+
#### Declaring a view
|
|
4826
|
+
|
|
4827
|
+
```python
|
|
4828
|
+
class PaidOrder(BaseSurrealModel):
|
|
4829
|
+
model_config = SurrealConfigDict(
|
|
4830
|
+
table_name="paid_orders", view_query="SELECT customer, amount FROM orders WHERE status = 'paid'"
|
|
4831
|
+
)
|
|
4832
|
+
|
|
4833
|
+
id: RecordID | None = None
|
|
4834
|
+
customer: str | None = None
|
|
4835
|
+
amount: float
|
|
4836
|
+
|
|
4837
|
+
|
|
4838
|
+
await PaidOrder.define_view()
|
|
4839
|
+
print([(str(order.id), order.amount) for order in await PaidOrder.objects().order_by("amount").exec()])
|
|
4840
|
+
# [('paid_orders:o2', 5.5), ('paid_orders:o1', 10.0)]
|
|
4841
|
+
```
|
|
4842
|
+
|
|
4843
|
+
- `view_query` is one `SELECT … FROM <table> [WHERE …] [GROUP BY … | GROUP ALL]` (a final `;` is dropped), checked
|
|
4844
|
+
when the class is defined: a mistake raises `TypeError` or `ValueError` at the `class` statement. It takes no
|
|
4845
|
+
`ORDER BY`, `LIMIT` or `FETCH` (SurrealDB refuses them in a view), and it is not a `SELECT VALUE`: a view stores
|
|
4846
|
+
records. The model's `get_view_query()` returns it as checked.
|
|
4847
|
+
- **A grouped view** selects its `GROUP BY` fields (or expressions of them) and aggregates SurrealDB keeps up to
|
|
4848
|
+
date: `count()`, `math::sum()`, `math::mean()`, `math::min()`, `math::max()`, `time::min()`, `time::max()` (and
|
|
4849
|
+
`math::stddev()`, `math::variance()` on 3.x), whatever their argument (`math::sum(amount ?? 0)`). `math::median()`,
|
|
4850
|
+
`array::group()`, `array::distinct()`, `array::join()` and `vector::sum()` are refused (inside an aggregate's
|
|
4851
|
+
argument they are allowed). On SurrealDB 3.x an expression around an aggregate (`math::sum(amount) / count()`) and
|
|
4852
|
+
a `GROUP BY` on an alias work; on 2.x and the embedded engine `define_view()` refuses them (see "SurrealDB 2.x and
|
|
4853
|
+
3.x" below).
|
|
4854
|
+
- Every source write runs the view's query for that record: when it fails — a function given a value of the wrong
|
|
4855
|
+
type, an aggregated field missing
|
|
4856
|
+
([surrealdb/surrealdb#7447](https://github.com/surrealdb/surrealdb/issues/7447)) — SurrealDB refuses the write, on
|
|
4857
|
+
every line.
|
|
4858
|
+
- **Ids**: a plain view keeps the source records' ids (`paid_orders:o1`); a grouped view's id is the array of its
|
|
4859
|
+
group values (`order_stats:['paid']`), and `GROUP ALL` gives `[]`. Declare `id: RecordID | None = None` and look a
|
|
4860
|
+
group up with `get(["paid"])` or `filter(id=["paid"])`; read a `GROUP ALL` view with `first()`. Declare sums and
|
|
4861
|
+
means `float`: SurrealDB 2.x and the embedded engine compute an `int` sum of integers and a `Decimal` mean, which a
|
|
4862
|
+
`float` field reads as a float.
|
|
4863
|
+
- A view model cannot be an edge (`BaseSurrealEdge`), use `AuthenticatedUserMixin` or declare computed fields: each
|
|
4864
|
+
raises `TypeError` at the `class` statement. A subclass inherits `view_query` and defines the view on its own
|
|
4865
|
+
table; `view_query=None` makes it an ordinary model. The `table_name` is any identifier the ORM reads (a SurrealQL
|
|
4866
|
+
keyword such as `select` is defined, but not read by the QuerySet, as for every model).
|
|
4867
|
+
|
|
4868
|
+
#### Applying and removing
|
|
4869
|
+
|
|
4870
|
+
- `define_view()` writes `DEFINE TABLE OVERWRITE … AS (…)`: SurrealDB fills the view from the source's records, and
|
|
4871
|
+
each call recomputes the view and drops the events, fields and indexes defined on it, so at start-up it costs one
|
|
4872
|
+
pass over the source. `overwrite=False` writes `IF NOT EXISTS` and keeps a view already defined, whatever its
|
|
4873
|
+
query. It returns the statement it applied; `view_ddl()` returns the statement without applying it.
|
|
4874
|
+
- The source table must exist (a record saved, or a `DEFINE TABLE`), else `SurrealDbError` naming it
|
|
4875
|
+
(`Can't apply view definition: 'order_stats' -> The table 'orders' does not exist`).
|
|
4876
|
+
- Redefining a view drops the events, fields and indexes defined on it: run `define_events()` after `define_view()`.
|
|
4877
|
+
- `remove_view()` writes `REMOVE TABLE IF EXISTS`, which removes the view and its records; with `missing_ok=False` a
|
|
4878
|
+
view that does not exist raises `SurrealDbNotFoundError`.
|
|
4879
|
+
- The table guard: when the model's table already exists as an ordinary table **without records** — SurrealDB
|
|
4880
|
+
creates one when `define_events()` or, on 2.x, a `watch()` runs before `define_view()` — `define_view()` turns it
|
|
4881
|
+
into the view. When it holds records, `define_view()` raises `SurrealDbError` before any DDL, because SurrealDB
|
|
4882
|
+
would delete them (the message gives the `raw_query("REMOVE TABLE …")` to run if they are not needed).
|
|
4883
|
+
`define_view(overwrite=False)` raises on any ordinary table, which `IF NOT EXISTS` would silently keep, and
|
|
4884
|
+
`remove_view()` raises on a table that is not a view.
|
|
4885
|
+
- A view may read another view: it is updated when the inner view changes, but not recomputed when the inner one is
|
|
4886
|
+
redefined.
|
|
4887
|
+
- **Remove the views that read another view first.** SurrealDB 3.x refuses to remove a table that a view reads — a
|
|
4888
|
+
source table or an inner view — and `remove_view()` passes that refusal on (`SurrealDbError`,
|
|
4889
|
+
`Cannot delete table … on which a view is defined`). On 2.x and the embedded engine `remove_view()` raises
|
|
4890
|
+
`SurrealDbError` before any DDL, because those engines would remove it and turn every view reading it into an
|
|
4891
|
+
ordinary table that keeps its last records and no longer updates; the ORM finds those views by their plain `FROM`
|
|
4892
|
+
targets. A raw `REMOVE TABLE` of a source table does that on 2.x and the embedded engine: such a former view then
|
|
4893
|
+
makes `define_view()` refuse (it holds records: run the `raw_query("REMOVE TABLE …")` the message gives) and
|
|
4894
|
+
`remove_view()` refuse (it is not a view).
|
|
4895
|
+
|
|
4896
|
+
#### Reading
|
|
4897
|
+
|
|
4898
|
+
- Every read works — filters, `order_by()`, aggregates, `get()`, `refresh()`, `Subquery`, `fetch()`, `using()`, and
|
|
4899
|
+
on WebSocket `live()`/`watch()`, which receive the records SurrealDB recomputes. `relate()` and `remove_relation()`
|
|
4900
|
+
from or to a view record write the edge table and are allowed.
|
|
4901
|
+
- On a view model, `filter(id__in=...)` is sent as `$ids CONTAINS id`: SurrealDB 3.x (3.1 and 3.3 measured) answers
|
|
4902
|
+
no record to `id IN` on a view table, while `CONTAINS` is right on every line; write raw SurrealQL over a view the
|
|
4903
|
+
same way.
|
|
4904
|
+
- `live()` instances carry the record id on both lines; `watch()` yields the server's envelopes as received: on 3.x
|
|
4905
|
+
their `result` has no `id`, read `record`.
|
|
4906
|
+
- On SurrealDB 3.x a live query on a view filtered on `id` never notifies (the server's view notifications carry no
|
|
4907
|
+
id); filter on the view's fields, e.g. `filter(status="paid")`.
|
|
4908
|
+
|
|
4909
|
+
#### Writes are refused
|
|
4910
|
+
|
|
4911
|
+
- Every ORM write of a view model raises `TypeError` before any signal, transaction or request: the instance
|
|
4912
|
+
methods `save()`, `upsert()`, `update()`, `merge()`, `patch()`, `delete()` and the atomic helpers
|
|
4913
|
+
`atomic_append()`, `atomic_remove()`, `atomic_set_add()`, `atomic_increment()`, `atomic_append_many()`,
|
|
4914
|
+
`atomic_set_add_many()`, `atomic_remove_many()`, and the QuerySet methods `bulk_create()`, `bulk_update()`,
|
|
4915
|
+
`bulk_delete()`, `patch()`, `update_or_create()`, `get_or_create()` (even when the record exists) and
|
|
4916
|
+
`delete_table()`. The message starts with the full ORM's `Cannot modify materialized view '<table>'` and names the
|
|
4917
|
+
call: write to the tables the view selects from instead.
|
|
4918
|
+
- `refresh()`, `relate()`, `remove_relation()`, `remove_all_relations()`, `define_events()` and `remove_event()` stay
|
|
4919
|
+
allowed: they do not write the view's table.
|
|
4920
|
+
- Raw SurrealQL (`raw_query()`, `QuerySet.query()`, the client's `query()`) is not checked: SurrealDB 3.2 and later
|
|
4921
|
+
refuse a write to a view, 2.x and 3.1 accept it and change the view's records.
|
|
4922
|
+
|
|
4923
|
+
#### Transactions, connections and the query cache
|
|
4924
|
+
|
|
4925
|
+
- `define_view()` and `remove_view()` take `tx=`. In a buffered transaction (SurrealDB 2.x, HTTP, the embedded
|
|
4926
|
+
engine) the statement is applied at commit, and the block's later source writes update the view; in an interactive
|
|
4927
|
+
one (WebSocket and SurrealDB 3.x) it is applied in the transaction, which reads the view and its own source writes
|
|
4928
|
+
(`objects(tx=tx)`), unseen outside the block until it commits. Either way, a block that raises leaves no view. The
|
|
4929
|
+
table guard and the 2.x checks run before anything is queued, on committed state; a refusal of a queued statement
|
|
4930
|
+
comes back when the block commits, as the transaction's own error.
|
|
4931
|
+
- Both run on the model's connection (its `connection` key, or the `using()` block's); a `tx=` of another connection
|
|
4932
|
+
raises `SurrealDbError` before any request. A view and its source tables live in one database: give a view model
|
|
4933
|
+
its sources' connection.
|
|
4934
|
+
- The query cache ([section 29](#29-query-cache-cache-querycache)): reads of a view model, and reads whose subqueries
|
|
4935
|
+
read one, are dropped by every ORM write, since a write to a source table changes the view and the ORM does not know
|
|
4936
|
+
which tables a view reads. `define_view()` and `remove_view()` drop cached reads like every write. Writes by raw
|
|
4937
|
+
SurrealQL or by another process are not seen, as for every table.
|
|
4938
|
+
- Defining and removing need a system user allowed to define: root, or a namespace or database `OWNER` or `EDITOR`.
|
|
4939
|
+
On a server, a `VIEWER` or a record user gets `SurrealDbAuthenticationError`.
|
|
4940
|
+
- Applying DDL fires no signal, and neither do the records SurrealDB computes for a view.
|
|
4941
|
+
|
|
4942
|
+
#### SurrealDB 2.x and 3.x
|
|
4943
|
+
|
|
4944
|
+
- Raw writes to a view are accepted by 2.x and 3.1, which change the view's records, and refused by 3.2 and later.
|
|
4945
|
+
The ORM refuses its own writes on every line.
|
|
4946
|
+
- Grouped views: 2.x and the embedded engine keep up to date only whole aggregates and `GROUP BY` fields, and
|
|
4947
|
+
compute anything else once per source record. So `define_view()` raises `SurrealDbError` before any DDL there for
|
|
4948
|
+
`math::stddev()` and `math::variance()`, an expression around an aggregate (`math::sum(amount) / count()`), a field
|
|
4949
|
+
outside `GROUP BY` and outside an aggregate (3.x refuses that one too), and a `GROUP BY` on an alias (2.x would put
|
|
4950
|
+
every record in one group).
|
|
4951
|
+
- Value types: `int` sums and `Decimal` means on 2.x, `float` on 3.x; `count()` of an array field adds the arrays'
|
|
4952
|
+
lengths on 2.x, and counts the records whose array is not empty on 3.x.
|
|
4953
|
+
- Live queries and events see an emptied group as an `UPDATE` then a `DELETE` on 2.x, a `DELETE` on 3.x.
|
|
4954
|
+
- Removing a view, or a source table, that another view reads: 3.x refuses it; on 2.x `remove_view()` raises before
|
|
4955
|
+
any DDL, and a raw `REMOVE TABLE` leaves the views reading it as ordinary tables keeping their last records.
|
|
4956
|
+
- On every line, a source write missing an aggregated field is refused
|
|
4957
|
+
([surrealdb/surrealdb#7447](https://github.com/surrealdb/surrealdb/issues/7447)): write `math::sum(amount ?? 0)`.
|
|
4958
|
+
- The [2.x/3.x table](#orm-behaviour-surrealdb-2x-vs-3x) lists these differences; the embedded engine behaves as a
|
|
4959
|
+
2.x server.
|
|
4960
|
+
|
|
4961
|
+
#### Security
|
|
4962
|
+
|
|
4963
|
+
The table name is checked as an identifier and written quoted in every statement the ORM sends. The query is written
|
|
4964
|
+
into `AS (…)` as it is, since SurrealDB takes no bound parameter in a definition; when the class is defined the ORM
|
|
4965
|
+
checks its structure: string literals and quoted identifiers terminated, brackets balanced, no NUL character, no
|
|
4966
|
+
SurrealQL comment marker outside a literal, one `SELECT` and no `;` outside brackets. The check catches mistakes —
|
|
4967
|
+
unbalanced brackets, a comment that would swallow the parenthesis the ORM appends, a second statement where one
|
|
4968
|
+
SELECT is expected — and is not an injection defence: a view's query is trusted application code, never user input.
|
|
4969
|
+
To filter on a value, store it in a record or a `DEFINE PARAM` and read it from the query; never format it into the
|
|
4970
|
+
text. A refusal names the view's table, never the statement, and keeps the server's reason whole, which names the
|
|
4971
|
+
source table to create.
|
|
4972
|
+
|
|
4973
|
+
Destructive DDL is guarded: `define_view()` never turns a table that holds records into a view and never lets
|
|
4974
|
+
`IF NOT EXISTS` keep an ordinary table, and `remove_view()` never removes a table that is not a view nor, on 2.x and
|
|
4975
|
+
the embedded engine, a view that another view reads. The guard is a check-then-act on committed state, not a lock: a
|
|
4976
|
+
record written or a table created by another process between its reads and the DDL is not seen. The write refusal
|
|
4977
|
+
guards against mistakes and is not an access control: on 2.x and 3.1 the server accepts writes to a view from any
|
|
4978
|
+
session allowed to write the table. A view is defined without a `PERMISSIONS` clause, so SurrealDB applies its
|
|
4979
|
+
default `PERMISSIONS NONE`: a record user reads nothing from a view, and `define_view()` resets permissions set by
|
|
4980
|
+
hand on it. Applying a view needs a privileged system user: run `define_view()` at start-up or deployment, not on a
|
|
4981
|
+
record user's session.
|
|
4982
|
+
|
|
4983
|
+
#### Coming from the full SurrealDB-ORM
|
|
4984
|
+
|
|
4985
|
+
- **`view_query` is the same key** (`SurrealConfigDict(view_query="SELECT …")`); `get_view_query()` is added and
|
|
4986
|
+
`_is_view()` kept.
|
|
4987
|
+
- **`TypeError` with the same first words** (`Cannot modify materialized view '<table>'`), on every write path above
|
|
4988
|
+
(full ORM: `save()`, `update()`, `merge()`, `delete()` only).
|
|
4989
|
+
- **`define_view()`/`remove_view()`/`view_ddl()`** apply, remove and render the view, where the full ORM applies one
|
|
4990
|
+
through migrations (`CreateTable`/`AlterTable`) or `define_table()`. Those come with v0.37.0, whose
|
|
4991
|
+
`define_table()` accepts a view model and applies it through `view_ddl()`, so full-ORM code calling
|
|
4992
|
+
`define_table()` on a view works from then on.
|
|
4993
|
+
- **`AS (…)` as in the full ORM, the table name quoted** (``DEFINE TABLE OVERWRITE `order_stats` AS (…)``).
|
|
4994
|
+
- **Generated models write `view_query=`**, as the full ORM's do
|
|
4995
|
+
([section 31](#31-schema-introspection-databaseintrospector-generate_models_from_db)).
|
|
4996
|
+
- **The documented `MaterializedView` base class with `source_query` is not shipped by the full ORM either**: declare
|
|
4997
|
+
`view_query` on a `BaseSurrealModel`.
|
|
4998
|
+
|
|
4760
4999
|
---
|
|
4761
5000
|
|
|
4762
5001
|
## Configuration Options
|
|
@@ -4879,7 +5118,7 @@ features introduced in SurrealDB 3.x. On 2.x the ORM degrades gracefully. Capabi
|
|
|
4879
5118
|
listed behave the same on both lines. The 2.x column was measured on 2.6.5 and holds unchanged
|
|
4880
5119
|
on 2.7.0. The 3.x column was measured on 3.2.4 and holds on 3.3.0, except where a row says
|
|
4881
5120
|
otherwise. The full suite and every divergence below were re-verified on both new versions.
|
|
4882
|
-
Rows added in v0.20.0 were measured directly on 2.7.0 and 3.3.0; rows added in v0.21.0 on 2.7.0 and 3.3.2, re-verified on 3.3.3, with 3.1.5 where it differs; rows added in v0.22.0 on 2.7.0 and 3.3.3, with 3.1.5 where it differs; rows added in v0.23.0 on 2.7.0 and 3.3.4, with 3.1.5 where it differs; rows added in v0.24.0 on 2.7.0 and 3.3.4, identical on 3.1.5; rows added in v0.26.0 on 2.7.0 and 3.3.4, in memory and on versioned storage, identical on 3.1.5; rows added in v0.27.0 on 2.7.0 and 3.3.4, with 3.1.5 where it differs; rows added in v0.28.0 on 2.7.0 and 3.3.4, identical on 3.1.5; the rows added in v0.28.1 on 2.7.0 and 3.3.4, identical on 3.1.5; rows added in v0.29.0 on 2.7.0 and 3.3.4 in one process, identical on 3.1.5. The row added in v0.29.2 was measured on 2.7.0, 3.3.4 and the embedded engine, identical on 3.1.5. Rows added in v0.30.0 were measured on 2.7.0, 3.3.4 and the embedded engine, with 3.1.5 where it differs. The row added in v0.30.3 was measured on 2.7.0 and 3.3.4, over WebSocket and HTTP, and on the embedded engine, identical on 3.1.5. The row added in v0.30.7 was measured on 2.7.0, 3.3.4 and the embedded engine, identical on 3.1.5: its parser refuses a statement that nests six objects, and `update()` nests at most four. Rows added in v0.31.0 were measured on 2.7.0, 3.3.4 and the embedded engine, identical on 3.1.5. The row added in v0.31.1 was measured on 2.7.0, 3.3.4 and the embedded engine, identical on 3.1.5.
|
|
5121
|
+
Rows added in v0.20.0 were measured directly on 2.7.0 and 3.3.0; rows added in v0.21.0 on 2.7.0 and 3.3.2, re-verified on 3.3.3, with 3.1.5 where it differs; rows added in v0.22.0 on 2.7.0 and 3.3.3, with 3.1.5 where it differs; rows added in v0.23.0 on 2.7.0 and 3.3.4, with 3.1.5 where it differs; rows added in v0.24.0 on 2.7.0 and 3.3.4, identical on 3.1.5; rows added in v0.26.0 on 2.7.0 and 3.3.4, in memory and on versioned storage, identical on 3.1.5; rows added in v0.27.0 on 2.7.0 and 3.3.4, with 3.1.5 where it differs; rows added in v0.28.0 on 2.7.0 and 3.3.4, identical on 3.1.5; the rows added in v0.28.1 on 2.7.0 and 3.3.4, identical on 3.1.5; rows added in v0.29.0 on 2.7.0 and 3.3.4 in one process, identical on 3.1.5. The row added in v0.29.2 was measured on 2.7.0, 3.3.4 and the embedded engine, identical on 3.1.5. Rows added in v0.30.0 were measured on 2.7.0, 3.3.4 and the embedded engine, with 3.1.5 where it differs. The row added in v0.30.3 was measured on 2.7.0 and 3.3.4, over WebSocket and HTTP, and on the embedded engine, identical on 3.1.5. The row added in v0.30.7 was measured on 2.7.0, 3.3.4 and the embedded engine, identical on 3.1.5: its parser refuses a statement that nests six objects, and `update()` nests at most four. Rows added in v0.31.0 were measured on 2.7.0, 3.3.4 and the embedded engine, identical on 3.1.5. The row added in v0.31.1 was measured on 2.7.0, 3.3.4 and the embedded engine, identical on 3.1.5. Rows added in v0.32.0 were measured on 2.7.0, 3.3.4 and the embedded engine; 3.1.5 behaves as 3.3.4 but accepts raw writes to a view.
|
|
4883
5122
|
The SDK's in-process engine has a table of its own: [ORM behaviour: the embedded engine](#orm-behaviour-the-embedded-engine).
|
|
4884
5123
|
|
|
4885
5124
|
| ORM capability | SurrealDB 2.6.x / 2.7.x | SurrealDB 3.2.x / 3.3.x | Since |
|
|
@@ -5007,6 +5246,14 @@ The SDK's in-process engine has a table of its own: [ORM behaviour: the embedded
|
|
|
5007
5246
|
| Event and function definitions read back (`EventState`, `FunctionState`) | a block on several lines, each statement ending with `;`; `option<T>` parameters as written | a block on one line, no `;` after a single statement; optional parameters rendered with `none` first | v0.31.0 |
|
|
5008
5247
|
| `define_function()`, `remove_function()` and `call_function()` | same on both lines: the next `call_function()` sees a redefinition on every connection of this process to the same namespace and database, whatever its URL | the same | v0.31.0 |
|
|
5009
5248
|
| Reading a datetime before year 1 or after year 9999 (SurrealDB stores years -262143 to +262142) | its RFC 3339 text, as `<string>` gives it (`"+10000-01-01T00:00:00Z"`): an untyped field holds it, a `datetime` field refuses it (`ValidationError`) | same on both lines | v0.31.1 |
|
|
5249
|
+
| Writes to a materialized view (`save()`, `merge()`, `delete()`, bulk writes … of a `view_query` model) | `TypeError` before any request; a raw write is accepted by the server and changes the view's records | `TypeError` before any request; the server refuses raw writes too (3.2 and later; 3.1 accepts them) | v0.32.0 |
|
|
5250
|
+
| Aggregates in a grouped view (`view_query` with `GROUP BY` or `GROUP ALL`) | `count()`, `math::sum()`, `math::mean()`, `math::min()`, `math::max()`, `time::min()`, `time::max()`; `math::stddev()`, `math::variance()`: `SurrealDbError` before any DDL | the same, plus `math::stddev()` and `math::variance()` | v0.32.0 |
|
|
5251
|
+
| An expression around an aggregate in a grouped view (`math::sum(amount) / count()`, `math::round(math::mean(amount))`, `count() * 2`) | `SurrealDbError` before any DDL: the server would compute it once per source record (a wrong `count()`, or every source write refused) | kept up to date | v0.32.0 |
|
|
5252
|
+
| A grouped view selecting a field outside `GROUP BY` and outside an aggregate (`amount`, `math::percentile(amount, 50)`, `*`) | `SurrealDbError` before any DDL: the server would keep the last source record's value, or refuse every source write | `SurrealDbError` when the view is defined (the server refuses it) | v0.32.0 |
|
|
5253
|
+
| A grouped view whose `GROUP BY` names the alias of a computed field (`time::month(at) AS month` … `GROUP BY month`) | `SurrealDbError` before any DDL: the server would put every source record in one group | one group per computed value | v0.32.0 |
|
|
5254
|
+
| Values a view computes (`math::sum()`, `math::mean()`, `count()` of an array) | a sum of integers is an `int`, a mean a `Decimal`; `count(tags)` of an array field adds the arrays' lengths | both are `float`; `count(tags)` counts the records whose array is not empty | v0.32.0 |
|
|
5255
|
+
| Live queries and events on a view (a group emptied by a delete; a live query filtered on `id`) | an emptied group: an `UPDATE` with the emptied values (a `count()` of 0), then a `DELETE`; a filter on `id` notifies | an emptied group: a `DELETE`; a filter on `id` never notifies (the view's notifications carry no `id`): filter on the view's fields | v0.32.0 |
|
|
5256
|
+
| Removing a materialized view, or a source table, that another view reads (`remove_view()`, `REMOVE TABLE`) | `remove_view()` raises `SurrealDbError` before any DDL; a raw `REMOVE TABLE` succeeds and turns each view reading the table into an ordinary table keeping its last records | `SurrealDbError` from the server (`Cannot delete table … on which a view is defined`): remove the views that read it first | v0.32.0 |
|
|
5010
5257
|
| Full suite and every divergence in this table, re-run on SurrealDB 2.7.0 and 3.3.0 | 2.7.0 identical to 2.6.5 (same passes, same 3.x-only skips) | 3.3.0 identical to 3.2.4, except the system-user signin row | v0.19.1 |
|
|
5011
5258
|
|
|
5012
5259
|
> **Note on record IDs**: A record loaded from the database has its `id` field set to a native `surrealdb.RecordID` object, not a plain string. Use `model.get_raw_id()` to obtain the bare identifier string (e.g. `"alice"`), or compare directly with `model.id == RecordID("User", "alice")`. In-memory instances you construct yourself retain whatever value you assign.
|
|
@@ -5040,6 +5287,7 @@ Capabilities not listed behave as on a 2.x server.
|
|
|
5040
5287
|
| Several connections (`add_connection()`) | `mem://`: each name is a database of its own; an on-disk store (`surrealkv://`, `file://`) is open under one name at a time — a second name raises `SurrealDbConnectionError` | each name opens its own clients; two databases on one server are isolated | v0.29.0 |
|
|
5041
5288
|
| Schema introspection (`DatabaseIntrospector`) | reads like a 2.x server (core 2.3.10): same state and same generated model types as on the servers; the anonymous owner session may run `INFO`; `COUNT` indexes cannot be defined | need a system user (root, namespace or database user) | v0.30.0 |
|
|
5042
5289
|
| Server-side events and stored functions (v0.31.0) | as on a 2.x server: `$input` is `NONE`; `Event(async_mode=True)` raises `SurrealDbError` before any DDL | `ASYNC` events on 3.x | v0.31.0 |
|
|
5290
|
+
| Materialized views (v0.32.0) | as on a 2.x server: `int` sums and `Decimal` means; in a grouped view, `math::stddev()`/`math::variance()`, an expression around an aggregate, a field outside `GROUP BY` and a `GROUP BY` on an alias raise `SurrealDbError` before any DDL; writes to a view refused by the ORM (the engine would accept them); no live queries | `float` sums and means on 3.x | v0.32.0 |
|
|
5043
5291
|
| Everything else | CRUD, QuerySet, aggregations, relations, typed edges, rich and geo fields, upsert/patch, server values, computed fields, stored functions, signals: the same as on a 2.x server | unchanged | v0.25.0 |
|
|
5044
5292
|
|
|
5045
5293
|
---
|
|
@@ -5086,7 +5334,8 @@ Contributions are welcome! Please:
|
|
|
5086
5334
|
| v0.29.0 | Tier 2 — Multi-database | ✅ Released |
|
|
5087
5335
|
| v0.30.0 | Tier 3 — Schema introspection | ✅ Released |
|
|
5088
5336
|
| v0.31.0 | Tier 3 — Server-side events and DDL helpers | ✅ Released |
|
|
5089
|
-
| v0.32.0
|
|
5337
|
+
| v0.32.0 | Tier 3 — Materialized views | ✅ Released |
|
|
5338
|
+
| v0.33.0 – v0.39.0 | Tier 3 — Advanced (search, DDL, migrations, CLI) | 📋 Planned |
|
|
5090
5339
|
| v0.40.0 | Beta Phase (API freeze, hardening) | 📋 Planned |
|
|
5091
5340
|
| v2.0.0 | Production / GA (aligned with SDK 2.0) | 📋 Planned |
|
|
5092
5341
|
|
|
@@ -5142,7 +5391,7 @@ SDK) and **server support**. Everything below is on the lite roadmap via the off
|
|
|
5142
5391
|
| Schema Introspection | ✅ v0.30.0 (`DatabaseIntrospector`, `generate_models_from_db()`) | ✅ |
|
|
5143
5392
|
| DEFINE EVENT | ✅ v0.31.0 (`Event`, `define_events()`, `ASYNC` on 3.x) | ✅ migrations (`DefineEvent`) |
|
|
5144
5393
|
| `define_function()` (DDL) | ✅ v0.31.0 | ❌ |
|
|
5145
|
-
| Materialized views | v0.32.0
|
|
5394
|
+
| Materialized views | ✅ v0.32.0 (`view_query`, `define_view()`) | ✅ |
|
|
5146
5395
|
| Full-Text Search | v0.34.0 | ✅ |
|
|
5147
5396
|
| Vector Search (KNN/HNSW) | v0.35.0 | ✅ |
|
|
5148
5397
|
| Hybrid Search (RRF) | v0.36.0 | ✅ |
|