1 | /* Copyright (c) 2010, 2015, Oracle and/or its affiliates. |
2 | Copyright (c) 2012, 2018, MariaDB |
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 St, Fifth Floor, Boston, MA 02110-1301 USA */ |
16 | |
17 | #include "mariadb.h" |
18 | #include "debug_sync.h" // DEBUG_SYNC |
19 | #include "table.h" // TABLE, FOREIGN_KEY_INFO |
20 | #include "sql_class.h" // THD |
21 | #include "sql_base.h" // open_and_lock_tables |
22 | #include "sql_table.h" // write_bin_log |
23 | #include "datadict.h" // dd_recreate_table() |
24 | #include "lock.h" // MYSQL_OPEN_* flags |
25 | #include "sql_acl.h" // DROP_ACL |
26 | #include "sql_parse.h" // check_one_table_access() |
27 | #include "sql_truncate.h" |
28 | #include "wsrep_mysqld.h" |
29 | #include "sql_show.h" //append_identifier() |
30 | #include "sql_select.h" |
31 | #include "sql_delete.h" |
32 | |
33 | /** |
34 | Append a list of field names to a string. |
35 | |
36 | @param str The string. |
37 | @param fields The list of field names. |
38 | |
39 | @return TRUE on failure, FALSE otherwise. |
40 | */ |
41 | |
42 | static bool fk_info_append_fields(THD *thd, String *str, |
43 | List<LEX_CSTRING> *fields) |
44 | { |
45 | bool res= FALSE; |
46 | LEX_CSTRING *field; |
47 | List_iterator_fast<LEX_CSTRING> it(*fields); |
48 | |
49 | while ((field= it++)) |
50 | { |
51 | res|= append_identifier(thd, str, field); |
52 | res|= str->append(", " ); |
53 | } |
54 | |
55 | str->chop(); |
56 | str->chop(); |
57 | |
58 | return res; |
59 | } |
60 | |
61 | |
62 | /** |
63 | Generate a foreign key description suitable for a error message. |
64 | |
65 | @param thd Thread context. |
66 | @param fk_info The foreign key information. |
67 | |
68 | @return A human-readable string describing the foreign key. |
69 | */ |
70 | |
71 | static const char *fk_info_str(THD *thd, FOREIGN_KEY_INFO *fk_info) |
72 | { |
73 | bool res= FALSE; |
74 | char buffer[STRING_BUFFER_USUAL_SIZE*2]; |
75 | String str(buffer, sizeof(buffer), system_charset_info); |
76 | |
77 | str.length(0); |
78 | |
79 | /* |
80 | `db`.`tbl`, CONSTRAINT `id` FOREIGN KEY (`fk`) REFERENCES `db`.`tbl` (`fk`) |
81 | */ |
82 | |
83 | res|= append_identifier(thd, &str, fk_info->foreign_db); |
84 | res|= str.append("." ); |
85 | res|= append_identifier(thd, &str, fk_info->foreign_table); |
86 | res|= str.append(", CONSTRAINT " ); |
87 | res|= append_identifier(thd, &str, fk_info->foreign_id); |
88 | res|= str.append(" FOREIGN KEY (" ); |
89 | res|= fk_info_append_fields(thd, &str, &fk_info->foreign_fields); |
90 | res|= str.append(") REFERENCES " ); |
91 | res|= append_identifier(thd, &str, fk_info->referenced_db); |
92 | res|= str.append("." ); |
93 | res|= append_identifier(thd, &str, fk_info->referenced_table); |
94 | res|= str.append(" (" ); |
95 | res|= fk_info_append_fields(thd, &str, &fk_info->referenced_fields); |
96 | res|= str.append(')'); |
97 | |
98 | return res ? NULL : thd->strmake(str.ptr(), str.length()); |
99 | } |
100 | |
101 | |
102 | /** |
103 | Check and emit a fatal error if the table which is going to be |
104 | affected by TRUNCATE TABLE is a parent table in some non-self- |
105 | referencing foreign key. |
106 | |
107 | @remark The intention is to allow truncate only for tables that |
108 | are not dependent on other tables. |
109 | |
110 | @param thd Thread context. |
111 | @param table Table handle. |
112 | |
113 | @retval FALSE This table is not parent in a non-self-referencing foreign |
114 | key. Statement can proceed. |
115 | @retval TRUE This table is parent in a non-self-referencing foreign key, |
116 | error was emitted. |
117 | */ |
118 | |
119 | static bool |
120 | fk_truncate_illegal_if_parent(THD *thd, TABLE *table) |
121 | { |
122 | FOREIGN_KEY_INFO *fk_info; |
123 | List<FOREIGN_KEY_INFO> fk_list; |
124 | List_iterator_fast<FOREIGN_KEY_INFO> it; |
125 | |
126 | /* |
127 | Bail out early if the table is not referenced by a foreign key. |
128 | In this case, the table could only be, if at all, a child table. |
129 | */ |
130 | if (! table->file->referenced_by_foreign_key()) |
131 | return FALSE; |
132 | |
133 | /* |
134 | This table _is_ referenced by a foreign key. At this point, only |
135 | self-referencing keys are acceptable. For this reason, get the list |
136 | of foreign keys referencing this table in order to check the name |
137 | of the child (dependent) tables. |
138 | */ |
139 | table->file->get_parent_foreign_key_list(thd, &fk_list); |
140 | |
141 | /* Out of memory when building list. */ |
142 | if (unlikely(thd->is_error())) |
143 | return TRUE; |
144 | |
145 | it.init(fk_list); |
146 | |
147 | /* Loop over the set of foreign keys for which this table is a parent. */ |
148 | while ((fk_info= it++)) |
149 | { |
150 | DBUG_ASSERT(!lex_string_cmp(system_charset_info, |
151 | fk_info->referenced_db, |
152 | &table->s->db)); |
153 | |
154 | DBUG_ASSERT(!lex_string_cmp(system_charset_info, |
155 | fk_info->referenced_table, |
156 | &table->s->table_name)); |
157 | |
158 | if (lex_string_cmp(system_charset_info, fk_info->foreign_db, |
159 | &table->s->db) || |
160 | lex_string_cmp(system_charset_info, fk_info->foreign_table, |
161 | &table->s->table_name)) |
162 | break; |
163 | } |
164 | |
165 | /* Table is parent in a non-self-referencing foreign key. */ |
166 | if (fk_info) |
167 | { |
168 | my_error(ER_TRUNCATE_ILLEGAL_FK, MYF(0), fk_info_str(thd, fk_info)); |
169 | return TRUE; |
170 | } |
171 | |
172 | return FALSE; |
173 | } |
174 | |
175 | |
176 | /* |
177 | Open and truncate a locked table. |
178 | |
179 | @param thd Thread context. |
180 | @param table_ref Table list element for the table to be truncated. |
181 | @param is_tmp_table True if element refers to a temp table. |
182 | |
183 | @retval TRUNCATE_OK Truncate was successful and statement can be safely |
184 | binlogged. |
185 | @retval TRUNCATE_FAILED_BUT_BINLOG Truncate failed but still go ahead with |
186 | binlogging as in case of non transactional tables |
187 | partial truncation is possible. |
188 | |
189 | @retval TRUNCATE_FAILED_SKIP_BINLOG Truncate was not successful hence donot |
190 | binlong the statement. |
191 | */ |
192 | |
193 | enum Sql_cmd_truncate_table::truncate_result |
194 | Sql_cmd_truncate_table::handler_truncate(THD *thd, TABLE_LIST *table_ref, |
195 | bool is_tmp_table) |
196 | { |
197 | int error= 0; |
198 | uint flags= 0; |
199 | DBUG_ENTER("Sql_cmd_truncate_table::handler_truncate" ); |
200 | |
201 | /* |
202 | Can't recreate, the engine must mechanically delete all rows |
203 | in the table. Use open_and_lock_tables() to open a write cursor. |
204 | */ |
205 | |
206 | /* If it is a temporary table, no need to take locks. */ |
207 | if (!is_tmp_table) |
208 | { |
209 | /* We don't need to load triggers. */ |
210 | DBUG_ASSERT(table_ref->trg_event_map == 0); |
211 | /* |
212 | Our metadata lock guarantees that no transaction is reading |
213 | or writing into the table. Yet, to open a write cursor we need |
214 | a thr_lock lock. Allow to open base tables only. |
215 | */ |
216 | table_ref->required_type= TABLE_TYPE_NORMAL; |
217 | /* |
218 | Ignore pending FLUSH TABLES since we don't want to release |
219 | the MDL lock taken above and otherwise there is no way to |
220 | wait for FLUSH TABLES in deadlock-free fashion. |
221 | */ |
222 | flags= MYSQL_OPEN_IGNORE_FLUSH; |
223 | /* |
224 | Even though we have an MDL lock on the table here, we don't |
225 | pass MYSQL_OPEN_HAS_MDL_LOCK to open_and_lock_tables |
226 | since to truncate a MERGE table, we must open and lock |
227 | merge children, and on those we don't have an MDL lock. |
228 | Thus clear the ticket to satisfy MDL asserts. |
229 | */ |
230 | table_ref->mdl_request.ticket= NULL; |
231 | } |
232 | |
233 | /* Open the table as it will handle some required preparations. */ |
234 | if (open_and_lock_tables(thd, table_ref, FALSE, flags)) |
235 | DBUG_RETURN(TRUNCATE_FAILED_SKIP_BINLOG); |
236 | |
237 | /* Whether to truncate regardless of foreign keys. */ |
238 | if (! (thd->variables.option_bits & OPTION_NO_FOREIGN_KEY_CHECKS)) |
239 | if (fk_truncate_illegal_if_parent(thd, table_ref->table)) |
240 | DBUG_RETURN(TRUNCATE_FAILED_SKIP_BINLOG); |
241 | |
242 | error= table_ref->table->file->ha_truncate(); |
243 | if (unlikely(error)) |
244 | { |
245 | table_ref->table->file->print_error(error, MYF(0)); |
246 | /* |
247 | If truncate method is not implemented then we don't binlog the |
248 | statement. If truncation has failed in a transactional engine then also |
249 | we don't binlog the statment. Only in non transactional engine we binlog |
250 | inspite of errors. |
251 | */ |
252 | if (error == HA_ERR_WRONG_COMMAND || |
253 | table_ref->table->file->has_transactions()) |
254 | DBUG_RETURN(TRUNCATE_FAILED_SKIP_BINLOG); |
255 | else |
256 | DBUG_RETURN(TRUNCATE_FAILED_BUT_BINLOG); |
257 | } |
258 | DBUG_RETURN(TRUNCATE_OK); |
259 | } |
260 | |
261 | |
262 | /* |
263 | Handle locking a base table for truncate. |
264 | |
265 | @param[in] thd Thread context. |
266 | @param[in] table_ref Table list element for the table to |
267 | be truncated. |
268 | @param[out] hton_can_recreate Set to TRUE if table can be dropped |
269 | and recreated. |
270 | |
271 | @retval FALSE Success. |
272 | @retval TRUE Error. |
273 | */ |
274 | |
275 | bool Sql_cmd_truncate_table::lock_table(THD *thd, TABLE_LIST *table_ref, |
276 | bool *hton_can_recreate) |
277 | { |
278 | TABLE *table= NULL; |
279 | DBUG_ENTER("Sql_cmd_truncate_table::lock_table" ); |
280 | |
281 | /* Lock types are set in the parser. */ |
282 | DBUG_ASSERT(table_ref->lock_type == TL_WRITE); |
283 | /* The handler truncate protocol dictates a exclusive lock. */ |
284 | DBUG_ASSERT(table_ref->mdl_request.type == MDL_EXCLUSIVE); |
285 | |
286 | /* |
287 | Before doing anything else, acquire a metadata lock on the table, |
288 | or ensure we have one. We don't use open_and_lock_tables() |
289 | right away because we want to be able to truncate (and recreate) |
290 | corrupted tables, those that we can't fully open. |
291 | |
292 | MySQL manual documents that TRUNCATE can be used to repair a |
293 | damaged table, i.e. a table that can not be fully "opened". |
294 | In particular MySQL manual says: As long as the table format |
295 | file tbl_name.frm is valid, the table can be re-created as |
296 | an empty table with TRUNCATE TABLE, even if the data or index |
297 | files have become corrupted. |
298 | */ |
299 | if (thd->locked_tables_mode) |
300 | { |
301 | if (!(table= find_table_for_mdl_upgrade(thd, table_ref->db.str, |
302 | table_ref->table_name.str, FALSE))) |
303 | DBUG_RETURN(TRUE); |
304 | |
305 | *hton_can_recreate= ha_check_storage_engine_flag(table->file->ht, |
306 | HTON_CAN_RECREATE); |
307 | table_ref->mdl_request.ticket= table->mdl_ticket; |
308 | } |
309 | else |
310 | { |
311 | handlerton *hton; |
312 | bool is_sequence; |
313 | |
314 | /* Acquire an exclusive lock. */ |
315 | DBUG_ASSERT(table_ref->next_global == NULL); |
316 | if (lock_table_names(thd, table_ref, NULL, |
317 | thd->variables.lock_wait_timeout, 0)) |
318 | DBUG_RETURN(TRUE); |
319 | |
320 | if (!ha_table_exists(thd, &table_ref->db, &table_ref->table_name, |
321 | &hton, &is_sequence) || |
322 | hton == view_pseudo_hton) |
323 | { |
324 | my_error(ER_NO_SUCH_TABLE, MYF(0), table_ref->db.str, |
325 | table_ref->table_name.str); |
326 | DBUG_RETURN(TRUE); |
327 | } |
328 | |
329 | if (!hton) |
330 | { |
331 | /* |
332 | The table exists, but its storage engine is unknown, perhaps not |
333 | loaded at the moment. We need to open and parse the frm to know the |
334 | storage engine in question, so let's proceed with the truncation and |
335 | try to open the table. This will produce the correct error message |
336 | about unknown engine. |
337 | */ |
338 | *hton_can_recreate= false; |
339 | } |
340 | else |
341 | *hton_can_recreate= !is_sequence && hton->flags & HTON_CAN_RECREATE; |
342 | } |
343 | |
344 | /* |
345 | A storage engine can recreate or truncate the table only if there |
346 | are no references to it from anywhere, i.e. no cached TABLE in the |
347 | table cache. |
348 | */ |
349 | if (thd->locked_tables_mode) |
350 | { |
351 | DEBUG_SYNC(thd, "upgrade_lock_for_truncate" ); |
352 | /* To remove the table from the cache we need an exclusive lock. */ |
353 | if (wait_while_table_is_used(thd, table, HA_EXTRA_PREPARE_FOR_DROP)) |
354 | DBUG_RETURN(TRUE); |
355 | m_ticket_downgrade= table->mdl_ticket; |
356 | /* Close if table is going to be recreated. */ |
357 | if (*hton_can_recreate) |
358 | close_all_tables_for_name(thd, table->s, HA_EXTRA_NOT_USED, NULL); |
359 | } |
360 | else |
361 | { |
362 | /* Table is already locked exclusively. Remove cached instances. */ |
363 | tdc_remove_table(thd, TDC_RT_REMOVE_ALL, table_ref->db.str, |
364 | table_ref->table_name.str, FALSE); |
365 | } |
366 | |
367 | DBUG_RETURN(FALSE); |
368 | } |
369 | |
370 | |
371 | /* |
372 | Optimized delete of all rows by doing a full generate of the table. |
373 | |
374 | @remark Will work even if the .MYI and .MYD files are destroyed. |
375 | In other words, it works as long as the .FRM is intact and |
376 | the engine supports re-create. |
377 | |
378 | @param thd Thread context. |
379 | @param table_ref Table list element for the table to be truncated. |
380 | |
381 | @retval FALSE Success. |
382 | @retval TRUE Error. |
383 | */ |
384 | |
385 | bool Sql_cmd_truncate_table::truncate_table(THD *thd, TABLE_LIST *table_ref) |
386 | { |
387 | int error; |
388 | bool binlog_stmt; |
389 | DBUG_ENTER("Sql_cmd_truncate_table::truncate_table" ); |
390 | |
391 | DBUG_ASSERT((!table_ref->table) || |
392 | (table_ref->table && table_ref->table->s)); |
393 | |
394 | /* Initialize, or reinitialize in case of reexecution (SP). */ |
395 | m_ticket_downgrade= NULL; |
396 | |
397 | /* If it is a temporary table, no need to take locks. */ |
398 | if (is_temporary_table(table_ref)) |
399 | { |
400 | /* In RBR, the statement is not binlogged if the table is temporary. */ |
401 | binlog_stmt= !thd->is_current_stmt_binlog_format_row(); |
402 | |
403 | error= handler_truncate(thd, table_ref, TRUE); |
404 | |
405 | /* |
406 | No need to invalidate the query cache, queries with temporary |
407 | tables are not in the cache. No need to write to the binary |
408 | log a failed row-by-row delete even if under RBR as the table |
409 | might not exist on the slave. |
410 | */ |
411 | } |
412 | else /* It's not a temporary table. */ |
413 | { |
414 | bool hton_can_recreate; |
415 | |
416 | if (WSREP(thd) && |
417 | wsrep_to_isolation_begin(thd, table_ref->db.str, table_ref->table_name.str, 0)) |
418 | DBUG_RETURN(TRUE); |
419 | if (lock_table(thd, table_ref, &hton_can_recreate)) |
420 | DBUG_RETURN(TRUE); |
421 | |
422 | if (hton_can_recreate) |
423 | { |
424 | /* |
425 | The storage engine can truncate the table by creating an |
426 | empty table with the same structure. |
427 | */ |
428 | error= dd_recreate_table(thd, table_ref->db.str, table_ref->table_name.str); |
429 | |
430 | if (thd->locked_tables_mode && thd->locked_tables_list.reopen_tables(thd, false)) |
431 | { |
432 | thd->locked_tables_list.unlink_all_closed_tables(thd, NULL, 0); |
433 | error=1; |
434 | } |
435 | |
436 | /* No need to binlog a failed truncate-by-recreate. */ |
437 | binlog_stmt= !error; |
438 | } |
439 | else |
440 | { |
441 | /* |
442 | The engine does not support truncate-by-recreate. |
443 | Attempt to use the handler truncate method. |
444 | */ |
445 | error= handler_truncate(thd, table_ref, FALSE); |
446 | |
447 | /* |
448 | All effects of a TRUNCATE TABLE operation are committed even if |
449 | truncation fails in the case of non transactional tables. Thus, the |
450 | query must be written to the binary log. The only exception is a |
451 | unimplemented truncate method. |
452 | */ |
453 | if (unlikely(error == TRUNCATE_OK || error == TRUNCATE_FAILED_BUT_BINLOG)) |
454 | binlog_stmt= true; |
455 | else |
456 | binlog_stmt= false; |
457 | } |
458 | |
459 | /* |
460 | If we tried to open a MERGE table and failed due to problems with the |
461 | children tables, the table will have been closed and table_ref->table |
462 | will be invalid. Reset the pointer here in any case as |
463 | query_cache_invalidate does not need a valid TABLE object. |
464 | */ |
465 | table_ref->table= NULL; |
466 | query_cache_invalidate3(thd, table_ref, FALSE); |
467 | } |
468 | |
469 | /* DDL is logged in statement format, regardless of binlog format. */ |
470 | if (binlog_stmt) |
471 | error|= write_bin_log(thd, !error, thd->query(), thd->query_length()); |
472 | |
473 | /* |
474 | A locked table ticket was upgraded to a exclusive lock. After the |
475 | the query has been written to the binary log, downgrade the lock |
476 | to a shared one. |
477 | */ |
478 | if (m_ticket_downgrade) |
479 | m_ticket_downgrade->downgrade_lock(MDL_SHARED_NO_READ_WRITE); |
480 | |
481 | DBUG_RETURN(error); |
482 | } |
483 | |
484 | /** |
485 | Execute a TRUNCATE statement at runtime. |
486 | |
487 | @param thd The current thread. |
488 | |
489 | @return FALSE on success. |
490 | */ |
491 | |
492 | bool Sql_cmd_truncate_table::execute(THD *thd) |
493 | { |
494 | bool res= TRUE; |
495 | TABLE_LIST *table= thd->lex->select_lex.table_list.first; |
496 | DBUG_ENTER("Sql_cmd_truncate_table::execute" ); |
497 | |
498 | if (check_one_table_access(thd, DROP_ACL, table)) |
499 | DBUG_RETURN(res); |
500 | |
501 | if (! (res= truncate_table(thd, table))) |
502 | my_ok(thd); |
503 | |
504 | DBUG_RETURN(res); |
505 | } |
506 | |