420 lines
13 KiB
C
420 lines
13 KiB
C
/*
|
|
* Copyright (c) 2016 MariaDB Corporation Ab
|
|
*
|
|
* Use of this software is governed by the Business Source License included
|
|
* in the LICENSE.TXT file and at www.mariadb.com/bsl.
|
|
*
|
|
* Change Date: 2019-07-01
|
|
*
|
|
* On the date above, in accordance with the Business Source License, use
|
|
* of this software will be governed by version 2 or later of the General
|
|
* Public License.
|
|
*/
|
|
#include <stdio.h>
|
|
#include <strings.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <maxscale/alloc.h>
|
|
|
|
/* Note that modutil contains much MySQL specific code */
|
|
#include <maxscale/modutil.h>
|
|
|
|
#include <maxscale/router.h>
|
|
#include "readwritesplit.h"
|
|
#include "rwsplit_internal.h"
|
|
/**
|
|
* @file rwsplit_tmp_table.c The functions that carry out checks on
|
|
* statements to see if they involve various operations involving temporary
|
|
* tables or multi-statement queries.
|
|
*
|
|
* @verbatim
|
|
* Revision History
|
|
*
|
|
* Date Who Description
|
|
* 08/08/2016 Martin Brampton Initial implementation
|
|
*
|
|
* @endverbatim
|
|
*/
|
|
|
|
/*
|
|
* The following are to do with checking whether the statement refers to
|
|
* temporary tables, or is a multi-statement request. Maybe they belong
|
|
* somewhere else, outside this router. Perhaps in the query classifier?
|
|
*/
|
|
|
|
/**
|
|
* @brief Check for dropping of temporary tables
|
|
*
|
|
* Check if the query is a DROP TABLE... query and
|
|
* if it targets a temporary table, remove it from the hashtable.
|
|
* @param router_cli_ses Router client session
|
|
* @param querybuf GWBUF containing the query
|
|
* @param type The type of the query resolved so far
|
|
*/
|
|
void check_drop_tmp_table(ROUTER_CLIENT_SES *router_cli_ses, GWBUF *querybuf,
|
|
mysql_server_cmd_t packet_type)
|
|
{
|
|
if (packet_type != MYSQL_COM_QUERY && packet_type != MYSQL_COM_DROP_DB)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int tsize = 0, klen = 0, i;
|
|
char **tbl = NULL;
|
|
char *hkey, *dbname;
|
|
MYSQL_session *my_data;
|
|
rses_property_t *rses_prop_tmp;
|
|
MYSQL_session *data = (MYSQL_session *)router_cli_ses->client_dcb->data;
|
|
|
|
rses_prop_tmp = router_cli_ses->rses_properties[RSES_PROP_TYPE_TMPTABLES];
|
|
dbname = (char *)data->db;
|
|
|
|
if (qc_is_drop_table_query(querybuf))
|
|
{
|
|
tbl = qc_get_table_names(querybuf, &tsize, false);
|
|
if (tbl != NULL)
|
|
{
|
|
for (i = 0; i < tsize; i++)
|
|
{
|
|
/* Not clear why the next six lines are outside the if block */
|
|
klen = strlen(dbname) + strlen(tbl[i]) + 2;
|
|
hkey = MXS_CALLOC(klen, sizeof(char));
|
|
MXS_ABORT_IF_NULL(hkey);
|
|
strcpy(hkey, dbname);
|
|
strcat(hkey, ".");
|
|
strcat(hkey, tbl[i]);
|
|
|
|
if (rses_prop_tmp && rses_prop_tmp->rses_prop_data.temp_tables)
|
|
{
|
|
if (hashtable_delete(rses_prop_tmp->rses_prop_data.temp_tables,
|
|
(void *)hkey))
|
|
{
|
|
MXS_INFO("Temporary table dropped: %s", hkey);
|
|
}
|
|
}
|
|
MXS_FREE(tbl[i]);
|
|
MXS_FREE(hkey);
|
|
}
|
|
|
|
MXS_FREE(tbl);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if the query targets a temporary table.
|
|
* @param router_cli_ses Router client session
|
|
* @param querybuf GWBUF containing the query
|
|
* @param type The type of the query resolved so far
|
|
* @return The type of the query
|
|
*/
|
|
bool is_read_tmp_table(ROUTER_CLIENT_SES *router_cli_ses,
|
|
GWBUF *querybuf,
|
|
qc_query_type_t qtype)
|
|
{
|
|
|
|
bool target_tmp_table = false;
|
|
int tsize = 0, klen = 0, i;
|
|
char **tbl = NULL;
|
|
char *dbname;
|
|
char hkey[MYSQL_DATABASE_MAXLEN + MYSQL_TABLE_MAXLEN + 2];
|
|
MYSQL_session *data;
|
|
bool rval = false;
|
|
rses_property_t *rses_prop_tmp;
|
|
|
|
if (router_cli_ses == NULL || querybuf == NULL)
|
|
{
|
|
MXS_ERROR("[%s] Error: NULL parameters passed: %p %p", __FUNCTION__,
|
|
router_cli_ses, querybuf);
|
|
return false;
|
|
}
|
|
|
|
if (router_cli_ses->client_dcb == NULL)
|
|
{
|
|
MXS_ERROR("[%s] Error: Client DCB is NULL.", __FUNCTION__);
|
|
return false;
|
|
}
|
|
|
|
rses_prop_tmp = router_cli_ses->rses_properties[RSES_PROP_TYPE_TMPTABLES];
|
|
data = (MYSQL_session *)router_cli_ses->client_dcb->data;
|
|
|
|
if (data == NULL)
|
|
{
|
|
MXS_ERROR("[%s] Error: User data in client DBC is NULL.", __FUNCTION__);
|
|
return false;
|
|
}
|
|
|
|
dbname = (char *)data->db;
|
|
|
|
if (qc_query_is_type(qtype, QUERY_TYPE_READ) ||
|
|
qc_query_is_type(qtype, QUERY_TYPE_LOCAL_READ) ||
|
|
qc_query_is_type(qtype, QUERY_TYPE_USERVAR_READ) ||
|
|
qc_query_is_type(qtype, QUERY_TYPE_SYSVAR_READ) ||
|
|
qc_query_is_type(qtype, QUERY_TYPE_GSYSVAR_READ))
|
|
{
|
|
tbl = qc_get_table_names(querybuf, &tsize, false);
|
|
|
|
if (tbl != NULL && tsize > 0)
|
|
{
|
|
/** Query targets at least one table */
|
|
for (i = 0; i < tsize && !target_tmp_table && tbl[i]; i++)
|
|
{
|
|
sprintf(hkey, "%s.%s", dbname, tbl[i]);
|
|
if (rses_prop_tmp && rses_prop_tmp->rses_prop_data.temp_tables)
|
|
{
|
|
if (hashtable_fetch(rses_prop_tmp->rses_prop_data.temp_tables, hkey))
|
|
{
|
|
/**Query target is a temporary table*/
|
|
rval = true;
|
|
MXS_INFO("Query targets a temporary table: %s", hkey);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tbl != NULL)
|
|
{
|
|
for (i = 0; i < tsize; i++)
|
|
{
|
|
MXS_FREE(tbl[i]);
|
|
}
|
|
MXS_FREE(tbl);
|
|
}
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* If query is of type QUERY_TYPE_CREATE_TMP_TABLE then find out
|
|
* the database and table name, create a hashvalue and
|
|
* add it to the router client session's property. If property
|
|
* doesn't exist then create it first.
|
|
* @param router_cli_ses Router client session
|
|
* @param querybuf GWBUF containing the query
|
|
* @param type The type of the query resolved so far
|
|
*/
|
|
void check_create_tmp_table(ROUTER_CLIENT_SES *router_cli_ses,
|
|
GWBUF *querybuf, qc_query_type_t type)
|
|
{
|
|
if (!qc_query_is_type(type, QUERY_TYPE_CREATE_TMP_TABLE))
|
|
{
|
|
return;
|
|
}
|
|
|
|
int klen = 0;
|
|
char *hkey, *dbname;
|
|
MYSQL_session *data;
|
|
rses_property_t *rses_prop_tmp;
|
|
HASHTABLE *h;
|
|
|
|
if (router_cli_ses == NULL || querybuf == NULL)
|
|
{
|
|
MXS_ERROR("[%s] Error: NULL parameters passed: %p %p", __FUNCTION__,
|
|
router_cli_ses, querybuf);
|
|
return;
|
|
}
|
|
|
|
if (router_cli_ses->client_dcb == NULL)
|
|
{
|
|
MXS_ERROR("[%s] Error: Client DCB is NULL.", __FUNCTION__);
|
|
return;
|
|
}
|
|
|
|
router_cli_ses->have_tmp_tables = true;
|
|
rses_prop_tmp = router_cli_ses->rses_properties[RSES_PROP_TYPE_TMPTABLES];
|
|
data = (MYSQL_session *)router_cli_ses->client_dcb->data;
|
|
|
|
if (data == NULL)
|
|
{
|
|
MXS_ERROR("[%s] Error: User data in master server DBC is NULL.",
|
|
__FUNCTION__);
|
|
return;
|
|
}
|
|
|
|
dbname = (char *)data->db;
|
|
|
|
bool is_temp = true;
|
|
char *tblname = NULL;
|
|
|
|
tblname = qc_get_created_table_name(querybuf);
|
|
|
|
if (tblname && strlen(tblname) > 0)
|
|
{
|
|
klen = strlen(dbname) + strlen(tblname) + 2;
|
|
hkey = MXS_CALLOC(klen, sizeof(char));
|
|
MXS_ABORT_IF_NULL(hkey);
|
|
strcpy(hkey, dbname);
|
|
strcat(hkey, ".");
|
|
strcat(hkey, tblname);
|
|
}
|
|
else
|
|
{
|
|
hkey = NULL;
|
|
}
|
|
|
|
if (rses_prop_tmp == NULL)
|
|
{
|
|
if ((rses_prop_tmp = (rses_property_t *)MXS_CALLOC(1, sizeof(rses_property_t))))
|
|
{
|
|
#if defined(SS_DEBUG)
|
|
rses_prop_tmp->rses_prop_chk_top = CHK_NUM_ROUTER_PROPERTY;
|
|
rses_prop_tmp->rses_prop_chk_tail = CHK_NUM_ROUTER_PROPERTY;
|
|
#endif
|
|
rses_prop_tmp->rses_prop_rsession = router_cli_ses;
|
|
rses_prop_tmp->rses_prop_refcount = 1;
|
|
rses_prop_tmp->rses_prop_next = NULL;
|
|
rses_prop_tmp->rses_prop_type = RSES_PROP_TYPE_TMPTABLES;
|
|
router_cli_ses->rses_properties[RSES_PROP_TYPE_TMPTABLES] = rses_prop_tmp;
|
|
}
|
|
}
|
|
if (rses_prop_tmp)
|
|
{
|
|
if (rses_prop_tmp->rses_prop_data.temp_tables == NULL)
|
|
{
|
|
h = hashtable_alloc(7, rwsplit_hashkeyfun, rwsplit_hashcmpfun);
|
|
hashtable_memory_fns(h, rwsplit_hstrdup, NULL, rwsplit_hfree, NULL);
|
|
if (h != NULL)
|
|
{
|
|
rses_prop_tmp->rses_prop_data.temp_tables = h;
|
|
}
|
|
else
|
|
{
|
|
MXS_ERROR("Failed to allocate a new hashtable.");
|
|
}
|
|
}
|
|
|
|
if (hkey && rses_prop_tmp->rses_prop_data.temp_tables &&
|
|
hashtable_add(rses_prop_tmp->rses_prop_data.temp_tables, (void *)hkey,
|
|
(void *)is_temp) == 0) /*< Conflict in hash table */
|
|
{
|
|
MXS_INFO("Temporary table conflict in hashtable: %s", hkey);
|
|
}
|
|
#if defined(SS_DEBUG)
|
|
{
|
|
bool retkey = hashtable_fetch(rses_prop_tmp->rses_prop_data.temp_tables, hkey);
|
|
if (retkey)
|
|
{
|
|
MXS_INFO("Temporary table added: %s", hkey);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
MXS_FREE(hkey);
|
|
MXS_FREE(tblname);
|
|
}
|
|
|
|
/**
|
|
* @brief Detect multi-statement queries
|
|
*
|
|
* It is possible that the session state is modified inside a multi-statement
|
|
* query which would leave any slave sessions in an inconsistent state. Due to
|
|
* this, for the duration of this session, all queries will be sent to the
|
|
* master
|
|
* if the current query contains a multi-statement query.
|
|
* @param rses Router client session
|
|
* @param buf Buffer containing the full query
|
|
* @return True if the query contains multiple statements
|
|
*/
|
|
bool check_for_multi_stmt(GWBUF *buf, void *protocol, mysql_server_cmd_t packet_type)
|
|
{
|
|
MySQLProtocol *proto = (MySQLProtocol *)protocol;
|
|
bool rval = false;
|
|
|
|
if (proto->client_capabilities & GW_MYSQL_CAPABILITIES_MULTI_STATEMENTS &&
|
|
packet_type == MYSQL_COM_QUERY)
|
|
{
|
|
char *ptr, *data = (char*)GWBUF_DATA(buf) + 5;
|
|
/** Payload size without command byte */
|
|
int buflen = gw_mysql_get_byte3((uint8_t *)GWBUF_DATA(buf)) - 1;
|
|
|
|
if ((ptr = strnchr_esc_mysql(data, ';', buflen)))
|
|
{
|
|
/** Skip stored procedures etc. */
|
|
while (ptr && is_mysql_sp_end(ptr, buflen - (ptr - data)))
|
|
{
|
|
ptr = strnchr_esc_mysql(ptr + 1, ';', buflen - (ptr - data) - 1);
|
|
}
|
|
|
|
if (ptr)
|
|
{
|
|
if (ptr < data + buflen &&
|
|
!is_mysql_statement_end(ptr, buflen - (ptr - data)))
|
|
{
|
|
rval = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* @brief Determine the type of a query
|
|
*
|
|
* @param querybuf GWBUF containing the query
|
|
* @param packet_type Integer denoting DB specific enum
|
|
* @param non_empty_packet Boolean to be set by this function
|
|
*
|
|
* @return qc_query_type_t the query type; also the non_empty_packet bool is set
|
|
*/
|
|
qc_query_type_t
|
|
determine_query_type(GWBUF *querybuf, int packet_type, bool non_empty_packet)
|
|
{
|
|
qc_query_type_t qtype = QUERY_TYPE_UNKNOWN;
|
|
|
|
if (non_empty_packet)
|
|
{
|
|
mysql_server_cmd_t my_packet_type = (mysql_server_cmd_t)packet_type;
|
|
switch (my_packet_type)
|
|
{
|
|
case MYSQL_COM_QUIT: /*< 1 QUIT will close all sessions */
|
|
case MYSQL_COM_INIT_DB: /*< 2 DDL must go to the master */
|
|
case MYSQL_COM_REFRESH: /*< 7 - I guess this is session but not sure */
|
|
case MYSQL_COM_DEBUG: /*< 0d all servers dump debug info to stdout */
|
|
case MYSQL_COM_PING: /*< 0e all servers are pinged */
|
|
case MYSQL_COM_CHANGE_USER: /*< 11 all servers change it accordingly */
|
|
qtype = QUERY_TYPE_SESSION_WRITE;
|
|
break;
|
|
|
|
case MYSQL_COM_CREATE_DB: /**< 5 DDL must go to the master */
|
|
case MYSQL_COM_DROP_DB: /**< 6 DDL must go to the master */
|
|
case MYSQL_COM_STMT_CLOSE: /*< free prepared statement */
|
|
case MYSQL_COM_STMT_SEND_LONG_DATA: /*< send data to column */
|
|
case MYSQL_COM_STMT_RESET: /*< resets the data of a prepared statement */
|
|
qtype = QUERY_TYPE_WRITE;
|
|
break;
|
|
|
|
case MYSQL_COM_QUERY:
|
|
qtype = qc_get_type(querybuf);
|
|
break;
|
|
|
|
case MYSQL_COM_STMT_PREPARE:
|
|
qtype = qc_get_type(querybuf);
|
|
qtype |= QUERY_TYPE_PREPARE_STMT;
|
|
break;
|
|
|
|
case MYSQL_COM_STMT_EXECUTE:
|
|
/** Parsing is not needed for this type of packet */
|
|
qtype = QUERY_TYPE_EXEC_STMT;
|
|
break;
|
|
|
|
case MYSQL_COM_SHUTDOWN: /**< 8 where should shutdown be routed ? */
|
|
case MYSQL_COM_STATISTICS: /**< 9 ? */
|
|
case MYSQL_COM_PROCESS_INFO: /**< 0a ? */
|
|
case MYSQL_COM_CONNECT: /**< 0b ? */
|
|
case MYSQL_COM_PROCESS_KILL: /**< 0c ? */
|
|
case MYSQL_COM_TIME: /**< 0f should this be run in gateway ? */
|
|
case MYSQL_COM_DELAYED_INSERT: /**< 10 ? */
|
|
case MYSQL_COM_DAEMON: /**< 1d ? */
|
|
default:
|
|
break;
|
|
} /**< switch by packet type */
|
|
}
|
|
return qtype;
|
|
}
|