Repository: edgedb/edgedb

Issue:
rolling back to nested savepoint error
This error seems correct to me. It doesn't seem like this is a bug, but the error message says it is.

```
EdgeDB 1.0-beta.2+dev.5539.g6596bc813 (repl 1.0.0-beta.1)
Type "\?" for help.
edgedb> START TRANSACTION;
OK: START TRANSACTION
edgedb[tx]> DECLARE SAVEPOINT p1;
OK: DECLARE SAVEPOINT
edgedb[tx]> DECLARE SAVEPOINT p2;
OK: DECLARE SAVEPOINT
edgedb[tx]> ROLLBACK TO SAVEPOINT p1;
OK: ROLLBACK TO SAVEPOINT
edgedb[tx]> ROLLBACK TO SAVEPOINT p2;
Error: ERROR: InternalServerError: savepoint "p2" does not exist
  Hint: This is most likely a bug in EdgeDB. Please consider opening an issue ticket at https://github.com/edgedb/edgedb/issues/new?template=bug_report.md
  Server traceback:
      edb.errors.InternalServerError: savepoint "p2" does not exist
edgedb[tx:failed]>
```
<!-- Please search existing issues to avoid creating duplicates. -->
- EdgeDB Version: EdgeDB 1.0-beta.2+dev.5539.g6596bc813 (repl 1.0.0-beta.1)
- OS Version: Linux 5.8.0-48-generic #54~20.04.1-Ubuntu SMP Sat Mar 20 13:40:25 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux



Discussion:
> It doesn't seem like this is a bug,

~Why? When you rollback to `p1` the state no longer knows about `p2` -- from its point of view `DECLARE p2` never happened.~

Oh, sorry, the bug is that the error is `InternalServerError` and not `TransactionError`, I see it now.
I was thinking that the bug is that it prints this part of the message.

```
 Hint: This is most likely a bug in EdgeDB. Please consider opening an issue ticket at https://github.com/edgedb/edgedb/issues/new?template=bug_report.md
  Server traceback:
      edb.errors.InternalServerError: savepoint "p2" does not exist
```

Erroneously printing this message seems like it would hurt user adoption. 
> Erroneously printing this message seems like it would hurt user adoption.

Yeah, absolutely, I'm looking into this. We do have a check for savepoint existence, but for some reason it's not engaging.

Resolving patch:
diff --git a/edb/server/compiler/dbstate.py b/edb/server/compiler/dbstate.py
--- a/edb/server/compiler/dbstate.py
+++ b/edb/server/compiler/dbstate.py
@@ -404,26 +404,37 @@ def rollback_to_savepoint(self, name: str) -> TransactionState:
             raise errors.TransactionError(
                 'savepoints can only be used in transaction blocks')
 
+        sp_ids_to_erase = []
         for sp in reversed(self._savepoints.values()):
             if sp.name == name:
                 self._current = sp
-                return sp
+                break
+
+            sp_ids_to_erase.append(sp.id)
+        else:
+            raise errors.TransactionError(f'there is no {name!r} savepoint')
+
+        for sp_id in sp_ids_to_erase:
+            self._savepoints.pop(sp_id)
 
-        raise errors.TransactionError(f'there is no {name!r} savepoint')
+        return sp
 
     def release_savepoint(self, name: str):
         if self.is_implicit():
             raise errors.TransactionError(
                 'savepoints can only be used in transaction blocks')
 
+        sp_ids_to_erase = []
         for sp in reversed(self._savepoints.values()):
+            sp_ids_to_erase.append(sp.id)
+
             if sp.name == name:
-                sp_id = sp.id
                 break
         else:
             raise errors.TransactionError(f'there is no {name!r} savepoint')
 
-        self._savepoints.pop(sp_id)
+        for sp_id in sp_ids_to_erase:
+            self._savepoints.pop(sp_id)
 
     def get_schema(self, std_schema: s_schema.FlatSchema) -> s_schema.Schema:
         assert isinstance(std_schema, s_schema.FlatSchema)


Test patch:
diff --git a/tests/test_server_proto.py b/tests/test_server_proto.py
--- a/tests/test_server_proto.py
+++ b/tests/test_server_proto.py
@@ -1263,6 +1263,34 @@ async def test_server_proto_tx_savepoint_11(self):
         finally:
             await con.query('ROLLBACK')
 
+    async def test_server_proto_tx_savepoint_12(self):
+        con = self.con
+
+        await con.query('START TRANSACTION')
+        await con.query('DECLARE SAVEPOINT p1')
+        await con.query('DECLARE SAVEPOINT p2')
+        await con.query('ROLLBACK TO SAVEPOINT p1')
+
+        try:
+            with self.assertRaises(edgedb.TransactionError):
+                await con.query('ROLLBACK TO SAVEPOINT p2')
+        finally:
+            await con.query('ROLLBACK')
+
+    async def test_server_proto_tx_savepoint_13(self):
+        con = self.con
+
+        await con.query('START TRANSACTION')
+        await con.query('DECLARE SAVEPOINT p1')
+        await con.query('DECLARE SAVEPOINT p2')
+        await con.query('RELEASE SAVEPOINT p1')
+
+        try:
+            with self.assertRaises(edgedb.TransactionError):
+                await con.query('ROLLBACK TO SAVEPOINT p2')
+        finally:
+            await con.query('ROLLBACK')
+
     async def test_server_proto_tx_01(self):
         await self.con.query('START TRANSACTION')