surreal-orm-lite 0.32.0__tar.gz → 0.32.1__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.0 → surreal_orm_lite-0.32.1}/CHANGELOG.md +44 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/PKG-INFO +11 -5
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/README.md +10 -4
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/pyproject.toml +1 -1
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/__init__.py +1 -1
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/model_base.py +14 -4
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/schema.py +76 -20
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/.gitignore +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/LICENSE +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/Makefile +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/_sdk.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/aggregations.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/auth.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/cache.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/changefeed.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/concurrency.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/connection_config.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/connection_manager.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/constants.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/decoding.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/define_parser.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/edge.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/embedded.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/encoding.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/enum.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/exceptions.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/functions.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/geo.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/introspection.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/live.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/model_auth.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/model_generator.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/py.typed +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/q.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/query_set.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/routing.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/schema_state.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/signals.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/subquery.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/surql_literal.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/testing/__init__.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/testing/database.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/testing/fixtures.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/time_travel.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/transaction.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/types.py +0 -0
- {surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/utils.py +0 -0
|
@@ -5,6 +5,50 @@ 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.1] - 2026-10-20
|
|
9
|
+
|
|
10
|
+
Two follow-ups to the materialized views of v0.32.0 (#286): a function SurrealDB lists as unusable
|
|
11
|
+
in a view is allowed inside the argument of each of several aggregates, and a chain of views can
|
|
12
|
+
be removed outer view first in one buffered transaction. No new public API.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **A listed function inside the argument of each of several aggregates passes the
|
|
17
|
+
`view_query` check** (#286). Since v0.32.0, a grouped view whose projection calls
|
|
18
|
+
`array::distinct()`, `array::group()`, `array::join()`, `math::median()` or `vector::sum()`
|
|
19
|
+
outside an aggregate's argument raises `ValueError` at the `class` statement, but only an element
|
|
20
|
+
that is one whole aggregate had its calls skipped: an expression combining several aggregates,
|
|
21
|
+
such as `math::sum(amount) + math::sum(array::len(array::distinct(tags)))`, was refused on every
|
|
22
|
+
line. Such an expression now passes the check; a listed function beside or around an aggregate
|
|
23
|
+
(`math::sum(amount) + array::len(array::distinct(tags))`) is still refused, with the same
|
|
24
|
+
message. SurrealDB 3.x keeps such a view up to date; on 2.x and the embedded engine
|
|
25
|
+
`define_view()` still refuses an expression around aggregates before any DDL, as in v0.32.0.
|
|
26
|
+
`generate_models_from_db()` follows the check: such a view table is now generated as a view
|
|
27
|
+
model instead of a class carrying a `# View: AS …` comment.
|
|
28
|
+
- **`remove_view()` accepts a chain of views removed outer view first in one buffered
|
|
29
|
+
transaction** (#286). On SurrealDB 2.x and the embedded engine, `remove_view()` refuses with
|
|
30
|
+
`SurrealDbError`, before any DDL, to remove a view that another view reads, and it read committed
|
|
31
|
+
state only, so this block was refused although the server accepts both statements at commit:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
async with SurrealDBConnectionManager.transaction() as tx:
|
|
35
|
+
await OrderStatsTotals.remove_view(tx=tx) # the outer view, queued
|
|
36
|
+
await OrderStats.remove_view(tx=tx) # refused: the outer view was still counted
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
In a buffered transaction (SurrealDB 2.x, HTTP, the embedded engine), a view whose
|
|
40
|
+
`remove_view(tx=tx)` the same block has already queued is no longer counted, unless the block
|
|
41
|
+
names it again afterwards (a `define_view(tx=tx)` of it). A view removed in another transaction,
|
|
42
|
+
or by a `REMOVE TABLE` you add to the block yourself (`tx.add(…)`), is still counted: the check
|
|
43
|
+
errs on the side of the refusal. SurrealDB 3.x decides by itself, as before.
|
|
44
|
+
|
|
45
|
+
### Notes
|
|
46
|
+
|
|
47
|
+
- README section 33 and the "ORM behaviour: SurrealDB 2.x vs 3.x" row on removing a view that
|
|
48
|
+
another view reads say so.
|
|
49
|
+
- Server pins unchanged: tested against SurrealDB 3.3.4 and 2.7.0 and on the embedded engine,
|
|
50
|
+
plus 3.1.5 for backward compatibility.
|
|
51
|
+
|
|
8
52
|
## [0.32.0] - 2026-10-20
|
|
9
53
|
|
|
10
54
|
Materialized views as read-only models: a model declares the `SELECT` that SurrealDB computes a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: surreal-orm-lite
|
|
3
|
-
Version: 0.32.
|
|
3
|
+
Version: 0.32.1
|
|
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
|
|
@@ -4848,9 +4848,11 @@ print([(str(order.id), order.amount) for order in await PaidOrder.objects().orde
|
|
|
4848
4848
|
date: `count()`, `math::sum()`, `math::mean()`, `math::min()`, `math::max()`, `time::min()`, `time::max()` (and
|
|
4849
4849
|
`math::stddev()`, `math::variance()` on 3.x), whatever their argument (`math::sum(amount ?? 0)`). `math::median()`,
|
|
4850
4850
|
`array::group()`, `array::distinct()`, `array::join()` and `vector::sum()` are refused (inside an aggregate's
|
|
4851
|
-
argument they are allowed).
|
|
4852
|
-
|
|
4853
|
-
3.x
|
|
4851
|
+
argument they are allowed). Since v0.32.1 that holds when an expression combines several aggregates too
|
|
4852
|
+
(`math::sum(amount) + math::sum(array::len(array::distinct(tags)))`); beside an aggregate
|
|
4853
|
+
(`math::sum(amount) + array::len(array::distinct(tags))`) they are still refused. On SurrealDB 3.x an expression
|
|
4854
|
+
around an aggregate (`math::sum(amount) / count()`) and a `GROUP BY` on an alias work; on 2.x and the embedded
|
|
4855
|
+
engine `define_view()` refuses them (see "SurrealDB 2.x and 3.x" below).
|
|
4854
4856
|
- Every source write runs the view's query for that record: when it fails — a function given a value of the wrong
|
|
4855
4857
|
type, an aggregated field missing
|
|
4856
4858
|
([surrealdb/surrealdb#7447](https://github.com/surrealdb/surrealdb/issues/7447)) — SurrealDB refuses the write, on
|
|
@@ -4928,6 +4930,10 @@ print([(str(order.id), order.amount) for order in await PaidOrder.objects().orde
|
|
|
4928
4930
|
(`objects(tx=tx)`), unseen outside the block until it commits. Either way, a block that raises leaves no view. The
|
|
4929
4931
|
table guard and the 2.x checks run before anything is queued, on committed state; a refusal of a queued statement
|
|
4930
4932
|
comes back when the block commits, as the transaction's own error.
|
|
4933
|
+
- A chain of views can be removed in one buffered block, outer view first (since v0.32.1): on 2.x and the embedded
|
|
4934
|
+
engine `remove_view(tx=tx)` does not count a view whose `remove_view(tx=tx)` the same block has already queued,
|
|
4935
|
+
unless the block names it again afterwards (a `define_view(tx=tx)` of it). A view removed in another transaction,
|
|
4936
|
+
or by a `REMOVE TABLE` you add to the block yourself, is still counted.
|
|
4931
4937
|
- Both run on the model's connection (its `connection` key, or the `using()` block's); a `tx=` of another connection
|
|
4932
4938
|
raises `SurrealDbError` before any request. A view and its source tables live in one database: give a view model
|
|
4933
4939
|
its sources' connection.
|
|
@@ -5253,7 +5259,7 @@ The SDK's in-process engine has a table of its own: [ORM behaviour: the embedded
|
|
|
5253
5259
|
| 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
5260
|
| 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
5261
|
| 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`
|
|
5262
|
+
| Removing a materialized view, or a source table, that another view reads (`remove_view()`, `REMOVE TABLE`) | `remove_view()` raises `SurrealDbError` before any DDL, unless its transaction removes those views first; a raw `REMOVE TABLE` makes them ordinary tables, last records kept | `SurrealDbError` from the server (`Cannot delete table … on which a view is defined`): remove the views that read it first | v0.32.0 |
|
|
5257
5263
|
| 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 |
|
|
5258
5264
|
|
|
5259
5265
|
> **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.
|
|
@@ -4797,9 +4797,11 @@ print([(str(order.id), order.amount) for order in await PaidOrder.objects().orde
|
|
|
4797
4797
|
date: `count()`, `math::sum()`, `math::mean()`, `math::min()`, `math::max()`, `time::min()`, `time::max()` (and
|
|
4798
4798
|
`math::stddev()`, `math::variance()` on 3.x), whatever their argument (`math::sum(amount ?? 0)`). `math::median()`,
|
|
4799
4799
|
`array::group()`, `array::distinct()`, `array::join()` and `vector::sum()` are refused (inside an aggregate's
|
|
4800
|
-
argument they are allowed).
|
|
4801
|
-
|
|
4802
|
-
3.x
|
|
4800
|
+
argument they are allowed). Since v0.32.1 that holds when an expression combines several aggregates too
|
|
4801
|
+
(`math::sum(amount) + math::sum(array::len(array::distinct(tags)))`); beside an aggregate
|
|
4802
|
+
(`math::sum(amount) + array::len(array::distinct(tags))`) they are still refused. On SurrealDB 3.x an expression
|
|
4803
|
+
around an aggregate (`math::sum(amount) / count()`) and a `GROUP BY` on an alias work; on 2.x and the embedded
|
|
4804
|
+
engine `define_view()` refuses them (see "SurrealDB 2.x and 3.x" below).
|
|
4803
4805
|
- Every source write runs the view's query for that record: when it fails — a function given a value of the wrong
|
|
4804
4806
|
type, an aggregated field missing
|
|
4805
4807
|
([surrealdb/surrealdb#7447](https://github.com/surrealdb/surrealdb/issues/7447)) — SurrealDB refuses the write, on
|
|
@@ -4877,6 +4879,10 @@ print([(str(order.id), order.amount) for order in await PaidOrder.objects().orde
|
|
|
4877
4879
|
(`objects(tx=tx)`), unseen outside the block until it commits. Either way, a block that raises leaves no view. The
|
|
4878
4880
|
table guard and the 2.x checks run before anything is queued, on committed state; a refusal of a queued statement
|
|
4879
4881
|
comes back when the block commits, as the transaction's own error.
|
|
4882
|
+
- A chain of views can be removed in one buffered block, outer view first (since v0.32.1): on 2.x and the embedded
|
|
4883
|
+
engine `remove_view(tx=tx)` does not count a view whose `remove_view(tx=tx)` the same block has already queued,
|
|
4884
|
+
unless the block names it again afterwards (a `define_view(tx=tx)` of it). A view removed in another transaction,
|
|
4885
|
+
or by a `REMOVE TABLE` you add to the block yourself, is still counted.
|
|
4880
4886
|
- Both run on the model's connection (its `connection` key, or the `using()` block's); a `tx=` of another connection
|
|
4881
4887
|
raises `SurrealDbError` before any request. A view and its source tables live in one database: give a view model
|
|
4882
4888
|
its sources' connection.
|
|
@@ -5202,7 +5208,7 @@ The SDK's in-process engine has a table of its own: [ORM behaviour: the embedded
|
|
|
5202
5208
|
| 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 |
|
|
5203
5209
|
| 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 |
|
|
5204
5210
|
| 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 |
|
|
5205
|
-
| 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`
|
|
5211
|
+
| Removing a materialized view, or a source table, that another view reads (`remove_view()`, `REMOVE TABLE`) | `remove_view()` raises `SurrealDbError` before any DDL, unless its transaction removes those views first; a raw `REMOVE TABLE` makes them ordinary tables, last records kept | `SurrealDbError` from the server (`Cannot delete table … on which a view is defined`): remove the views that read it first | v0.32.0 |
|
|
5206
5212
|
| 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 |
|
|
5207
5213
|
|
|
5208
5214
|
> **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.
|
|
@@ -30,6 +30,7 @@ from .schema import (
|
|
|
30
30
|
check_view_config,
|
|
31
31
|
check_view_query,
|
|
32
32
|
dependent_views,
|
|
33
|
+
dependents_not_removed,
|
|
33
34
|
ensure_async_events_supported,
|
|
34
35
|
ensure_view_query_supported,
|
|
35
36
|
ensure_view_removable,
|
|
@@ -54,7 +55,7 @@ from .signals import (
|
|
|
54
55
|
pre_save,
|
|
55
56
|
pre_update,
|
|
56
57
|
)
|
|
57
|
-
from .transaction import Transaction
|
|
58
|
+
from .transaction import BufferedTransaction, Transaction
|
|
58
59
|
from .types import Duration, _duration_from_sdk, _timedelta_from_sdk
|
|
59
60
|
from .utils import (
|
|
60
61
|
PLAIN_CONTENT,
|
|
@@ -1145,6 +1146,13 @@ class BaseSurrealModel(BaseModel):
|
|
|
1145
1146
|
their queries only (a view reading the table through a graph path or a sub-select is left to the server), and
|
|
1146
1147
|
the line is read from ``client.version()`` only when one is found.
|
|
1147
1148
|
|
|
1149
|
+
A chain of views can be removed in one buffered transaction (SurrealDB 2.x, HTTP, the embedded engine), outer
|
|
1150
|
+
view first (v0.32.1): a view whose removal the same *tx* has already queued with ``remove_view()`` — the last
|
|
1151
|
+
statement of the block naming it — is not counted, though the committed ``INFO FOR DB`` still lists it. It is
|
|
1152
|
+
counted again when the block names it after that removal (a ``define_view()`` of it), when it is removed in
|
|
1153
|
+
another transaction or by a ``tx.add("REMOVE TABLE …")`` written by hand. On SurrealDB 3.x the server decides,
|
|
1154
|
+
as before.
|
|
1155
|
+
|
|
1148
1156
|
Args:
|
|
1149
1157
|
missing_ok: ``True`` (default) emits ``REMOVE TABLE IF EXISTS …``, so a missing view is not an error;
|
|
1150
1158
|
``False`` raises ``SurrealDbNotFoundError`` for it.
|
|
@@ -1178,9 +1186,11 @@ class BaseSurrealModel(BaseModel):
|
|
|
1178
1186
|
if existing == "unreadable":
|
|
1179
1187
|
raise SurrealDbError(view_table_refused("G3", cls.__name__, table, removal=True))
|
|
1180
1188
|
if existing == "view":
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1189
|
+
dependents = dependent_views(tables, table)
|
|
1190
|
+
if isinstance(tx, BufferedTransaction):
|
|
1191
|
+
# Issue #286: the views this block has already queued for removal are gone when it commits.
|
|
1192
|
+
dependents = dependents_not_removed(dependents, tx.statements)
|
|
1193
|
+
await ensure_view_removable(client, table, dependents, model=cls.__name__, embedded=manager.is_embedded())
|
|
1184
1194
|
await apply_ddl_statements(
|
|
1185
1195
|
[statement],
|
|
1186
1196
|
client=client,
|
|
@@ -37,8 +37,9 @@ the DDL, not a lock. Before the guard, ``define_view()`` refuses on SurrealDB 2.
|
|
|
37
37
|
projection those engines compute once per source record (``ensure_view_query_supported()``, C1), reading the line
|
|
38
38
|
from ``client.version()`` only when the query holds one; 3.x keeps such a view up to date or refuses it itself. On the
|
|
39
39
|
guard's one ``INFO FOR DB``, ``remove_view()`` finds the views that read the view it removes (``dependent_views()``,
|
|
40
|
-
through the plain ``FROM`` targets ``view_reads_table()`` reads
|
|
41
|
-
the
|
|
40
|
+
through the plain ``FROM`` targets ``view_reads_table()`` reads; in a buffered transaction, without those whose
|
|
41
|
+
removal the block has already queued, ``dependents_not_removed()``) and, when there is one, refuses on SurrealDB 2.x
|
|
42
|
+
and the embedded engine before any DDL (``ensure_view_removable()``, G5): those engines would remove it and turn those
|
|
42
43
|
views into ordinary tables that no longer update, where 3.x refuses the removal itself.
|
|
43
44
|
|
|
44
45
|
The module sits low in the package: it imports neither ``model_base`` nor ``connection_manager``, so both can import
|
|
@@ -556,16 +557,21 @@ class ViewElement:
|
|
|
556
557
|
(``math::sum(amount)``), else ``None``.
|
|
557
558
|
- ``calls``: the lower-cased names of the functions it calls (a ``::`` path, or ``count``), in order, nested calls
|
|
558
559
|
included.
|
|
560
|
+
- ``free_calls``: the calls of ``calls`` that stand outside the argument of every call of an aggregate SurrealDB
|
|
561
|
+
keeps up to date (``count``, ``math::sum`` … ``math::variance``), in order — the aggregate calls themselves
|
|
562
|
+
included, the calls inside their arguments left out (v0.32.1, issue #286): ``("math::sum", "math::sum")`` for
|
|
563
|
+
``math::sum(a) + math::sum(array::len(array::distinct(tags)))``.
|
|
559
564
|
- ``fields``: the fields it reads, in order, a repeated one repeated (``("*",)`` for ``*``); ``()`` when
|
|
560
565
|
``aggregate`` is set — the aggregate's argument is read per record, as SurrealDB intends.
|
|
561
|
-
- ``opaque``: it holds a sub-select or a graph path, which the reading does not analyse (``calls
|
|
562
|
-
are then empty).
|
|
566
|
+
- ``opaque``: it holds a sub-select or a graph path, which the reading does not analyse (``calls``,
|
|
567
|
+
``free_calls`` and ``fields`` are then empty).
|
|
563
568
|
"""
|
|
564
569
|
|
|
565
570
|
text: str
|
|
566
571
|
alias: str | None
|
|
567
572
|
aggregate: str | None
|
|
568
573
|
calls: tuple[str, ...]
|
|
574
|
+
free_calls: tuple[str, ...]
|
|
569
575
|
fields: tuple[str, ...]
|
|
570
576
|
opaque: bool
|
|
571
577
|
|
|
@@ -613,8 +619,12 @@ def check_view_query(value: object, *, model: str) -> str:
|
|
|
613
619
|
- V6: it is a ``SELECT VALUE``: a view stores records;
|
|
614
620
|
- V7: it groups its records (a top-level ``GROUP BY`` or ``GROUP ALL``) and its projection calls one of
|
|
615
621
|
``array::distinct``, ``array::group``, ``array::join``, ``math::median``, ``vector::sum`` (in any case) outside
|
|
616
|
-
|
|
617
|
-
aggregate's argument (``math::max(array::len(array::distinct(tags)))``) they work on every line
|
|
622
|
+
the argument of every aggregate call (``ViewElement.free_calls``): SurrealDB documents them as unusable in a
|
|
623
|
+
view. Inside an aggregate's argument (``math::max(array::len(array::distinct(tags)))``) they work on every line,
|
|
624
|
+
also when an element combines several aggregates (``math::sum(amount) +
|
|
625
|
+
math::sum(array::len(array::distinct(tags)))``, since v0.32.1): such an expression around aggregates passes V7,
|
|
626
|
+
and ``define_view()`` refuses it on SurrealDB 2.x and the embedded engine (C1). Beside or around an aggregate
|
|
627
|
+
(``math::sum(a) + array::len(array::distinct(tags))``) they are still refused.
|
|
618
628
|
|
|
619
629
|
The check catches mistakes — unbalanced brackets, a comment that would swallow the parenthesis the ORM appends, a
|
|
620
630
|
second statement where one SELECT is expected — and is not an injection defence: a view's query is trusted
|
|
@@ -643,13 +653,7 @@ def check_view_query(value: object, *, model: str) -> str:
|
|
|
643
653
|
outline = view_query_outline(text)
|
|
644
654
|
if outline.grouped:
|
|
645
655
|
unusable = next(
|
|
646
|
-
(
|
|
647
|
-
call
|
|
648
|
-
for element in outline.elements
|
|
649
|
-
if element.aggregate is None
|
|
650
|
-
for call in element.calls
|
|
651
|
-
if call in _NOT_VIEW_AGGREGATES
|
|
652
|
-
),
|
|
656
|
+
(call for element in outline.elements for call in element.free_calls if call in _NOT_VIEW_AGGREGATES),
|
|
653
657
|
None,
|
|
654
658
|
)
|
|
655
659
|
if unusable is not None:
|
|
@@ -760,6 +764,29 @@ def _view_fields(blanked: str) -> tuple[str, ...]:
|
|
|
760
764
|
return tuple(name for name in _VIEW_FIELD.findall(plain) if name.upper() not in _PROJECTION_KEYWORDS)
|
|
761
765
|
|
|
762
766
|
|
|
767
|
+
def _view_calls(blanked: str) -> tuple[tuple[str, ...], tuple[str, ...]]:
|
|
768
|
+
"""``(calls, free_calls)`` of the blanked expression *blanked*, as :class:`ViewElement` describes them.
|
|
769
|
+
|
|
770
|
+
Each call is read in order; one that starts inside the argument of an aggregate call read before it (up to that
|
|
771
|
+
call's closing bracket, found on the blanked text, so a bracket inside a literal is never one) is a call, not a free
|
|
772
|
+
call (issue #286)."""
|
|
773
|
+
calls: list[str] = []
|
|
774
|
+
free_calls: list[str] = []
|
|
775
|
+
argument_end = -1
|
|
776
|
+
for match in _VIEW_CALL.finditer(blanked):
|
|
777
|
+
name = match.group(1).lower()
|
|
778
|
+
if "::" not in name and name != "count":
|
|
779
|
+
continue
|
|
780
|
+
calls.append(name)
|
|
781
|
+
if match.start() < argument_end:
|
|
782
|
+
continue
|
|
783
|
+
free_calls.append(name)
|
|
784
|
+
if name in _ALL_VIEW_AGGREGATES:
|
|
785
|
+
closing = _closing_bracket(blanked, match.end() - 1)
|
|
786
|
+
argument_end = len(blanked) if closing is None else closing
|
|
787
|
+
return tuple(calls), tuple(free_calls)
|
|
788
|
+
|
|
789
|
+
|
|
763
790
|
def _view_element(text: str, blanked: str, top: str) -> ViewElement:
|
|
764
791
|
"""One projection element from its three slices (*text*, *blanked*, *top*, of the same length)."""
|
|
765
792
|
alias: str | None = None
|
|
@@ -774,8 +801,8 @@ def _view_element(text: str, blanked: str, top: str) -> ViewElement:
|
|
|
774
801
|
written = expression[left:right]
|
|
775
802
|
plain = blanked[left:right] if right > left else ""
|
|
776
803
|
if _VIEW_OPAQUE.search(plain) is not None:
|
|
777
|
-
return ViewElement(written, alias, None, (), (), True)
|
|
778
|
-
calls =
|
|
804
|
+
return ViewElement(written, alias, None, (), (), (), True)
|
|
805
|
+
calls, free_calls = _view_calls(plain)
|
|
779
806
|
aggregate: str | None = None
|
|
780
807
|
first = _VIEW_CALL.match(plain)
|
|
781
808
|
if (
|
|
@@ -785,7 +812,7 @@ def _view_element(text: str, blanked: str, top: str) -> ViewElement:
|
|
|
785
812
|
):
|
|
786
813
|
aggregate = first.group(1).lower()
|
|
787
814
|
fields = () if aggregate is not None else _view_fields(plain)
|
|
788
|
-
return ViewElement(written, alias, aggregate, calls, fields, False)
|
|
815
|
+
return ViewElement(written, alias, aggregate, calls, free_calls, fields, False)
|
|
789
816
|
|
|
790
817
|
|
|
791
818
|
def view_query_outline(text: str) -> ViewOutline:
|
|
@@ -795,9 +822,9 @@ def view_query_outline(text: str) -> ViewOutline:
|
|
|
795
822
|
the server. Literals are blanked first (``define_parser.blank_literals()``), so a keyword, a call or a comma inside
|
|
796
823
|
a string or a quoted name is never read. ``grouped`` is a top-level ``GROUP BY``/``GROUP ALL``; the projection
|
|
797
824
|
runs from ``SELECT`` to the top-level ``FROM`` (or the end) and is split at its top-level commas; each element's
|
|
798
|
-
alias, calls, whole aggregate and fields are read as :class:`ViewElement` describes. A sub-select or a
|
|
799
|
-
(``->``, ``<-``) makes the element opaque, and a backquoted or ``⟨⟩`` field name is invisible: both are
|
|
800
|
-
the server. Pure.
|
|
825
|
+
alias, calls, free calls, whole aggregate and fields are read as :class:`ViewElement` describes. A sub-select or a
|
|
826
|
+
graph path (``->``, ``<-``) makes the element opaque, and a backquoted or ``⟨⟩`` field name is invisible: both are
|
|
827
|
+
left to the server. Pure.
|
|
801
828
|
"""
|
|
802
829
|
blanked, top = _top_level(text)
|
|
803
830
|
grouping = _VIEW_GROUP.search(top)
|
|
@@ -1116,6 +1143,33 @@ def dependent_views(tables: Mapping[str, Any], table: str) -> tuple[str, ...]:
|
|
|
1116
1143
|
return tuple(sorted(names))
|
|
1117
1144
|
|
|
1118
1145
|
|
|
1146
|
+
def dependents_not_removed(dependents: Sequence[str], statements: Sequence[str]) -> tuple[str, ...]:
|
|
1147
|
+
"""The views of *dependents* that the *statements* queued in a buffered transaction do not remove, in order
|
|
1148
|
+
(v0.32.1, issue #286).
|
|
1149
|
+
|
|
1150
|
+
G5 (:func:`ensure_view_removable`) reads committed state, where a view whose removal the same buffered block has
|
|
1151
|
+
already queued is still listed: removing a chain of views outer view first in one block would be refused. A
|
|
1152
|
+
dependent is left out when the last of *statements* that names it — as a whole name, in any case, quoted or not —
|
|
1153
|
+
is exactly the removal ``remove_view()`` queues for it (:func:`remove_view_statement`, with ``missing_ok`` ``True``
|
|
1154
|
+
or ``False``). It is kept when no statement names it, when a statement names it after its removal (a
|
|
1155
|
+
``DEFINE TABLE … AS`` that defines it again, or any other), when its removal was written by hand
|
|
1156
|
+
(``tx.add("REMOVE TABLE …")``), and when its name is not an identifier: a reading of the text, not a parser, that
|
|
1157
|
+
errs on the side of the refusal. Pure.
|
|
1158
|
+
"""
|
|
1159
|
+
left: list[str] = []
|
|
1160
|
+
for name in dependents:
|
|
1161
|
+
try:
|
|
1162
|
+
removals = {remove_view_statement(name, True), remove_view_statement(name, False)}
|
|
1163
|
+
except (TypeError, ValueError):
|
|
1164
|
+
left.append(name)
|
|
1165
|
+
continue
|
|
1166
|
+
naming = re.compile(rf"(?<!\w){re.escape(name)}(?!\w)", re.IGNORECASE)
|
|
1167
|
+
last = next((statement for statement in reversed(statements) if naming.search(statement)), None)
|
|
1168
|
+
if last not in removals:
|
|
1169
|
+
left.append(name)
|
|
1170
|
+
return tuple(left)
|
|
1171
|
+
|
|
1172
|
+
|
|
1119
1173
|
async def ensure_view_removable(client: Any, table: str, dependents: Sequence[str], *, model: str, embedded: bool) -> None:
|
|
1120
1174
|
"""Refuse, on a connection before SurrealDB 3.x, to remove the view *table* that other views read — before any
|
|
1121
1175
|
DDL (G5, v0.32.0).
|
|
@@ -1123,7 +1177,9 @@ async def ensure_view_removable(client: Any, table: str, dependents: Sequence[st
|
|
|
1123
1177
|
While a view reads a table — a source table or an inner view —, SurrealDB 3.x refuses to remove it (``Cannot
|
|
1124
1178
|
delete table … on which a view is defined``). SurrealDB 2.x and the embedded engine remove it, and every view that
|
|
1125
1179
|
read it loses its ``AS`` clause: an ordinary table that keeps its last records and no longer updates. *dependents*
|
|
1126
|
-
are those views (:func:`dependent_views`, over the table guard's one ``INFO FOR DB
|
|
1180
|
+
are those views (:func:`dependent_views`, over the table guard's one ``INFO FOR DB``; in a buffered transaction,
|
|
1181
|
+
without those whose removal the block has already queued, :func:`dependents_not_removed`): none, and nothing is
|
|
1182
|
+
read.
|
|
1127
1183
|
Else the line is read from ``client.version()`` with the lite's rule (``time_travel.line_major()``): on 3.x — or a
|
|
1128
1184
|
version that cannot be read — the removal goes on and the server refuses it; before 3.x, G5 names the dependent
|
|
1129
1185
|
views, sorted. On the embedded engine ``version()`` answers the SDK's version (``surrealdb-2.0.0``), whose bundled
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/connection_config.py
RENAMED
|
File without changes
|
{surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/connection_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/testing/__init__.py
RENAMED
|
File without changes
|
{surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/testing/database.py
RENAMED
|
File without changes
|
{surreal_orm_lite-0.32.0 → surreal_orm_lite-0.32.1}/src/surreal_orm_lite/testing/fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|