From 23d1dd42def55f5da72277bc34b58cdf345123c2 Mon Sep 17 00:00:00 2001 From: Johan Wikman Date: Sun, 25 Feb 2018 21:33:16 +0200 Subject: [PATCH] MXS-1475 Add sql_mode parser Given the value in a statement like "SET SQL_MODE=..." this parser is capable of deducing whether SQL_MODE is set to DEFAULT or ORACLE or something else. --- .../MySQL/mariadbclient/sqlmodeparser.hh | 264 ++++++++++++++++++ .../MySQL/mariadbclient/test/CMakeLists.txt | 3 + .../mariadbclient/test/test_sqlmodeparser.cc | 174 ++++++++++++ 3 files changed, 441 insertions(+) create mode 100644 server/modules/protocol/MySQL/mariadbclient/sqlmodeparser.hh create mode 100644 server/modules/protocol/MySQL/mariadbclient/test/test_sqlmodeparser.cc diff --git a/server/modules/protocol/MySQL/mariadbclient/sqlmodeparser.hh b/server/modules/protocol/MySQL/mariadbclient/sqlmodeparser.hh new file mode 100644 index 000000000..21a7074d7 --- /dev/null +++ b/server/modules/protocol/MySQL/mariadbclient/sqlmodeparser.hh @@ -0,0 +1,264 @@ +#pragma once +/* + * 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: 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 +#include +#include + + +class SqlModeParser : public maxscale::CustomParser +{ +public: + enum sql_mode_t + { + DEFAULT, // "set sql_mode=DEFAULT" + ORACLE, // "set sql_mode=ORACLE", "set sql_mode='PIPES_AS_CONCAT,ORACLE', autocommit=false", etc. + SOMETHING // "set sql_mode=PIPES_AS_CONCAT" + }; + + enum + { + UNUSED_FIRST = 0xFF, + TK_DEFAULT, + TK_ORACLE, + }; + + SqlModeParser() + { + } + + /** + * Given the trimmed value from the right of a "SET SQL_MODE=..." statement + * return whether SQL_MODE is set to ORACLE or DEFAULT. + * + * @param pBegin Beginning of string. + * @param pEnd One past end of string. + * + * @return DEFAULT if the value is DEFAULT, + * ORACLE if the string contains ORACLE, + * SOMETHING otherwise. + */ + sql_mode_t get_sql_mode(const char* pBegin, const char* pEnd) + { + sql_mode_t sql_mode = SOMETHING; + + m_pSql = pBegin; + m_pI = m_pSql; + m_pEnd = pEnd; + + return parse(); + } + + /** + * Returns a @c sql_mode_t as a string. + * + * @param sql_mode An SQL mode. + * + * @return The corresponding string. + */ + static const char* to_string(sql_mode_t sql_mode) + { + switch (sql_mode) + { + case DEFAULT: + return "DEFAULT"; + + case ORACLE: + return "ORACLE"; + + case SOMETHING: + return "SOMETHING"; + + default: + ss_dassert(!true); + return "UNKNOWN"; + } + } + +private: + bool consume_id() + { + // Consumes "[a-zA-Z]([a-zA-Z0-9_])* + + bool rv = false; + + if (is_alpha(*m_pI)) + { + rv = true; + + ++m_pI; + + while ((m_pI < m_pEnd) && (is_alpha(*m_pI) || is_number(*m_pI) || (*m_pI == '_'))) + { + ++m_pI; + } + } + + return rv; + } + + sql_mode_t parse() + { + sql_mode_t rv = SOMETHING; + + token_t token = next_token(); + + switch (token) + { + case '\'': + case '"': + case '`': + rv = parse_string(token); + break; + + case TK_DEFAULT: + rv = DEFAULT; + break; + + case TK_ORACLE: + rv = ORACLE; + break; + + case PARSER_UNKNOWN_TOKEN: + default: + ; + } + + return rv; + } + + sql_mode_t parse_string(char quote) + { + sql_mode_t rv = SOMETHING; + + bool parsed; + char c; + + do + { + parsed = parse_setting(&rv); + + if (parsed) + { + bypass_whitespace(); + + if (peek_current_char(&c) && (c == ',')) + { + ++m_pI; + } + } + } + while (parsed && (c == ',')); + + return rv; + } + + bool parse_setting(sql_mode_t* pSql_mode) + { + bool rv = true; + + token_t token = next_token(); + + switch (token) + { + case TK_ORACLE: + *pSql_mode = ORACLE; + break; + + case PARSER_UNKNOWN_TOKEN: + if (consume_id()) + { + *pSql_mode = SOMETHING; + } + else + { + rv = false; + } + break; + + case PARSER_EXHAUSTED: + log_exhausted(); + rv = false; + break; + + default: + log_unexpected(); + rv = false; + } + + return rv; + } + + token_t next_token(token_required_t required = TOKEN_NOT_REQUIRED) + { + token_t token = PARSER_UNKNOWN_TOKEN; + + bypass_whitespace(); + + if (m_pI == m_pEnd) + { + token = PARSER_EXHAUSTED; + } + else if (*m_pI == ';') + { + ++m_pI; + + while ((m_pI != m_pEnd) && isspace(*m_pI)) + { + ++m_pI; + } + + if (m_pI != m_pEnd) + { + MXS_WARNING("Non-space data found after semi-colon: '%.*s'.", + (int)(m_pEnd - m_pI), m_pI); + } + + token = PARSER_EXHAUSTED; + } + else + { + switch (*m_pI) + { + case '\'': + case '"': + case '`': + case ',': + token = *m_pI; + ++m_pI; + break; + + case 'd': + case 'D': + token = expect_token(MXS_CP_EXPECT_TOKEN("DEFAULT"), TK_DEFAULT); + break; + + case 'o': + case 'O': + token = expect_token(MXS_CP_EXPECT_TOKEN("ORACLE"), TK_ORACLE); + break; + + default: + ; + } + } + + if ((token == PARSER_EXHAUSTED) && (required == TOKEN_REQUIRED)) + { + log_exhausted(); + } + + return token; + } +}; diff --git a/server/modules/protocol/MySQL/mariadbclient/test/CMakeLists.txt b/server/modules/protocol/MySQL/mariadbclient/test/CMakeLists.txt index 07994287b..14cfdef41 100644 --- a/server/modules/protocol/MySQL/mariadbclient/test/CMakeLists.txt +++ b/server/modules/protocol/MySQL/mariadbclient/test/CMakeLists.txt @@ -1,8 +1,11 @@ add_executable(test_setparser test_setparser.cc) add_executable(test_setsqlmodeparser test_setsqlmodeparser.cc) +add_executable(test_sqlmodeparser test_sqlmodeparser.cc) target_link_libraries(test_setsqlmodeparser maxscale-common) target_link_libraries(test_setparser maxscale-common) +target_link_libraries(test_sqlmodeparser maxscale-common) add_test(test_setparser test_setarser) add_test(test_setsqlmodeparser test_setsqlmodeparser) +add_test(test_sqlmodeparser test_sqlmodeparser) diff --git a/server/modules/protocol/MySQL/mariadbclient/test/test_sqlmodeparser.cc b/server/modules/protocol/MySQL/mariadbclient/test/test_sqlmodeparser.cc new file mode 100644 index 000000000..4762d597d --- /dev/null +++ b/server/modules/protocol/MySQL/mariadbclient/test/test_sqlmodeparser.cc @@ -0,0 +1,174 @@ +/* + * 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: 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 "../sqlmodeparser.hh" +#include +#include +#include +#include +#include + +using namespace std; + +namespace +{ + +typedef SqlModeParser P; + +struct TEST_CASE +{ + const char* zValue; + SqlModeParser::sql_mode_t sql_mode; +} test_cases[] = +{ + { + "DEFAULT", + P::DEFAULT + }, + { + "ORACLE", + P::ORACLE + }, + { + "BLAH", + P::SOMETHING + }, + { + "'BLAH'", + P::SOMETHING + }, + { + "'ORACLE'", + P::ORACLE + }, + { + "'BLAH, A, B, ORACLE'", + P::ORACLE + }, + { + "'BLAH, A, B, XYZ_123'", + P::SOMETHING + }, + { + "'A,B, ORACLE'", + P::ORACLE + }, +}; + +const int N_TEST_CASES = sizeof(test_cases) / sizeof(test_cases[0]); + +int test(const char* zValue, SqlModeParser::sql_mode_t expected_sql_mode) +{ + int rv = EXIT_SUCCESS; + + SqlModeParser parser; + + SqlModeParser::sql_mode_t sql_mode = parser.get_sql_mode(zValue, zValue + strlen(zValue)); + + if (sql_mode == expected_sql_mode) + { + cout << "OK"; + } + else + { + cout << "ERROR: Expected " + << "'" << SqlModeParser::to_string(expected_sql_mode) << "'" + << ", got " + << "'" << SqlModeParser::to_string(sql_mode) << "'" + << "."; + rv = EXIT_FAILURE; + } + + cout << endl; + + return rv; +} + +int test(const TEST_CASE& test_case) +{ + int rv = EXIT_SUCCESS; + + cout << test_case.zValue << ": "; + + rv = test(test_case.zValue, test_case.sql_mode); + + return rv; +} + +int test_contiguous() +{ + int rv = EXIT_SUCCESS; + + cout << "Test contiguous statements\n" + << "--------------------------" << endl; + + for (int i = 0; i < N_TEST_CASES; ++i) + { + if (test(test_cases[i]) == EXIT_FAILURE) + { + rv = EXIT_FAILURE; + } + } + + cout << endl; + + return rv; +} + +int test() +{ + int rv = EXIT_SUCCESS; + + if (test_contiguous() != EXIT_SUCCESS) + { + rv = EXIT_FAILURE; + } + + if (rv == EXIT_SUCCESS) + { + cout << "OK" << endl; + } + else + { + cout << "ERROR" << endl; + } + + return rv; +} + +} + + +int main(int argc, char* argv[]) +{ + int rv = EXIT_SUCCESS; + + srand(time(NULL)); + + set_datadir(strdup("/tmp")); + set_langdir(strdup(".")); + set_process_datadir(strdup("/tmp")); + + if (mxs_log_init(NULL, ".", MXS_LOG_TARGET_DEFAULT)) + { + rv = test(); + + mxs_log_finish(); + } + else + { + cerr << "error: Could not initialize log." << endl; + } + + return rv; +}