From 8b8b689596a9d54ec5f5250236c4e5e71edbe8e4 Mon Sep 17 00:00:00 2001
From: Nikita Pettik <korablev@tarantool.org>
Date: Fri, 16 Aug 2019 21:13:52 +0300
Subject: [PATCH] txn: erase old savepoint in case of name collision

Name duplicates are allowed for savepoints (both in our SQL
implementation and in ANSI specification). ANSI SQL states that previous
savepoint should be deleted. What is more, our doc confirms this fact
and says that "...it is released before the new savepoint is set."
Unfortunately, it's not true - currently old savepoint remains in the
list. For instance:

SAVEPOINT t;
SAVEPOINT t;
RELEASE SAVEPOINT t;
RELEASE SAVEPOINT t; -- no error is raised

Let's fix this and remove old savepoint from the list.
---
 src/box/txn.c                | 14 ++++++++++++--
 test/sql/savepoints.result   | 24 ++++++++++++++++++++++++
 test/sql/savepoints.test.lua | 19 +++++++++++++++++++
 3 files changed, 55 insertions(+), 2 deletions(-)

diff --git a/src/box/txn.c b/src/box/txn.c
index 1002c21368..38b1b595f0 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -750,10 +750,20 @@ txn_savepoint_new(struct txn *txn, const char *name)
 	svp->stmt = stailq_last(&txn->stmts);
 	svp->in_sub_stmt = txn->in_sub_stmt;
 	svp->fk_deferred_count = txn->fk_deferred_count;
-	if (name != NULL)
+	if (name != NULL) {
+		/*
+		 * If savepoint with given name already exists,
+		 * erase it from the list. This has to be done
+		 * in accordance with ANSI SQL compliance.
+		 */
+		struct txn_savepoint *old_svp =
+			txn_savepoint_by_name(txn, name);
+		if (old_svp != NULL)
+			rlist_del(&old_svp->link);
 		memcpy(svp->name, name, name_len + 1);
-	else
+	} else {
 		svp->name[0] = 0;
+	}
 	rlist_add_entry(&txn->savepoints, svp, link);
 	return svp;
 }
diff --git a/test/sql/savepoints.result b/test/sql/savepoints.result
index b5a0b7f464..e48db302a8 100644
--- a/test/sql/savepoints.result
+++ b/test/sql/savepoints.result
@@ -95,3 +95,27 @@ release_sv_fail();
 box.commit();
 ---
 ...
+-- Make sure that if the current transaction has a savepoint
+-- with the same name, the old savepoint is deleted and
+-- a new one is set. Note that no error should be raised.
+--
+collision_sv_2 = function()
+    box.begin()
+    box.execute('SAVEPOINT t1;')
+    box.execute('SAVEPOINT t2;')
+    local _,err = box.execute('SAVEPOINT t1;')
+    assert(err == nil)
+    box.execute('RELEASE SAVEPOINT t1;')
+    local _,err = box.execute('RELEASE SAVEPOINT t1;')
+    assert(err ~= nil)
+    local _, err = box.execute('ROLLBACK TO t2;')
+    assert(err == nil)
+end;
+---
+...
+collision_sv_2();
+---
+...
+box.commit();
+---
+...
diff --git a/test/sql/savepoints.test.lua b/test/sql/savepoints.test.lua
index a4312c114c..99622a4db0 100644
--- a/test/sql/savepoints.test.lua
+++ b/test/sql/savepoints.test.lua
@@ -56,3 +56,22 @@ release_sv_fail = function()
 end;
 release_sv_fail();
 box.commit();
+
+-- Make sure that if the current transaction has a savepoint
+-- with the same name, the old savepoint is deleted and
+-- a new one is set. Note that no error should be raised.
+--
+collision_sv_2 = function()
+    box.begin()
+    box.execute('SAVEPOINT t1;')
+    box.execute('SAVEPOINT t2;')
+    local _,err = box.execute('SAVEPOINT t1;')
+    assert(err == nil)
+    box.execute('RELEASE SAVEPOINT t1;')
+    local _,err = box.execute('RELEASE SAVEPOINT t1;')
+    assert(err ~= nil)
+    local _, err = box.execute('ROLLBACK TO t2;')
+    assert(err == nil)
+end;
+collision_sv_2();
+box.commit();
-- 
GitLab