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