![](https://secure.gravatar.com/avatar/39b623a1559cf9c69ac3d9d4fb44e7fe.jpg?s=120&d=mm&r=g)
Hi, Nirbhay! I like the approach, there were only few comments, but still there were some :) See below. On May 16, Nirbhay Choubey wrote:
revision-id: c285dbece2881e1d864bb758d77edc899d568c04 (mariadb-10.1.8-82-gc285dbe) parent(s): 222ca736f737e888115c732c8ecad84faf0a2529 author: Nirbhay Choubey committer: Nirbhay Choubey timestamp: 2016-05-16 23:59:10 -0400 message:
MDEV-5535: Cannot reopen temporary table
Would be nice to have a somewhat more verbose commit comment here :)
diff --git a/mysql-test/t/reopen_temp_table-master.test b/mysql-test/t/reopen_temp_table-master.test new file mode 100644 index 0000000..5ac2ca8 --- /dev/null +++ b/mysql-test/t/reopen_temp_table-master.test @@ -0,0 +1 @@ +--tmpdir=$MYSQLTEST_VARDIR//tmp
Eh... I suppose you mean reopen_temp_table-master.opt And, in fact, you can simply use reopen_temp_table.opt Btw, why two slashes? And why do you need to set tmpdir at all? I suspect you don't - because your tests apparently succeed without it :)
diff --git a/sql/sql_insert.cc b/sql/sql_insert.cc index aa2cae5..f729733 100644 --- a/sql/sql_insert.cc +++ b/sql/sql_insert.cc @@ -4051,7 +4051,12 @@ static TABLE *create_table_from_items(THD *thd, } else { - if (open_temporary_table(thd, create_table)) + /* + The pointer to the newly created temporary table has been stored in + table->create_info. + */ + create_table->table= create_info->table;
Where was this happening before you've added an explicit assignment? (note the assert below - it worked, so somewhere this must've been assigned)
+ if (!create_table->table) { /* This shouldn't happen as creation of temporary table should make @@ -4060,7 +4065,6 @@ static TABLE *create_table_from_items(THD *thd, */ DBUG_ASSERT(0); } - DBUG_ASSERT(create_table->table == create_info->table);
why?
} } else @@ -4308,6 +4326,27 @@ bool select_create::send_eof() DBUG_RETURN(true); }
+ if (table->s->tmp_table) + { + /* + Now is good time to add the new table to THD temporary tables list. + But, before that we need to check if same table got created by the sub- + statement. + */ + if (thd->temporary_tables.find_table_share(table->s->table_cache_key.str, + table->s->table_cache_key.length)) + { + my_error(ER_TABLE_EXISTS_ERROR, MYF(0), table->alias.c_ptr());
ER_TABLE_EXISTS_ERROR, eh? I'm not sure it's the best way to solve this. It's an error that neither CREATE ... IF NOT EXISTS or CREATE OR REPLACE can fix. But I don't have a good solution for this. If a concurrent thread would've tried to create a table meanwhile (assuming, non-temporary), it would wait on the metadata lock, that protects table creation. so, logically, trying to create a table from inside create table or trying to drop a used table from stored function (your ER_CANT_REOPEN_TABLE) should fail with ER_LOCK_DEADLOCK or ER_LOCK_ABORTED - because it's, basically, trying to get a conflicting metadata lock within the same thread, clearly a case of the deadlock. but somehow I believe that returning ER_LOCK_DEADLOCK on a purely myisam test case with temporary tables - that will be confusing as hell :( so, ok, let's keep your ER_TABLE_EXISTS_ERROR. But, if possible, please commit this code (save_tmp_table_share and ER_CANT_REOPEN_TABLE) in a separate commit, after the main MDEV-5535 commit. To have it clearly distinct in the history, in case we'll want to change this behaviour later.
+ abort_result_set(); + DBUG_RETURN(true); + } + else + { + DBUG_ASSERT(save_tmp_table_share); + thd->temporary_tables.relink_table_share(save_tmp_table_share); + } + } + /* Do an implicit commit at end of statement for non-temporary tables. This can fail, but we should unlock the table diff --git a/sql/sql_table.cc b/sql/sql_table.cc index 40032c3..93fba14 100644 --- a/sql/sql_table.cc +++ b/sql/sql_table.cc @@ -2281,23 +2281,17 @@ int mysql_rm_table_no_locks(THD *thd, TABLE_LIST *tables, bool if_exists, */ DBUG_ASSERT(!(thd->locked_tables_mode && table->open_type != OT_BASE_ONLY && - find_temporary_table(thd, table) && + thd->temporary_tables.find_table(table) && table->mdl_request.ticket != NULL));
- /* - drop_temporary_table may return one of the following error codes: - . 0 - a temporary table was successfully dropped. - . 1 - a temporary table was not found. - . -1 - a temporary table is used by an outer statement. - */ if (table->open_type == OT_BASE_ONLY || !is_temporary_table(table)) error= 1; else { table_creation_was_logged= table->table->s->table_creation_was_logged; - if ((error= drop_temporary_table(thd, table->table, &is_trans)) == -1) + if (thd->temporary_tables.drop_table(table->table, &is_trans, true)) { - DBUG_ASSERT(thd->in_sub_stmt); + error= 1; goto err;
Why are changes in this hunk? (comment, assert, -1, etc)
} table->table= 0; diff --git a/sql/table.h b/sql/table.h index ab39603..8b7c665 100644 --- a/sql/table.h +++ b/sql/table.h @@ -600,6 +601,7 @@ struct TABLE_STATISTICS_CB struct TABLE_SHARE { TABLE_SHARE() {} /* Remove gcc warning */ + TABLE_SHARE *next, *prev;
Eh? Did you forget to remove this?
/** Category of this table. */ TABLE_CATEGORY table_category; diff --git a/sql/temporary_tables.cc b/sql/temporary_tables.cc new file mode 100644 index 0000000..ed5b6b8 --- /dev/null +++ b/sql/temporary_tables.cc @@ -0,0 +1,1457 @@ +/* + Copyright (c) 2016 MariaDB Corporation + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "sql_acl.h" /* TMP_TABLE_ACLS */ +#include "sql_base.h" /* free_io_cache, + tdc_create_key */ +#include "lock.h" /* mysql_lock_remove */ +#include "log_event.h" /* Query_log_event */ +#include "sql_show.h" /* append_identifier */ +#include "sql_handler.h" /* mysql_ha_rm_temporary_tables */ +#include "temporary_tables.h" /* Temporary_tables */ +#include "rpl_rli.h" /* rpl_group_info */ + +#define IS_USER_TABLE(A) ((A->tmp_table == TRANSACTIONAL_TMP_TABLE) || \ + (A->tmp_table == NON_TRANSACTIONAL_TMP_TABLE)) + + +/* + Initialize the Temporary_tables object. Currently it always returns + false (success). + + @param thd [IN] Thread handle + + @return false Success + true Error +*/ +bool Temporary_tables::init(THD *thd) +{ + DBUG_ENTER("Temporary_tables::init"); + this->m_thd= thd; + DBUG_RETURN(false); +} + + +/* + Check whether temporary tables exist. The decision is made based on the + existence of TMP_TABLE_SHAREs. + + @param check_slave [IN] Also check the slave temporary tables. + + @return false Temporary tables exist + true No temporary table exist +*/ +bool Temporary_tables::is_empty(bool check_slave)
not a very good idea. You always use it as is_empty(true) or is_empty(false). that is, the caller always knows whether to check slave or not. but then you use a common function and it needs to do a completely unnecessary if() inside. Better to have two functions, like is_empty() and is_empty_slave(). Or at least make this function inline, then the compiler will have a chance to optimize it.
+{ + DBUG_ENTER("Temporary_tables::is_empty"); + + bool result; + + if (!m_thd) + { + DBUG_RETURN(true); + } + + rpl_group_info *rgi_slave= m_thd->rgi_slave; + + if (check_slave && rgi_slave) + { + result= (rgi_slave->rli->save_temp_table_shares == NULL) ? true : false; + } + else + { + result= (m_table_shares == NULL) ? true : false; + } + + DBUG_RETURN(result); +} + + + /* + Reset the Temporary_tables object. Currently, it always returns + false (success). + + @return false Success + true Error +*/ +bool Temporary_tables::reset() +{ + DBUG_ENTER("Temporary_tables::reset"); + m_table_shares= 0; + DBUG_RETURN(false); +} + + +/* + Create a temporary table, open it and return the TABLE handle. + + @param hton [IN] Handlerton + @param frm [IN] Binary frm image + @param path [IN] File path (without extension) + @param db [IN] Schema name + @param table_name [IN] Table name + @param open_in_engine [IN] Whether open table in SE + @param created [OUT] Whether table was created?
can it ever happen for *create==true but a return value is NULL? or the other way around? how?
+ + + @return Success A pointer to table object + Failure NULL +*/ +TABLE *Temporary_tables::create_and_open_table(handlerton *hton, + LEX_CUSTRING *frm, + const char *path, + const char *db, + const char *table_name, + bool open_in_engine, + bool *created) +{ + DBUG_ENTER("Temporary_tables::create_and_open_table"); + + TMP_TABLE_SHARE *share; + TABLE *table= NULL; + bool locked; + + *created= false; + + if (wait_for_prior_commit()) + { + DBUG_RETURN(NULL); + } + + locked= lock_tables();
old code didn't seem to have it here. how comes?
+ + if ((share= create_table(hton, frm, path, db, table_name))) + { + *created= true; + table= open_table(share, table_name, open_in_engine); + } + + if (locked) + { + DBUG_ASSERT(m_locked); + unlock_tables(); + } + + DBUG_RETURN(table); +} + +/* + Check whether an open table with db/table name is in use. + + @param db [IN] Database name + @param table_name [IN] Table name + + @return Success Pointer to first used table instance. + Failure NULL +*/ +TABLE *Temporary_tables::find_table(const char *db, + const char *table_name) +{ + DBUG_ENTER("Temporary_tables::find_table"); + + TABLE *table; + char key[MAX_DBKEY_LENGTH]; + uint key_length; + bool locked; + + if (is_empty(true)) + { + DBUG_RETURN(NULL); + } + + key_length= create_table_def_key(key, db, table_name); + + if (wait_for_prior_commit()) + { + DBUG_RETURN(NULL); + } + + locked= lock_tables(); + table = find_table(key, key_length, TABLE_IN_USE); + if (locked) + { + DBUG_ASSERT(m_locked); + unlock_tables(); + } + + DBUG_RETURN(table); +} + + +/* + Check whether an open table specified in TABLE_LIST is in use. + + @return tl [IN] TABLE_LIST + + @return Success Pointer to first used table instance. + Failure NULL +*/ +TABLE *Temporary_tables::find_table(const TABLE_LIST *tl) +{ + DBUG_ENTER("Temporary_tables::find_table"); + TABLE *table= find_table(tl->get_db_name(), tl->get_table_name()); + DBUG_RETURN(table); +} + + +/* + Check whether an open table with the specified key is in use. + The key, in this case, is not the usual key used for temporary tables. + It does not contain server_id & pseudo_thread_id. This function is + essentially used use to check whether there is any temporary table + which _shadows_ a base table. + (see: Query_cache::send_result_to_client()) + + @return Success A pointer to table share object + Failure NULL +*/ +TABLE *Temporary_tables::find_table_reduced_key_length(const char *key, + uint key_length) +{ + DBUG_ENTER("Temporary_tables::find_table_reduced_key_length"); + + TABLE *result= NULL; + bool locked; + + locked= lock_tables(); + + for (TMP_TABLE_SHARE *share= m_table_shares; share; share= share->next) + { + if ((share->table_cache_key.length - TMP_TABLE_KEY_EXTRA) == key_length + && !memcmp(share->table_cache_key.str, key, key_length)) + { + /* + A matching TMP_TABLE_SHARE is found. We now need to find a TABLE + instance in use. + */ + for (TABLE *table= share->table; table; table= table->next) + { + if (table->query_id != 0) + { + result= table; + break; + } + } + } + } + + if (locked) + { + DBUG_ASSERT(m_locked); + unlock_tables(); + } + + DBUG_RETURN(result); +} + + +/* + Lookup the TMP_TABLE_SHARE using the given db/table_name.The server_id and + pseudo_thread_id used to generate table definition key is taken from m_thd + (see create_table_def_key()). Return NULL is none found. + + @return Success A pointer to table share object + Failure NULL +*/ +TMP_TABLE_SHARE *Temporary_tables::find_table_share(const char *db, + const char *table_name) +{ + DBUG_ENTER("Temporary_tables::find_table_share"); + + TMP_TABLE_SHARE *share; + char key[MAX_DBKEY_LENGTH]; + uint key_length; + + key_length= create_table_def_key(key, db, table_name); + share= find_table_share(key, key_length); + + DBUG_RETURN(share); +} + + +/* + Lookup TMP_TABLE_SHARE using the specified TABLE_LIST element. + Return NULL is none found. + + @return Success A pointer to table share object + Failure NULL +*/ +TMP_TABLE_SHARE *Temporary_tables::find_table_share(const TABLE_LIST *tl) +{ + DBUG_ENTER("Temporary_tables::find_table_share"); + TMP_TABLE_SHARE *share= find_table_share(tl->get_db_name(), + tl->get_table_name()); + DBUG_RETURN(share); +} + + +/* + Lookup TMP_TABLE_SHARE using the specified table definition key. + Return NULL is none found. + + @return Success A pointer to table share object + Failure NULL +*/ +TMP_TABLE_SHARE *Temporary_tables::find_table_share(const char *key, + uint key_length) +{ + DBUG_ENTER("Temporary_tables::find_table_share"); + + TMP_TABLE_SHARE *share; + TMP_TABLE_SHARE *result= NULL; + bool locked; + + if (wait_for_prior_commit()) + { + DBUG_RETURN(NULL); + } + + locked= lock_tables(); + + for (share= m_table_shares; share; share= share->next) + { + if (share->table_cache_key.length == key_length && + !(memcmp(share->table_cache_key.str, key, key_length))) + { + result= share; + break; + } + } + + if (locked) + { + DBUG_ASSERT(m_locked); + unlock_tables(); + } + + DBUG_RETURN(result); +} + + +/* + Find a temporary table specified by TABLE_LIST instance in the open table + list and prepare its TABLE instance for use. If + + This function tries to resolve this table in the list of temporary tables + of this thread. Temporary tables are thread-local and "shadow" base + tables with the same name. + + @note In most cases one should use Temporary_tables::open_tables() instead + of this call. + + @note One should finalize process of opening temporary table for table + list element by calling open_and_process_table(). This function + is responsible for table version checking and handling of merge + tables. + + @note We used to check global_read_lock before opening temporary tables. + However, that limitation was artificial and is removed now. + + @param tl [IN] TABLE_LIST + + @return Error status. + @retval false On success. If a temporary table exists for the given + key, tl->table is set. + @retval TRUE On error. my_error() has been called.
letter case is a bit weird, 'false', but 'TRUE' :)
+*/ +bool Temporary_tables::open_table(TABLE_LIST *tl) +{ + DBUG_ENTER("Temporary_tables::open_table"); + + TMP_TABLE_SHARE *share; + TABLE *table= NULL; + bool locked; + + /* + Code in open_table() assumes that TABLE_LIST::table can be non-zero only + for pre-opened temporary tables. + */ + DBUG_ASSERT(tl->table == NULL); + + /* + This function should not be called for cases when derived or I_S + tables can be met since table list elements for such tables can + have invalid db or table name. + Instead Temporary_tables::open_tables() should be used. + */ + DBUG_ASSERT(!tl->derived && !tl->schema_table); + + if (tl->open_type == OT_BASE_ONLY || is_empty(true)) + { + DBUG_PRINT("info", ("skip_temporary is set or no temporary tables")); + DBUG_RETURN(false); + } + + if (wait_for_prior_commit())
this wasn't in the original code
+ { + DBUG_RETURN(true); + } + + locked= lock_tables();
neither was this
+ + /* + First check if there is a reusable open table available in the + open table list. + */ + if (find_and_use_table(tl, &table)) + { + if (locked) + { + DBUG_ASSERT(m_locked); + unlock_tables(); + } + DBUG_RETURN(true); /* Error */ + } + + /* + No reusable table was found. We will have to open a new instance. + */ + if (!table && (share= find_table_share(tl))) + { + table= open_table(share, tl->get_table_name(), true); + } + + if (locked) + { + DBUG_ASSERT(m_locked); + unlock_tables(); + } + + if (!table) + { + if (tl->open_type == OT_TEMPORARY_ONLY && + tl->open_strategy == TABLE_LIST::OPEN_NORMAL) + { + my_error(ER_NO_SUCH_TABLE, MYF(0), tl->db, tl->table_name); + DBUG_RETURN(true); + } + DBUG_RETURN(false); + } + +#ifdef WITH_PARTITION_STORAGE_ENGINE + if (tl->partition_names) + { + /* Partitioned temporary tables is not supported. */ + DBUG_ASSERT(!table->part_info); + my_error(ER_PARTITION_CLAUSE_ON_NONPARTITIONED, MYF(0)); + DBUG_RETURN(true); + } +#endif + + table->query_id= m_thd->query_id; + m_thd->thread_specific_used= true; + + /* It is neither a derived table nor non-updatable view. */ + tl->updatable= true; + tl->table= table; + + table->init(m_thd, tl); + + DBUG_PRINT("info", ("Using temporary table")); + DBUG_RETURN(false); +} + + +/* + Pre-open temporary tables corresponding to table list elements. + + @note One should finalize process of opening temporary tables + by calling open_tables(). This function is responsible + for table version checking and handling of merge tables.
what kind of version checking can there be for temporary tables?
+ + @param tl [IN] TABLE_LIST + + @return false On success. If a temporary table exists + for the given element, tl->table is set. + true On error. my_error() has been called. +*/ +bool Temporary_tables::open_tables(TABLE_LIST *tl) +{ + DBUG_ENTER("Temporary_tables::open_tables"); + + TABLE_LIST *first_not_own= m_thd->lex->first_not_own_table(); + + for (TABLE_LIST *table= tl; table && table != first_not_own; + table= table->next_global) + { + if (table->derived || table->schema_table) + { + /* + Derived and I_S tables will be handled by a later call to open_tables(). + */ + continue; + } + + if ((m_thd->temporary_tables.open_table(table)))
eh? couldn't you simply do if (open_table(table)) ? why do you need this "m_thd->temporary_tables." ?
+ { + DBUG_RETURN(true); + } + } + + DBUG_RETURN(false); +} + + +/* + Close all temporary tables created by 'CREATE TEMPORARY TABLE' for thread + creates one DROP TEMPORARY TABLE binlog event for each pseudo-thread. + + Temporary tables created in a sql slave is closed by + Relay_log_info::close_temporary_tables(). + + @return false Success + true Failure +*/ +bool Temporary_tables::close_tables() +{ + DBUG_ENTER("Temporary_tables::close_tables"); + + TMP_TABLE_SHARE *share; + TMP_TABLE_SHARE *share_next; + TABLE *table, *table_next; + bool error= false; + + if (!m_table_shares) + { + DBUG_RETURN(false); + } + DBUG_ASSERT(!m_thd->rgi_slave); + + /* + Ensure we don't have open HANDLERs for tables we are about to close. + This is necessary when Temporary_tables::close_tables() is called as + part of execution of BINLOG statement (e.g. for format description event). + */ + mysql_ha_rm_temporary_tables(m_thd); + + /* Close all open temporary tables. */ + for (TMP_TABLE_SHARE *share= m_table_shares; share; share= share->next) + { + /* Traverse the table list. */ + table= share->table; + while (table) + { + table_next= table->next; + free_table(table); + table= table_next; + } + } + + // Write DROP TEMPORARY TABLE query log events to binary log. + if (mysql_bin_log.is_open()) + { + error= log_events_and_free_shares(); + } + else + { + share= m_table_shares; + while (share) + { + share_next= share->next; + free_table_share(share, true); + share= share_next; + } + } + reset(); + + DBUG_RETURN(error); +} + + +/* + Rename a temporary table. + + @param table [IN] Table handle + @param db [IN] New schema name + @param table_name [IN] New table name + + @return false Success + true Error +*/ +bool Temporary_tables::rename_table(TABLE *table, + const char *db, + const char *table_name) +{ + DBUG_ENTER("Temporary_tables::rename_table"); + + char *key; + uint key_length; + + TMP_TABLE_SHARE *share= static_cast
(table->s); + + if (!(key= (char *) alloc_root(&share->mem_root, MAX_DBKEY_LENGTH))) + { + DBUG_RETURN(true); + } + + /* + Temporary tables are renamed by simply changing their table definition key. + */ + key_length= create_table_def_key(key, db, table_name); + share->set_table_cache_key(key, key_length); + + DBUG_RETURN(false); +} + + +/* + Drop a temporary table. + + Try to locate the table in the list of open temporary tables. + If the table is found: + - If the table is locked with LOCK TABLES or by prelocking, + unlock it and remove it from the list of locked tables + (THD::lock). Currently only transactional temporary tables + are locked. + - Close the temporary table, remove its .FRM. + - Remove the table share from the list of temporary table shares. + + This function is used to drop user temporary tables, as well as + internal tables created in CREATE TEMPORARY TABLE ... SELECT + or ALTER TABLE. + + @param table [IN] Temporary table to be deleted + @param is_trans [OUT] Is set to the type of the table: + transactional (e.g. innodb) as true or + non-transactional (e.g. myisam) as false. + @paral delete_table [IN] Whether to delete the table files. + + @return false Table was dropped + true Error +*/ +bool Temporary_tables::drop_table(TABLE *table, + bool *is_trans, + bool delete_table) +{ + DBUG_ENTER("Temporary_tables::drop_table"); + + TMP_TABLE_SHARE *share; + TABLE *tab; + TABLE *tab_next; + bool result, locked; + + DBUG_ASSERT(table); + DBUG_ASSERT(table->query_id); + DBUG_PRINT("tmptable", ("Dropping table: '%s'.'%s'", + table->s->db.str, table->s->table_name.str)); + + if (wait_for_prior_commit()) + { + DBUG_RETURN(true); + }
old code didn't seem to have that. Why do you?
+ + locked= lock_tables(); + + share= static_cast
(table->s);
suggestion: have a function:
static inline TMP_TABLE_SHARE *tmp_table_share(TABLE *t) {
DBUG_ASSERT(t->s->tmp_table);
return static_cast
+ + /* Table might be in use by some outer statement. */ + for (tab= share->table; tab; tab= tab->next) + { + if (tab != table && tab->query_id != 0) + { + /* Found a table instance in use. This table cannot be be dropped. */ + my_error(ER_CANT_REOPEN_TABLE, MYF(0), table->alias.c_ptr()); + result= true; + goto end; + } + } + + if (is_trans) + *is_trans= table->file->has_transactions(); + + /* + Iterate over the list of open tables and close all tables referencing the + same table share.
the comment sounds a bit odd now, when all open tables are in the list inside the share.
+ */ + tab= share->table; + while (tab) + { + tab_next= tab->next; + if ((result= free_table(tab))) goto end; + tab= tab_next; + } + + result= free_table_share(share, delete_table); + +end: + if (locked) + { + DBUG_ASSERT(m_locked); + unlock_tables(); + } + + DBUG_RETURN(result); +} + + +/** + Delete the temporary table files. + + @param base [IN] Handlerton for table to be deleted. + @param path [IN] Path to the table to be deleted (i.e. path + to its .frm without an extension). + + @return false Success + true Error +*/ +bool Temporary_tables::remove_table(handlerton *base, const char *path) +{ + DBUG_ENTER("Temporary_tables::remove_table"); + + bool error= false; + handler *file; + char frm_path[FN_REFLEN + 1]; + + strxnmov(frm_path, sizeof(frm_path) - 1, path, reg_ext, NullS); + if (mysql_file_delete(key_file_frm, frm_path, MYF(0))) + { + error= true; + } + file= get_new_handler((TABLE_SHARE*) 0, current_thd->mem_root, base); + if (file && file->ha_delete_table(path)) + { + error= true; + sql_print_warning("Could not remove temporary table: '%s', error: %d", + path, my_errno); + } + + delete file; + DBUG_RETURN(error); +} + + +/* + Mark all temporary tables which were used by the current statement or + sub-statement as free for reuse, but only if the query_id can be cleared. + + @remark For temp tables associated with a open SQL HANDLER the query_id + is not reset until the HANDLER is closed. +*/ +void Temporary_tables::mark_tables_as_free_for_reuse() +{ + DBUG_ENTER("Temporary_tables::mark_tables_as_free_for_reuse"); + + bool locked; + + if (m_thd->query_id == 0) + { + /* + Thread has not executed any statement and has not used any + temporary tables. + */ + DBUG_VOID_RETURN; + } + + locked= lock_tables(); + + for (TMP_TABLE_SHARE *share= m_table_shares; share; share= share->next) + { + for (TABLE *table= share->table; table; table= table->next) + { + if ((table->query_id == m_thd->query_id) && !table->open_by_handler) + { + mark_table_as_free_for_reuse(table); + } + } + } + + if (locked) + { + DBUG_ASSERT(m_locked); + unlock_tables(); + } + + DBUG_VOID_RETURN; +} + + +/* + Reset a single temporary table. Effectively this "closes" one temporary + table in a session. + + @param table Temporary table +*/ +void Temporary_tables::mark_table_as_free_for_reuse(TABLE *table) +{ + DBUG_ENTER("Temporary_tables::mark_table_as_free_for_reuse"); + + DBUG_ASSERT(table->s->tmp_table); + + table->query_id= 0; + table->file->ha_reset(); + + /* Detach temporary MERGE children from temporary parent. */ + DBUG_ASSERT(table->file); + table->file->extra(HA_EXTRA_DETACH_CHILDREN); + + /* + Reset temporary table lock type to it's default value (TL_WRITE). + + Statements such as INSERT INTO .. SELECT FROM tmp, CREATE TABLE + .. SELECT FROM tmp and UPDATE may under some circumstances modify + the lock type of the tables participating in the statement. This + isn't a problem for non-temporary tables since their lock type is + reset at every open, but the same does not occur for temporary + tables for historical reasons. + + Furthermore, the lock type of temporary tables is not really that + important because they can only be used by one query at a time. + Nonetheless, it's safer from a maintenance point of view to reset + the lock type of this singleton TABLE object as to not cause problems + when the table is reused. + + Even under LOCK TABLES mode its okay to reset the lock type as + LOCK TABLES is allowed (but ignored) for a temporary table. + */ + table->reginfo.lock_type= TL_WRITE; + DBUG_VOID_RETURN; +} + + +TMP_TABLE_SHARE *Temporary_tables::unlink_table_share(TABLE_SHARE *share) +{ + DBUG_ENTER("Temporary_tables::unlink_table"); + TMP_TABLE_SHARE *tmp_table_share; + + lock_tables(); + tmp_table_share= static_cast
(share); + unlink (&m_table_shares, tmp_table_share); + unlock_tables(); + + DBUG_RETURN(tmp_table_share); +} + + +void Temporary_tables::relink_table_share(TMP_TABLE_SHARE *share) +{ + DBUG_ENTER("Temporary_tables::relink_table"); + + lock_tables(); + link (&m_table_shares, share); + unlock_tables(); + + DBUG_VOID_RETURN; +} + + +/* + Create a table definition key. + + @param key [OUT] Buffer for the key to be created (must + be of size MAX_DBKRY_LENGTH) + @param db [IN] Database name + @param table_name [IN] Table name + + @return Key length. + + @note + The table key is create from: + db + \0 + table_name + \0 + + Additionally, we add the following to make each temporary table unique on + the slave. + + 4 bytes of master thread id + 4 bytes of pseudo thread id +*/ + +uint Temporary_tables::create_table_def_key(char *key, const char *db, + const char *table_name) +{ + DBUG_ENTER("Temporary_tables::create_table_def_key"); + + uint key_length; + + key_length= tdc_create_key(key, db, table_name); + int4store(key + key_length, m_thd->variables.server_id); + int4store(key + key_length + 4, m_thd->variables.pseudo_thread_id); + key_length += TMP_TABLE_KEY_EXTRA; + + DBUG_RETURN(key_length); +} + + +/* + Create a temporary table. + + @param hton [IN] Handlerton + @param frm [IN] Binary frm image + @param path [IN] File path (without extension) + @param db [IN] Schema name + @param table_name [IN] Table name + + @return Success A pointer to table share object + Failure NULL +*/ +TMP_TABLE_SHARE *Temporary_tables::create_table(handlerton *hton, + LEX_CUSTRING *frm, + const char *path, + const char *db, + const char *table_name) +{ + DBUG_ENTER("Temporary_tables::create_table"); + + TMP_TABLE_SHARE *share; + char key_cache[MAX_DBKEY_LENGTH]; + char *saved_key_cache; + char *tmp_path; + uint key_length; + int res; + + if (wait_for_prior_commit())
you're doing it in almost every method. And when one method calls another, you're doing it twice. Or thrice. Isn't is too much? (I don't know)
+ { + DBUG_RETURN(NULL); + } + + /* Create the table definition key for the temporary table. */ + key_length= create_table_def_key(key_cache, db, table_name); + + if (!(share= (TMP_TABLE_SHARE *) my_malloc(sizeof(TMP_TABLE_SHARE) + + strlen(path) + + 1 + key_length, MYF(MY_WME)))) + { + DBUG_RETURN(NULL); /* Out of memory */ + } + + tmp_path= (char *)(share + 1); + saved_key_cache= strmov(tmp_path, path) + 1; + memcpy(saved_key_cache, key_cache, key_length); + + init_tmp_table_share(m_thd, share, saved_key_cache, key_length, + strend(saved_key_cache) + 1, tmp_path); + + share->table= 0; + share->db_plugin= ha_lock_engine(m_thd, hton); + + /* + Prefer using frm image over file. The image might not be available in + ALTER TABLE, when the discovering engine took over the ownership (see + TABLE::read_frm_image). + */ + res= (frm->str) + ? share->init_from_binary_frm_image(m_thd, false, frm->str, frm->length) + : open_table_def(m_thd, share, GTS_TABLE | GTS_USE_DISCOVERY); + + if (res) + { + /* + No need to lock share->mutex as this is not needed for temporary tables. + */ + ::free_table_share(share); + my_free(share); + DBUG_RETURN(NULL); + } + + share->m_psi= PSI_CALL_get_table_share(true, share); + + /* Add share to the head of the temporary table share list. */ + link
(&m_table_shares, share); + + DBUG_RETURN(share); +} + + +/* + Find a table with the specified key. + + @param key [IN] Key + @param key_length [IN] Key length + @param state [IN] Open table state to look for + + @return Success Pointer to the table instance. + Failure NULL +*/ +TABLE *Temporary_tables::find_table(const char *key, uint key_length, + Table_state state) +{ + DBUG_ENTER("Temporary_tables::find_table"); + + for (TMP_TABLE_SHARE *share= m_table_shares; share; share= share->next) + { + if (share->table_cache_key.length == key_length && + !(memcmp(share->table_cache_key.str, key, key_length))) + { + /* A matching TMP_TABLE_SHARE is found. */ + for (TABLE *table= share->table; table; table= table->next) + { + switch (state) + { + case TABLE_IN_USE: + if (table->query_id > 0) DBUG_RETURN(table); + break; + case TABLE_NOT_IN_USE: + if (table->query_id == 0) DBUG_RETURN(table); + break; + case TABLE_ANY: + DBUG_RETURN(table); + default: /* Invalid */ + DBUG_ASSERT(0); + DBUG_RETURN(NULL); + } + } + } + } + + DBUG_RETURN(NULL); +} + + +/* + Find a reusable table in the open table list using the specified TABLE_LIST. + + @param tl [IN] Table list + @param out_table [OUT] Pointer to the requested TABLE object + + @return Success false + Failure true +*/ +bool Temporary_tables::find_and_use_table(const TABLE_LIST *tl, + TABLE **out_table) +{ + DBUG_ENTER("Temporary_tables::find_and_use_table"); + + char key[MAX_DBKEY_LENGTH]; + uint key_length; + bool result; + + key_length= create_table_def_key(key, tl->get_db_name(), + tl->get_table_name()); + result= use_table(find_table(key, key_length, TABLE_NOT_IN_USE), out_table); + + DBUG_RETURN(result); +} + + +/* + Open a table from the specified TABLE_SHARE with the given alias. + + @param share [IN] Table share + @param alias [IN] Table alias + @param open_in_engine [IN] Whether open table in SE + + @return Success A pointer to table object + Failure NULL +*/ +TABLE *Temporary_tables::open_table(TMP_TABLE_SHARE *share, + const char *alias, + bool open_in_engine) +{ + DBUG_ENTER("Temporary_tables::open_table"); + + TABLE *table; + + if (wait_for_prior_commit()) + { + DBUG_RETURN(NULL); + } + + if (!(table= (TABLE *) my_malloc(sizeof(TABLE), MYF(MY_WME)))) + { + DBUG_RETURN(NULL); /* Out of memory */ + } + + if (open_table_from_share(m_thd, share, alias, + (open_in_engine) ? + (uint) (HA_OPEN_KEYFILE | HA_OPEN_RNDFILE | + HA_GET_INDEX) : 0, + (uint) (READ_KEYINFO | COMPUTE_TYPES | + EXTRA_RECORD), + ha_open_options, + table, + open_in_engine ? false : true)) + { + my_free(table); + DBUG_RETURN(NULL); + } + + table->reginfo.lock_type= TL_WRITE; /* Simulate locked */ + table->grant.privilege= TMP_TABLE_ACLS; + share->tmp_table= (table->file->has_transactions() ? + TRANSACTIONAL_TMP_TABLE : NON_TRANSACTIONAL_TMP_TABLE); + + table->pos_in_table_list= 0; + table->query_id= m_thd->query_id; + + /* Add table to the head of table list. */ + link<TABLE>(&share->table, table);
I don't know if that's correct. You use TABLE::next and TABLE::prev pointers to link all tables of a given TMP_TABLE_SHARE into a list. But these pointers are used to link all tables into a THD::open_tables list (and may be for other purposes too?). In fact, TABLE has dedicated pointers for this list that you want: /** Links for the list of all TABLE objects for this share. Declared as private to avoid direct manipulation with those objects. One should use methods of I_P_List template instead. */ TABLE *share_all_next, **share_all_prev;
+ + /* Increment Slave_open_temp_table_definitions status variable count. */ + if (m_thd->rgi_slave) + { + thread_safe_increment32(&slave_open_temp_tables); + } + + DBUG_PRINT("tmptable", ("Opened table: '%s'.'%s' 0x%lx", table->s->db.str, + table->s->table_name.str, (long) table)); + DBUG_RETURN(table); +} + +
function comment?
+bool Temporary_tables::use_table(TABLE *table, TABLE **out_table) +{ + DBUG_ENTER("Temporary_tables::use_table"); + + *out_table= table; + if (!table) + DBUG_RETURN(false); +
old code had a block here about "Temporary tables are not safe for parallel replication". it's in wait_for_prior_commit now, but you aren't calling it here. why?
+ /* + We need to set the THD as it may be different in case of + parallel replication + */ + if (table->in_use != m_thd) + { + table->in_use= m_thd; + } + + DBUG_RETURN(false); +} + + +/* + Close a temporary table. + + @param table [IN] Table handle + + @return false Success + true Error +*/ +bool Temporary_tables::close_table(TABLE *table) +{ + DBUG_ENTER("Temporary_tables::close_table"); + + DBUG_PRINT("tmptable", ("closing table: '%s'.'%s' 0x%lx alias: '%s'", + table->s->db.str, table->s->table_name.str, + (long) table, table->alias.c_ptr())); + + free_io_cache(table);
don't forget to remove free_io_cache() calls when rebasing your work on top of the latest 10.2 (they were removed from close_temporary() in 260dd476b05 commit)
+ closefrm(table, false); + my_free(table); + + /* Decrement Slave_open_temp_table_definitions status variable count. */ + if (m_thd->rgi_slave) + { + thread_safe_decrement32(&slave_open_temp_tables); + } + + DBUG_RETURN(false); +} + + +bool Temporary_tables::wait_for_prior_commit() +{ + DBUG_ENTER("Temporary_tables::wait_for_prior_commit"); + + /* + Temporary tables are not safe for parallel replication. They were + designed to be visible to one thread only, so have no table locking. + Thus there is no protection against two conflicting transactions + committing in parallel and things like that. + + So for now, anything that uses temporary tables will be serialised + with anything before it, when using parallel replication. + + TODO: We might be able to introduce a reference count or something + on temp tables, and have slave worker threads wait for it to reach + zero before being allowed to use the temp table. Might not be worth + it though, as statement-based replication using temporary tables is + in any case rather fragile. + */ + if (m_thd->rgi_slave && + m_thd->rgi_slave->rli->save_temp_table_shares && + m_thd->rgi_slave->is_parallel_exec && + m_thd->wait_for_prior_commit()) + DBUG_RETURN(true); + + DBUG_RETURN(false); +} + + +/* + Write query log events with "DROP TEMPORARY TABLES .." for each pseudo + thread to the binary log. + + @return false Success + true Error +*/ +bool Temporary_tables::log_events_and_free_shares() +{ + DBUG_ENTER("Temporary_tables::log_events_and_free_shares"); + + DBUG_ASSERT(!m_thd->rgi_slave); + + TMP_TABLE_SHARE *share; + TMP_TABLE_SHARE *next; + TMP_TABLE_SHARE *prev_share; + // Assume thd->variables.option_bits has OPTION_QUOTE_SHOW_CREATE. + bool was_quote_show= true; + bool error= false; + bool found_user_tables= false; + // Better add "IF EXISTS" in case a RESET MASTER has been done. + const char stub[]= "DROP /*!40005 TEMPORARY */ TABLE IF EXISTS "; + char buf[FN_REFLEN]; + + String s_query(buf, sizeof(buf), system_charset_info); + s_query.copy(stub, sizeof(stub) - 1, system_charset_info); + + /* + Insertion sort of temporary tables by pseudo_thread_id to build ordered + list of sublists of equal pseudo_thread_id. + */ + + for (prev_share= m_table_shares, share= prev_share->next; + share; + prev_share= share, share= share->next) + { + TMP_TABLE_SHARE *prev_sorted; /* Same as for prev_share */ + TMP_TABLE_SHARE *sorted; + + if (IS_USER_TABLE(share)) + { + if (!found_user_tables) + found_user_tables= true; + + for (prev_sorted= NULL, sorted= m_table_shares; + sorted != share; + prev_sorted= sorted, sorted= sorted->next) + { + if (!IS_USER_TABLE(sorted) || + tmpkeyval(sorted) > tmpkeyval(share)) + { + /* + Move into the sorted part of the list from the unsorted. + */ + prev_share->next= share->next; + share->next= sorted; + if (prev_sorted) + { + prev_sorted->next= share; + } + else + { + m_table_shares= share; + } + share= prev_share; + break; + } + } + } + } + + /* + We always quote db & table names. + */ + if (found_user_tables && + !(was_quote_show= MY_TEST(m_thd->variables.option_bits & + OPTION_QUOTE_SHOW_CREATE))) + { + m_thd->variables.option_bits |= OPTION_QUOTE_SHOW_CREATE; + } + + /* + Scan sorted temporary tables to generate sequence of DROP. + */ + for (share= m_table_shares; share; share= next) + { + if (IS_USER_TABLE(share)) + { + bool save_thread_specific_used= m_thd->thread_specific_used; + my_thread_id save_pseudo_thread_id= m_thd->variables.pseudo_thread_id; + char db_buf[FN_REFLEN]; + String db(db_buf, sizeof(db_buf), system_charset_info); + + /* + Set pseudo_thread_id to be that of the processed table. + */ + m_thd->variables.pseudo_thread_id= tmpkeyval(share); + + db.copy(share->db.str, share->db.length, system_charset_info); + /* + Reset s_query() if changed by previous loop. + */ + s_query.length(sizeof(stub) - 1); + + /* + Loop forward through all tables that belong to a common database + within the sublist of common pseudo_thread_id to create single + DROP query. + */ + for (; + share && + IS_USER_TABLE(share) && + tmpkeyval(share) == m_thd->variables.pseudo_thread_id && + share->db.length == db.length() && + memcmp(share->db.str, db.ptr(), db.length()) == 0; + share= next) + { + /* + We are going to add ` around the table names and possible more + due to special characters. + */ + append_identifier(m_thd, &s_query, share->table_name.str, + share->table_name.length); + s_query.append(','); + next= share->next; + remove_table(share->db_type(), share->path.str); + ::free_table_share(share); + my_free(share); + } + + m_thd->clear_error(); + CHARSET_INFO *cs_save= m_thd->variables.character_set_client; + m_thd->variables.character_set_client= system_charset_info; + m_thd->thread_specific_used= true; + + Query_log_event qinfo(m_thd, s_query.ptr(), + s_query.length() - 1 /* to remove trailing ',' */, + false, true, false, 0); + qinfo.db= db.ptr(); + qinfo.db_len= db.length(); + m_thd->variables.character_set_client= cs_save; + + m_thd->get_stmt_da()->set_overwrite_status(true); + if ((error= (mysql_bin_log.write(&qinfo) || error))) + { + /* + If we're here following THD::cleanup, thence the connection + has been closed already. So lets print a message to the + error log instead of pushing yet another error into the + stmt_da. + + Also, we keep the error flag so that we propagate the error + up in the stack. This way, if we're the SQL thread we notice + that Temporary_tables::close_tables failed. (Actually, the SQL + thread only calls Temporary_tables::close_tables while applying + old Start_log_event_v3 events.) + */ + sql_print_error("Failed to write the DROP statement for " + "temporary tables to binary log"); + } + + m_thd->get_stmt_da()->set_overwrite_status(false); + m_thd->variables.pseudo_thread_id= save_pseudo_thread_id; + m_thd->thread_specific_used= save_thread_specific_used; + } + else + { + next= share->next; + free_table_share(share, true); + } + } + + if (!was_quote_show) + { + /* + Restore option. + */ + m_thd->variables.option_bits&= ~OPTION_QUOTE_SHOW_CREATE; + } + + DBUG_RETURN(error); +} + +/* + Delete the files and free the specified table share. +*/ +bool Temporary_tables::free_table_share(TMP_TABLE_SHARE *share, + bool delete_table) +{ + DBUG_ENTER("Temporary_tables::free_table_share"); + + bool result= false; + + /* Delete the share from table share list */ + unlink
(&m_table_shares, share); + + if (delete_table) + { + result= remove_table(share->db_type(), share->path.str); + } + + ::free_table_share(share); + my_free(share); + + DBUG_RETURN(result); +} + + +/* + Free the specified table object. +*/ +bool Temporary_tables::free_table(TABLE *table) +{ + DBUG_ENTER("Temporary_tables::free_table"); + + bool result= false; + + /* Delete the table from table list */ + unlink<TABLE>(&static_cast (table->s)->table, table); + + /* + If LOCK TABLES list is not empty and contains this table, unlock the table + and remove the table from this list. + */ + mysql_lock_remove(m_thd, m_thd->lock, table); + + result= close_table(table); + + DBUG_RETURN(result); +} + + +bool Temporary_tables::lock_tables() +{ + /* Do not proceed if a lock has already been taken. */ + if (m_locked) + { + return false; + } + + rpl_group_info *rgi_slave= m_thd->rgi_slave; + if (rgi_slave) + { + mysql_mutex_lock(&rgi_slave->rli->data_lock); + m_table_shares= rgi_slave->rli->save_temp_table_shares; + m_locked= true; + } + return m_locked; +} + + +void Temporary_tables::unlock_tables() +{ + if (!m_locked) + { + return; + } + + rpl_group_info *rgi_slave= m_thd->rgi_slave; + if (rgi_slave) + { + rgi_slave->rli->save_temp_table_shares= m_table_shares; + mysql_mutex_unlock(&rgi_slave->rli->data_lock); + m_locked= false; + /* + Temporary tables are shared with other by sql execution threads. + As a safety measure, clear the pointer to the common area. + */ + reset(); + } + return; +} + diff --git a/sql/temporary_tables.h b/sql/temporary_tables.h new file mode 100644 index 0000000..5a5ed0d --- /dev/null +++ b/sql/temporary_tables.h @@ -0,0 +1,150 @@ +#ifndef TEMPORARY_TABLES_H +#define TEMPORARY_TABLES_H +/* + Copyright (c) 2016 MariaDB Corporation + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#define TMP_TABLE_KEY_EXTRA 8 + +struct TMP_TABLE_SHARE: public TABLE_SHARE +{ + /* List of TABLE instances created out of this TABLE_SHARE. */ + TABLE *table;
Hmm, if you will use TABLE::share_all_prev and TABLE::share_all_next, as I've written above, you should also use TDC_element that keeps the list of TABLE's per TABLE_SHARE. And also it has next/prev pointers to link all TABLE_SHARE's in a list... Perhaps you won't need TMP_TABLE_SHARE after all? Could you talk with Svoj about it, please? perhaps you can simply reuse existing TABLE_SHARE code for linking TABLE_SHARE's into a list and linking TABLE's into a per-TABLE_SHARE list? TDC_element is a bit of an overkill for temporary tables, we can either live with that or extract those bits that you need into a "mini-TDC_element" ? Dunno, best to discuss it with Svoj.
+ + /* Pointers to access TMP_TABLE_SHARE instances. */ + TMP_TABLE_SHARE *next; + TMP_TABLE_SHARE *prev; +}; + + +class Temporary_tables +{ +public: + Temporary_tables() : m_thd(0), m_table_shares(0), m_locked(false) + {} + bool init(THD *thd); + bool is_empty(bool check_slave); + bool reset(); + + TABLE *create_and_open_table(handlerton *hton, LEX_CUSTRING *frm, + const char *path, const char *db, + const char *table_name, bool open_in_engine, + bool *created); + + TABLE *find_table(const char *db, const char *table_name); + TABLE *find_table(const TABLE_LIST *tl); + TABLE *find_table_reduced_key_length(const char *key, uint key_length); + + TMP_TABLE_SHARE *find_table_share(const char *db, const char *table_name); + TMP_TABLE_SHARE *find_table_share(const TABLE_LIST *tl); + TMP_TABLE_SHARE *find_table_share(const char *key, uint key_length); + + bool open_table(TABLE_LIST *tl); + bool open_tables(TABLE_LIST *tl); + + bool close_tables(); + bool rename_table(TABLE *table, const char *db, const char *table_name); + bool drop_table(TABLE *table, bool *is_trans, bool delete_table); + bool remove_table(handlerton *hton, const char *path); + void mark_tables_as_free_for_reuse(); + void mark_table_as_free_for_reuse(TABLE *table); + + TMP_TABLE_SHARE *unlink_table_share(TABLE_SHARE *share); + void relink_table_share(TMP_TABLE_SHARE *share); + +private: + /* THD handler */ + THD *m_thd;
already discussed
+ + /* List of temporary table shares */ + TMP_TABLE_SHARE *m_table_shares; + + /* Whether a lock has been acquired. */ + bool m_locked; + + /* Opened table states. */ + enum Table_state { + TABLE_IN_USE, + TABLE_NOT_IN_USE, + TABLE_ANY + }; + + uint create_table_def_key(char *key, + const char *db, + const char *table_name); + + TMP_TABLE_SHARE *create_table(handlerton *hton, + LEX_CUSTRING *frm, + const char *path, + const char *db, + const char *table_name); + + TABLE *find_table(const char *key, uint key_length, Table_state state); + + bool find_and_use_table(const TABLE_LIST *tl, TABLE **out_table); + + TABLE *open_table(TMP_TABLE_SHARE *share, const char *alias, + bool open_in_engine); + + bool use_table(TABLE *table, TABLE **out_table); + bool close_table(TABLE *table); + bool wait_for_prior_commit(); + bool log_events_and_free_shares(); + + bool free_table_share(TMP_TABLE_SHARE *share, bool delete_table); + bool free_table(TABLE *table); + + bool lock_tables(); + void unlock_tables(); + + /* List operations */ + template <class T> + void link(T **list, T *element) + { + element->next= *list; + if (element->next) + element->next->prev= element; + *list= element; + (*list)->prev= 0; + } + + template <class T> + void unlink(T **list, T *element) + { + if (element->prev) + { + element->prev->next= element->next; + if (element->prev->next) + element->next->prev= element->prev; + } + else + { + DBUG_ASSERT(element == *list); + + *list= element->next; + if (*list) + element->next->prev= 0; + } + }
No need to reinvent the wheel, you can use I_P_List for that.
+ + uint tmpkeyval(TMP_TABLE_SHARE *share) + { + return uint4korr(share->table_cache_key.str + + share->table_cache_key.length - 4); + } +}; + +#endif /* TEMPORARY_TABLES_H */
Regards, Sergei Chief Architect MariaDB and security@mariadb.org