1 /* Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
2    Copyright (c) 2009, 2021, MariaDB Corporation.
3 
4    This program is free software; you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation; version 2 of the License.
7 
8    This program is distributed in the hope that it will be useful,
9    but WITHOUT ANY WARRANTY; without even the implied warranty of
10    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11    GNU General Public License for more details.
12 
13    You should have received a copy of the GNU General Public License
14    along with this program; if not, write to the Free Software
15    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1335  USA */
16 
17 
18 #ifdef USE_PRAGMA_IMPLEMENTATION
19 #pragma implementation                         // gcc: Class implementation
20 #endif
21 
22 #include "mariadb.h"
23 #include "sql_priv.h"
24 #include "transaction.h"
25 #include "debug_sync.h"         // DEBUG_SYNC
26 #include "sql_acl.h"
27 #include "semisync_master.h"
28 #ifdef WITH_WSREP
29 #include "wsrep_trans_observer.h"
30 #endif /* WITH_WSREP */
31 
32 /**
33   Helper: Tell tracker (if any) that transaction ended.
34 */
trans_track_end_trx(THD * thd)35 void trans_track_end_trx(THD *thd)
36 {
37 #ifndef EMBEDDED_LIBRARY
38   if (thd->variables.session_track_transaction_info > TX_TRACK_NONE)
39     thd->session_tracker.transaction_info.end_trx(thd);
40 #endif //EMBEDDED_LIBRARY
41 }
42 
43 
44 /**
45   Helper: transaction ended, SET TRANSACTION one-shot variables
46   revert to session values. Let the transaction state tracker know.
47 */
trans_reset_one_shot_chistics(THD * thd)48 void trans_reset_one_shot_chistics(THD *thd)
49 {
50 #ifndef EMBEDDED_LIBRARY
51   if (thd->variables.session_track_transaction_info > TX_TRACK_NONE)
52   {
53     thd->session_tracker.transaction_info.set_read_flags(thd, TX_READ_INHERIT);
54     thd->session_tracker.transaction_info.set_isol_level(thd, TX_ISOL_INHERIT);
55   }
56 #endif //EMBEDDED_LIBRARY
57   thd->tx_isolation= (enum_tx_isolation) thd->variables.tx_isolation;
58   thd->tx_read_only= thd->variables.tx_read_only;
59 }
60 
61 /* Conditions under which the transaction state must not change. */
trans_check(THD * thd)62 static bool trans_check(THD *thd)
63 {
64   DBUG_ENTER("trans_check");
65 
66   /*
67     Always commit statement transaction before manipulating with
68     the normal one.
69   */
70   DBUG_ASSERT(thd->transaction.stmt.is_empty());
71 
72   if (unlikely(thd->in_sub_stmt))
73     my_error(ER_COMMIT_NOT_ALLOWED_IN_SF_OR_TRG, MYF(0));
74   if (thd->transaction.xid_state.is_explicit_XA())
75     thd->transaction.xid_state.er_xaer_rmfail();
76   else
77     DBUG_RETURN(FALSE);
78 
79   DBUG_RETURN(TRUE);
80 }
81 
82 
83 /**
84   Begin a new transaction.
85 
86   @note Beginning a transaction implicitly commits any current
87         transaction and releases existing locks.
88 
89   @param thd     Current thread
90   @param flags   Transaction flags
91 
92   @retval FALSE  Success
93   @retval TRUE   Failure
94 */
95 
trans_begin(THD * thd,uint flags)96 bool trans_begin(THD *thd, uint flags)
97 {
98   int res= FALSE;
99   DBUG_ENTER("trans_begin");
100 
101   if (trans_check(thd))
102     DBUG_RETURN(TRUE);
103 
104   thd->locked_tables_list.unlock_locked_tables(thd);
105 
106   DBUG_ASSERT(!thd->locked_tables_mode);
107 
108   if (thd->in_multi_stmt_transaction_mode() ||
109       (thd->variables.option_bits & OPTION_TABLE_LOCK))
110   {
111     thd->variables.option_bits&= ~OPTION_TABLE_LOCK;
112     thd->server_status&=
113       ~(SERVER_STATUS_IN_TRANS | SERVER_STATUS_IN_TRANS_READONLY);
114     DBUG_PRINT("info", ("clearing SERVER_STATUS_IN_TRANS"));
115     res= MY_TEST(ha_commit_trans(thd, TRUE));
116 #ifdef WITH_WSREP
117     if (wsrep_thd_is_local(thd))
118     {
119       res= res || wsrep_after_statement(thd);
120     }
121 #endif /* WITH_WSREP */
122   }
123 
124   thd->variables.option_bits&= ~(OPTION_BEGIN | OPTION_KEEP_LOG);
125 
126   /*
127     The following set should not be needed as transaction state should
128     already be reset. We should at some point change this to an assert.
129   */
130   thd->transaction.all.reset();
131   thd->has_waiter= false;
132   thd->waiting_on_group_commit= false;
133   thd->transaction.start_time.reset(thd);
134 
135   if (res)
136     DBUG_RETURN(TRUE);
137 
138   /*
139     Release transactional metadata locks only after the
140     transaction has been committed.
141   */
142   thd->release_transactional_locks();
143 
144   // The RO/RW options are mutually exclusive.
145   DBUG_ASSERT(!((flags & MYSQL_START_TRANS_OPT_READ_ONLY) &&
146                 (flags & MYSQL_START_TRANS_OPT_READ_WRITE)));
147   if (flags & MYSQL_START_TRANS_OPT_READ_ONLY)
148   {
149     thd->tx_read_only= true;
150 #ifndef EMBEDDED_LIBRARY
151     if (thd->variables.session_track_transaction_info > TX_TRACK_NONE)
152       thd->session_tracker.transaction_info.set_read_flags(thd, TX_READ_ONLY);
153 #endif //EMBEDDED_LIBRARY
154   }
155   else if (flags & MYSQL_START_TRANS_OPT_READ_WRITE)
156   {
157     /*
158       Explicitly starting a RW transaction when the server is in
159       read-only mode, is not allowed unless the user has SUPER priv.
160       Implicitly starting a RW transaction is allowed for backward
161       compatibility.
162     */
163     const bool user_is_super=
164       MY_TEST(thd->security_ctx->master_access & SUPER_ACL);
165     if (opt_readonly && !user_is_super)
166     {
167       my_error(ER_OPTION_PREVENTS_STATEMENT, MYF(0), "--read-only");
168       DBUG_RETURN(true);
169     }
170     thd->tx_read_only= false;
171     /*
172       This flags that tx_read_only was set explicitly, rather than
173       just from the session's default.
174     */
175 #ifndef EMBEDDED_LIBRARY
176     if (thd->variables.session_track_transaction_info > TX_TRACK_NONE)
177       thd->session_tracker.transaction_info.set_read_flags(thd, TX_READ_WRITE);
178 #endif //EMBEDDED_LIBRARY
179   }
180 
181 #ifdef WITH_WSREP
182   if (wsrep_thd_is_local(thd))
183   {
184     if (wsrep_sync_wait(thd))
185       DBUG_RETURN(TRUE);
186     if (!thd->tx_read_only &&
187         wsrep_start_transaction(thd, thd->wsrep_next_trx_id()))
188       DBUG_RETURN(TRUE);
189   }
190 #endif /* WITH_WSREP */
191 
192   thd->variables.option_bits|= OPTION_BEGIN;
193   thd->server_status|= SERVER_STATUS_IN_TRANS;
194   if (thd->tx_read_only)
195     thd->server_status|= SERVER_STATUS_IN_TRANS_READONLY;
196   DBUG_PRINT("info", ("setting SERVER_STATUS_IN_TRANS"));
197 
198 #ifndef EMBEDDED_LIBRARY
199   if (thd->variables.session_track_transaction_info > TX_TRACK_NONE)
200     thd->session_tracker.transaction_info.add_trx_state(thd, TX_EXPLICIT);
201 #endif //EMBEDDED_LIBRARY
202 
203   /* ha_start_consistent_snapshot() relies on OPTION_BEGIN flag set. */
204   if (flags & MYSQL_START_TRANS_OPT_WITH_CONS_SNAPSHOT)
205   {
206 #ifndef EMBEDDED_LIBRARY
207     if (thd->variables.session_track_transaction_info > TX_TRACK_NONE)
208       thd->session_tracker.transaction_info.add_trx_state(thd, TX_WITH_SNAPSHOT);
209 #endif //EMBEDDED_LIBRARY
210     res= ha_start_consistent_snapshot(thd);
211   }
212 
213   DBUG_RETURN(MY_TEST(res));
214 }
215 
216 
217 /**
218   Commit the current transaction, making its changes permanent.
219 
220   @param thd     Current thread
221 
222   @retval FALSE  Success
223   @retval TRUE   Failure
224 */
225 
trans_commit(THD * thd)226 bool trans_commit(THD *thd)
227 {
228   int res;
229   DBUG_ENTER("trans_commit");
230 
231   if (trans_check(thd))
232     DBUG_RETURN(TRUE);
233 
234   thd->server_status&=
235     ~(SERVER_STATUS_IN_TRANS | SERVER_STATUS_IN_TRANS_READONLY);
236   DBUG_PRINT("info", ("clearing SERVER_STATUS_IN_TRANS"));
237   res= ha_commit_trans(thd, TRUE);
238 
239   mysql_mutex_assert_not_owner(&LOCK_prepare_ordered);
240   mysql_mutex_assert_not_owner(mysql_bin_log.get_log_lock());
241   mysql_mutex_assert_not_owner(&LOCK_after_binlog_sync);
242   mysql_mutex_assert_not_owner(&LOCK_commit_ordered);
243 
244     /*
245       if res is non-zero, then ha_commit_trans has rolled back the
246       transaction, so the hooks for rollback will be called.
247     */
248   if (res)
249   {
250 #ifdef HAVE_REPLICATION
251     repl_semisync_master.wait_after_rollback(thd, FALSE);
252 #endif
253   }
254   else
255   {
256 #ifdef HAVE_REPLICATION
257     repl_semisync_master.wait_after_commit(thd, FALSE);
258 #endif
259   }
260   thd->variables.option_bits&= ~(OPTION_BEGIN | OPTION_KEEP_LOG);
261   thd->transaction.all.reset();
262   thd->lex->start_transaction_opt= 0;
263 
264   trans_track_end_trx(thd);
265 
266   DBUG_RETURN(MY_TEST(res));
267 }
268 
269 
270 /**
271   Implicitly commit the current transaction.
272 
273   @note A implicit commit does not releases existing table locks.
274 
275   @param thd     Current thread
276 
277   @retval FALSE  Success
278   @retval TRUE   Failure
279 */
280 
trans_commit_implicit(THD * thd)281 bool trans_commit_implicit(THD *thd)
282 {
283   bool res= FALSE;
284   DBUG_ENTER("trans_commit_implicit");
285 
286   if (trans_check(thd))
287     DBUG_RETURN(TRUE);
288 
289   if (thd->variables.option_bits & OPTION_GTID_BEGIN)
290     DBUG_PRINT("error", ("OPTION_GTID_BEGIN is set. "
291                          "Master and slave will have different GTID values"));
292 
293   if (thd->in_multi_stmt_transaction_mode() ||
294       (thd->variables.option_bits & OPTION_TABLE_LOCK))
295   {
296     /* Safety if one did "drop table" on locked tables */
297     if (!thd->locked_tables_mode)
298       thd->variables.option_bits&= ~OPTION_TABLE_LOCK;
299     thd->server_status&=
300       ~(SERVER_STATUS_IN_TRANS | SERVER_STATUS_IN_TRANS_READONLY);
301     DBUG_PRINT("info", ("clearing SERVER_STATUS_IN_TRANS"));
302     res= MY_TEST(ha_commit_trans(thd, TRUE));
303   }
304 
305   thd->variables.option_bits&= ~(OPTION_BEGIN | OPTION_KEEP_LOG);
306   thd->transaction.all.reset();
307 
308   /*
309     Upon implicit commit, reset the current transaction
310     isolation level and access mode. We do not care about
311     @@session.completion_type since it's documented
312     to not have any effect on implicit commit.
313   */
314   trans_reset_one_shot_chistics(thd);
315 
316   trans_track_end_trx(thd);
317 
318   DBUG_RETURN(res);
319 }
320 
321 
322 /**
323   Rollback the current transaction, canceling its changes.
324 
325   @param thd     Current thread
326 
327   @retval FALSE  Success
328   @retval TRUE   Failure
329 */
330 
trans_rollback(THD * thd)331 bool trans_rollback(THD *thd)
332 {
333   int res;
334   DBUG_ENTER("trans_rollback");
335 
336   if (trans_check(thd))
337     DBUG_RETURN(TRUE);
338 
339   thd->server_status&=
340     ~(SERVER_STATUS_IN_TRANS | SERVER_STATUS_IN_TRANS_READONLY);
341   DBUG_PRINT("info", ("clearing SERVER_STATUS_IN_TRANS"));
342   res= ha_rollback_trans(thd, TRUE);
343 #ifdef HAVE_REPLICATION
344   repl_semisync_master.wait_after_rollback(thd, FALSE);
345 #endif
346   thd->variables.option_bits&= ~(OPTION_BEGIN | OPTION_KEEP_LOG);
347   /* Reset the binlog transaction marker */
348   thd->variables.option_bits&= ~OPTION_GTID_BEGIN;
349   thd->transaction.all.reset();
350   thd->lex->start_transaction_opt= 0;
351 
352   trans_track_end_trx(thd);
353 
354   DBUG_RETURN(MY_TEST(res));
355 }
356 
357 
358 /**
359   Implicitly rollback the current transaction, typically
360   after deadlock was discovered.
361 
362   @param thd     Current thread
363 
364   @retval False Success
365   @retval True  Failure
366 
367   @note ha_rollback_low() which is indirectly called by this
368         function will mark XA transaction for rollback by
369         setting appropriate RM error status if there was
370         transaction rollback request.
371 */
372 
trans_rollback_implicit(THD * thd)373 bool trans_rollback_implicit(THD *thd)
374 {
375   int res;
376   DBUG_ENTER("trans_rollback_implict");
377 
378   /*
379     Always commit/rollback statement transaction before manipulating
380     with the normal one.
381     Don't perform rollback in the middle of sub-statement, wait till
382     its end.
383   */
384   DBUG_ASSERT(thd->transaction.stmt.is_empty() && !thd->in_sub_stmt);
385 
386   thd->server_status&= ~SERVER_STATUS_IN_TRANS;
387   DBUG_PRINT("info", ("clearing SERVER_STATUS_IN_TRANS"));
388   res= ha_rollback_trans(thd, true);
389   /*
390     We don't reset OPTION_BEGIN flag below to simulate implicit start
391     of new transacton in @@autocommit=1 mode. This is necessary to
392     preserve backward compatibility.
393   */
394   thd->variables.option_bits&= ~(OPTION_KEEP_LOG);
395   thd->transaction.all.reset();
396 
397   /* Rollback should clear transaction_rollback_request flag. */
398   DBUG_ASSERT(! thd->transaction_rollback_request);
399 
400   trans_track_end_trx(thd);
401 
402   DBUG_RETURN(MY_TEST(res));
403 }
404 
405 
406 /**
407   Commit the single statement transaction.
408 
409   @note Note that if the autocommit is on, then the following call
410         inside InnoDB will commit or rollback the whole transaction
411         (= the statement). The autocommit mechanism built into InnoDB
412         is based on counting locks, but if the user has used LOCK
413         TABLES then that mechanism does not know to do the commit.
414 
415   @param thd     Current thread
416 
417   @retval FALSE  Success
418   @retval TRUE   Failure
419 */
420 
trans_commit_stmt(THD * thd)421 bool trans_commit_stmt(THD *thd)
422 {
423   DBUG_ENTER("trans_commit_stmt");
424   int res= FALSE;
425   /*
426     We currently don't invoke commit/rollback at end of
427     a sub-statement.  In future, we perhaps should take
428     a savepoint for each nested statement, and release the
429     savepoint when statement has succeeded.
430   */
431   DBUG_ASSERT(! thd->in_sub_stmt);
432 
433   thd->merge_unsafe_rollback_flags();
434 
435   if (thd->transaction.stmt.ha_list)
436   {
437     res= ha_commit_trans(thd, FALSE);
438     if (! thd->in_active_multi_stmt_transaction())
439     {
440       trans_reset_one_shot_chistics(thd);
441     }
442   }
443 
444   mysql_mutex_assert_not_owner(&LOCK_prepare_ordered);
445   mysql_mutex_assert_not_owner(mysql_bin_log.get_log_lock());
446   mysql_mutex_assert_not_owner(&LOCK_after_binlog_sync);
447   mysql_mutex_assert_not_owner(&LOCK_commit_ordered);
448 
449     /*
450       if res is non-zero, then ha_commit_trans has rolled back the
451       transaction, so the hooks for rollback will be called.
452     */
453   if (res)
454   {
455 #ifdef HAVE_REPLICATION
456     repl_semisync_master.wait_after_rollback(thd, FALSE);
457 #endif
458   }
459   else
460   {
461 #ifdef HAVE_REPLICATION
462     repl_semisync_master.wait_after_commit(thd, FALSE);
463 #endif
464   }
465 
466   thd->transaction.stmt.reset();
467 
468   DBUG_RETURN(MY_TEST(res));
469 }
470 
471 
472 /**
473   Rollback the single statement transaction.
474 
475   @param thd     Current thread
476 
477   @retval FALSE  Success
478   @retval TRUE   Failure
479 */
trans_rollback_stmt(THD * thd)480 bool trans_rollback_stmt(THD *thd)
481 {
482   DBUG_ENTER("trans_rollback_stmt");
483 
484   /*
485     We currently don't invoke commit/rollback at end of
486     a sub-statement.  In future, we perhaps should take
487     a savepoint for each nested statement, and release the
488     savepoint when statement has succeeded.
489   */
490   DBUG_ASSERT(! thd->in_sub_stmt);
491 
492   thd->merge_unsafe_rollback_flags();
493 
494   if (thd->transaction.stmt.ha_list)
495   {
496     ha_rollback_trans(thd, FALSE);
497     if (! thd->in_active_multi_stmt_transaction())
498       trans_reset_one_shot_chistics(thd);
499   }
500 
501 #ifdef HAVE_REPLICATION
502   repl_semisync_master.wait_after_rollback(thd, FALSE);
503 #endif
504 
505   thd->transaction.stmt.reset();
506 
507   DBUG_RETURN(FALSE);
508 }
509 
510 /* Find a named savepoint in the current transaction. */
511 static SAVEPOINT **
find_savepoint(THD * thd,LEX_CSTRING name)512 find_savepoint(THD *thd, LEX_CSTRING name)
513 {
514   SAVEPOINT **sv= &thd->transaction.savepoints;
515 
516   while (*sv)
517   {
518     if (my_strnncoll(system_charset_info, (uchar *) name.str, name.length,
519                      (uchar *) (*sv)->name, (*sv)->length) == 0)
520       break;
521     sv= &(*sv)->prev;
522   }
523 
524   return sv;
525 }
526 
527 
528 /**
529   Set a named transaction savepoint.
530 
531   @param thd    Current thread
532   @param name   Savepoint name
533 
534   @retval FALSE  Success
535   @retval TRUE   Failure
536 */
537 
trans_savepoint(THD * thd,LEX_CSTRING name)538 bool trans_savepoint(THD *thd, LEX_CSTRING name)
539 {
540   SAVEPOINT **sv, *newsv;
541   DBUG_ENTER("trans_savepoint");
542 
543   if (!(thd->in_multi_stmt_transaction_mode() || thd->in_sub_stmt) ||
544       !opt_using_transactions)
545     DBUG_RETURN(FALSE);
546 
547   if (thd->transaction.xid_state.check_has_uncommitted_xa())
548     DBUG_RETURN(TRUE);
549 
550   sv= find_savepoint(thd, name);
551 
552   if (*sv) /* old savepoint of the same name exists */
553   {
554     newsv= *sv;
555     ha_release_savepoint(thd, *sv);
556     *sv= (*sv)->prev;
557   }
558   else if ((newsv= (SAVEPOINT *) alloc_root(&thd->transaction.mem_root,
559                                             savepoint_alloc_size)) == NULL)
560   {
561     my_error(ER_OUT_OF_RESOURCES, MYF(0));
562     DBUG_RETURN(TRUE);
563   }
564 
565   newsv->name= strmake_root(&thd->transaction.mem_root, name.str, name.length);
566   newsv->length= (uint)name.length;
567 
568   /*
569     if we'll get an error here, don't add new savepoint to the list.
570     we'll lose a little bit of memory in transaction mem_root, but it'll
571     be free'd when transaction ends anyway
572   */
573   if (unlikely(ha_savepoint(thd, newsv)))
574     DBUG_RETURN(TRUE);
575 
576   newsv->prev= thd->transaction.savepoints;
577   thd->transaction.savepoints= newsv;
578 
579   /*
580     Remember locks acquired before the savepoint was set.
581     They are used as a marker to only release locks acquired after
582     the setting of this savepoint.
583     Note: this works just fine if we're under LOCK TABLES,
584     since mdl_savepoint() is guaranteed to be beyond
585     the last locked table. This allows to release some
586     locks acquired during LOCK TABLES.
587   */
588   newsv->mdl_savepoint= thd->mdl_context.mdl_savepoint();
589 
590   DBUG_RETURN(FALSE);
591 }
592 
593 
594 /**
595   Rollback a transaction to the named savepoint.
596 
597   @note Modifications that the current transaction made to
598         rows after the savepoint was set are undone in the
599         rollback.
600 
601   @note Savepoints that were set at a later time than the
602         named savepoint are deleted.
603 
604   @param thd    Current thread
605   @param name   Savepoint name
606 
607   @retval FALSE  Success
608   @retval TRUE   Failure
609 */
610 
trans_rollback_to_savepoint(THD * thd,LEX_CSTRING name)611 bool trans_rollback_to_savepoint(THD *thd, LEX_CSTRING name)
612 {
613   int res= FALSE;
614   SAVEPOINT *sv= *find_savepoint(thd, name);
615   DBUG_ENTER("trans_rollback_to_savepoint");
616 
617   if (sv == NULL)
618   {
619     my_error(ER_SP_DOES_NOT_EXIST, MYF(0), "SAVEPOINT", name.str);
620     DBUG_RETURN(TRUE);
621   }
622 
623   if (thd->transaction.xid_state.check_has_uncommitted_xa())
624     DBUG_RETURN(TRUE);
625 
626   if (ha_rollback_to_savepoint(thd, sv))
627     res= TRUE;
628   else if (((thd->variables.option_bits & OPTION_KEEP_LOG) ||
629             thd->transaction.all.modified_non_trans_table) &&
630            !thd->slave_thread)
631     push_warning(thd, Sql_condition::WARN_LEVEL_WARN,
632                  ER_WARNING_NOT_COMPLETE_ROLLBACK,
633                  ER_THD(thd, ER_WARNING_NOT_COMPLETE_ROLLBACK));
634 
635   thd->transaction.savepoints= sv;
636 
637   if (res)
638     /* An error occurred during rollback; we cannot release any MDL */;
639   else if (thd->variables.sql_log_bin &&
640            (WSREP_EMULATE_BINLOG_NNULL(thd) || mysql_bin_log.is_open()))
641     /* In some cases (such as with non-transactional tables) we may
642     choose to preserve events that were added after the SAVEPOINT,
643     delimiting them by SAVEPOINT and ROLLBACK TO SAVEPOINT statements.
644     Prematurely releasing MDL on such objects would break replication. */;
645   else if (ha_rollback_to_savepoint_can_release_mdl(thd))
646     thd->mdl_context.rollback_to_savepoint(sv->mdl_savepoint);
647 
648   DBUG_RETURN(MY_TEST(res));
649 }
650 
651 
652 /**
653   Remove the named savepoint from the set of savepoints of
654   the current transaction.
655 
656   @note No commit or rollback occurs. It is an error if the
657         savepoint does not exist.
658 
659   @param thd    Current thread
660   @param name   Savepoint name
661 
662   @retval FALSE  Success
663   @retval TRUE   Failure
664 */
665 
trans_release_savepoint(THD * thd,LEX_CSTRING name)666 bool trans_release_savepoint(THD *thd, LEX_CSTRING name)
667 {
668   int res= FALSE;
669   SAVEPOINT *sv= *find_savepoint(thd, name);
670   DBUG_ENTER("trans_release_savepoint");
671 
672   if (sv == NULL)
673   {
674     my_error(ER_SP_DOES_NOT_EXIST, MYF(0), "SAVEPOINT", name.str);
675     DBUG_RETURN(TRUE);
676   }
677 
678   if (ha_release_savepoint(thd, sv))
679     res= TRUE;
680 
681   thd->transaction.savepoints= sv->prev;
682 
683   DBUG_RETURN(MY_TEST(res));
684 }
685