Repository: edgedb/edgedb

Issue:
EXPLAIN should rollback any side effects
In simpler words `EXPLAIN ANALYZE INSERT ... ` should not commit anything to the DB.


Discussion:

Resolving patch:
diff --git a/edb/server/compiler/compiler.py b/edb/server/compiler/compiler.py
--- a/edb/server/compiler/compiler.py
+++ b/edb/server/compiler/compiler.py
@@ -1358,6 +1358,7 @@ def _compile_ql_explain(
     return dataclasses.replace(
         query,
         is_explain=True,
+        append_rollback=ql.analyze,
         cacheable=False,
         sql=(sql_bytes,),
         sql_hash=sql_hash,
@@ -1579,7 +1580,6 @@ def _compile_ql_transaction(
         action = dbstate.TxAction.DECLARE_SAVEPOINT
 
         sp_name = ql.name
-        sp_id = sp_id
 
     elif isinstance(ql, qlast.ReleaseSavepoint):
         ctx.state.current_tx().release_savepoint(ql.name)
@@ -1964,6 +1964,9 @@ def _try_compile(
                 unit.is_explain = True
                 unit.query_asts = comp.query_asts
 
+            if comp.append_rollback:
+                unit.append_rollback = True
+
             if is_trailing_stmt:
                 unit.cardinality = comp.cardinality
 
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
@@ -99,6 +99,7 @@ class Query(BaseQuery):
     cacheable: bool = True
     is_explain: bool = False
     query_asts: Any = None
+    append_rollback: bool = False
 
 
 @dataclasses.dataclass(frozen=True)
@@ -307,6 +308,7 @@ class QueryUnit:
 
     is_explain: bool = False
     query_asts: Any = None
+    append_rollback: bool = False
 
     @property
     def has_ddl(self) -> bool:


Test patch:
diff --git a/tests/test_edgeql_explain.py b/tests/test_edgeql_explain.py
--- a/tests/test_edgeql_explain.py
+++ b/tests/test_edgeql_explain.py
@@ -113,8 +113,8 @@ def assert_plan(self, data, shape, message=None):
         assert_data_shape.assert_data_shape(
             data, shape, fail=self.fail, message=message)
 
-    async def explain(self, query, *, analyze=False):
-        return json.loads(await self.con.query_single(
+    async def explain(self, query, *, analyze=False, con=None):
+        return json.loads(await (con or self.con).query_single(
             f'explain {"analyze " if analyze else ""}{query}'
         ))[0]
 
@@ -488,3 +488,44 @@ async def test_edgeql_explain_type_intersect_01(self):
         }
 
         self.assert_plan(res['Plan'], shape)
+
+    async def test_edgeql_explain_insert_01(self):
+        # Use an ad-hoc connection to avoid TRANSACTION_ISOLATION
+        con = await self.connect(database=self.con.dbname)
+        try:
+            res = await self.explain('''
+                insert User { name := 'Fantix' }
+            ''', analyze=True, con=con)
+            self.assert_plan(res['Plan'], {
+                'Node Type': 'Nested Loop',
+            })
+            self.assertFalse(await con.query('''
+                select User { id, name } filter .name = 'Fantix'
+            '''))
+        finally:
+            await con.aclose()
+
+    async def test_edgeql_explain_insert_02(self):
+        async with self.con.transaction():
+            await self.con.execute('''
+                insert User { name := 'Sully' }
+            ''')
+            res = await self.explain('''
+                insert User { name := 'Fantix' }
+            ''', analyze=True)
+            self.assert_plan(res['Plan'], {
+                'Node Type': 'Nested Loop',
+            })
+            self.assertTrue(await self.con.query('''
+                select User { id, name } filter .name = 'Sully'
+            '''))
+            self.assertFalse(await self.con.query('''
+                select User { id, name } filter .name = 'Fantix'
+            '''))
+
+        self.assertTrue(await self.con.query('''
+            select User { id, name } filter .name = 'Sully'
+        '''))
+        self.assertFalse(await self.con.query('''
+            select User { id, name } filter .name = 'Fantix'
+        '''))