2019-06-25 09:19:55 +03:00

1407 lines
33 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/bsl11.
*
* Change Date: 2023-01-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 "avrorouter.hh"
#include "rpl.hh"
#include <sstream>
#include <algorithm>
#include <maxbase/assert.h>
#include <maxscale/mysql_utils.hh>
#include <maxscale/protocol/mysql.hh>
void gtid_pos_t::extract(const REP_HEADER& hdr, uint8_t* ptr)
{
domain = extract_field(ptr + 8, 32);
server_id = hdr.serverid;
seq = extract_field(ptr, 64);
event_num = 0;
timestamp = hdr.timestamp;
}
bool gtid_pos_t::parse(const char* str)
{
bool rval = false;
char buf[strlen(str) + 1];
strcpy(buf, str);
char* saved, * dom = strtok_r(buf, ":-\n", &saved);
char* serv_id = strtok_r(NULL, ":-\n", &saved);
char* sequence = strtok_r(NULL, ":-\n", &saved);
char* subseq = strtok_r(NULL, ":-\n", &saved);
if (dom && serv_id && sequence)
{
domain = strtol(dom, NULL, 10);
server_id = strtol(serv_id, NULL, 10);
seq = strtol(sequence, NULL, 10);
event_num = subseq ? strtol(subseq, NULL, 10) : 0;
rval = true;
}
return rval;
}
gtid_pos_t gtid_pos_t::from_string(std::string str)
{
gtid_pos_t gtid;
gtid.parse(str.c_str());
return gtid;
}
std::string gtid_pos_t::to_string() const
{
std::stringstream ss;
ss << domain << "-" << server_id << "-" << seq;
return ss.str();
}
bool gtid_pos_t::empty() const
{
return timestamp == 0 && domain == 0 && server_id == 0 && seq == 0 && event_num == 0;
}
json_t* Column::to_json() const
{
json_t* obj = json_object();
json_object_set_new(obj, "name", json_string(name.c_str()));
json_object_set_new(obj, "type", json_string(type.c_str()));
json_object_set_new(obj, "length", json_integer(length));
return obj;
}
Column Column::from_json(json_t* json)
{
json_t* name = json_object_get(json, "name");
json_t* type = json_object_get(json, "type");
json_t* length = json_object_get(json, "length");
if (name && json_is_string(name)
&& type && json_is_string(type)
&& length && json_is_integer(length))
{
return Column(json_string_value(name),
json_string_value(type),
json_integer_value(length));
}
// Invalid JSON, return empty Column
return Column("");
}
json_t* TableCreateEvent::to_json() const
{
json_t* arr = json_array();
for (auto it = columns.begin(); it != columns.end(); it++)
{
json_array_append_new(arr, it->to_json());
}
json_t* obj = json_object();
json_object_set_new(obj, "table", json_string(table.c_str()));
json_object_set_new(obj, "database", json_string(database.c_str()));
json_object_set_new(obj, "version", json_integer(version));
json_object_set_new(obj, "columns", arr);
return obj;
}
STableCreateEvent TableCreateEvent::from_json(json_t* obj)
{
STableCreateEvent rval;
json_t* table = json_object_get(obj, "table");
json_t* database = json_object_get(obj, "database");
json_t* version = json_object_get(obj, "version");
json_t* columns = json_object_get(obj, "columns");
if (json_is_string(table) && json_is_string(database)
&& json_is_integer(version) && json_is_array(columns))
{
std::string tbl = json_string_value(table);
std::string db = json_string_value(database);
int ver = json_integer_value(version);
std::vector<Column> cols;
size_t i = 0;
json_t* val;
json_array_foreach(columns, i, val)
{
cols.emplace_back(Column::from_json(val));
}
auto is_empty = [](const Column& col) {
return col.name.empty();
};
if (std::none_of(cols.begin(), cols.end(), is_empty))
{
rval.reset(new TableCreateEvent(db, tbl, ver, std::move(cols)));
}
}
return rval;
}
/**
* Extract the table definition from a CREATE TABLE statement
* @param sql The SQL statement
* @param size Length of the statement
* @return Pointer to the start of the definition of NULL if the query is
* malformed.
*/
static const char* get_table_definition(const char* sql, int len, int* size)
{
const char* rval = NULL;
const char* ptr = sql;
const char* end = sql + len;
while (ptr < end && *ptr != '(')
{
ptr++;
}
/** We assume at least the parentheses are in the statement */
if (ptr < end - 2)
{
int depth = 0;
ptr++;
const char* start = ptr; // Skip first parenthesis
while (ptr < end)
{
switch (*ptr)
{
case '(':
depth++;
break;
case ')':
depth--;
break;
default:
break;
}
/** We found the last closing parenthesis */
if (depth < 0)
{
*size = ptr - start;
rval = start;
break;
}
ptr++;
}
}
return rval;
}
/**
* Extract the table name from a CREATE TABLE statement
* @param sql SQL statement
* @param dest Destination where the table name is extracted. Must be at least
* MYSQL_TABLE_MAXLEN bytes long.
* @return True if extraction was successful
*/
static bool get_table_name(const char* sql, char* dest)
{
bool rval = false;
const char* ptr = strchr(sql, '(');
if (ptr)
{
ptr--;
while (*ptr == '`' || isspace(*ptr))
{
ptr--;
}
const char* end = ptr + 1;
while (*ptr != '`' && *ptr != '.' && !isspace(*ptr))
{
ptr--;
}
ptr++;
memcpy(dest, ptr, end - ptr);
dest[end - ptr] = '\0';
rval = true;
}
return rval;
}
/**
* Extract the database name from a CREATE TABLE statement
*
* @param sql SQL statement
* @param dest Destination where the database name is extracted. Must be at least
* MYSQL_DATABASE_MAXLEN bytes long.
*
* @return True if a database name was extracted
*/
static bool get_database_name(const char* sql, char* dest)
{
bool rval = false;
const char* ptr = strchr(sql, '(');
if (ptr)
{
ptr--;
while (ptr >= sql && (*ptr == '`' || isspace(*ptr)))
{
ptr--;
}
while (ptr >= sql && *ptr != '`' && *ptr != '.' && !isspace(*ptr))
{
ptr--;
}
while (ptr >= sql && (*ptr == '`' || isspace(*ptr)))
{
ptr--;
}
if (ptr >= sql && *ptr == '.')
{
// The query defines an explicit database
while (ptr >= sql && (*ptr == '`' || *ptr == '.' || isspace(*ptr)))
{
ptr--;
}
const char* end = ptr + 1;
while (ptr >= sql && *ptr != '`' && *ptr != '.' && !isspace(*ptr))
{
ptr--;
}
ptr++;
memcpy(dest, ptr, end - ptr);
dest[end - ptr] = '\0';
rval = true;
}
}
return rval;
}
void make_valid_avro_identifier(char* ptr)
{
while (*ptr)
{
if (!isalnum(*ptr) && *ptr != '_')
{
*ptr = '_';
}
ptr++;
}
}
const char* next_field_definition(const char* ptr)
{
int depth = 0;
bool quoted = false;
char qchar;
while (*ptr)
{
if (!quoted)
{
if (*ptr == '(')
{
depth++;
}
else if (*ptr == ')')
{
depth--;
}
else if (*ptr == '"' || *ptr == '\'')
{
qchar = *ptr;
quoted = true;
}
else if (*ptr == ',' && depth == 0 && !quoted)
{
ptr++;
break;
}
}
else if (qchar == *ptr)
{
quoted = false;
}
ptr++;
}
return ptr;
}
static const char* extract_field_name(const char* ptr, char* dest, size_t size)
{
bool bt = false;
while (*ptr && (isspace(*ptr) || (bt = *ptr == '`')))
{
ptr++;
if (bt)
{
break;
}
}
if (!bt)
{
if (strncasecmp(ptr, "constraint", 10) == 0 || strncasecmp(ptr, "index", 5) == 0
|| strncasecmp(ptr, "key", 3) == 0 || strncasecmp(ptr, "fulltext", 8) == 0
|| strncasecmp(ptr, "spatial", 7) == 0 || strncasecmp(ptr, "foreign", 7) == 0
|| strncasecmp(ptr, "unique", 6) == 0 || strncasecmp(ptr, "primary", 7) == 0)
{
// Found a keyword
return NULL;
}
}
const char* start = ptr;
if (!bt)
{
while (*ptr && !isspace(*ptr))
{
ptr++;
}
}
else
{
while (*ptr && *ptr != '`')
{
ptr++;
}
}
if (ptr > start)
{
/** Valid identifier */
size_t bytes = ptr - start;
memcpy(dest, start, bytes);
dest[bytes] = '\0';
make_valid_avro_identifier(dest);
mxb_assert(strlen(dest) > 0);
}
else
{
ptr = NULL;
}
return ptr;
}
int extract_type_length(const char* ptr, char* dest)
{
/** Skip any leading whitespace */
while (*ptr && (isspace(*ptr) || *ptr == '`'))
{
ptr++;
}
/** The field type definition starts here */
const char* start = ptr;
/** Skip characters until we either hit a whitespace character or the start
* of the length definition. */
while (*ptr && isalpha(*ptr))
{
ptr++;
}
/** Store type */
for (const char* c = start; c < ptr; c++)
{
*dest++ = tolower(*c);
}
*dest++ = '\0';
/** Skip whitespace */
while (*ptr && isspace(*ptr))
{
ptr++;
}
int rval = -1; // No length defined
/** Start of length definition */
if (*ptr == '(')
{
ptr++;
char* end;
int val = strtol(ptr, &end, 10);
if (*end == ')')
{
rval = val;
}
}
return rval;
}
int count_columns(const char* ptr)
{
int i = 2;
while ((ptr = strchr(ptr, ',')))
{
ptr++;
i++;
}
return i;
}
/**
* Process a table definition into an array of column names
* @param nameptr table definition
* @return Number of processed columns or -1 on error
*/
static void process_column_definition(const char* nameptr, std::vector<Column>& columns)
{
char colname[512];
while ((nameptr = extract_field_name(nameptr, colname, sizeof(colname))))
{
char type[100] = "";
int len = extract_type_length(nameptr, type);
nameptr = next_field_definition(nameptr);
fix_reserved_word(colname);
columns.emplace_back(colname, type, len);
}
}
int resolve_table_version(const char* db, const char* table)
{
int version = 0;
char buf[PATH_MAX + 1];
do
{
version++;
snprintf(buf, sizeof(buf), "%s.%s.%06d.avsc", db, table, version);
}
while (access(buf, F_OK) == 0);
return version;
}
/**
* @brief Handle a query event which contains a CREATE TABLE statement
*
* @param ident Table identifier in database.table format
* @param sql The CREATE TABLE statement
* @param len Length of @c sql
*
* @return New CREATE_TABLE object or NULL if an error occurred
*/
STableCreateEvent table_create_alloc(char* ident, const char* sql, int len)
{
/** Extract the table definition so we can get the column names from it */
int stmt_len = 0;
const char* statement_sql = get_table_definition(sql, len, &stmt_len);
mxb_assert(statement_sql);
char* tbl_start = strchr(ident, '.');
mxb_assert(tbl_start);
*tbl_start++ = '\0';
char table[MYSQL_TABLE_MAXLEN + 1];
char database[MYSQL_DATABASE_MAXLEN + 1];
strcpy(database, ident);
strcpy(table, tbl_start);
std::vector<Column> columns;
process_column_definition(statement_sql, columns);
STableCreateEvent rval;
if (!columns.empty())
{
int version = resolve_table_version(database, table);
rval.reset(new(std::nothrow) TableCreateEvent(database, table, version, std::move(columns)));
}
else
{
MXS_ERROR("No columns in a CREATE TABLE statement: %.*s", stmt_len, statement_sql);
}
return rval;
}
/**
* @brief Extract a table map from a table map event
*
* This assumes that the complete event minus the replication header is stored
* at @p ptr
* @param ptr Pointer to the start of the event payload
* @param post_header_len Length of the event specific header, 8 or 6 bytes
* @return New TABLE_MAP or NULL if memory allocation failed
*/
TableMapEvent* table_map_alloc(uint8_t* ptr, uint8_t hdr_len, TableCreateEvent* create)
{
uint64_t table_id = 0;
size_t id_size = hdr_len == 6 ? 4 : 6;
memcpy(&table_id, ptr, id_size);
ptr += id_size;
uint16_t flags = 0;
memcpy(&flags, ptr, 2);
ptr += 2;
uint8_t schema_name_len = *ptr++;
char schema_name[schema_name_len + 2];
/** Copy the NULL byte after the schema name */
memcpy(schema_name, ptr, schema_name_len + 1);
ptr += schema_name_len + 1;
uint8_t table_name_len = *ptr++;
char table_name[table_name_len + 2];
/** Copy the NULL byte after the table name */
memcpy(table_name, ptr, table_name_len + 1);
ptr += table_name_len + 1;
uint64_t column_count = mxq::leint_value(ptr);
ptr += mxq::leint_bytes(ptr);
/** Column types */
uint8_t* column_types = ptr;
ptr += column_count;
size_t metadata_size = 0;
uint8_t* metadata = (uint8_t*) mxq::lestr_consume(&ptr, &metadata_size);
uint8_t* nullmap = ptr;
size_t nullmap_size = (column_count + 7) / 8;
Bytes cols(column_types, column_types + column_count);
Bytes nulls(nullmap, nullmap + nullmap_size);
Bytes meta(metadata, metadata + metadata_size);
return new(std::nothrow) TableMapEvent(schema_name,
table_name,
table_id,
create->version,
std::move(cols),
std::move(nulls),
std::move(meta));
}
Rpl::Rpl(SERVICE* service,
SRowEventHandler handler,
pcre2_code* match,
pcre2_code* exclude,
gtid_pos_t gtid)
: m_handler(handler)
, m_service(service)
, m_binlog_checksum(0)
, m_event_types(0)
, m_gtid(gtid)
, m_match(match)
, m_exclude(exclude)
, m_md_match(m_match ? pcre2_match_data_create_from_pattern(m_match, NULL) : nullptr)
, m_md_exclude(m_exclude ? pcre2_match_data_create_from_pattern(m_exclude, NULL) : nullptr)
{
/** For detection of CREATE/ALTER TABLE statements */
static const char* create_table_regex = "(?i)^[[:space:]]*create[a-z0-9[:space:]_]+table";
static const char* alter_table_regex = "(?i)^[[:space:]]*alter[[:space:]]+table";
int pcreerr;
size_t erroff;
m_create_table_re = pcre2_compile((PCRE2_SPTR) create_table_regex,
PCRE2_ZERO_TERMINATED,
0,
&pcreerr,
&erroff,
NULL);
m_alter_table_re = pcre2_compile((PCRE2_SPTR) alter_table_regex,
PCRE2_ZERO_TERMINATED,
0,
&pcreerr,
&erroff,
NULL);
mxb_assert_message(m_create_table_re && m_alter_table_re,
"CREATE TABLE and ALTER TABLE regex compilation should not fail");
}
void Rpl::flush()
{
m_handler->flush_tables();
}
void Rpl::add_create(STableCreateEvent create)
{
auto it = m_created_tables.find(create->id());
if (it == m_created_tables.end() || create->version > it->second->version)
{
m_created_tables[create->id()] = create;
}
}
/**
* Read one token (i.e. SQL keyword)
*/
static const char* get_token(const char* ptr, const char* end, char* dest)
{
while (ptr < end && isspace(*ptr))
{
ptr++;
}
const char* start = ptr;
while (ptr < end && !isspace(*ptr))
{
ptr++;
}
size_t len = ptr - start;
memcpy(dest, start, len);
dest[len] = '\0';
return ptr;
}
/**
* Consume one token
*/
static bool chomp_one_token(const char* expected, const char** ptr, const char* end, char* buf)
{
bool rval = false;
const char* next = get_token(*ptr, end, buf);
if (strcasecmp(buf, expected) == 0)
{
rval = true;
*ptr = next;
}
return rval;
}
/**
* Consume all tokens in a group
*/
static bool chomp_tokens(const char** tokens, const char** ptr, const char* end, char* buf)
{
bool next = true;
bool rval = false;
do
{
next = false;
for (int i = 0; tokens[i]; i++)
{
if (chomp_one_token(tokens[i], ptr, end, buf))
{
rval = true;
next = true;
break;
}
}
}
while (next);
return rval;
}
/**
* Remove any extra characters from a string
*/
static void remove_extras(char* str)
{
char* end = strchr(str, '\0') - 1;
while (end > str && (*end == '`' || *end == ')' || *end == '('))
{
*end-- = '\0';
}
char* start = str;
while (start < end && (*start == '`' || *start == ')' || *start == '('))
{
start++;
}
size_t len = strlen(start);
memmove(str, start, len);
str[len] = '\0';
mxb_assert(strlen(str) == len);
}
static void remove_backticks(char* src)
{
char* dest = src;
while (*src)
{
if (*src != '`')
{
// Non-backtick character, keep it
*dest = *src;
dest++;
}
src++;
}
mxb_assert(dest == src || (*dest != '\0' && dest < src));
*dest = '\0';
}
static const char* TOK_CREATE[] =
{
"CREATE",
NULL
};
static const char* TOK_TABLE[] =
{
"TABLE",
NULL
};
static const char* TOK_GROUP_REPLACE[] =
{
"OR",
"REPLACE",
NULL
};
static const char* TOK_GROUP_EXISTS[] =
{
"IF",
"NOT",
"EXISTS",
NULL
};
/**
* Extract both tables from a `CREATE TABLE t1 LIKE t2` statement
*/
static bool extract_create_like_identifier(const char* sql, size_t len, char* target, char* source)
{
bool rval = false;
char buffer[len + 1];
buffer[0] = '\0';
const char* ptr = sql;
const char* end = ptr + sizeof(buffer);
if (chomp_tokens(TOK_CREATE, &ptr, end, buffer))
{
chomp_tokens(TOK_GROUP_REPLACE, &ptr, end, buffer);
if (chomp_tokens(TOK_TABLE, &ptr, end, buffer))
{
chomp_tokens(TOK_GROUP_EXISTS, &ptr, end, buffer);
// Read the target table name
ptr = get_token(ptr, end, buffer);
strcpy(target, buffer);
// Skip the LIKE token
ptr = get_token(ptr, end, buffer);
// Read the source table name
ptr = get_token(ptr, end, buffer);
remove_extras(buffer);
strcpy(source, buffer);
rval = true;
}
}
return rval;
}
/**
* Create a table from another table
*/
STableCreateEvent Rpl::table_create_copy(const char* sql, size_t len, const char* db)
{
STableCreateEvent rval;
char target[MYSQL_TABLE_MAXLEN + 1] = "";
char source[MYSQL_TABLE_MAXLEN + 1] = "";
if (extract_create_like_identifier(sql, len, target, source))
{
char table_ident[MYSQL_TABLE_MAXLEN + MYSQL_DATABASE_MAXLEN + 2] = "";
if (strchr(source, '.') == NULL)
{
strcpy(table_ident, db);
strcat(table_ident, ".");
}
strcat(table_ident, source);
auto it = m_created_tables.find(table_ident);
if (it != m_created_tables.end())
{
rval.reset(new(std::nothrow) TableCreateEvent(*it->second));
char* table = strchr(target, '.');
table = table ? table + 1 : target;
rval->table = table;
rval->version = 1;
rval->was_used = false;
}
else
{
MXS_ERROR("Could not find table '%s' that '%s' is being created from: %.*s",
table_ident,
target,
(int)len,
sql);
}
}
return rval;
}
static const char* get_next_def(const char* sql, const char* end)
{
int depth = 0;
while (sql < end)
{
if (*sql == ',' && depth == 0)
{
return sql + 1;
}
else if (*sql == '(')
{
depth++;
}
else if (*sql == ')')
{
depth--;
}
sql++;
}
return NULL;
}
static const char* get_tok(const char* sql, int* toklen, const char* end)
{
const char* start = sql;
while (isspace(*start))
{
start++;
}
int len = 0;
int depth = 0;
while (start + len < end)
{
if (isspace(start[len]) && depth == 0)
{
*toklen = len;
return start;
}
else if (start[len] == '(')
{
depth++;
}
else if (start[len] == ')')
{
depth--;
}
len++;
}
if (len > 0 && start + len <= end)
{
*toklen = len;
return start;
}
return NULL;
}
static void rskip_whitespace(const char* sql, const char** end)
{
const char* ptr = *end;
while (ptr > sql && isspace(*ptr))
{
ptr--;
}
*end = ptr;
}
static void rskip_token(const char* sql, const char** end)
{
const char* ptr = *end;
while (ptr > sql && !isspace(*ptr))
{
ptr--;
}
*end = ptr;
}
static bool get_placement_specifier(const char* sql, const char* end, const char** tgt, int* tgt_len)
{
bool rval = false;
mxb_assert(end > sql);
end--;
*tgt = NULL;
*tgt_len = 0;
// Skip any trailing whitespace
rskip_whitespace(sql, &end);
if (*end == '`')
{
// Identifier, possibly AFTER `column`
const char* id_end = end;
end--;
while (end > sql && *end != '`')
{
end--;
}
const char* id_start = end + 1;
mxb_assert(*end == '`' && *id_end == '`');
end--;
rskip_whitespace(sql, &end);
rskip_token(sql, &end);
// end points to the character _before_ the token
end++;
if (strncasecmp(end, "AFTER", 5) == 0)
{
// This column comes after the specified column
rval = true;
*tgt = id_start;
*tgt_len = id_end - id_start;
}
}
else
{
// Something else, possibly FIRST or un-backtick'd AFTER
const char* id_end = end + 1; // Points to either a trailing space or one-after-the-end
rskip_token(sql, &end);
// end points to the character _before_ the token
end++;
if (strncasecmp(end, "FIRST", 5) == 0)
{
// Put this column first
rval = true;
}
else
{
const char* id_start = end + 1;
// Skip the whitespace and until the start of the current token
rskip_whitespace(sql, &end);
rskip_token(sql, &end);
// end points to the character _before_ the token
end++;
if (strncasecmp(end, "AFTER", 5) == 0)
{
// This column comes after the specified column
rval = true;
*tgt = id_start;
*tgt_len = id_end - id_start;
}
}
}
return rval;
}
static bool tok_eq(const char* a, const char* b, size_t len)
{
size_t i = 0;
while (i < len)
{
if (tolower(a[i]) - tolower(b[i]) != 0)
{
return false;
}
i++;
}
return true;
}
static void skip_whitespace(const char** saved)
{
const char* ptr = *saved;
while (*ptr && isspace(*ptr))
{
ptr++;
}
*saved = ptr;
}
static void skip_token(const char** saved)
{
const char* ptr = *saved;
while (*ptr && !isspace(*ptr) && *ptr != '(' && *ptr != '.')
{
ptr++;
}
*saved = ptr;
}
static void skip_non_backtick(const char** saved)
{
const char* ptr = *saved;
while (*ptr && *ptr != '`')
{
ptr++;
}
*saved = ptr;
}
const char* keywords[] =
{
"CREATE",
"DROP",
"ALTER",
"IF",
"EXISTS",
"REPLACE",
"OR",
"TABLE",
"NOT",
NULL
};
static bool token_is_keyword(const char* tok, size_t len)
{
for (int i = 0; keywords[i]; i++)
{
if (strncasecmp(keywords[i], tok, len) == 0 && strlen(keywords[i]) == len)
{
return true;
}
}
return false;
}
void read_table_identifier(const char* db, const char* sql, const char* end, char* dest, int size)
{
const char* start;
int len = 0;
bool is_keyword = true;
while (is_keyword)
{
skip_whitespace(&sql); // Leading whitespace
if (*sql == '`')
{
// Quoted identifier, not a keyword
is_keyword = false;
sql++;
start = sql;
skip_non_backtick(&sql);
len = sql - start;
sql++;
}
else
{
start = sql;
skip_token(&sql);
len = sql - start;
is_keyword = token_is_keyword(start, len);
}
}
skip_whitespace(&sql); // Space after first identifier
if (*sql != '.')
{
// No explicit database
snprintf(dest, size, "%s.%.*s", db, len, start);
}
else
{
// Explicit database, skip the period
sql++;
skip_whitespace(&sql); // Space after first identifier
const char* id_start;
int id_len = 0;
if (*sql == '`')
{
sql++;
id_start = sql;
skip_non_backtick(&sql);
id_len = sql - id_start;
sql++;
}
else
{
id_start = sql;
skip_token(&sql);
id_len = sql - id_start;
}
snprintf(dest, size, "%.*s.%.*s", len, start, id_len, id_start);
}
}
void make_avro_token(char* dest, const char* src, int length)
{
while (length > 0 && (*src == '(' || *src == ')' || *src == '`' || isspace(*src)))
{
src++;
length--;
}
const char* end = src;
for (int i = 0; i < length; i++)
{
if (end[i] == '(' || end[i] == ')' || end[i] == '`' || isspace(end[i]))
{
length = i;
break;
}
}
memcpy(dest, src, length);
dest[length] = '\0';
fix_reserved_word(dest);
}
static bool not_column_operation(const char* tok, int len)
{
const char* keywords[] =
{
"PRIMARY",
"UNIQUE",
"FULLTEXT",
"SPATIAL",
"PERIOD",
"PRIMARY",
"KEY",
"KEYS",
"INDEX",
"FOREIGN",
"CONSTRAINT",
NULL
};
for (int i = 0; keywords[i]; i++)
{
if (tok_eq(tok, keywords[i], strlen(keywords[i])))
{
return true;
}
}
return false;
}
bool Rpl::table_create_alter(STableCreateEvent create, const char* sql, const char* end)
{
const char* tbl = strcasestr(sql, "table"), * def;
if ((def = strchr(tbl, ' ')))
{
int len = 0;
const char* tok = get_tok(def, &len, end);
if (tok)
{
MXS_INFO("Alter table '%.*s'; %.*s\n", len, tok, (int)(end - sql), sql);
def = tok + len;
}
int updates = 0;
while (tok && (tok = get_tok(tok + len, &len, end)))
{
const char* ptok = tok;
int plen = len;
tok = get_tok(tok + len, &len, end);
if (tok)
{
if (not_column_operation(tok, len))
{
MXS_INFO("Statement doesn't affect columns, not processing: %s", sql);
return true;
}
else if (tok_eq(tok, "column", len))
{
// Skip the optional COLUMN keyword
tok = get_tok(tok + len, &len, end);
}
char avro_token[len + 1];
make_avro_token(avro_token, tok, len);
if (tok_eq(ptok, "add", plen))
{
bool is_new = true;
for (auto it = create->columns.begin(); it != create->columns.end(); it++)
{
if (it->name == avro_token)
{
is_new = false;
break;
}
}
if (is_new)
{
char field_type[200] = ""; // Enough to hold all types
int field_length = extract_type_length(tok + len, field_type);
create->columns.emplace_back(std::string(avro_token),
std::string(field_type),
field_length);
updates++;
}
tok = get_next_def(tok, end);
len = 0;
}
else if (tok_eq(ptok, "drop", plen))
{
for (auto it = create->columns.begin(); it != create->columns.end(); it++)
{
if (it->name == avro_token)
{
create->columns.erase(it);
break;
}
}
updates++;
tok = get_next_def(tok, end);
len = 0;
}
else if (tok_eq(ptok, "change", plen))
{
for (auto it = create->columns.begin(); it != create->columns.end(); it++)
{
if (it->name == avro_token)
{
if ((tok = get_tok(tok + len, &len, end)))
{
char avro_token[len + 1];
make_avro_token(avro_token, tok, len);
char field_type[200] = ""; // Enough to hold all types
int field_length = extract_type_length(tok + len, field_type);
it->name = avro_token;
it->type = field_type;
it->length = field_length;
updates++;
}
}
}
tok = get_next_def(tok, end);
len = 0;
}
}
else
{
break;
}
}
/** Only increment the create version if it has an associated .avro
* file. The .avro file is only created if it is actually used. */
if (updates > 0 && create->was_used)
{
create->version++;
create->was_used = false;
/**
* Although the table was only altered, we can treat it like it
* would have been just created. This allows for a minimal API that
* only creates and replaces tables.
*
* TODO: Add DROP TABLE entry point for pruning old tables
*/
m_handler->create_table(create);
}
}
return true;
}
bool Rpl::table_matches(const std::string& ident)
{
bool rval = false;
if (!m_match || pcre2_match(m_match,
(PCRE2_SPTR)ident.c_str(),
PCRE2_ZERO_TERMINATED,
0,
0,
m_md_match,
NULL) > 0)
{
if (!m_exclude || pcre2_match(m_exclude,
(PCRE2_SPTR)ident.c_str(),
PCRE2_ZERO_TERMINATED,
0,
0,
m_md_exclude,
NULL) == PCRE2_ERROR_NOMATCH)
{
rval = true;
}
}
return rval;
}