1 /* Copyright (c) 2010, 2021, Oracle and/or its affiliates.
2 
3    This program is free software; you can redistribute it and/or modify
4    it under the terms of the GNU General Public License, version 2.0,
5    as published by the Free Software Foundation.
6 
7    This program is also distributed with certain software (including
8    but not limited to OpenSSL) that is licensed under separate terms,
9    as designated in a particular file or component or in included license
10    documentation.  The authors of MySQL hereby grant you an additional
11    permission to link the program and your derivative works with the
12    separately licensed software that they have included with MySQL.
13 
14    This program is distributed in the hope that it will be useful,
15    but WITHOUT ANY WARRANTY; without even the implied warranty of
16    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17    GNU General Public License, version 2.0, for more details.
18 
19    You should have received a copy of the GNU General Public License
20    along with this program; if not, write to the Free Software
21    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA */
22 
23 #include "sql_truncate.h"
24 
25 #include "debug_sync.h"  // DEBUG_SYNC
26 #include "table.h"       // TABLE, FOREIGN_KEY_INFO
27 #include "sql_class.h"   // THD
28 #include "sql_base.h"    // open_and_lock_tables
29 #include "sql_table.h"   // write_bin_log
30 #include "datadict.h"    // dd_recreate_table()
31 #include "lock.h"        // MYSQL_OPEN_* flags
32 #include "auth_common.h" // DROP_ACL
33 #include "sql_parse.h"   // check_one_table_access()
34 #include "sql_show.h"    //append_identifier()
35 #ifdef WITH_WSREP
36 #include "wsrep_mysqld.h"
37 #endif /* WITH_WSREP */
38 #include "sql_audit.h"
39 
40 
41 /**
42   Append a list of field names to a string.
43 
44   @param  str     The string.
45   @param  fields  The list of field names.
46 
47   @return TRUE on failure, FALSE otherwise.
48 */
49 
fk_info_append_fields(String * str,List<LEX_STRING> * fields)50 static bool fk_info_append_fields(String *str, List<LEX_STRING> *fields)
51 {
52   bool res= FALSE;
53   LEX_STRING *field;
54   List_iterator_fast<LEX_STRING> it(*fields);
55 
56   while ((field= it++))
57   {
58     append_identifier(NULL, str, field->str, field->length);
59     res|= str->append(", ");
60   }
61 
62   str->chop();
63   str->chop();
64 
65   return res;
66 }
67 
68 
69 /**
70   Generate a foreign key description suitable for a error message.
71 
72   @param thd          Thread context.
73   @param fk_info   The foreign key information.
74 
75   @return A human-readable string describing the foreign key.
76 */
77 
fk_info_str(THD * thd,FOREIGN_KEY_INFO * fk_info)78 static const char *fk_info_str(THD *thd, FOREIGN_KEY_INFO *fk_info)
79 {
80   bool res= FALSE;
81   char buffer[STRING_BUFFER_USUAL_SIZE*2];
82   String str(buffer, sizeof(buffer), system_charset_info);
83 
84   str.length(0);
85 
86   /*
87     `db`.`tbl`, CONSTRAINT `id` FOREIGN KEY (`fk`) REFERENCES `db`.`tbl` (`fk`)
88   */
89 
90   append_identifier(NULL, &str, fk_info->foreign_db->str,
91                     fk_info->foreign_db->length);
92   res|= str.append(".");
93   append_identifier(NULL, &str, fk_info->foreign_table->str,
94                     fk_info->foreign_table->length);
95   res|= str.append(", CONSTRAINT ");
96   append_identifier(NULL, &str, fk_info->foreign_id->str,
97                     fk_info->foreign_id->length);
98   res|= str.append(" FOREIGN KEY (");
99   res|= fk_info_append_fields(&str, &fk_info->foreign_fields);
100   res|= str.append(") REFERENCES ");
101   append_identifier(NULL, &str, fk_info->referenced_db->str,
102                     fk_info->referenced_db->length);
103   res|= str.append(".");
104   append_identifier(NULL, &str, fk_info->referenced_table->str,
105                     fk_info->referenced_table->length);
106   res|= str.append(" (");
107   res|= fk_info_append_fields(&str, &fk_info->referenced_fields);
108   res|= str.append(')');
109 
110   return res ? NULL : thd->strmake(str.ptr(), str.length());
111 }
112 
113 
114 /**
115   Check and emit a fatal error if the table which is going to be
116   affected by TRUNCATE TABLE is a parent table in some non-self-
117   referencing foreign key.
118 
119   @remark The intention is to allow truncate only for tables that
120           are not dependent on other tables.
121 
122   @param  thd    Thread context.
123   @param  table  Table handle.
124 
125   @retval FALSE  This table is not parent in a non-self-referencing foreign
126                  key. Statement can proceed.
127   @retval TRUE   This table is parent in a non-self-referencing foreign key,
128                  error was emitted.
129 */
130 
131 static bool
fk_truncate_illegal_if_parent(THD * thd,TABLE * table)132 fk_truncate_illegal_if_parent(THD *thd, TABLE *table)
133 {
134   FOREIGN_KEY_INFO *fk_info;
135   List<FOREIGN_KEY_INFO> fk_list;
136   List_iterator_fast<FOREIGN_KEY_INFO> it;
137 
138   /*
139     Bail out early if the table is not referenced by a foreign key.
140     In this case, the table could only be, if at all, a child table.
141   */
142   if (! table->file->referenced_by_foreign_key())
143     return FALSE;
144 
145   /*
146     This table _is_ referenced by a foreign key. At this point, only
147     self-referencing keys are acceptable. For this reason, get the list
148     of foreign keys referencing this table in order to check the name
149     of the child (dependent) tables.
150   */
151   table->file->get_parent_foreign_key_list(thd, &fk_list);
152 
153   /* Out of memory when building list. */
154   if (thd->is_error())
155     return TRUE;
156 
157   it.init(fk_list);
158 
159   /* Loop over the set of foreign keys for which this table is a parent. */
160   while ((fk_info= it++))
161   {
162     assert(!my_strcasecmp(system_charset_info,
163                           fk_info->referenced_db->str,
164                           table->s->db.str));
165 
166     assert(!my_strcasecmp(system_charset_info,
167                           fk_info->referenced_table->str,
168                           table->s->table_name.str));
169 
170     if (my_strcasecmp(system_charset_info, fk_info->foreign_db->str,
171                       table->s->db.str) ||
172         my_strcasecmp(system_charset_info, fk_info->foreign_table->str,
173                       table->s->table_name.str))
174       break;
175   }
176 
177   /* Table is parent in a non-self-referencing foreign key. */
178   if (fk_info)
179   {
180     my_error(ER_TRUNCATE_ILLEGAL_FK, MYF(0), fk_info_str(thd, fk_info));
181     return TRUE;
182   }
183 
184   return FALSE;
185 }
186 
187 
188 /*
189   Open and truncate a locked table.
190 
191   @param  thd           Thread context.
192   @param  table_ref     Table list element for the table to be truncated.
193   @param  is_tmp_table  True if element refers to a temp table.
194 
195   @retval TRUNCATE_OK   Truncate was successful and statement can be safely
196                         binlogged.
197   @retval TRUNCATE_FAILED_BUT_BINLOG Truncate failed but still go ahead with
198                         binlogging as in case of non transactional tables
199                         partial truncation is possible.
200 
201   @retval TRUNCATE_FAILED_SKIP_BINLOG Truncate was not successful hence donot
202                         binlong the statement.
203 */
204 
205 enum Sql_cmd_truncate_table::truncate_result
handler_truncate(THD * thd,TABLE_LIST * table_ref,bool is_tmp_table)206 Sql_cmd_truncate_table::handler_truncate(THD *thd, TABLE_LIST *table_ref,
207                                              bool is_tmp_table)
208 {
209   int error= 0;
210   uint flags= 0;
211   DBUG_ENTER("Sql_cmd_truncate_table::handler_truncate");
212 
213   /*
214     Can't recreate, the engine must mechanically delete all rows
215     in the table. Use open_and_lock_tables() to open a write cursor.
216   */
217 
218   /* If it is a temporary table, no need to take locks. */
219   if (!is_tmp_table)
220   {
221     /* We don't need to load triggers. */
222     assert(table_ref->trg_event_map == 0);
223     /*
224       Our metadata lock guarantees that no transaction is reading
225       or writing into the table. Yet, to open a write cursor we need
226       a thr_lock lock. Allow to open base tables only.
227     */
228     table_ref->required_type= FRMTYPE_TABLE;
229     /*
230       Ignore pending FLUSH TABLES since we don't want to release
231       the MDL lock taken above and otherwise there is no way to
232       wait for FLUSH TABLES in deadlock-free fashion.
233     */
234     flags= MYSQL_OPEN_IGNORE_FLUSH;
235     /*
236       Even though we have an MDL lock on the table here, we don't
237       pass MYSQL_OPEN_HAS_MDL_LOCK to open_and_lock_tables
238       since to truncate a MERGE table, we must open and lock
239       merge children, and on those we don't have an MDL lock.
240       Thus clear the ticket to satisfy MDL asserts.
241     */
242     table_ref->mdl_request.ticket= NULL;
243   }
244 
245   /* Open the table as it will handle some required preparations. */
246   if (open_and_lock_tables(thd, table_ref, flags))
247     DBUG_RETURN(TRUNCATE_FAILED_SKIP_BINLOG);
248 
249   /* Whether to truncate regardless of foreign keys. */
250   if (! (thd->variables.option_bits & OPTION_NO_FOREIGN_KEY_CHECKS))
251     if (fk_truncate_illegal_if_parent(thd, table_ref->table))
252       DBUG_RETURN(TRUNCATE_FAILED_SKIP_BINLOG);
253 
254   error= table_ref->table->file->ha_truncate();
255   if (error)
256   {
257     table_ref->table->file->print_error(error, MYF(0));
258     /*
259       If truncate method is not implemented then we don't binlog the
260       statement. If truncation has failed in a transactional engine then also we
261       donot binlog the statment. Only in non transactional engine we binlog
262       inspite of errors.
263      */
264     if (error == HA_ERR_WRONG_COMMAND ||
265         table_ref->table->file->has_transactions())
266       DBUG_RETURN(TRUNCATE_FAILED_SKIP_BINLOG);
267     else
268       DBUG_RETURN(TRUNCATE_FAILED_BUT_BINLOG);
269   }
270   DBUG_RETURN(TRUNCATE_OK);
271 }
272 
273 
274 /*
275   Close and recreate a temporary table. In case of success,
276   write truncate statement into the binary log if in statement
277   mode.
278 
279   @param  thd     Thread context.
280   @param  table   The temporary table.
281 
282   @retval  FALSE  Success.
283   @retval  TRUE   Error.
284 */
285 
recreate_temporary_table(THD * thd,TABLE * table)286 static bool recreate_temporary_table(THD *thd, TABLE *table)
287 {
288   bool error= TRUE;
289   TABLE_SHARE *share= table->s;
290   HA_CREATE_INFO create_info;
291   handlerton *table_type= table->s->db_type();
292   DBUG_ENTER("recreate_temporary_table");
293 
294   table->file->info(HA_STATUS_AUTO | HA_STATUS_NO_LOCK);
295 
296   /*
297     If LOCK TABLES list is not empty and contains this table
298     then unlock the table and remove it from this list.
299   */
300   mysql_lock_remove(thd, thd->lock, table);
301 
302   /* Don't free share. */
303   close_temporary_table(thd, table, FALSE, FALSE);
304 
305   /*
306     We must use share->normalized_path.str since for temporary tables it
307     differs from what dd_recreate_table() would generate based
308     on table and schema names.
309   */
310   ha_create_table(thd, share->normalized_path.str, share->db.str,
311                   share->table_name.str, &create_info, true, true);
312 
313   if (open_table_uncached(thd, share->path.str, share->db.str,
314                           share->table_name.str, true, true))
315   {
316     error= FALSE;
317     thd->thread_specific_used= TRUE;
318   }
319   else
320     rm_temporary_table(table_type, share->path.str);
321 
322   free_table_share(share);
323   my_free(table);
324 
325   DBUG_RETURN(error);
326 }
327 
328 
329 /*
330   Handle locking a base table for truncate.
331 
332   @param[in]  thd               Thread context.
333   @param[in]  table_ref         Table list element for the table to
334                                 be truncated.
335   @param[out] hton_can_recreate Set to TRUE if table can be dropped
336                                 and recreated.
337 
338   @retval  FALSE  Success.
339   @retval  TRUE   Error.
340 */
341 
lock_table(THD * thd,TABLE_LIST * table_ref,bool * hton_can_recreate)342 bool Sql_cmd_truncate_table::lock_table(THD *thd, TABLE_LIST *table_ref,
343                                         bool *hton_can_recreate)
344 {
345   TABLE *table= NULL;
346   DBUG_ENTER("Sql_cmd_truncate_table::lock_table");
347 
348   /* Lock types are set in the parser. */
349   assert(table_ref->lock_type == TL_WRITE);
350   /* The handler truncate protocol dictates a exclusive lock. */
351   assert(table_ref->mdl_request.type == MDL_EXCLUSIVE);
352 
353   /*
354     Before doing anything else, acquire a metadata lock on the table,
355     or ensure we have one.  We don't use open_and_lock_tables()
356     right away because we want to be able to truncate (and recreate)
357     corrupted tables, those that we can't fully open.
358 
359     MySQL manual documents that TRUNCATE can be used to repair a
360     damaged table, i.e. a table that can not be fully "opened".
361     In particular MySQL manual says: As long as the table format
362     file tbl_name.frm is valid, the table can be re-created as
363     an empty table with TRUNCATE TABLE, even if the data or index
364     files have become corrupted.
365   */
366   if (thd->locked_tables_mode)
367   {
368     if (!(table= find_table_for_mdl_upgrade(thd, table_ref->db,
369                                             table_ref->table_name, FALSE)))
370       DBUG_RETURN(TRUE);
371 
372     *hton_can_recreate= ha_check_storage_engine_flag(table->s->db_type(),
373                                                      HTON_CAN_RECREATE);
374     table_ref->mdl_request.ticket= table->mdl_ticket;
375   }
376   else
377   {
378     /* Acquire an exclusive lock. */
379     assert(table_ref->next_global == NULL);
380     if (lock_table_names(thd, table_ref, NULL,
381                          thd->variables.lock_wait_timeout, 0))
382       DBUG_RETURN(TRUE);
383 
384     if (dd_check_storage_engine_flag(thd, table_ref->db, table_ref->table_name,
385                                      HTON_CAN_RECREATE, hton_can_recreate))
386       DBUG_RETURN(TRUE);
387   }
388 
389   /*
390     A storage engine can recreate or truncate the table only if there
391     are no references to it from anywhere, i.e. no cached TABLE in the
392     table cache.
393   */
394   if (thd->locked_tables_mode)
395   {
396     DEBUG_SYNC(thd, "upgrade_lock_for_truncate");
397     /* To remove the table from the cache we need an exclusive lock. */
398     if (wait_while_table_is_used(thd, table, HA_EXTRA_FORCE_REOPEN))
399       DBUG_RETURN(TRUE);
400     m_ticket_downgrade= table->mdl_ticket;
401     /* Close if table is going to be recreated. */
402     if (*hton_can_recreate)
403       close_all_tables_for_name(thd, table->s, false, NULL);
404   }
405   else
406   {
407     /* Table is already locked exclusively. Remove cached instances. */
408     tdc_remove_table(thd, TDC_RT_REMOVE_ALL, table_ref->db,
409                      table_ref->table_name, FALSE);
410   }
411 
412   DBUG_RETURN(FALSE);
413 }
414 
415 
416 /*
417   Optimized delete of all rows by doing a full generate of the table.
418 
419   @remark Will work even if the .MYI and .MYD files are destroyed.
420           In other words, it works as long as the .FRM is intact and
421           the engine supports re-create.
422 
423   @param  thd         Thread context.
424   @param  table_ref   Table list element for the table to be truncated.
425 
426   @retval  FALSE  Success.
427   @retval  TRUE   Error.
428 */
429 
truncate_table(THD * thd,TABLE_LIST * table_ref)430 bool Sql_cmd_truncate_table::truncate_table(THD *thd, TABLE_LIST *table_ref)
431 {
432   int error;
433   bool binlog_stmt;
434   DBUG_ENTER("Sql_cmd_truncate_table::truncate_table");
435 
436   assert((!table_ref->table) ||
437          (table_ref->table && table_ref->table->s));
438 
439   /* Initialize, or reinitialize in case of reexecution (SP). */
440   m_ticket_downgrade= NULL;
441 
442   /* If it is a temporary table, no need to take locks. */
443   if (is_temporary_table(table_ref))
444   {
445     TABLE *tmp_table= table_ref->table;
446 
447     /* In RBR, the statement is not binlogged if the table is temporary. */
448     binlog_stmt= !thd->is_current_stmt_binlog_format_row();
449 
450     /* Note that a temporary table cannot be partitioned. */
451     if (ha_check_storage_engine_flag(tmp_table->s->db_type(), HTON_CAN_RECREATE))
452     {
453       if ((error= recreate_temporary_table(thd, tmp_table)))
454         binlog_stmt= FALSE; /* No need to binlog failed truncate-by-recreate. */
455 
456       assert(! thd->get_transaction()->cannot_safely_rollback(
457                                                               Transaction_ctx::STMT));
458     }
459     else
460     {
461       /*
462         The engine does not support truncate-by-recreate. Open the
463         table and invoke the handler truncate. In such a manner this
464         can in fact open several tables if it's a temporary MyISAMMRG
465         table.
466       */
467       error= handler_truncate(thd, table_ref, TRUE);
468     }
469 
470     /*
471       No need to invalidate the query cache, queries with temporary
472       tables are not in the cache. No need to write to the binary
473       log a failed row-by-row delete even if under RBR as the table
474       might not exist on the slave.
475     */
476   }
477   else /* It's not a temporary table. */
478   {
479     /*
480       Truncate is allowed for performance schema tables in both read_only and
481       super_read_only mode.
482     */
483     if (is_perfschema_db(table_ref->db))
484       thd->set_skip_readonly_check();
485 
486     bool hton_can_recreate;
487 
488 #ifdef WITH_WSREP
489     if (WSREP(thd) && wsrep_to_isolation_begin(thd,
490                                                 table_ref->db,
491                                                 table_ref->table_name, NULL))
492         DBUG_RETURN(TRUE);
493 #endif /* WITH_WSREP */
494     if (lock_table(thd, table_ref, &hton_can_recreate))
495       DBUG_RETURN(TRUE);
496 
497     if (hton_can_recreate)
498     {
499 #ifndef EMBEDDED_LIBRARY
500       if (mysql_audit_table_access_notify(thd, table_ref))
501       {
502         DBUG_RETURN(true);
503       }
504 #endif /* !EMBEDDED_LIBRARY */
505      /*
506         The storage engine can truncate the table by creating an
507         empty table with the same structure.
508       */
509       error= dd_recreate_table(thd, table_ref->db, table_ref->table_name);
510 
511       if (thd->locked_tables_mode && thd->locked_tables_list.reopen_tables(thd))
512           thd->locked_tables_list.unlink_all_closed_tables(thd, NULL, 0);
513 
514       /* No need to binlog a failed truncate-by-recreate. */
515       binlog_stmt= !error;
516     }
517     else
518     {
519       /*
520         The engine does not support truncate-by-recreate.
521         Attempt to use the handler truncate method.
522         MYSQL_AUDIT_TABLE_ACCESS_READ audit event is generated when opening
523         tables using open_tables function.
524       */
525       error= handler_truncate(thd, table_ref, FALSE);
526 
527       /*
528         All effects of a TRUNCATE TABLE operation are committed even if
529         truncation fails in the case of non transactional tables. Thus, the
530         query must be written to the binary log. The only exception is a
531         unimplemented truncate method.
532       */
533       if (error == TRUNCATE_OK || error == TRUNCATE_FAILED_BUT_BINLOG)
534         binlog_stmt= true;
535       else
536         binlog_stmt= false;
537     }
538 
539     /*
540       If we tried to open a MERGE table and failed due to problems with the
541       children tables, the table will have been closed and table_ref->table
542       will be invalid. Reset the pointer here in any case as
543       query_cache_invalidate does not need a valid TABLE object.
544     */
545     table_ref->table= NULL;
546     query_cache.invalidate(thd, table_ref, FALSE);
547   }
548 
549   /* DDL is logged in statement format, regardless of binlog format. */
550   if (binlog_stmt)
551     error|= write_bin_log(thd, !error, thd->query().str, thd->query().length);
552 
553   /*
554     A locked table ticket was upgraded to a exclusive lock. After the
555     the query has been written to the binary log, downgrade the lock
556     to a shared one.
557   */
558   if (m_ticket_downgrade)
559     m_ticket_downgrade->downgrade_lock(MDL_SHARED_NO_READ_WRITE);
560 
561   DBUG_RETURN(error);
562 }
563 
564 
565 /**
566   Execute a TRUNCATE statement at runtime.
567 
568   @param  thd   The current thread.
569 
570   @return FALSE on success.
571 */
execute(THD * thd)572 bool Sql_cmd_truncate_table::execute(THD *thd)
573 {
574   bool res= TRUE;
575   TABLE_LIST *first_table= thd->lex->select_lex->table_list.first;
576   DBUG_ENTER("Sql_cmd_truncate_table::execute");
577 
578   if (check_one_table_access(thd, DROP_ACL, first_table))
579     DBUG_RETURN(res);
580 
581   if (! (res= truncate_table(thd, first_table)))
582     my_ok(thd);
583   DBUG_RETURN(res);
584 }
585 
586