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.
Files changed (47) hide show
  1. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/CHANGELOG.md +68 -0
  2. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/PKG-INFO +30 -11
  3. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/README.md +29 -10
  4. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/pyproject.toml +1 -1
  5. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/__init__.py +1 -1
  6. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/concurrency.py +7 -2
  7. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/connection_manager.py +11 -5
  8. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/model_base.py +2 -2
  9. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/transaction.py +206 -43
  10. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/.gitignore +0 -0
  11. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/LICENSE +0 -0
  12. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/Makefile +0 -0
  13. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/_sdk.py +0 -0
  14. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/aggregations.py +0 -0
  15. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/auth.py +0 -0
  16. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/cache.py +0 -0
  17. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/changefeed.py +0 -0
  18. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/connection_config.py +0 -0
  19. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/constants.py +0 -0
  20. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/decoding.py +0 -0
  21. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/define_parser.py +0 -0
  22. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/edge.py +0 -0
  23. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/embedded.py +0 -0
  24. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/encoding.py +0 -0
  25. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/enum.py +0 -0
  26. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/exceptions.py +0 -0
  27. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/functions.py +0 -0
  28. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/geo.py +0 -0
  29. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/introspection.py +0 -0
  30. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/live.py +0 -0
  31. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/model_auth.py +0 -0
  32. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/model_generator.py +0 -0
  33. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/py.typed +0 -0
  34. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/q.py +0 -0
  35. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/query_set.py +0 -0
  36. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/routing.py +0 -0
  37. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/schema.py +0 -0
  38. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/schema_state.py +0 -0
  39. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/signals.py +0 -0
  40. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/subquery.py +0 -0
  41. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/surql_literal.py +0 -0
  42. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/testing/__init__.py +0 -0
  43. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/testing/database.py +0 -0
  44. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/testing/fixtures.py +0 -0
  45. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/time_travel.py +0 -0
  46. {surreal_orm_lite-0.31.1 → surreal_orm_lite-0.31.2}/src/surreal_orm_lite/types.py +0 -0
  47. {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.1
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 (…)", chained to that error. An explicit
736
- `tx.commit()` after a failure raises the same error. When the block's own exception propagates,
737
- that exception wins, as before. Reads the ORM answers itself are not failed statements: a table
738
- that does not exist reads as no rows, and `get()` raises `SurrealDbNotFoundError`.
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 can still carry the server's message, statement excerpt included ([#278](https://github.com/EulogySnowfall/SurrealDB-ORM-lite/issues/278)).
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 can still carry the server's
4723
- message, statement excerpt included ([#278](https://github.com/EulogySnowfall/SurrealDB-ORM-lite/issues/278)). Applying a definition needs a privileged system user: run
4724
- `define_events()` and `define_function()` at start-up or deployment, not on a record user's session.
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`) | the SDK's error classes | v0.25.0 |
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 (…)", chained to that error. An explicit
685
- `tx.commit()` after a failure raises the same error. When the block's own exception propagates,
686
- that exception wins, as before. Reads the ORM answers itself are not failed statements: a table
687
- that does not exist reads as no rows, and `get()` raises `SurrealDbNotFoundError`.
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 can still carry the server's message, statement excerpt included ([#278](https://github.com/EulogySnowfall/SurrealDB-ORM-lite/issues/278)).
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 can still carry the server's
4672
- message, statement excerpt included ([#278](https://github.com/EulogySnowfall/SurrealDB-ORM-lite/issues/278)). Applying a definition needs a privileged system user: run
4673
- `define_events()` and `define_function()` at start-up or deployment, not on a record user's session.
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`) | the SDK's error classes | v0.25.0 |
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 |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "surreal-orm-lite"
3
- version = "0.31.1"
3
+ version = "0.31.2"
4
4
  description = "Lightweight Django-style ORM for SurrealDB using the official Python SDK. Async support with Pydantic validation."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,4 +1,4 @@
1
- __version__ = "0.31.1"
1
+ __version__ = "0.31.2"
2
2
 
3
3
  from ._sdk import BoundExcluded, BoundIncluded
4
4
  from .aggregations import Aggregation, Avg, Count, Max, Min, Sum
@@ -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
- current = current.__cause__ or current.__context__
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
 
@@ -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 (…)", chained to the failed statement's error. When the body's own
1686
- exception propagates, that exception wins. A read the ORM answers itself (a missing table
1687
- read as no rows, ``get()`` raising ``SurrealDbNotFoundError``) is not a failed statement.
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 can still carry the server's message, statement excerpt included (#278).
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 can still carry the server's message, statement excerpt included (#278).
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 can still carry the server's message,
853
- statement excerpt included (#278).
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 that
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
- if not isinstance(raw, dict) or not isinstance(raw.get("result"), list):
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
- renamed = _PARAMETER.sub(
244
- lambda match: f"${prefix}{match[1]}" if match[1] in variables else match[0],
245
- statement,
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
- raw = await self._client.query_raw(self.build_query(), _encoding.encode(self.variables))
277
- self.raise_for_status(raw)
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 that
352
- statement's error, which is chained as the cause): the transaction was cancelled, nothing was
353
- committed. ``SurrealDbConflictError`` when that statement's error was a retryable conflict.
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
- raise error_type(f"{_CANCELLED_AFTER_FAILURE} ({str(failure) or type(failure).__name__})") from failure
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: