surreal-orm-lite 0.31.1__tar.gz → 0.31.2__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.1 → surreal_orm_lite-0.31.2}/CHANGELOG.md +68 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/PKG-INFO +30 -11
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/README.md +29 -10
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/pyproject.toml +1 -1
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/__init__.py +1 -1
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/concurrency.py +7 -2
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/connection_manager.py +11 -5
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/model_base.py +2 -2
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/transaction.py +206 -43
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/.gitignore +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/LICENSE +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/Makefile +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/_sdk.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/aggregations.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/auth.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/cache.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/changefeed.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/connection_config.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/constants.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/decoding.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/define_parser.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/edge.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/embedded.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/encoding.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/enum.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/exceptions.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/functions.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/geo.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/introspection.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/live.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/model_auth.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/model_generator.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/py.typed +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/q.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/query_set.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/routing.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/schema.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/schema_state.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/signals.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/subquery.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/surql_literal.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/testing/__init__.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/testing/database.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/testing/fixtures.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/time_travel.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/types.py +0 -0
- {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/utils.py +0 -0
|
@@ -5,6 +5,74 @@ 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.31.2] - 2026-10-19
|
|
9
|
+
|
|
10
|
+
Transaction errors give the server's reason and the failed statement's position, without the
|
|
11
|
+
server's excerpt of the statement, and a buffered commit raises `SurrealDbError` on the embedded
|
|
12
|
+
engine too (#278). No new public API.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Transaction errors give the server's reason and the statement's position, without the
|
|
17
|
+
server's excerpt of the statement** (#278). When the server refused the query as a whole (a
|
|
18
|
+
statement it cannot parse), the error read "Transaction failed: unrecognized query_raw()
|
|
19
|
+
response shape: {…}" and quoted the server's raw response, its excerpt of the statement
|
|
20
|
+
included. It now gives the server's reason and the position:
|
|
21
|
+
|
|
22
|
+
- at a buffered commit (SurrealDB 2.x, HTTP, the embedded engine), "Transaction failed: the
|
|
23
|
+
server refused statement 2 of 3: … (at line 1, column 7 of the statement)", the line and column
|
|
24
|
+
of the statement as written, before the ORM renames its `$name` parameters for the batch; a
|
|
25
|
+
delimiter left unclosed names the statement it opens in, even when the server first reports it
|
|
26
|
+
in a later statement or on the batch's `COMMIT` line;
|
|
27
|
+
- for a write or a definition in an interactive transaction (SurrealDB 3.x over WebSocket),
|
|
28
|
+
"Transaction failed: the server refused the statement: … (at line 1, column 7 of the
|
|
29
|
+
statement)";
|
|
30
|
+
- a failed statement's error, "Transaction failed and rolled back: …", keeps only the first line
|
|
31
|
+
of a reason that quotes the query;
|
|
32
|
+
- a response of any other shape raises "Transaction failed: unrecognized query_raw() response
|
|
33
|
+
shape (<type>)", without the response.
|
|
34
|
+
|
|
35
|
+
The server's full text stays reachable as the error's `__context__`, which a formatted traceback
|
|
36
|
+
does not print. A read in a transaction still raises the SDK's own error, as it does outside a
|
|
37
|
+
transaction.
|
|
38
|
+
|
|
39
|
+
- **A buffered commit on the embedded engine raises `SurrealDbError`** (#278). The engine reports a
|
|
40
|
+
query it cannot parse as a bare `RuntimeError`, which propagated out of the `transaction()` block;
|
|
41
|
+
the commit now raises `SurrealDbError`, or `SurrealDbConflictError` for a retryable conflict, as
|
|
42
|
+
on a server.
|
|
43
|
+
- **A statement holding conflict words is not taken for a conflict** (#278). When the server could
|
|
44
|
+
not parse a statement holding words such as "write conflict", `is_conflict_error()` recognised
|
|
45
|
+
the transaction's error as a retryable conflict, so `retry_on_conflict` retried it, and an
|
|
46
|
+
interactive transaction's cancellation error was a `SurrealDbConflictError`. Conflicts are now
|
|
47
|
+
recognised from the server's reason, not from its excerpt of the statement.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- **The cancellation error names the failed statement's error type** (#278). Since v0.30.3,
|
|
52
|
+
leaving an interactive `transaction()` block after a failed statement, or an explicit
|
|
53
|
+
`tx.commit()`, raises "The transaction was cancelled: a statement in it failed; nothing was
|
|
54
|
+
committed (…)", whose parenthesis repeated the failed statement's error text. The parenthesis now
|
|
55
|
+
holds that error's type name, for example `(SurrealDbError)`. The error itself is still chained
|
|
56
|
+
as `__cause__`, and a retryable conflict still raises `SurrealDbConflictError`, so
|
|
57
|
+
`retry_on_conflict` behaves as before.
|
|
58
|
+
- **`is_conflict_error()` follows the chain a traceback prints** (#278): a context suppressed with
|
|
59
|
+
`raise … from None` is no longer read, since the ORM keeps the server's full text there, which
|
|
60
|
+
may quote the statement; `__cause__` and an implicit `__context__` are still followed. A conflict
|
|
61
|
+
your code re-raises as another exception with `raise … from None` is therefore no longer
|
|
62
|
+
recognised, nor retried by `retry_on_conflict`: chain it (`raise … from exc`, or `raise …`
|
|
63
|
+
without `from` inside the `except` block), or raise a `SurrealDbConflictError`, to keep it
|
|
64
|
+
retryable.
|
|
65
|
+
|
|
66
|
+
### Notes
|
|
67
|
+
|
|
68
|
+
- The v0.31.0 caution on the DDL appliers with `tx=` (#278), that an error raised when the
|
|
69
|
+
transaction ends can carry the server's excerpt of the statement, no longer applies: that error
|
|
70
|
+
now gives the server's reason and the statement's position instead. README sections 11 and 32
|
|
71
|
+
and the embedded-engine table say so.
|
|
72
|
+
- Same behaviour on SurrealDB 2.x, 3.x and the embedded engine. Server pins unchanged: tested
|
|
73
|
+
against SurrealDB 3.3.4 and 2.7.0 and on the embedded engine, plus 3.1.5 for backward
|
|
74
|
+
compatibility.
|
|
75
|
+
|
|
8
76
|
## [0.31.1] - 2026-10-19
|
|
9
77
|
|
|
10
78
|
A datetime outside years 1-9999 no longer fails the response it is in (#279). SurrealDB stores
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: surreal-orm-lite
|
|
3
|
-
Version: 0.31.
|
|
3
|
+
Version: 0.31.2
|
|
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
|
|
@@ -732,10 +732,11 @@ cancelled when the block ends, nothing of the block is committed, and leaving th
|
|
|
732
732
|
`SurrealDbError` (`SurrealDbConflictError` for a retryable conflict, so `retry_on_conflict` still
|
|
733
733
|
retries the block). A buffered transaction fails as a whole at commit; an interactive one raises
|
|
734
734
|
the failed statement's error at once, then, when the block ends, "The transaction was cancelled: a
|
|
735
|
-
statement in it failed; nothing was committed (
|
|
736
|
-
`tx.commit()` after a failure raises the same error. When
|
|
737
|
-
that exception wins, as before. Reads the ORM answers itself
|
|
738
|
-
that does not exist reads as no rows, and `get()` raises
|
|
735
|
+
statement in it failed; nothing was committed (SurrealDbError)", which names the type of that
|
|
736
|
+
error and is chained to it. An explicit `tx.commit()` after a failure raises the same error. When
|
|
737
|
+
the block's own exception propagates, that exception wins, as before. Reads the ORM answers itself
|
|
738
|
+
are not failed statements: a table that does not exist reads as no rows, and `get()` raises
|
|
739
|
+
`SurrealDbNotFoundError`.
|
|
739
740
|
|
|
740
741
|
```python
|
|
741
742
|
async with SurrealDBConnectionManager.transaction() as tx:
|
|
@@ -747,6 +748,18 @@ async with SurrealDBConnectionManager.transaction() as tx:
|
|
|
747
748
|
# leaving the block raised SurrealDbError: alice was not saved either
|
|
748
749
|
```
|
|
749
750
|
|
|
751
|
+
**Transaction errors leave out the server's excerpt of the statement** (v0.31.2). When the server
|
|
752
|
+
refuses the query as a whole (a statement it cannot parse), the error gives the server's reason and
|
|
753
|
+
the position, without the server's excerpt of the statement: a buffered commit raises "Transaction
|
|
754
|
+
failed: the server refused statement 2 of 3: … (at line 1, column 7 of the statement)", the
|
|
755
|
+
embedded engine included, and a write or a definition in an interactive transaction raises
|
|
756
|
+
"Transaction failed: the server refused the statement: … (at line 1, column 7 of the statement)".
|
|
757
|
+
The line and column are those of the statement as written, before the ORM renames its `$name`
|
|
758
|
+
parameters for the batch, and a delimiter left unclosed names the statement it opens in, even when
|
|
759
|
+
the server stops in a later statement. The server's full text stays reachable as the error's
|
|
760
|
+
`__context__`, which a traceback does not print. A read in a transaction is not affected: it raises
|
|
761
|
+
the SDK's own error, as it does outside a transaction.
|
|
762
|
+
|
|
750
763
|
### 12. Upsert & get_or_create / update_or_create
|
|
751
764
|
|
|
752
765
|
```python
|
|
@@ -891,7 +904,11 @@ except SurrealDbConflictError:
|
|
|
891
904
|
- Total attempts = `max_retries + 1`; after they are exhausted the conflict is re-raised as
|
|
892
905
|
`SurrealDbConflictError`. The numeric parameters are validated at decoration time.
|
|
893
906
|
- Detection anchors on SurrealDB's own retryable marker ("This transaction can be retried"), so
|
|
894
|
-
a non-retryable failure (e.g. a duplicate-key error) is **not** retried.
|
|
907
|
+
a non-retryable failure (e.g. a duplicate-key error) is **not** retried. `is_conflict_error()`
|
|
908
|
+
reads the exception and its chain as a traceback prints it: a context suppressed with
|
|
909
|
+
`raise … from None` is not read (v0.31.2), so a conflict you re-raise as another exception with
|
|
910
|
+
`raise … from None` is no longer recognised: chain it (`raise … from exc`, or `raise …` without
|
|
911
|
+
`from` inside the `except` block), or raise a `SurrealDbConflictError`.
|
|
895
912
|
- The exception type is identical on 2.6.x and 3.x; conflicts simply arise more often on 3.x
|
|
896
913
|
(optimistic MVCC) than on 2.6.x (see the behaviour table), except on change-feed tables, where
|
|
897
914
|
2.x refuses concurrent writes that 3.x accepts (see [section 22](#22-change-feeds-changes)).
|
|
@@ -4685,7 +4702,8 @@ await SurrealDBConnectionManager.remove_function("fn::greet")
|
|
|
4685
4702
|
(SurrealDB 2.x, HTTP, the embedded engine) only queues the statement, so a refusal comes back when the block
|
|
4686
4703
|
commits, as the transaction's own error, never as `SurrealDbAuthenticationError` or `SurrealDbNotFoundError`,
|
|
4687
4704
|
`missing_ok=False` included. When a definition is applied in a transaction (`tx=`), an error raised when the
|
|
4688
|
-
transaction ends
|
|
4705
|
+
transaction ends gives the server's reason and the statement's position, without the server's excerpt of the
|
|
4706
|
+
statement (v0.31.2, [#278](https://github.com/EulogySnowfall/SurrealDB-ORM-lite/issues/278)).
|
|
4689
4707
|
- Live queries see the rows an event writes (`watch()` on their table). The query cache
|
|
4690
4708
|
([section 29](#29-query-cache-cache-querycache)) does not: it forgets the tables the ORM itself writes, so a cached
|
|
4691
4709
|
read of a table that events write stays as it was until its time to live runs out. Do not cache such reads, or call
|
|
@@ -4719,9 +4737,10 @@ swallow the clauses the ORM appends, a second statement where one expression is
|
|
|
4719
4737
|
defence: expressions are trusted application code, never user input. To use a value, read it from the record
|
|
4720
4738
|
(`$after.email`) or from a `DEFINE PARAM`, never format it into the text. Kinds are parsed and written in canonical
|
|
4721
4739
|
form. An `Event`'s `repr()` leaves out its expressions, and a refusal raised without `tx=` names the event or the
|
|
4722
|
-
function, never its statement; with `tx=`, an error raised when the transaction ends
|
|
4723
|
-
|
|
4724
|
-
|
|
4740
|
+
function, never its statement; with `tx=`, an error raised when the transaction ends gives the server's reason and
|
|
4741
|
+
the statement's position, without the server's excerpt of the statement (v0.31.2,
|
|
4742
|
+
[#278](https://github.com/EulogySnowfall/SurrealDB-ORM-lite/issues/278)). Applying a definition needs a privileged
|
|
4743
|
+
system user: run `define_events()` and `define_function()` at start-up or deployment, not on a record user's session.
|
|
4725
4744
|
|
|
4726
4745
|
#### Coming from the full SurrealDB-ORM
|
|
4727
4746
|
|
|
@@ -5016,7 +5035,7 @@ Capabilities not listed behave as on a 2.x server.
|
|
|
5016
5035
|
| Anonymous session | the in-process owner (full access); `info()` returns `None` | not used by the ORM (the configured user signs in); an anonymous `info()` raises `SurrealDbAuthenticationError` | v0.25.0 |
|
|
5017
5036
|
| `invalidate()` | back to the in-process owner | signs in again as the configured user | v0.25.0 |
|
|
5018
5037
|
| Reading a table that does not exist | `[]`, raw queries included (as on 2.x) | the ORM reads `[]`; a raw query on 3.x raises `NotFoundError` | v0.25.0 |
|
|
5019
|
-
| Parse errors | bare `RuntimeError` (the DDL helpers raise `SurrealDbError`)
|
|
5038
|
+
| Parse errors | bare `RuntimeError` (the DDL helpers and the commit of `transaction()` raise `SurrealDbError`) | the SDK's error classes | v0.25.0 |
|
|
5020
5039
|
| Time travel (`as_of()`) | `surrealkv+versioned://` only (`?versioned=true` is ignored by SDK 2.0.0); any other URL → `SurrealDbError` before any request; `fetch()` refused (2.x core) | versioned storage required (see the 2.x/3.x table) | v0.26.0 |
|
|
5021
5040
|
| 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 |
|
|
5022
5041
|
| 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 |
|
|
@@ -681,10 +681,11 @@ cancelled when the block ends, nothing of the block is committed, and leaving th
|
|
|
681
681
|
`SurrealDbError` (`SurrealDbConflictError` for a retryable conflict, so `retry_on_conflict` still
|
|
682
682
|
retries the block). A buffered transaction fails as a whole at commit; an interactive one raises
|
|
683
683
|
the failed statement's error at once, then, when the block ends, "The transaction was cancelled: a
|
|
684
|
-
statement in it failed; nothing was committed (
|
|
685
|
-
`tx.commit()` after a failure raises the same error. When
|
|
686
|
-
that exception wins, as before. Reads the ORM answers itself
|
|
687
|
-
that does not exist reads as no rows, and `get()` raises
|
|
684
|
+
statement in it failed; nothing was committed (SurrealDbError)", which names the type of that
|
|
685
|
+
error and is chained to it. An explicit `tx.commit()` after a failure raises the same error. When
|
|
686
|
+
the block's own exception propagates, that exception wins, as before. Reads the ORM answers itself
|
|
687
|
+
are not failed statements: a table that does not exist reads as no rows, and `get()` raises
|
|
688
|
+
`SurrealDbNotFoundError`.
|
|
688
689
|
|
|
689
690
|
```python
|
|
690
691
|
async with SurrealDBConnectionManager.transaction() as tx:
|
|
@@ -696,6 +697,18 @@ async with SurrealDBConnectionManager.transaction() as tx:
|
|
|
696
697
|
# leaving the block raised SurrealDbError: alice was not saved either
|
|
697
698
|
```
|
|
698
699
|
|
|
700
|
+
**Transaction errors leave out the server's excerpt of the statement** (v0.31.2). When the server
|
|
701
|
+
refuses the query as a whole (a statement it cannot parse), the error gives the server's reason and
|
|
702
|
+
the position, without the server's excerpt of the statement: a buffered commit raises "Transaction
|
|
703
|
+
failed: the server refused statement 2 of 3: … (at line 1, column 7 of the statement)", the
|
|
704
|
+
embedded engine included, and a write or a definition in an interactive transaction raises
|
|
705
|
+
"Transaction failed: the server refused the statement: … (at line 1, column 7 of the statement)".
|
|
706
|
+
The line and column are those of the statement as written, before the ORM renames its `$name`
|
|
707
|
+
parameters for the batch, and a delimiter left unclosed names the statement it opens in, even when
|
|
708
|
+
the server stops in a later statement. The server's full text stays reachable as the error's
|
|
709
|
+
`__context__`, which a traceback does not print. A read in a transaction is not affected: it raises
|
|
710
|
+
the SDK's own error, as it does outside a transaction.
|
|
711
|
+
|
|
699
712
|
### 12. Upsert & get_or_create / update_or_create
|
|
700
713
|
|
|
701
714
|
```python
|
|
@@ -840,7 +853,11 @@ except SurrealDbConflictError:
|
|
|
840
853
|
- Total attempts = `max_retries + 1`; after they are exhausted the conflict is re-raised as
|
|
841
854
|
`SurrealDbConflictError`. The numeric parameters are validated at decoration time.
|
|
842
855
|
- Detection anchors on SurrealDB's own retryable marker ("This transaction can be retried"), so
|
|
843
|
-
a non-retryable failure (e.g. a duplicate-key error) is **not** retried.
|
|
856
|
+
a non-retryable failure (e.g. a duplicate-key error) is **not** retried. `is_conflict_error()`
|
|
857
|
+
reads the exception and its chain as a traceback prints it: a context suppressed with
|
|
858
|
+
`raise … from None` is not read (v0.31.2), so a conflict you re-raise as another exception with
|
|
859
|
+
`raise … from None` is no longer recognised: chain it (`raise … from exc`, or `raise …` without
|
|
860
|
+
`from` inside the `except` block), or raise a `SurrealDbConflictError`.
|
|
844
861
|
- The exception type is identical on 2.6.x and 3.x; conflicts simply arise more often on 3.x
|
|
845
862
|
(optimistic MVCC) than on 2.6.x (see the behaviour table), except on change-feed tables, where
|
|
846
863
|
2.x refuses concurrent writes that 3.x accepts (see [section 22](#22-change-feeds-changes)).
|
|
@@ -4634,7 +4651,8 @@ await SurrealDBConnectionManager.remove_function("fn::greet")
|
|
|
4634
4651
|
(SurrealDB 2.x, HTTP, the embedded engine) only queues the statement, so a refusal comes back when the block
|
|
4635
4652
|
commits, as the transaction's own error, never as `SurrealDbAuthenticationError` or `SurrealDbNotFoundError`,
|
|
4636
4653
|
`missing_ok=False` included. When a definition is applied in a transaction (`tx=`), an error raised when the
|
|
4637
|
-
transaction ends
|
|
4654
|
+
transaction ends gives the server's reason and the statement's position, without the server's excerpt of the
|
|
4655
|
+
statement (v0.31.2, [#278](https://github.com/EulogySnowfall/SurrealDB-ORM-lite/issues/278)).
|
|
4638
4656
|
- Live queries see the rows an event writes (`watch()` on their table). The query cache
|
|
4639
4657
|
([section 29](#29-query-cache-cache-querycache)) does not: it forgets the tables the ORM itself writes, so a cached
|
|
4640
4658
|
read of a table that events write stays as it was until its time to live runs out. Do not cache such reads, or call
|
|
@@ -4668,9 +4686,10 @@ swallow the clauses the ORM appends, a second statement where one expression is
|
|
|
4668
4686
|
defence: expressions are trusted application code, never user input. To use a value, read it from the record
|
|
4669
4687
|
(`$after.email`) or from a `DEFINE PARAM`, never format it into the text. Kinds are parsed and written in canonical
|
|
4670
4688
|
form. An `Event`'s `repr()` leaves out its expressions, and a refusal raised without `tx=` names the event or the
|
|
4671
|
-
function, never its statement; with `tx=`, an error raised when the transaction ends
|
|
4672
|
-
|
|
4673
|
-
|
|
4689
|
+
function, never its statement; with `tx=`, an error raised when the transaction ends gives the server's reason and
|
|
4690
|
+
the statement's position, without the server's excerpt of the statement (v0.31.2,
|
|
4691
|
+
[#278](https://github.com/EulogySnowfall/SurrealDB-ORM-lite/issues/278)). Applying a definition needs a privileged
|
|
4692
|
+
system user: run `define_events()` and `define_function()` at start-up or deployment, not on a record user's session.
|
|
4674
4693
|
|
|
4675
4694
|
#### Coming from the full SurrealDB-ORM
|
|
4676
4695
|
|
|
@@ -4965,7 +4984,7 @@ Capabilities not listed behave as on a 2.x server.
|
|
|
4965
4984
|
| Anonymous session | the in-process owner (full access); `info()` returns `None` | not used by the ORM (the configured user signs in); an anonymous `info()` raises `SurrealDbAuthenticationError` | v0.25.0 |
|
|
4966
4985
|
| `invalidate()` | back to the in-process owner | signs in again as the configured user | v0.25.0 |
|
|
4967
4986
|
| Reading a table that does not exist | `[]`, raw queries included (as on 2.x) | the ORM reads `[]`; a raw query on 3.x raises `NotFoundError` | v0.25.0 |
|
|
4968
|
-
| Parse errors | bare `RuntimeError` (the DDL helpers raise `SurrealDbError`)
|
|
4987
|
+
| Parse errors | bare `RuntimeError` (the DDL helpers and the commit of `transaction()` raise `SurrealDbError`) | the SDK's error classes | v0.25.0 |
|
|
4969
4988
|
| Time travel (`as_of()`) | `surrealkv+versioned://` only (`?versioned=true` is ignored by SDK 2.0.0); any other URL → `SurrealDbError` before any request; `fetch()` refused (2.x core) | versioned storage required (see the 2.x/3.x table) | v0.26.0 |
|
|
4970
4989
|
| 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 |
|
|
4971
4990
|
| 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 |
|
|
@@ -42,7 +42,9 @@ def is_conflict_error(error: BaseException) -> bool:
|
|
|
42
42
|
A :class:`SurrealDbConflictError` always qualifies. Otherwise the exception
|
|
43
43
|
and its ``__cause__`` / ``__context__`` chain are scanned for one of the
|
|
44
44
|
conflict signal phrases (so a conflict wrapped in another exception is still
|
|
45
|
-
recognised).
|
|
45
|
+
recognised). The chain is the one a traceback prints: a context suppressed with
|
|
46
|
+
``raise … from None`` is not part of it (#278) — the ORM keeps a server's full
|
|
47
|
+
text there, and a statement it quotes may hold conflict words.
|
|
46
48
|
"""
|
|
47
49
|
if isinstance(error, SurrealDbConflictError):
|
|
48
50
|
return True
|
|
@@ -53,7 +55,10 @@ def is_conflict_error(error: BaseException) -> bool:
|
|
|
53
55
|
text = str(current).lower()
|
|
54
56
|
if any(signal in text for signal in _CONFLICT_SIGNALS):
|
|
55
57
|
return True
|
|
56
|
-
|
|
58
|
+
if current.__cause__ is not None:
|
|
59
|
+
current = current.__cause__
|
|
60
|
+
else:
|
|
61
|
+
current = None if current.__suppress_context__ else current.__context__
|
|
57
62
|
return False
|
|
58
63
|
|
|
59
64
|
|
{surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/connection_manager.py
RENAMED
|
@@ -1682,9 +1682,15 @@ class SurrealDBConnectionManager:
|
|
|
1682
1682
|
raises ``SurrealDbError`` (``SurrealDbConflictError`` for a retryable conflict, so
|
|
1683
1683
|
``retry_on_conflict`` still retries). A buffered batch fails as a whole at commit; an
|
|
1684
1684
|
interactive transaction raises "The transaction was cancelled: a statement in it failed;
|
|
1685
|
-
nothing was committed (
|
|
1686
|
-
|
|
1687
|
-
read as no rows, ``get()`` raising
|
|
1685
|
+
nothing was committed (SurrealDbError)", which names the type of the failed statement's
|
|
1686
|
+
error and is chained to it. When the body's own exception propagates, that exception wins.
|
|
1687
|
+
A read the ORM answers itself (a missing table read as no rows, ``get()`` raising
|
|
1688
|
+
``SurrealDbNotFoundError``) is not a failed statement.
|
|
1689
|
+
|
|
1690
|
+
A query the server refuses as a whole (a statement it cannot parse) raises ``SurrealDbError``
|
|
1691
|
+
with the server's reason and the position, without the server's excerpt of the statement
|
|
1692
|
+
(issue #278): "Transaction failed: the server refused statement 2 of 3: … (at line 1, column 7
|
|
1693
|
+
of the statement)" at a buffered commit, the embedded engine included.
|
|
1688
1694
|
|
|
1689
1695
|
SurrealDB has no savepoints/nested transactions; nested ``transaction()`` calls
|
|
1690
1696
|
open INDEPENDENT transactions, not savepoints.
|
|
@@ -2488,7 +2494,7 @@ class SurrealDBConnectionManager:
|
|
|
2488
2494
|
without *tx*, a refusal names the event and its table, never its statement, whose ``when`` and
|
|
2489
2495
|
``then`` may hold a secret). In a buffered transaction (*tx* over HTTP, on SurrealDB 2.x or the
|
|
2490
2496
|
embedded engine) the statement is only queued: a refusal comes back when the block commits, as the
|
|
2491
|
-
transaction's own error, never as ``SurrealDbAuthenticationError``. With *tx*, an error raised when the transaction ends
|
|
2497
|
+
transaction's own error, never as ``SurrealDbAuthenticationError``. With *tx*, an error raised when the transaction ends gives the server's reason and the statement's position, without the server's excerpt of the statement (#278).
|
|
2492
2498
|
:raises SurrealDbAuthenticationError: the session may not define events.
|
|
2493
2499
|
"""
|
|
2494
2500
|
check_call_transaction(cls._connection_name or "default", tx, operation="define_event()")
|
|
@@ -2610,7 +2616,7 @@ class SurrealDBConnectionManager:
|
|
|
2610
2616
|
statement (``Can't apply function definition: 'fn::greet' -> …``: without *tx*, a refusal names the
|
|
2611
2617
|
function, never its statement, whose body may hold a secret). In a buffered transaction (*tx* over HTTP,
|
|
2612
2618
|
on SurrealDB 2.x or the embedded engine) the statement is only queued: a refusal comes back when the
|
|
2613
|
-
block commits, as the transaction's own error, never as ``SurrealDbAuthenticationError``. With *tx*, an error raised when the transaction ends
|
|
2619
|
+
block commits, as the transaction's own error, never as ``SurrealDbAuthenticationError``. With *tx*, an error raised when the transaction ends gives the server's reason and the statement's position, without the server's excerpt of the statement (#278).
|
|
2614
2620
|
"""
|
|
2615
2621
|
check_call_transaction(cls._connection_name or "default", tx, operation="define_function()")
|
|
2616
2622
|
statement = function_ddl(
|
|
@@ -849,8 +849,8 @@ class BaseSurrealModel(BaseModel):
|
|
|
849
849
|
|
|
850
850
|
Without *tx*, a refusal names the event and its table, never its statement, whose ``when`` and ``then``
|
|
851
851
|
may hold a secret: ``Can't apply event definition: 'email_audit' on table 'account' -> <the server's
|
|
852
|
-
reason>``. With *tx*, an error raised when the transaction ends
|
|
853
|
-
statement excerpt
|
|
852
|
+
reason>``. With *tx*, an error raised when the transaction ends gives the server's reason and the
|
|
853
|
+
statement's position, without the server's excerpt of the statement (#278).
|
|
854
854
|
|
|
855
855
|
Args:
|
|
856
856
|
overwrite: see :meth:`event_ddl`.
|
|
@@ -24,14 +24,23 @@ Query cache (v0.28.0): an ORM write that joins a transaction notes the tables it
|
|
|
24
24
|
(``_note_cache_write``); the commit puts them in flight and invalidates their cached reads when
|
|
25
25
|
it returns or raises (a failed commit's state is unknown). ``cancel()`` forgets them: nothing
|
|
26
26
|
became visible.
|
|
27
|
+
|
|
28
|
+
Errors (issue #278): the errors a failed transaction raises give the server's reason and the failed statement's
|
|
29
|
+
position, without the server's excerpt of the statement; the server's own text stays reachable as their suppressed
|
|
30
|
+
``__context__``. A query the server or the embedded engine refuses at a buffered commit raises ``SurrealDbError`` on
|
|
31
|
+
every engine (the embedded engine's bare ``RuntimeError`` included), ``SurrealDbConflictError`` for a retryable
|
|
32
|
+
conflict; a transport error (a lost connection, a timeout) is not a refusal and propagates unchanged, since the
|
|
33
|
+
commit's outcome is unknown. A read relays the SDK's own error, as outside a transaction. After a failed statement,
|
|
34
|
+
an interactive transaction's cancellation error names only that statement's error type.
|
|
27
35
|
"""
|
|
28
36
|
|
|
29
37
|
import contextlib
|
|
30
38
|
import re
|
|
31
39
|
from abc import ABC, abstractmethod
|
|
32
40
|
from collections.abc import Awaitable, Callable, Iterable
|
|
33
|
-
from typing import Any
|
|
41
|
+
from typing import Any, NoReturn
|
|
34
42
|
|
|
43
|
+
from . import _sdk
|
|
35
44
|
from . import cache as _cache
|
|
36
45
|
from . import encoding as _encoding
|
|
37
46
|
from .exceptions import SurrealDbConflictError, SurrealDbError
|
|
@@ -41,10 +50,19 @@ _PARAMETER = re.compile(r"\$(\w+)")
|
|
|
41
50
|
|
|
42
51
|
_NOT_EXECUTED_FILLER = "The query was not executed due to a failed transaction"
|
|
43
52
|
|
|
44
|
-
#: The error ``InteractiveTransaction.commit()`` raises after a failed statement (issue #268), followed by
|
|
45
|
-
#: statement's error.
|
|
53
|
+
#: The error ``InteractiveTransaction.commit()`` raises after a failed statement (issue #268), followed by the type
|
|
54
|
+
#: name of that statement's error in parentheses (issue #278), e.g. ``(SurrealDbError)``.
|
|
46
55
|
_CANCELLED_AFTER_FAILURE = "The transaction was cancelled: a statement in it failed; nothing was committed"
|
|
47
56
|
|
|
57
|
+
#: Where a server's error starts quoting the query it refused (issue #278): a parse error's `` --> [line:column]``, on
|
|
58
|
+
#: a line of its own, followed by an excerpt of the query.
|
|
59
|
+
_QUERY_EXCERPT = re.compile(r"\n\s*--> \[(\d+):(\d+)\]")
|
|
60
|
+
|
|
61
|
+
#: Maps a position of a buffered batch's query (line, column) to the queued statement it falls in (issue #278):
|
|
62
|
+
#: ``(number, count, line, column)``, the line and column in the text the caller queued; ``None`` outside every
|
|
63
|
+
#: statement (:meth:`BufferedTransaction._locate`).
|
|
64
|
+
_Locate = Callable[[int, int], tuple[int, int, int, int] | None]
|
|
65
|
+
|
|
48
66
|
|
|
49
67
|
def _is_pure_filler(result: Any) -> bool:
|
|
50
68
|
"""True if a statement's message is ONLY the generic 'not executed' filler.
|
|
@@ -65,6 +83,111 @@ def _as_conflict(exc: Exception) -> Exception:
|
|
|
65
83
|
return exc
|
|
66
84
|
|
|
67
85
|
|
|
86
|
+
def _error_type(server_text: str) -> type[SurrealDbError]:
|
|
87
|
+
"""``SurrealDbConflictError`` when *server_text* reports a retryable conflict, else ``SurrealDbError``."""
|
|
88
|
+
from .concurrency import is_conflict_error
|
|
89
|
+
|
|
90
|
+
return SurrealDbConflictError if is_conflict_error(SurrealDbError(server_text)) else SurrealDbError
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _server_reason(message: str) -> tuple[str, list[tuple[int, int]]]:
|
|
94
|
+
"""The server's reason in *message*, without any of the query's text, and the positions it gives (issue #278).
|
|
95
|
+
|
|
96
|
+
A message that quotes the query — a parse error, from `` --> [line:column]`` on, and on some errors in the lines
|
|
97
|
+
before it too (a regex's) — is cut to its first line, and every line and column it gives is returned, in order
|
|
98
|
+
(an unclosed delimiter gives where the parser stopped, then where the delimiter opens). Any other message is the
|
|
99
|
+
reason as it is, with no position.
|
|
100
|
+
"""
|
|
101
|
+
positions = [(int(found[1]), int(found[2])) for found in _QUERY_EXCERPT.finditer(message)]
|
|
102
|
+
if not positions:
|
|
103
|
+
return message, []
|
|
104
|
+
return message.partition("\n")[0].rstrip(), positions
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _refusal(message: str, locate: _Locate | None = None) -> SurrealDbError:
|
|
108
|
+
"""The ORM's error for a query the server refused as a whole with *message* (issue #278).
|
|
109
|
+
|
|
110
|
+
``Transaction failed: the server refused <what>: <reason> (at line <l>, column <c> of <where>)``: the reason is
|
|
111
|
+
:func:`_server_reason`'s, never the query's text. With *locate* (a buffered batch), the position names the queued
|
|
112
|
+
statement, ``statement 2 of 3``, with the line and column in the text the caller queued; without it, the position
|
|
113
|
+
is the statement's (an interactive transaction runs one at a time). The server gives an unclosed delimiter two
|
|
114
|
+
positions, where it stopped and then where the delimiter opens: the stop can fall in a later statement or on the
|
|
115
|
+
``COMMIT`` line. So the statement named is the one that holds the last position falling in a queued statement,
|
|
116
|
+
and the position given is the first the server gives in that statement (where it stopped, when that is in it
|
|
117
|
+
too); outside every statement, the query's first position. ``SurrealDbConflictError`` when the reason reports a
|
|
118
|
+
retryable conflict: the excerpt, which may quote conflict words, is not read.
|
|
119
|
+
"""
|
|
120
|
+
reason, positions = _server_reason(message)
|
|
121
|
+
error_type = _error_type(reason)
|
|
122
|
+
if not positions:
|
|
123
|
+
return error_type(f"Transaction failed: the server refused the query: {reason}")
|
|
124
|
+
line, column = positions[0]
|
|
125
|
+
what, where = "the statement", "the statement"
|
|
126
|
+
if locate is not None:
|
|
127
|
+
places = [place for place in (locate(*position) for position in positions) if place is not None]
|
|
128
|
+
if not places:
|
|
129
|
+
what, where = "the query", "the query"
|
|
130
|
+
else:
|
|
131
|
+
number, count, line, column = next(place for place in places if place[0] == places[-1][0])
|
|
132
|
+
what = f"statement {number} of {count}"
|
|
133
|
+
at = f"at line {line}, column {column} of {where}"
|
|
134
|
+
return error_type(f"Transaction failed: the server refused {what}: {reason} ({at})")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _raise_hiding(error: SurrealDbError, server_text: str) -> NoReturn:
|
|
138
|
+
"""Raise *error* ``from None``, *server_text* kept as its ``__context__`` (a ``SurrealDbError``) (issue #278).
|
|
139
|
+
|
|
140
|
+
A formatted traceback shows *error* alone: the server's text, which may quote the statement, stays reachable for
|
|
141
|
+
debugging without being printed.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
raise SurrealDbError(server_text)
|
|
145
|
+
except SurrealDbError:
|
|
146
|
+
raise error from None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _raise_for_status(raw: Any, locate: _Locate | None = None) -> None:
|
|
150
|
+
""":meth:`Transaction.raise_for_status`, with *locate* for a buffered batch (issue #278): a position then names
|
|
151
|
+
the queued statement (``statement 2 of 3``)."""
|
|
152
|
+
if not isinstance(raw, dict) or not isinstance(raw.get("result"), list):
|
|
153
|
+
envelope = raw.get("error") if isinstance(raw, dict) else None
|
|
154
|
+
message = envelope.get("message") if isinstance(envelope, dict) else None
|
|
155
|
+
if isinstance(message, str) and message:
|
|
156
|
+
_raise_hiding(_refusal(message, locate), message)
|
|
157
|
+
_raise_hiding(
|
|
158
|
+
SurrealDbError(f"Transaction failed: unrecognized query_raw() response shape ({type(raw).__name__})"),
|
|
159
|
+
repr(raw),
|
|
160
|
+
)
|
|
161
|
+
statements = raw["result"]
|
|
162
|
+
errors = [s for s in statements if isinstance(s, dict) and s.get("status") == "ERR"]
|
|
163
|
+
if not errors:
|
|
164
|
+
return
|
|
165
|
+
# Prefer a real error (kind != NotExecuted, and not the generic "not executed"
|
|
166
|
+
# filler); else the first ERR whose message is not that filler (the real cause is
|
|
167
|
+
# sometimes a NotExecuted statement, e.g. "Cannot COMMIT: Transaction conflict ...",
|
|
168
|
+
# and some servers omit `details` on filler statements); else the first ERR.
|
|
169
|
+
root = (
|
|
170
|
+
next(
|
|
171
|
+
(
|
|
172
|
+
e
|
|
173
|
+
for e in errors
|
|
174
|
+
if (e.get("details") or {}).get("kind") != "NotExecuted" and not _is_pure_filler(e.get("result"))
|
|
175
|
+
),
|
|
176
|
+
None,
|
|
177
|
+
)
|
|
178
|
+
or next((e for e in errors if not _is_pure_filler(e.get("result"))), None)
|
|
179
|
+
or errors[0]
|
|
180
|
+
)
|
|
181
|
+
message = str(root.get("result") or "transaction failed")
|
|
182
|
+
reason, positions = _server_reason(message)
|
|
183
|
+
# Type retryable conflicts so callers / retry_on_conflict can catch them, from the reason alone (#278): a real
|
|
184
|
+
# conflict has no excerpt, and an excerpt may quote conflict words.
|
|
185
|
+
error = _error_type(reason)(f"Transaction failed and rolled back: {reason}")
|
|
186
|
+
if not positions:
|
|
187
|
+
raise error
|
|
188
|
+
_raise_hiding(error, message)
|
|
189
|
+
|
|
190
|
+
|
|
68
191
|
class Transaction(ABC):
|
|
69
192
|
"""Base transaction: shared post-commit machinery + the call-site interface.
|
|
70
193
|
|
|
@@ -167,7 +290,7 @@ class Transaction(ABC):
|
|
|
167
290
|
|
|
168
291
|
@staticmethod
|
|
169
292
|
def raise_for_status(raw: Any) -> None:
|
|
170
|
-
"""Inspect a ``query_raw()`` response and raise if any statement failed.
|
|
293
|
+
"""Inspect a ``query_raw()`` response and raise if the query or any statement failed.
|
|
171
294
|
|
|
172
295
|
``query()`` silently returns ``None`` on a failed transaction, so the
|
|
173
296
|
commit/write paths use ``query_raw()`` and call this. Any statement with
|
|
@@ -175,35 +298,16 @@ class Transaction(ABC):
|
|
|
175
298
|
interactive transaction when it ends (its ``commit()`` cancels after a failed statement,
|
|
176
299
|
issue #268). Prefers the root cause (the first ERR whose ``details.kind`` is not
|
|
177
300
|
``"NotExecuted"``).
|
|
301
|
+
|
|
302
|
+
The error leaves out the server's excerpt of the query (issue #278). A query the server
|
|
303
|
+
refused as a whole (an ``error`` envelope, e.g. a statement it cannot parse) raises
|
|
304
|
+
``Transaction failed: the server refused …`` with the server's reason and the position it
|
|
305
|
+
gives (:func:`_refusal`); a failed statement raises ``Transaction failed and rolled back:
|
|
306
|
+
<the server's reason>``, a reason that quotes the query cut to its first line. Whenever the
|
|
307
|
+
message leaves part of the server's text out, that text is kept as the error's suppressed
|
|
308
|
+
``__context__``. ``SurrealDbConflictError`` for a retryable conflict.
|
|
178
309
|
"""
|
|
179
|
-
|
|
180
|
-
raise SurrealDbError(f"Transaction failed: unrecognized query_raw() response shape: {raw!r}")
|
|
181
|
-
statements = raw["result"]
|
|
182
|
-
errors = [s for s in statements if isinstance(s, dict) and s.get("status") == "ERR"]
|
|
183
|
-
if not errors:
|
|
184
|
-
return
|
|
185
|
-
# Prefer a real error (kind != NotExecuted, and not the generic "not executed"
|
|
186
|
-
# filler); else the first ERR whose message is not that filler (the real cause is
|
|
187
|
-
# sometimes a NotExecuted statement, e.g. "Cannot COMMIT: Transaction conflict ...",
|
|
188
|
-
# and some servers omit `details` on filler statements); else the first ERR.
|
|
189
|
-
root = (
|
|
190
|
-
next(
|
|
191
|
-
(
|
|
192
|
-
e
|
|
193
|
-
for e in errors
|
|
194
|
-
if (e.get("details") or {}).get("kind") != "NotExecuted" and not _is_pure_filler(e.get("result"))
|
|
195
|
-
),
|
|
196
|
-
None,
|
|
197
|
-
)
|
|
198
|
-
or next((e for e in errors if not _is_pure_filler(e.get("result"))), None)
|
|
199
|
-
or errors[0]
|
|
200
|
-
)
|
|
201
|
-
message = root.get("result") or "transaction failed"
|
|
202
|
-
full = f"Transaction failed and rolled back: {message}"
|
|
203
|
-
# Type retryable conflicts so callers / retry_on_conflict can catch them.
|
|
204
|
-
if isinstance(_as_conflict(SurrealDbError(full)), SurrealDbConflictError):
|
|
205
|
-
raise SurrealDbConflictError(full)
|
|
206
|
-
raise SurrealDbError(full)
|
|
310
|
+
_raise_for_status(raw)
|
|
207
311
|
|
|
208
312
|
|
|
209
313
|
class BufferedTransaction(Transaction):
|
|
@@ -219,6 +323,9 @@ class BufferedTransaction(Transaction):
|
|
|
219
323
|
self.statements: list[str] = []
|
|
220
324
|
self.variables: dict[str, Any] = {}
|
|
221
325
|
self._counter: int = 0
|
|
326
|
+
# Issue #278: per queued statement, the length of its prefix and where add() inserted it (offsets in the
|
|
327
|
+
# caller's text), so that a position the server gives is moved back to the caller's column.
|
|
328
|
+
self._renamed_at: list[tuple[int, list[int]]] = []
|
|
222
329
|
|
|
223
330
|
@property
|
|
224
331
|
def is_interactive(self) -> bool:
|
|
@@ -234,19 +341,27 @@ class BufferedTransaction(Transaction):
|
|
|
234
341
|
Each ``$name`` of ``variables`` becomes ``$t<N>_name``, in a single pass over the statement (#261): a name is
|
|
235
342
|
matched whole, so one that is a prefix of another is not corrupted, a renamed name is never renamed again, and
|
|
236
343
|
a parameter that is not one of ``variables`` (``$this``, ``$value``…) is left alone. Returns None: nothing
|
|
237
|
-
executes until commit, so buffered writes cannot return rows.
|
|
344
|
+
executes until commit, so buffered writes cannot return rows. Where each prefix goes is noted, so that a
|
|
345
|
+
position the server reports at commit is given in the caller's text (issue #278).
|
|
238
346
|
"""
|
|
239
347
|
variables = _encoding.encode(variables)
|
|
240
348
|
prefix = f"t{self._counter}_"
|
|
241
349
|
renamed = statement
|
|
350
|
+
inserted: list[int] = []
|
|
242
351
|
if variables:
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
352
|
+
names = set(variables)
|
|
353
|
+
|
|
354
|
+
def rename(match: re.Match[str]) -> str:
|
|
355
|
+
if match[1] not in names:
|
|
356
|
+
return match[0]
|
|
357
|
+
inserted.append(match.start() + 1) # right after the "$"
|
|
358
|
+
return f"${prefix}{match[1]}"
|
|
359
|
+
|
|
360
|
+
renamed = _PARAMETER.sub(rename, statement)
|
|
247
361
|
for name, value in variables.items():
|
|
248
362
|
self.variables[f"{prefix}{name}"] = value
|
|
249
363
|
self.statements.append(renamed)
|
|
364
|
+
self._renamed_at.append((len(prefix), inserted))
|
|
250
365
|
self._counter += 1
|
|
251
366
|
return None
|
|
252
367
|
|
|
@@ -264,6 +379,42 @@ class BufferedTransaction(Transaction):
|
|
|
264
379
|
body = "\n".join(self.statements)
|
|
265
380
|
return f"BEGIN TRANSACTION;\n{body}\nCOMMIT TRANSACTION;"
|
|
266
381
|
|
|
382
|
+
def _locate(self, line: int, column: int) -> tuple[int, int, int, int] | None:
|
|
383
|
+
"""The queued statement that *line* of :meth:`build_query`'s query falls in, and the position in the caller's
|
|
384
|
+
text: ``(number, count, line, column)``; ``None`` outside every statement (issue #278).
|
|
385
|
+
|
|
386
|
+
Line 1 is ``BEGIN TRANSACTION;``; each statement starts on the line after the previous one ends.
|
|
387
|
+
"""
|
|
388
|
+
first = 2
|
|
389
|
+
for index, statement in enumerate(self.statements):
|
|
390
|
+
last = first + statement.count("\n")
|
|
391
|
+
if first <= line <= last:
|
|
392
|
+
own_line = line - first + 1
|
|
393
|
+
return index + 1, len(self.statements), own_line, self._caller_column(index, own_line, column)
|
|
394
|
+
first = last + 1
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
def _caller_column(self, index: int, line: int, column: int) -> int:
|
|
398
|
+
"""*column* of *line* of the queued statement *index*, moved back over the prefixes :meth:`add` inserted
|
|
399
|
+
before it on that line. SurrealDB counts columns in characters, as Python does; a column inside a prefix
|
|
400
|
+
becomes the name's first character. Unchanged when ``statements`` no longer matches what :meth:`add` noted.
|
|
401
|
+
"""
|
|
402
|
+
if len(self._renamed_at) != len(self.statements):
|
|
403
|
+
return column
|
|
404
|
+
size, inserted = self._renamed_at[index]
|
|
405
|
+
line_start = sum(len(text) + 1 for text in self.statements[index].split("\n")[: line - 1])
|
|
406
|
+
offset = line_start + column - 1
|
|
407
|
+
on_earlier_lines = before = 0
|
|
408
|
+
for earlier, original in enumerate(inserted):
|
|
409
|
+
start = original + earlier * size # where this prefix starts in the queued text
|
|
410
|
+
if start + size <= line_start:
|
|
411
|
+
on_earlier_lines += 1
|
|
412
|
+
if start + size <= offset:
|
|
413
|
+
before += 1
|
|
414
|
+
elif start <= offset:
|
|
415
|
+
return start - line_start + 1 - (before - on_earlier_lines) * size
|
|
416
|
+
return column - (before - on_earlier_lines) * size
|
|
417
|
+
|
|
267
418
|
async def commit(self) -> None:
|
|
268
419
|
# The tables noted are in flight during the commit and invalidated when it returns
|
|
269
420
|
# or raises (a failed commit's state is unknown).
|
|
@@ -273,8 +424,17 @@ class BufferedTransaction(Transaction):
|
|
|
273
424
|
return
|
|
274
425
|
# query_raw (NOT query): query() returns None on a failed tx; query_raw gives
|
|
275
426
|
# per-statement status for raise_for_status.
|
|
276
|
-
|
|
277
|
-
|
|
427
|
+
try:
|
|
428
|
+
raw = await self._client.query_raw(self.build_query(), _encoding.encode(self.variables))
|
|
429
|
+
except RuntimeError as exc:
|
|
430
|
+
# The embedded engine raises a bare RuntimeError for a query it refuses as a whole (issue
|
|
431
|
+
# #278): the same error as a server's envelope, the engine's error kept as the suppressed
|
|
432
|
+
# __context__. A server client's RuntimeError is not the server's answer: it propagates.
|
|
433
|
+
embedded = _sdk.SdkAsyncEmbeddedConnection
|
|
434
|
+
if embedded is None or not isinstance(self._client, embedded):
|
|
435
|
+
raise
|
|
436
|
+
raise _refusal(str(exc), self._locate) from None
|
|
437
|
+
_raise_for_status(raw, self._locate)
|
|
278
438
|
finally:
|
|
279
439
|
self._forget_cache_writes()
|
|
280
440
|
|
|
@@ -284,6 +444,7 @@ class BufferedTransaction(Transaction):
|
|
|
284
444
|
self._forget_cache_writes()
|
|
285
445
|
self.statements = []
|
|
286
446
|
self.variables = {}
|
|
447
|
+
self._renamed_at = []
|
|
287
448
|
|
|
288
449
|
|
|
289
450
|
class InteractiveTransaction(Transaction):
|
|
@@ -348,9 +509,10 @@ class InteractiveTransaction(Transaction):
|
|
|
348
509
|
"""Commit the transaction; after a failed statement, cancel it and raise instead (issue #268).
|
|
349
510
|
|
|
350
511
|
Raises:
|
|
351
|
-
SurrealDbError: a statement failed in this transaction (:data:`_CANCELLED_AFTER_FAILURE`, followed by
|
|
352
|
-
statement's error,
|
|
353
|
-
committed. ``SurrealDbConflictError`` when that
|
|
512
|
+
SurrealDbError: a statement failed in this transaction (:data:`_CANCELLED_AFTER_FAILURE`, followed by the
|
|
513
|
+
type name of that statement's error, e.g. ``(SurrealDbError)``; the error itself is chained as the
|
|
514
|
+
cause): the transaction was cancelled, nothing was committed. ``SurrealDbConflictError`` when that
|
|
515
|
+
statement's error was a retryable conflict.
|
|
354
516
|
"""
|
|
355
517
|
failure = self._first_failure()
|
|
356
518
|
if failure is not None:
|
|
@@ -358,7 +520,8 @@ class InteractiveTransaction(Transaction):
|
|
|
358
520
|
|
|
359
521
|
await self.cancel()
|
|
360
522
|
error_type = SurrealDbConflictError if is_conflict_error(failure) else SurrealDbError
|
|
361
|
-
|
|
523
|
+
# Only the type name (issue #278): the failed statement's error is the cause, its text is not repeated.
|
|
524
|
+
raise error_type(f"{_CANCELLED_AFTER_FAILURE} ({type(failure).__name__})") from failure
|
|
362
525
|
# The tables noted are in flight during the commit and invalidated when it returns
|
|
363
526
|
# or raises (a failed commit's state is unknown).
|
|
364
527
|
try:
|
|
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.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/connection_config.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
|
|
File without changes
|
{surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/testing/__init__.py
RENAMED
|
File without changes
|
{surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/testing/database.py
RENAMED
|
File without changes
|
{surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/testing/fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|