diff --git a/CMakeLists.txt b/CMakeLists.txt index bf0c1ecac..84b36d030 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,8 +78,10 @@ if(NOT OPENSSL_FOUND) else() if(OPENSSL_VERSION VERSION_LESS 1 AND NOT FORCE_OPENSSL100) add_definitions("-DOPENSSL_0_9") - else() + elseif(OPENSSL_VERSION VERSION_LESS 1.1) add_definitions("-DOPENSSL_1_0") + else() + add_definitions("-DOPENSSL_1_1") endif() endif() diff --git a/Documentation/About/Limitations.md b/Documentation/About/Limitations.md index ff16f4963..c7ad8f75a 100644 --- a/Documentation/About/Limitations.md +++ b/Documentation/About/Limitations.md @@ -124,7 +124,6 @@ Sending of binary data with `LOAD DATA LOCAL INFILE` is not supported. Read queries are routed to the master server in the following situations: * query is executed inside an open transaction -* query is a prepared statement * statement includes a stored procedure or an UDF call * if there are multiple statements inside one query e.g. `INSERT INTO ... ; SELECT LAST_INSERT_ID();` @@ -140,19 +139,14 @@ requests without waiting for the protocol to be idle. If you are using the MariaDB Connector/J, add `useBatchMultiSend=false` to the JDBC connection string to disable batched statement execution. -#### Backend write timeout handling +#### Prepared Statement Limitations -The backend connections opened by readwritesplit will not be kept alive if they -aren't used. To keep all of the connections alive, a session command must be -periodically executed (for example `SET @a = 1;`). +Readwritesplit does not support the parallel execution of binary protocol +prepared statements that use cursors. In practice this means that only one +open cursor is allowed when readwritesplit is used. -If the backend server is configured with a low -[wait_timeout](https://mariadb.com/kb/en/mariadb/server-system-variables/#wait_timeout), -it is possible that connections get closed during long sessions. It is -recommended to set the `wait_timeout` to a high value and let MariaDB MaxScale -handle the client timeouts. This can be achieved by using the -[_connection_timeout_](../Getting-Started/Configuration-Guide.md#connection_timeout) -parameter for the service. +Opening more than one cursor will cause the execution of the prepared +statements to stall. #### Limitations in multi-statement handling diff --git a/Documentation/Changelog.md b/Documentation/Changelog.md index 20b4d173e..34a68e631 100644 --- a/Documentation/Changelog.md +++ b/Documentation/Changelog.md @@ -21,6 +21,7 @@ * MaxScale now supports IPv6 For more details, please refer to: +* [MariaDB MaxScale 2.1.4 Release Notes](Release-Notes/MaxScale-2.1.4-Release-Notes.md) * [MariaDB MaxScale 2.1.3 Release Notes](Release-Notes/MaxScale-2.1.3-Release-Notes.md) * [MariaDB MaxScale 2.1.2 Release Notes](Release-Notes/MaxScale-2.1.2-Release-Notes.md) * [MariaDB MaxScale 2.1.1 Release Notes](Release-Notes/MaxScale-2.1.1-Release-Notes.md) diff --git a/Documentation/Getting-Started/Configuration-Guide.md b/Documentation/Getting-Started/Configuration-Guide.md index 8e58b3e3b..b28165bb3 100644 --- a/Documentation/Getting-Started/Configuration-Guide.md +++ b/Documentation/Getting-Started/Configuration-Guide.md @@ -584,6 +584,43 @@ documentation for more details. Enable or disable the admin interface. This allows the admin interface to be completely disabled to prevent access to it. +#### `sql_mode` + +Specifies whether the query classifier parser should initially expect _MariaDB_ +or _PL/SQL_ kind of SQL. + +The allowed values are: + `default`: The parser expects regular _MariaDB_ SQL. + `oracle` : The parser expects PL/SQL. + +``` +sql_mode=oracle +``` + +The default value is `default`. + +**NOTE** If `sql_mode` is set to `oracle`, then MaxScale will also assume +that `autocommit` initially is off. + +At runtime, MariaDB MaxScale will recognize statements like +``` +set sql_mode=oracle; +``` +and +``` +set sql_mode=default; +``` +and change mode accordingly. + +**NOTE** If `set sql_mode=oracle;` is encountered, then MaxScale will also +behave as if `autocommit` had been turned off and conversely, if +`set sql_mode=default;` is encountered, then MaxScale will also behave +as if `autocommit` had been turned on. + +Note that MariaDB MaxScale is **not** explicitly aware of the sql mode of +the server, so the value of `sql_mode` should reflect the sql mode used +when the server is started. + ### Service A service represents the database service that MariaDB MaxScale offers to the diff --git a/Documentation/Monitors/MySQL-Monitor.md b/Documentation/Monitors/MySQL-Monitor.md index 95747c76a..35500446e 100644 --- a/Documentation/Monitors/MySQL-Monitor.md +++ b/Documentation/Monitors/MySQL-Monitor.md @@ -201,6 +201,19 @@ external agent that automatically reintegrates failed servers into the cluster. One of these agents is the _replication-manager_ which automatically configures the failed servers as new slaves of the current master. +### `allow_external_slaves` + +Allow the use of external slaves. This option is enabled by default. + +If a slave server is replicating from a master that is not being monitored by +the MySQL monitor, the slaves will be assigned the _Slave of External Server_ +status (a status mainly for informational purposes). + +When the `allow_external_slaves` option is enabled, the server will also be +assigned the _Slave_ status which allows them to be used like normal slave +servers. When the option is disabled, the servers will only receive the _Slave +of External Server_ status and they will not be used. + ### `journal_max_age` The maximum journal file age in seconds. The default value is 28800 seconds. diff --git a/Documentation/Release-Notes/MaxScale-2.1.4-Release-Notes.md b/Documentation/Release-Notes/MaxScale-2.1.4-Release-Notes.md new file mode 100644 index 000000000..1fa22e4aa --- /dev/null +++ b/Documentation/Release-Notes/MaxScale-2.1.4-Release-Notes.md @@ -0,0 +1,65 @@ +# MariaDB MaxScale 2.1.4 Release Notes + +Release 2.1.4 is a GA release. + +This document describes the changes in release 2.1.4, when compared to +release [2.1.3](MaxScale-2.1.3-Release-Notes.md). + +If you are upgrading from release 2.0, please also read the following +release notes: +[2.1.3](./MaxScale-2.1.3-Release-Notes.md) +[2.1.2](./MaxScale-2.1.2-Release-Notes.md) +[2.1.1](./MaxScale-2.1.1-Release-Notes.md) +[2.1.0](./MaxScale-2.1.0-Release-Notes.md) + +For any problems you encounter, please consider submitting a bug +report at [Jira](https://jira.mariadb.org). + +## Changed Features + +### Masking + +* The masking filter now has a default fill character `X`, which + is used if only a _value_ has been specified and the length of + the value does not match the length of the value received from + the server. + + Please refer to the + [masking documentation](../Filters/Masking.md) + for details. + +### maxadmin + +* Error message for failed login attempt has been improved. + +## Bug fixes + +[Here is a list of bugs fixed since the release of MaxScale 2.1.3.](https://jira.mariadb.org/issues/?jql=project%20%3D%20MXS%20AND%20issuetype%20%3D%20Bug%20AND%20status%20%3D%20Closed%20AND%20fixVersion%20%3D%202.1.4) + +* [MXS-1304](https://jira.mariadb.org/browse/MXS-1304) Invalid write in gw_str_xor +* [MXS-1299](https://jira.mariadb.org/browse/MXS-1299) CREATE TABLE LIKE fails with avrorouter +* [MXS-1296](https://jira.mariadb.org/browse/MXS-1296) Lowercase start transaction is not detected +* [MXS-1294](https://jira.mariadb.org/browse/MXS-1294) cdc_schema.py uses Python 3 +* [MXS-1287](https://jira.mariadb.org/browse/MXS-1287) Slaves of external servers can't be used as slaves +* [MXS-1279](https://jira.mariadb.org/browse/MXS-1279) Runtime changes to monitor credentials expect wrong parameter names +* [MXS-1271](https://jira.mariadb.org/browse/MXS-1271) cdc.py consuming 100% of CPU and never sending to kafka + +## Known Issues and Limitations + +There are some limitations and known issues within this version of MaxScale. +For more information, please refer to the [Limitations](../About/Limitations.md) document. + +## Packaging + +RPM and Debian packages are provided for the Linux distributions supported +by MariaDB Enterprise. + +Packages can be downloaded [here](https://mariadb.com/resources/downloads). + +## Source Code + +The source code of MaxScale is tagged at GitHub with a tag, which is identical +with the version of MaxScale. For instance, the tag of version X.Y.Z of MaxScale +is X.Y.Z. + +The source code is available [here](https://github.com/mariadb-corporation/MaxScale). diff --git a/Documentation/Upgrading/Upgrading-To-MaxScale-2.1.md b/Documentation/Upgrading/Upgrading-To-MaxScale-2.1.md index 539adb261..aee13f8dc 100644 --- a/Documentation/Upgrading/Upgrading-To-MaxScale-2.1.md +++ b/Documentation/Upgrading/Upgrading-To-MaxScale-2.1.md @@ -6,7 +6,8 @@ MariaDB MaxScale from version 2.0 to 2.1. For more information about MariaDB MaxScale 2.1, please refer to the [ChangeLog](../Changelog.md). -For a complete list of changes in MaxScale 2.1.0, refer to the +For a complete list of changes in MaxScale 2.1, refer to the +[MaxScale 2.1.4 Release Notes](../Release-Notes/MaxScale-2.1.4-Release-Notes.md). [MaxScale 2.1.3 Release Notes](../Release-Notes/MaxScale-2.1.3-Release-Notes.md). [MaxScale 2.1.2 Release Notes](../Release-Notes/MaxScale-2.1.2-Release-Notes.md). [MaxScale 2.1.1 Release Notes](../Release-Notes/MaxScale-2.1.1-Release-Notes.md). diff --git a/avro/maxavro_record.c b/avro/maxavro_record.c index 0cccafc9e..b736b443a 100644 --- a/avro/maxavro_record.c +++ b/avro/maxavro_record.c @@ -182,7 +182,7 @@ json_t* maxavro_record_read_json(MAXAVRO_FILE *file) { long pos = ftell(file->file); MXS_ERROR("Failed to read field value '%s', type '%s' at " - "file offset %ld, record numer %lu.", + "file offset %ld, record number %lu.", file->schema->fields[i].name, type_to_string(file->schema->fields[i].type), pos, file->records_read); diff --git a/cmake/FindMySQL.cmake b/cmake/FindMySQL.cmake index ee2972915..c084c967e 100644 --- a/cmake/FindMySQL.cmake +++ b/cmake/FindMySQL.cmake @@ -6,7 +6,7 @@ # MYSQL_EMBEDDED_LIBRARIES - The MySQL embedded library # MYSQL_EMBEDDED_INCLUDE_DIR - Path to MySQL headers -find_file(MYSQL_EMBEDDED_VERSION_H mysql_version.h PATHS ${MYSQL_EMBEDDED_INCLUDE_DIR} PATH_SUFFIXES mysql) +find_file(MYSQL_EMBEDDED_VERSION_H mysql_version.h HINTS ${MYSQL_EMBEDDED_INCLUDE_DIR} PATH_SUFFIXES mysql) if(MYSQL_EMBEDDED_VERSION_H MATCHES "MYSQL_EMBEDDED_VERSION_H-NOTFOUND") message(FATAL_ERROR "Cannot find the mysql_version.h header") else() diff --git a/include/maxscale/backend.hh b/include/maxscale/backend.hh index a7c84911c..b3df34430 100644 --- a/include/maxscale/backend.hh +++ b/include/maxscale/backend.hh @@ -159,7 +159,7 @@ public: * * @return True if data was written successfully */ - bool write(GWBUF* buffer, response_type type = EXPECT_RESPONSE); + virtual bool write(GWBUF* buffer, response_type type = EXPECT_RESPONSE); /** * @brief Write an authentication switch request to the backend server diff --git a/include/maxscale/buffer.h b/include/maxscale/buffer.h index be21992a5..067c867ce 100644 --- a/include/maxscale/buffer.h +++ b/include/maxscale/buffer.h @@ -212,6 +212,21 @@ extern void gwbuf_free(GWBUF *buf); */ extern GWBUF *gwbuf_clone(GWBUF *buf); +/** + * @brief Deep clone a GWBUF + * + * Clone the data inside a GWBUF into a new buffer. The created buffer has its + * own internal buffer and any modifications to the deep cloned buffer will not + * reflect on the original one. Any buffer objects attached to the original buffer + * will not be copied. Only the buffer type of the original buffer will be copied + * over to the cloned buffer. + * + * @param buf Buffer to clone + * + * @return Deep copy of @c buf or NULL on error + */ +extern GWBUF* gwbuf_deep_clone(const GWBUF* buf); + /** * Compare two GWBUFs. Two GWBUFs are considered identical if their * content is identical, irrespective of whether one is segmented and diff --git a/include/maxscale/config.h b/include/maxscale/config.h index 157e695fe..45dd8b9e1 100644 --- a/include/maxscale/config.h +++ b/include/maxscale/config.h @@ -25,6 +25,7 @@ #include #include #include +#include MXS_BEGIN_DECLS @@ -204,6 +205,7 @@ typedef struct bool skip_permission_checks; /**< Skip service and monitor permission checks */ char qc_name[PATH_MAX]; /**< The name of the query classifier to load */ char* qc_args; /**< Arguments for the query classifier */ + qc_sql_mode_t qc_sql_mode; /**< The query classifier sql mode */ char admin_host[MAX_ADMIN_HOST_LEN]; /**< Admin interface host */ uint16_t admin_port; /**< Admin interface port */ bool admin_auth; /**< Admin interface authentication */ diff --git a/include/maxscale/customparser.hh b/include/maxscale/customparser.hh new file mode 100644 index 000000000..64e4d6bb5 --- /dev/null +++ b/include/maxscale/customparser.hh @@ -0,0 +1,238 @@ +#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 +#include + +namespace maxscale +{ + +#define MXS_CP_EXPECT_TOKEN(string_literal) string_literal, (sizeof(string_literal) - 1) + +// For debugging purposes. +// #define MXS_CP_LOG_UNEXPECTED_AND_EXHAUSTED +#undef MXS_CP_LOG_UNEXPECTED_AND_EXHAUSTED + +class CustomParser +{ + CustomParser(const CustomParser&); + CustomParser& operator = (const CustomParser&); + +public: + typedef int32_t token_t; + + enum token_required_t + { + TOKEN_REQUIRED, + TOKEN_NOT_REQUIRED, + }; + + enum + { + PARSER_UNKNOWN_TOKEN = -2, + PARSER_EXHAUSTED = -1 + }; + + CustomParser() + : m_pSql(NULL) + , m_len(0) + , m_pI(NULL) + , m_pEnd(NULL) + { + } + +protected: + /** + * To be called when unexpected data is encountered. For debugging + * purposes, logging will only be performed if the define + * MXS_CP_LOG_UNEXPECTED_AND_EXHAUSTED is defined. + */ + void log_unexpected() + { +#ifdef MXS_CP_LOG_UNEXPECTED_AND_EXHAUSTED + MXS_NOTICE("Custom parser: In statement '%.*s', unexpected token at '%.*s'.", + (int)m_len, m_pSql, (int)(m_pEnd - m_pI), m_pI); +#endif + } + + /** + * To be called when there is no more data even though there is + * expected to be. For debugging purposes, logging will only be + * performed if the define MXS_CP_LOG_UNEXPECTED_AND_EXHAUSTED + * is defined. + */ + void log_exhausted() + { +#ifdef MXS_CP_LOG_UNEXPECTED_AND_EXHAUSTED + MXS_NOTICE("Custom parser: More tokens expected in statement '%.*s'.", (int)m_len, m_pSql); +#endif + } + + /** + * Is the character an alphabetic character. + * + * @param c A char + * + * @return True if @c c is between 'a' and 'z' or 'A' and 'Z', inclusive. + */ + static bool is_alpha(char c) + { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + } + + /** + * Is the character a number + * + * @param c A char + * + * @return True if @c c is between '0' and '9' inclusive. + */ + static bool is_number(char c) + { + return (c >= '0' && c <= '9'); + } + + /** + * Is a character some offset from the current position, a specific one. + * + * @param uc An UPPERCASE character. + * @param offset How many characters from the current position. + * + * @return True if the character at the position is the one specified or + * its lowercase equivalent. + */ + bool is_next_alpha(char uc, int offset = 1) const + { + ss_dassert(uc >= 'A' && uc <= 'Z'); + + char lc = uc + ('a' - 'A'); + + return + ((m_pI + offset) < m_pEnd) && + ((*(m_pI + offset) == uc) || (*(m_pI + offset) == lc)); + } + + /** + * Peek current character. + * + * @param pC Upon successful return will be the current character. + * + * @return True, if the current character was returned, false otherwise. + * False will only be returned if the current position is at + * the end. + */ + bool peek_current_char(char* pC) const + { + if (m_pI != m_pEnd) + { + *pC = *m_pI; + } + + return m_pI != m_pEnd; + } + + /** + * Peek next character. + * + * @param pC Upon successful return will be the next character. + * + * @return True, if the next character was returned, false otherwise. + * False will only be returned if the current position is at + * the end. + */ + bool peek_next_char(char* pC) const + { + bool rc = (m_pI + 1 < m_pEnd); + + if (rc) + { + *pC = *(m_pI + 1); + } + + return rc; + } + + /** + * Convert a character to upper case. + * + * @param c The character to convert. + * + * @return The uppercase equivalent. If @c c is already uppercase, + * then it is returned. + */ + static char toupper(char c) + { + // Significantly faster than library version. + return (c >= 'a' && c <='z') ? c - ('a' - 'A') : c; + } + + /** + * Bypass all whitespace from current position. + */ + void bypass_whitespace() + { + m_pI = modutil_MySQL_bypass_whitespace(const_cast(m_pI), m_pEnd - m_pI); + } + + /** + * Check whether an expected token is available. + * + * @param zWord A token. + * @param len The token length. + * @param token The value to be returned if the next token is the + * expected one. + * + * @return @c token if the current token is the expected one, + * otherwise PARSER_UNKNOWN_TOKEN. + */ + token_t expect_token(const char* zWord, int len, token_t token) + { + const char* pI = m_pI; + const char* pEnd = zWord + len; + + while ((pI < m_pEnd) && (zWord < pEnd) && (toupper(*pI) == *zWord)) + { + ++pI; + ++zWord; + } + + if (zWord == pEnd) + { + if ((pI == m_pEnd) || (!isalpha(*pI))) // Handwritten isalpha not faster than library version. + { + m_pI = pI; + } + else + { + token = PARSER_UNKNOWN_TOKEN; + } + } + else + { + token = PARSER_UNKNOWN_TOKEN; + } + + return token; + } + +protected: + const char* m_pSql; + int m_len; + const char* m_pI; + const char* m_pEnd; +}; + +} diff --git a/include/maxscale/encryption.h b/include/maxscale/encryption.h new file mode 100644 index 000000000..48b928856 --- /dev/null +++ b/include/maxscale/encryption.h @@ -0,0 +1,27 @@ +#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: 2020-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 + +#include + +MXS_BEGIN_DECLS + + +EVP_CIPHER_CTX* mxs_evp_cipher_ctx_alloc(); +void mxs_evp_cipher_ctx_free(EVP_CIPHER_CTX* ctx); +uint8_t* mxs_evp_cipher_ctx_buf(EVP_CIPHER_CTX* ctx); +uint8_t* mxs_evp_cipher_ctx_oiv(EVP_CIPHER_CTX* ctx); + +MXS_END_DECLS diff --git a/include/maxscale/protocol/mysql.h b/include/maxscale/protocol/mysql.h index 24d86aed1..90795e36b 100644 --- a/include/maxscale/protocol/mysql.h +++ b/include/maxscale/protocol/mysql.h @@ -84,9 +84,13 @@ MXS_BEGIN_DECLS * [10-11] warning_count (2) -- number of warnings */ #define MYSQL_PS_ID_OFFSET MYSQL_HEADER_LEN + 1 +#define MYSQL_PS_ID_SIZE 4 #define MYSQL_PS_COLS_OFFSET MYSQL_HEADER_LEN + 5 +#define MYSQL_PS_COLS_SIZE 2 #define MYSQL_PS_PARAMS_OFFSET MYSQL_HEADER_LEN + 7 +#define MYSQL_PS_PARAMS_SIZE 2 #define MYSQL_PS_WARN_OFFSET MYSQL_HEADER_LEN + 10 +#define MYSQL_PS_WARN_SIZE 2 /** Name of the default server side authentication plugin */ #define DEFAULT_MYSQL_AUTH_PLUGIN "mysql_native_password" @@ -308,6 +312,14 @@ typedef struct #endif } MySQLProtocol; +typedef struct +{ + uint32_t id; + uint16_t columns; + uint16_t parameters; + uint16_t warnings; +} MXS_PS_RESPONSE; + /** Defines for response codes */ #define MYSQL_REPLY_ERR 0xff #define MYSQL_REPLY_OK 0x00 @@ -504,4 +516,44 @@ const char* mxs_mysql_get_current_db(MXS_SESSION* session); */ void mxs_mysql_set_current_db(MXS_SESSION* session, const char* db); +/** + * @brief Get the command byte + * + * @param buffer Buffer containing a complete MySQL packet + * + * @return The command byte + */ +uint8_t mxs_mysql_get_command(GWBUF* buffer); + +/** + * @brief Extract PS response values + * + * @param buffer Buffer containing a complete response to a binary protocol + * preparation of a prepared statement + * @param out Destination where the values are extracted + * + * @return True if values were extracted successfully + */ +bool mxs_mysql_extract_ps_response(GWBUF* buffer, MXS_PS_RESPONSE* out); + +/** + * @brief Extract the ID from a COM_STMT command + * + * All the COM_STMT type commands store the statement ID in the same place. + * + * @param buffer Buffer containing one of the COM_STMT commands (not COM_STMT_PREPARE) + * + * @return The statement ID + */ +uint32_t mxs_mysql_extract_ps_id(GWBUF* buffer); + +/** + * @brief Determine if a packet contains a one way message + * + * @param cmd Command to inspect + * + * @return True if a response is expected from the server + */ +bool mxs_mysql_command_will_respond(uint8_t cmd); + MXS_END_DECLS diff --git a/include/maxscale/query_classifier.h b/include/maxscale/query_classifier.h index d64ddf824..5e1102422 100644 --- a/include/maxscale/query_classifier.h +++ b/include/maxscale/query_classifier.h @@ -29,6 +29,16 @@ typedef enum qc_init_kind QC_INIT_BOTH = 0x03 } qc_init_kind_t; +/** + * qc_sql_mode_t specifies what should be assumed of the statements + * that will be parsed. + */ +typedef enum qc_sql_mode +{ + QC_SQL_MODE_DEFAULT, /*< Assume the statements are MariaDB SQL. */ + QC_SQL_MODE_ORACLE /*< Assume the statements are PL/SQL. */ +} qc_sql_mode_t; + /** * @c qc_collect_info_t specifies what information should be collected during parsing. */ @@ -83,19 +93,22 @@ typedef enum qc_query_type typedef enum qc_query_op { QUERY_OP_UNDEFINED = 0, - QUERY_OP_SELECT, - QUERY_OP_UPDATE, - QUERY_OP_INSERT, - QUERY_OP_DELETE, - QUERY_OP_TRUNCATE, + QUERY_OP_ALTER, - QUERY_OP_CREATE, - QUERY_OP_DROP, QUERY_OP_CHANGE_DB, - QUERY_OP_LOAD, - QUERY_OP_GRANT, - QUERY_OP_REVOKE, + QUERY_OP_CREATE, + QUERY_OP_DELETE, + QUERY_OP_DROP, QUERY_OP_EXECUTE, + QUERY_OP_EXPLAIN, + QUERY_OP_GRANT, + QUERY_OP_INSERT, + QUERY_OP_LOAD, + QUERY_OP_REVOKE, + QUERY_OP_SELECT, + QUERY_OP_SHOW, + QUERY_OP_TRUNCATE, + QUERY_OP_UPDATE, } qc_query_op_t; /** @@ -173,12 +186,13 @@ typedef struct query_classifier /** * Called once to setup the query classifier * - * @param args The value of `query_classifier_args` in the configuration file. + * @param sql_mode The default sql mode. + * @param args The value of `query_classifier_args` in the configuration file. * * @return QC_RESULT_OK, if the query classifier could be setup, otherwise * some specific error code. */ - int32_t (*qc_setup)(const char* args); + int32_t (*qc_setup)(qc_sql_mode_t sql_mode, const char* args); /** * Called once at process startup, after @c qc_setup has successfully @@ -393,6 +407,24 @@ typedef struct query_classifier * version = major * 10000 + minor * 100 + patch */ void (*qc_get_server_version)(uint64_t* version); + + /** + * Gets the sql mode of the *calling* thread. + * + * @param sql_mode The mode. + * + * @return QC_RESULT_OK + */ + int32_t (*qc_get_sql_mode)(qc_sql_mode_t* sql_mode); + + /** + * Sets the sql mode for the *calling* thread. + * + * @param sql_mode The mode. + * + * @return QC_RESULT_OK if @sql_mode is valid, otherwise QC_RESULT_ERROR. + */ + int32_t (*qc_set_sql_mode)(qc_sql_mode_t sql_mode); } QUERY_CLASSIFIER; /** @@ -406,6 +438,7 @@ typedef struct query_classifier * * @param plugin_name The name of the plugin from which the query classifier * should be loaded. + * @param sql_mode The default sql mode. * @param plugin_args The arguments to be provided to the query classifier. * * @return True if the query classifier could be loaded and initialized, @@ -413,7 +446,7 @@ typedef struct query_classifier * * @see qc_end qc_thread_init */ -bool qc_setup(const char* plugin_name, const char* plugin_args); +bool qc_setup(const char* plugin_name, qc_sql_mode_t sql_mode, const char* plugin_args); /** * Intializes the query classifier. @@ -658,6 +691,13 @@ char* qc_get_prepare_name(GWBUF* stmt); */ GWBUF* qc_get_preparable_stmt(GWBUF* stmt); +/** + * Gets the sql mode of the *calling* thread. + * + * @return The mode. + */ +qc_sql_mode_t qc_get_sql_mode(); + /** * Returns the tables accessed by the statement. * @@ -756,6 +796,13 @@ static inline bool qc_query_is_type(uint32_t typemask, qc_query_type_t type) */ bool qc_query_has_clause(GWBUF* stmt); +/** + * Sets the sql mode for the *calling* thread. + * + * @param sql_mode The mode. + */ +void qc_set_sql_mode(qc_sql_mode_t sql_mode); + /** * Returns the string representation of a query type. * diff --git a/include/maxscale/session.h b/include/maxscale/session.h index 58cc5c779..aaa071ace 100644 --- a/include/maxscale/session.h +++ b/include/maxscale/session.h @@ -146,6 +146,7 @@ typedef struct session int refcount; /*< Reference count on the session */ mxs_session_trx_state_t trx_state; /*< The current transaction state. */ bool autocommit; /*< Whether autocommit is on. */ + intptr_t client_protocol_data; /*< Owned and managed by the client protocol. */ struct { GWBUF *buffer; /**< Buffer containing the statement */ diff --git a/include/maxscale/session_command.hh b/include/maxscale/session_command.hh index 59c198f43..5cd1e8350 100644 --- a/include/maxscale/session_command.hh +++ b/include/maxscale/session_command.hh @@ -54,10 +54,11 @@ public: uint64_t get_position() const; /** - * @brief Creates a copy of the internal buffer - * @return A copy of the internal buffer + * @brief Creates a deep copy of the internal buffer + * + * @return A deep copy of the internal buffer or NULL on error */ - mxs::Buffer copy_buffer() const; + GWBUF* deep_copy_buffer(); /** * @brief Create a new session command diff --git a/maxscale-system-test/CMakeLists.txt b/maxscale-system-test/CMakeLists.txt index 4bb650376..29acfa96c 100644 --- a/maxscale-system-test/CMakeLists.txt +++ b/maxscale-system-test/CMakeLists.txt @@ -344,6 +344,10 @@ add_test_executable(mxs280_select_outfile.cpp mxs280_select_outfile replication # Tries prepared stmt 'SELECT 1,1,1,1...." with different nu,ber of '1' add_test_executable(mxs314.cpp mxs314 galera LABELS MySQLProtocol LIGHT GALERA_BACKEND) +# Binary protocol prepared statement tests +add_test_executable(binary_ps.cpp binary_ps replication LABELS readwritesplit LIGHT REPL_BACKEND) +add_test_executable(binary_ps_cursor.cpp binary_ps_cursor replication LABELS readwritesplit LIGHT REPL_BACKEND) + # Creates and closes a lot of connections, checks that 'maxadmin list servers' shows 0 connections at the end add_test_executable(mxs321.cpp mxs321 replication LABELS maxscale REPL_BACKEND) @@ -591,6 +595,8 @@ add_test_script(ssl sql_queries ssl LABELS maxscale readwritesplit REPL_BACKEND) #add_test_script(ssl_load load_balancing ssl_load LABELS maxscale readwritesplit REPL_BACKEND) # Check load balancing, client ssl is ON, Galera backend + +# Galera version is also disabled due to C/C 3.0 strangeness #add_test_script(ssl_load_galera load_balancing_galera ssl_load_galera LABELS maxscale readwritesplit GALERA_BACKEND) # Testing slaves who have lost their master and how MaxScale works with them diff --git a/maxscale-system-test/binary_ps.cpp b/maxscale-system-test/binary_ps.cpp new file mode 100644 index 000000000..f582adb9f --- /dev/null +++ b/maxscale-system-test/binary_ps.cpp @@ -0,0 +1,67 @@ +/** + * Test binary protocol prepared statement routing + */ +#include "testconnections.h" + +int main(int argc, char** argv) +{ + + TestConnections test(argc, argv); + char server_id[test.galera->N][1024]; + + test.repl->connect(); + + /** Get server_id for each node */ + for (int i = 0; i < test.repl->N; i++) + { + sprintf(server_id[i], "%d", test.repl->get_server_id(i)); + } + + test.connect_maxscale(); + + test.set_timeout(20); + + MYSQL_STMT* stmt = mysql_stmt_init(test.conn_rwsplit); + const char* write_query = "SELECT @@server_id, @@last_insert_id"; + const char* read_query = "SELECT @@server_id"; + char buffer[100] = ""; + char buffer2[100] = ""; + my_bool err = false; + my_bool isnull = false; + MYSQL_BIND bind[2] = {}; + + bind[0].buffer_length = sizeof(buffer); + bind[0].buffer = buffer; + bind[0].error = &err; + bind[0].is_null = &isnull; + bind[1].buffer_length = sizeof(buffer2); + bind[1].buffer = buffer2; + bind[1].error = &err; + bind[1].is_null = &isnull; + + // Execute a write, should return the master's server ID + test.add_result(mysql_stmt_prepare(stmt, write_query, strlen(write_query)), "Failed to prepare"); + test.add_result(mysql_stmt_execute(stmt), "Failed to execute"); + test.add_result(mysql_stmt_bind_result(stmt, bind), "Failed to bind result"); + test.add_result(mysql_stmt_fetch(stmt), "Failed to fetch result"); + test.add_result(strcmp(buffer, server_id[0]), "Expected server_id '%s', got '%s'", server_id[0], buffer); + + mysql_stmt_close(stmt); + stmt = mysql_stmt_init(test.conn_rwsplit); + + // Execute read, should return a slave server ID + test.add_result(mysql_stmt_prepare(stmt, read_query, strlen(read_query)), "Failed to prepare"); + test.add_result(mysql_stmt_execute(stmt), "Failed to execute"); + test.add_result(mysql_stmt_bind_result(stmt, bind), "Failed to bind result"); + test.add_result(mysql_stmt_fetch(stmt), "Failed to fetch result"); + test.add_result(strcmp(buffer, server_id[1]) && strcmp(buffer, server_id[2]) && strcmp(buffer, server_id[3]), + "Expected one of the slave server IDs (%s, %s or %s), not '%s'", + server_id[1], server_id[2], server_id[3], buffer); + + + mysql_stmt_close(stmt); + + test.close_maxscale_connections(); + + return test.global_result; +} diff --git a/maxscale-system-test/binary_ps_cursor.cpp b/maxscale-system-test/binary_ps_cursor.cpp new file mode 100644 index 000000000..b7eadd753 --- /dev/null +++ b/maxscale-system-test/binary_ps_cursor.cpp @@ -0,0 +1,191 @@ +/** + * Test that binary protocol cursors work as expected + */ +#include "testconnections.h" +#include + +using std::cout; +using std::endl; + +void test1(TestConnections& test) +{ + test.connect_maxscale(); + test.set_timeout(20); + + MYSQL_STMT* stmt = mysql_stmt_init(test.conn_rwsplit); + const char* query = "SELECT @@server_id"; + char buffer[100] = ""; + my_bool err = false; + my_bool isnull = false; + MYSQL_BIND bind[1] = {}; + + bind[0].buffer_length = sizeof(buffer); + bind[0].buffer = buffer; + bind[0].error = &err; + bind[0].is_null = &isnull; + + cout << "Prepare" << endl; + test.add_result(mysql_stmt_prepare(stmt, query, strlen(query)), "Failed to prepare"); + + unsigned long cursor_type = CURSOR_TYPE_READ_ONLY; + unsigned long rows = 0; + test.add_result(mysql_stmt_attr_set(stmt, STMT_ATTR_CURSOR_TYPE, &cursor_type), "Failed to set attributes"); + test.add_result(mysql_stmt_attr_set(stmt, STMT_ATTR_PREFETCH_ROWS, &rows), "Failed to set attributes"); + + cout << "Execute" << endl; + test.add_result(mysql_stmt_execute(stmt), "Failed to execute"); + cout << "Bind result" << endl; + test.add_result(mysql_stmt_bind_result(stmt, bind), "Failed to bind result"); + cout << "Fetch row" << endl; + test.add_result(mysql_stmt_fetch(stmt), "Failed to fetch result"); + + test.add_result(strlen(buffer) == 0, "Expected result buffer to not be empty"); + + cout << "Close statement" << endl; + mysql_stmt_close(stmt); + test.close_maxscale_connections(); + +} + +void test2(TestConnections& test) +{ + test.set_timeout(20); + + MYSQL* conn = open_conn_db_timeout(test.rwsplit_port, test.maxscale_ip(), "test", + test.maxscale_user, test.maxscale_password, 1, false); + + MYSQL_STMT* stmt1 = mysql_stmt_init(conn); + MYSQL_STMT* stmt2 = mysql_stmt_init(conn); + const char* query1 = "SELECT @@server_id"; + const char* query2 = "SELECT @@server_id, @@last_insert_id"; + char buffer1[100] = ""; + char buffer2[100] = ""; + char buffer2_2[100] = ""; + my_bool err = false; + my_bool isnull = false; + MYSQL_BIND bind1[1] = {}; + MYSQL_BIND bind2[2] = {}; + + bind1[0].buffer_length = sizeof(buffer1); + bind1[0].buffer = buffer1; + bind1[0].error = &err; + bind1[0].is_null = &isnull; + + bind2[0].buffer_length = sizeof(buffer2); + bind2[0].buffer = buffer2; + bind2[0].error = &err; + bind2[0].is_null = &isnull; + bind2[1].buffer_length = sizeof(buffer2); + bind2[1].buffer = buffer2_2; + bind2[1].error = &err; + bind2[1].is_null = &isnull; + + cout << "First prepare, should go to slave" << endl; + test.add_result(mysql_stmt_prepare(stmt1, query1, strlen(query1)), "Failed to prepare"); + + unsigned long cursor_type = CURSOR_TYPE_READ_ONLY; + unsigned long rows = 0; + test.add_result(mysql_stmt_attr_set(stmt1, STMT_ATTR_CURSOR_TYPE, &cursor_type), "Failed to set attributes"); + test.add_result(mysql_stmt_attr_set(stmt1, STMT_ATTR_PREFETCH_ROWS, &rows), "Failed to set attributes"); + + test.add_result(mysql_stmt_execute(stmt1), "Failed to execute"); + test.add_result(mysql_stmt_bind_result(stmt1, bind1), "Failed to bind result"); + + int rc1 = mysql_stmt_fetch(stmt1); + test.add_result(rc1, "Failed to fetch result: %d %s %s", rc1, mysql_stmt_error(stmt1), mysql_error(conn)); + mysql_stmt_close(stmt1); + + cout << "Second prepare, should go to master" << endl; + test.add_result(mysql_stmt_prepare(stmt2, query2, strlen(query2)), "Failed to prepare"); + test.add_result(mysql_stmt_attr_set(stmt2, STMT_ATTR_CURSOR_TYPE, &cursor_type), "Failed to set attributes"); + test.add_result(mysql_stmt_attr_set(stmt2, STMT_ATTR_PREFETCH_ROWS, &rows), "Failed to set attributes"); + + test.add_result(mysql_stmt_execute(stmt2), "Failed to execute"); + test.add_result(mysql_stmt_bind_result(stmt2, bind2), "Failed to bind result"); + + int rc2 = mysql_stmt_fetch(stmt2); + test.add_result(rc2, "Failed to fetch result: %d %s %s", rc2, mysql_stmt_error(stmt2), mysql_error(conn)); + mysql_stmt_close(stmt2); + + /** Get the master's server_id */ + char server_id[1024]; + test.repl->connect(); + sprintf(server_id, "%d", test.repl->get_server_id(0)); + + test.add_result(strcmp(buffer1, buffer2) == 0, "Expected results to differ"); + test.add_result(strcmp(buffer2, server_id) != 0, + "Expected prepare 2 to go to the master (%s) but it's %s", + server_id[0], buffer2); +} + +void test3(TestConnections& test) +{ + test.connect_maxscale(); + test.set_timeout(20); + + MYSQL_STMT* stmt = mysql_stmt_init(test.conn_rwsplit); + const char* query = "SELECT @@server_id"; + char buffer[100] = ""; + my_bool err = false; + my_bool isnull = false; + MYSQL_BIND bind[1] = {}; + + bind[0].buffer_length = sizeof(buffer); + bind[0].buffer = buffer; + bind[0].error = &err; + bind[0].is_null = &isnull; + + test.add_result(mysql_stmt_prepare(stmt, query, strlen(query)), "Failed to prepare"); + + cout << "Start transaction" << endl; + test.add_result(mysql_query(test.conn_rwsplit, "START TRANSACTION"), + "START TRANSACTION should succeed: %s", + mysql_error(test.conn_rwsplit)); + + + unsigned long cursor_type = CURSOR_TYPE_READ_ONLY; + unsigned long rows = 0; + test.add_result(mysql_stmt_attr_set(stmt, STMT_ATTR_CURSOR_TYPE, &cursor_type), "Failed to set attributes"); + test.add_result(mysql_stmt_attr_set(stmt, STMT_ATTR_PREFETCH_ROWS, &rows), "Failed to set attributes"); + + cout << "Execute" << endl; + test.add_result(mysql_stmt_execute(stmt), "Failed to execute"); + test.add_result(mysql_stmt_bind_result(stmt, bind), "Failed to bind result"); + test.add_result(mysql_stmt_fetch(stmt), "Failed to fetch result"); + + test.add_result(strlen(buffer) == 0, "Expected result buffer to not be empty"); + + cout << "Commit" << endl; + test.add_result(mysql_query(test.conn_rwsplit, "COMMIT"), + "COMMIT should succeed: %s", + mysql_error(test.conn_rwsplit)); + + mysql_stmt_close(stmt); + test.close_maxscale_connections(); + + char server_id[1024]; + test.repl->connect(); + sprintf(server_id, "%d", test.repl->get_server_id(0)); + test.add_result(strcmp(buffer, server_id) != 0, + "Expected the execute inside a transaction to go to the master (%s) but it's %s", + server_id[0], buffer); +} + +int main(int argc, char** argv) +{ + TestConnections test(argc, argv); + + cout << "Test 1: Testing simple cursor usage" << endl; + test1(test); + cout << "Done" << endl << endl; + + cout << "Test 2: Testing read-write splitting with cursors" << endl; + test2(test); + cout << "Done" << endl << endl; + + cout << "Test 3: Testing transactions with cursors" << endl; + test3(test); + cout << "Done" << endl << endl; + + return test.global_result; +} diff --git a/maxscale-system-test/testconnections.cpp b/maxscale-system-test/testconnections.cpp index bb112901f..f10e5d7d5 100644 --- a/maxscale-system-test/testconnections.cpp +++ b/maxscale-system-test/testconnections.cpp @@ -1672,7 +1672,7 @@ void *timeout_thread( void *ptr ) Test->timeout--; } Test->tprintf("\n **** Timeout! *** \n"); - delete Test; + Test->~TestConnections(); exit(250); } diff --git a/query_classifier/qc_mysqlembedded/qc_mysqlembedded.cc b/query_classifier/qc_mysqlembedded/qc_mysqlembedded.cc index 467456079..e72aebcaa 100644 --- a/query_classifier/qc_mysqlembedded/qc_mysqlembedded.cc +++ b/query_classifier/qc_mysqlembedded/qc_mysqlembedded.cc @@ -29,7 +29,6 @@ #if defined(MYSQL_CLIENT) #undef MYSQL_CLIENT #endif - #include #include #include @@ -55,8 +54,11 @@ #include #include +#include + #include #include +#include #include // assumes it is being compiled agains Connector-C, // so we need to make certain Connector-C constants visible. @@ -72,6 +74,47 @@ #include #include +/** + * Defines what a particular name should be mapped to. + */ +typedef struct name_mapping +{ + const char* from; + const char* to; +} NAME_MAPPING; + +static NAME_MAPPING function_name_mappings_default[] = +{ + { NULL, NULL } +}; + +static NAME_MAPPING function_name_mappings_oracle[] = +{ + { "concat_operator_oracle", "concat" }, + { "case", "decode" }, + { NULL, NULL } +}; + +static const char* map_function_name(NAME_MAPPING* function_name_mappings, const char* from) +{ + NAME_MAPPING* map = function_name_mappings; + const char* to = NULL; + + while (!to && map->from) + { + if (strcasecmp(from, map->from) == 0) + { + to = map->to; + } + else + { + ++map; + } + } + + return to ? to : from; +} + #define MYSQL_COM_QUERY_HEADER_SIZE 5 /*< 3 bytes size, 1 sequence, 1 command */ #define MAX_QUERYBUF_SIZE 2048 typedef struct parsing_info_st @@ -89,19 +132,14 @@ typedef struct parsing_info_st size_t function_infos_len; size_t function_infos_capacity; GWBUF* preparable_stmt; + qc_parse_result_t result; + int32_t type_mask; + NAME_MAPPING* function_name_mappings; #if defined(SS_DEBUG) skygw_chk_t pi_chk_tail; #endif } parsing_info_t; -static thread_local struct -{ - // The version information is not used; the embedded library parses according - // to the version of the embedded library it has been linked with. However, we - // need to store the information so that qc_[get|set]_server_version will work. - uint64_t version; -} this_thread; - #define QTYPE_LESS_RESTRICTIVE_THAN_WRITE(t) (t= 10 && MYSQL_VERSION_MINOR >= 3 +inline void get_string_and_length(const LEX_CSTRING& ls, const char** s, size_t* length) +{ + *s = ls.str; + *length = ls.length; +} +#else +inline void get_string_and_length(const char* cs, const char** s, size_t* length) +{ + *s = cs; + *length = cs ? strlen(cs) : 0; +} +#endif + +static struct +{ + qc_sql_mode_t sql_mode; + pthread_mutex_t sql_mode_mutex; + NAME_MAPPING* function_name_mappings; +} this_unit = +{ + QC_SQL_MODE_DEFAULT, + PTHREAD_MUTEX_INITIALIZER, + function_name_mappings_default +}; + +static thread_local struct +{ + qc_sql_mode_t sql_mode; + NAME_MAPPING* function_name_mappings; + // The version information is not used; the embedded library parses according + // to the version of the embedded library it has been linked with. However, we + // need to store the information so that qc_[get|set]_server_version will work. + uint64_t version; +} this_thread = +{ + QC_SQL_MODE_DEFAULT, + function_name_mappings_default, + 0 +}; + /** * Ensures that the query is parsed. If it is not already parsed, it * will be parsed. @@ -133,8 +213,38 @@ bool ensure_query_is_parsed(GWBUF* query) if (!parsed) { + // Instead of modifying global_system_variables, from which + // thd->variables.sql_mode will be initialied, we should modify + // thd->variables.sql_mode _after_ it has been created and + // initialized. + // + // However, for whatever reason, the offset of that variable is + // different when accessed from within libmysqld and qc_mysqlembedded, + // so we will not modify the right variable even if it appears we do. + // + // So, for the time being we modify global_system_variables.sql_mode and + // serialize the parsing. That's ok, since qc_mysqlembedded is only + // used for verifying the behaviour of qc_sqlite. + + ss_debug(int rv); + + ss_debug(rv = )pthread_mutex_lock(&this_unit.sql_mode_mutex); + ss_dassert(rv == 0); + + if (this_thread.sql_mode == QC_SQL_MODE_ORACLE) + { + global_system_variables.sql_mode |= MODE_ORACLE; + } + else + { + global_system_variables.sql_mode &= ~MODE_ORACLE; + } + parsed = parse_query(query); + ss_debug(rv = )pthread_mutex_unlock(&this_unit.sql_mode_mutex); + ss_dassert(rv == 0); + if (!parsed) { MXS_ERROR("Unable to parse query, out of resources?"); @@ -156,7 +266,11 @@ int32_t qc_mysql_parse(GWBUF* querybuf, uint32_t collect, int32_t* result) if (parsed) { - *result = QC_QUERY_PARSED; + parsing_info_t* pi = (parsing_info_t*) gwbuf_get_buffer_object_data(querybuf, + GWBUF_PARSING_INFO); + ss_dassert(pi); + + *result = pi->result; } else { @@ -168,6 +282,8 @@ int32_t qc_mysql_parse(GWBUF* querybuf, uint32_t collect, int32_t* result) int32_t qc_mysql_get_type_mask(GWBUF* querybuf, uint32_t* type_mask) { + int32_t rv = QC_RESULT_OK; + *type_mask = QUERY_TYPE_UNKNOWN; MYSQL* mysql; bool succp; @@ -198,12 +314,25 @@ int32_t qc_mysql_get_type_mask(GWBUF* querybuf, uint32_t* type_mask) if (mysql != NULL) { *type_mask = resolve_query_type(pi, (THD *) mysql->thd); +#if MYSQL_VERSION_MAJOR >= 10 && MYSQL_VERSION_MINOR >= 3 + // If in 10.3 mode we need to ensure that sequence related functions + // are taken into account. That we can ensure by querying for the fields. + const QC_FIELD_INFO* field_infos; + uint32_t n_field_infos; + + rv = qc_mysql_get_field_info(querybuf, &field_infos, &n_field_infos); + + if (rv == QC_RESULT_OK) + { + *type_mask |= pi->type_mask; + } +#endif } } } retblock: - return QC_RESULT_OK; + return rv; } /** @@ -278,7 +407,11 @@ static bool parse_query(GWBUF* querybuf) * Create parse_tree inside thd. * thd and lex are readable even if creating parse tree fails. */ - create_parse_tree(thd); + if (create_parse_tree(thd)) + { + pi->result = QC_QUERY_PARSED; + } + /** Add complete parsing info struct to the query buffer */ gwbuf_add_buffer_object(querybuf, GWBUF_PARSING_INFO, @@ -462,7 +595,7 @@ static bool create_parse_tree(THD* thd) } return_here: - return failp; + return !failp; } /** @@ -601,6 +734,12 @@ static uint32_t resolve_query_type(parsing_info_t *pi, THD* thd) goto return_qtype; } + if (lex->describe) + { + type = QUERY_TYPE_READ; + goto return_qtype; + } + if (skygw_stmt_causes_implicit_commit(lex, &set_autocommit_stmt)) { if (MXS_LOG_PRIORITY_IS_ENABLED(LOG_INFO)) @@ -716,6 +855,10 @@ static uint32_t resolve_query_type(parsing_info_t *pi, THD* thd) } } } + else + { + type |= QUERY_TYPE_READ; + } goto return_qtype; } @@ -770,7 +913,6 @@ static uint32_t resolve_query_type(parsing_info_t *pi, THD* thd) break; case SQLCOM_SELECT: - case SQLCOM_SHOW_SLAVE_STAT: type |= QUERY_TYPE_READ; break; @@ -811,15 +953,30 @@ static uint32_t resolve_query_type(parsing_info_t *pi, THD* thd) goto return_qtype; break; - case SQLCOM_SHOW_FIELDS: - type |= QUERY_TYPE_READ; - break; - case SQLCOM_SHOW_TABLES: type |= QUERY_TYPE_SHOW_TABLES; goto return_qtype; break; + case SQLCOM_SHOW_CREATE: + case SQLCOM_SHOW_CREATE_DB: + case SQLCOM_SHOW_CREATE_FUNC: + case SQLCOM_SHOW_CREATE_PROC: + case SQLCOM_SHOW_FIELDS: + case SQLCOM_SHOW_FUNC_CODE: + case SQLCOM_SHOW_GRANTS: + case SQLCOM_SHOW_PROC_CODE: + case SQLCOM_SHOW_SLAVE_HOSTS: + case SQLCOM_SHOW_SLAVE_STAT: + case SQLCOM_SHOW_STATUS: + type |= QUERY_TYPE_READ; + goto return_qtype; + break; + + case SQLCOM_END: + goto return_qtype; + break; + default: type |= QUERY_TYPE_WRITE; break; @@ -857,8 +1014,6 @@ static uint32_t resolve_query_type(parsing_info_t *pi, THD* thd) Item::Type itype; itype = item->type(); - MXS_DEBUG("%lu [resolve_query_type] Item %s:%s", - pthread_self(), item->name, STRITEMTYPE(itype)); if (itype == Item::SUBSELECT_ITEM) { @@ -952,10 +1107,18 @@ static uint32_t resolve_query_type(parsing_info_t *pi, THD* thd) /** System session variable */ case Item_func::GSYSVAR_FUNC: { - const char* name = item->name; + const char* name; + size_t length; + get_string_and_length(item->name, &name, &length); + + const char last_insert_id[] = "@@last_insert_id"; + const char identity[] = "@@identity"; + if (name && - ((strcasecmp(name, "@@last_insert_id") == 0) || - (strcasecmp(name, "@@identity") == 0))) + (((length == sizeof(last_insert_id) - 1) && + (strcasecmp(name, last_insert_id) == 0)) || + ((length == sizeof(identity) - 1) && + (strcasecmp(name, identity) == 0)))) { func_qtype |= QUERY_TYPE_MASTER_READ; } @@ -1273,6 +1436,33 @@ static TABLE_LIST* skygw_get_affected_tables(void* lexptr) return tbl; } +static bool is_show_command(int sql_command) +{ + bool rv = false; + + switch (sql_command) + { + case SQLCOM_SHOW_CREATE: + case SQLCOM_SHOW_DATABASES: + case SQLCOM_SHOW_FIELDS: + case SQLCOM_SHOW_KEYS: + case SQLCOM_SHOW_MASTER_STAT: + case SQLCOM_SHOW_SLAVE_STAT: + case SQLCOM_SHOW_STATUS: + case SQLCOM_SHOW_TABLES: + case SQLCOM_SHOW_TABLE_STATUS: + case SQLCOM_SHOW_VARIABLES: + case SQLCOM_SHOW_WARNS: + rv = true; + break; + + default: + break; + } + + return rv; +} + int32_t qc_mysql_get_table_names(GWBUF* querybuf, int32_t fullnames, char*** tablesp, int32_t* tblsize) { LEX* lex; @@ -1295,6 +1485,11 @@ int32_t qc_mysql_get_table_names(GWBUF* querybuf, int32_t fullnames, char*** tab goto retblock; } + if (lex->describe || is_show_command(lex->sql_command)) + { + goto retblock; + } + lex->current_select = lex->all_selects_list; while (lex->current_select) @@ -1428,16 +1623,19 @@ int32_t qc_mysql_query_has_clause(GWBUF* buf, int32_t* has_clause) if (lex) { - SELECT_LEX* current = lex->all_selects_list; - - while (current && !*has_clause) + if (!lex->describe && !is_show_command(lex->sql_command)) { - if (current->where || current->having) - { - *has_clause = true; - } + SELECT_LEX* current = lex->all_selects_list; - current = current->next_select_in_list(); + while (current && !*has_clause) + { + if (current->where || current->having) + { + *has_clause = true; + } + + current = current->next_select_in_list(); + } } } } @@ -1498,6 +1696,9 @@ static parsing_info_t* parsing_info_init(void (*donefun)(void *)) /** Set handle and free function to parsing info struct */ pi->pi_handle = mysql; pi->pi_done_fp = donefun; + pi->result = QC_QUERY_INVALID; + ss_dassert(this_thread.function_name_mappings); + pi->function_name_mappings = this_thread.function_name_mappings; retblock: return pi; @@ -1600,6 +1801,11 @@ int32_t qc_mysql_get_database_names(GWBUF* querybuf, char*** databasesp, int* si goto retblock; } + if (lex->describe || is_show_command(lex->sql_command)) + { + goto retblock; + } + lex->current_select = lex->all_selects_list; while (lex->current_select) @@ -1663,94 +1869,128 @@ int32_t qc_mysql_get_operation(GWBUF* querybuf, int32_t* operation) if (lex) { - switch (lex->sql_command) + if (lex->describe) { - case SQLCOM_SELECT: - *operation = QUERY_OP_SELECT; - break; + *operation = QUERY_OP_EXPLAIN; + } + else + { + switch (lex->sql_command) + { + case SQLCOM_SELECT: + *operation = QUERY_OP_SELECT; + break; - case SQLCOM_CREATE_DB: - case SQLCOM_CREATE_EVENT: - case SQLCOM_CREATE_FUNCTION: - case SQLCOM_CREATE_INDEX: - case SQLCOM_CREATE_PROCEDURE: - case SQLCOM_CREATE_SERVER: - case SQLCOM_CREATE_SPFUNCTION: - case SQLCOM_CREATE_TABLE: - case SQLCOM_CREATE_TRIGGER: - case SQLCOM_CREATE_USER: - case SQLCOM_CREATE_VIEW: - *operation = QUERY_OP_CREATE; - break; + case SQLCOM_CREATE_DB: + case SQLCOM_CREATE_EVENT: + case SQLCOM_CREATE_FUNCTION: + case SQLCOM_CREATE_INDEX: + case SQLCOM_CREATE_PROCEDURE: +#if MYSQL_VERSION_MAJOR >= 10 && MYSQL_VERSION_MINOR >= 3 + case SQLCOM_CREATE_SEQUENCE: +#endif + case SQLCOM_CREATE_SERVER: + case SQLCOM_CREATE_SPFUNCTION: + case SQLCOM_CREATE_TABLE: + case SQLCOM_CREATE_TRIGGER: + case SQLCOM_CREATE_USER: + case SQLCOM_CREATE_VIEW: + *operation = QUERY_OP_CREATE; + break; - case SQLCOM_ALTER_DB: - case SQLCOM_ALTER_DB_UPGRADE: - case SQLCOM_ALTER_EVENT: - case SQLCOM_ALTER_FUNCTION: - case SQLCOM_ALTER_PROCEDURE: - case SQLCOM_ALTER_SERVER: - case SQLCOM_ALTER_TABLE: - case SQLCOM_ALTER_TABLESPACE: - *operation = QUERY_OP_ALTER; - break; + case SQLCOM_ALTER_DB: + case SQLCOM_ALTER_DB_UPGRADE: + case SQLCOM_ALTER_EVENT: + case SQLCOM_ALTER_FUNCTION: + case SQLCOM_ALTER_PROCEDURE: + case SQLCOM_ALTER_SERVER: + case SQLCOM_ALTER_TABLE: + case SQLCOM_ALTER_TABLESPACE: + *operation = QUERY_OP_ALTER; + break; - case SQLCOM_UPDATE: - case SQLCOM_UPDATE_MULTI: - *operation = QUERY_OP_UPDATE; - break; + case SQLCOM_UPDATE: + case SQLCOM_UPDATE_MULTI: + *operation = QUERY_OP_UPDATE; + break; - case SQLCOM_INSERT: - case SQLCOM_INSERT_SELECT: - case SQLCOM_REPLACE: - case SQLCOM_REPLACE_SELECT: - *operation = QUERY_OP_INSERT; - break; + case SQLCOM_INSERT: + case SQLCOM_INSERT_SELECT: + case SQLCOM_REPLACE: + case SQLCOM_REPLACE_SELECT: + *operation = QUERY_OP_INSERT; + break; - case SQLCOM_DELETE: - case SQLCOM_DELETE_MULTI: - *operation = QUERY_OP_DELETE; - break; + case SQLCOM_DELETE: + case SQLCOM_DELETE_MULTI: + *operation = QUERY_OP_DELETE; + break; - case SQLCOM_TRUNCATE: - *operation = QUERY_OP_TRUNCATE; - break; + case SQLCOM_TRUNCATE: + *operation = QUERY_OP_TRUNCATE; + break; - case SQLCOM_DROP_DB: - case SQLCOM_DROP_EVENT: - case SQLCOM_DROP_FUNCTION: - case SQLCOM_DROP_INDEX: - case SQLCOM_DROP_PROCEDURE: - case SQLCOM_DROP_SERVER: - case SQLCOM_DROP_TABLE: - case SQLCOM_DROP_TRIGGER: - case SQLCOM_DROP_USER: - case SQLCOM_DROP_VIEW: - *operation = QUERY_OP_DROP; - break; + case SQLCOM_DROP_DB: + case SQLCOM_DROP_EVENT: + case SQLCOM_DROP_FUNCTION: + case SQLCOM_DROP_INDEX: + case SQLCOM_DROP_PROCEDURE: +#if MYSQL_VERSION_MAJOR >= 10 && MYSQL_VERSION_MINOR >= 3 + case SQLCOM_DROP_SEQUENCE: +#endif + case SQLCOM_DROP_SERVER: + case SQLCOM_DROP_TABLE: + case SQLCOM_DROP_TRIGGER: + case SQLCOM_DROP_USER: + case SQLCOM_DROP_VIEW: + *operation = QUERY_OP_DROP; + break; - case SQLCOM_CHANGE_DB: - *operation = QUERY_OP_CHANGE_DB; - break; + case SQLCOM_CHANGE_DB: + *operation = QUERY_OP_CHANGE_DB; + break; - case SQLCOM_LOAD: - *operation = QUERY_OP_LOAD; - break; + case SQLCOM_LOAD: + *operation = QUERY_OP_LOAD; + break; - case SQLCOM_GRANT: - *operation = QUERY_OP_GRANT; - break; + case SQLCOM_GRANT: + *operation = QUERY_OP_GRANT; + break; - case SQLCOM_REVOKE: - case SQLCOM_REVOKE_ALL: - *operation = QUERY_OP_REVOKE; - break; + case SQLCOM_REVOKE: + case SQLCOM_REVOKE_ALL: + *operation = QUERY_OP_REVOKE; + break; - case SQLCOM_EXECUTE: - *operation = QUERY_OP_EXECUTE; - break; + case SQLCOM_SHOW_CREATE: + case SQLCOM_SHOW_CREATE_DB: + case SQLCOM_SHOW_CREATE_FUNC: + case SQLCOM_SHOW_CREATE_PROC: + case SQLCOM_SHOW_DATABASES: + case SQLCOM_SHOW_FIELDS: + case SQLCOM_SHOW_FUNC_CODE: + case SQLCOM_SHOW_GRANTS: + case SQLCOM_SHOW_KEYS: + case SQLCOM_SHOW_MASTER_STAT: + case SQLCOM_SHOW_PROC_CODE: + case SQLCOM_SHOW_SLAVE_HOSTS: + case SQLCOM_SHOW_SLAVE_STAT: + case SQLCOM_SHOW_STATUS: + case SQLCOM_SHOW_TABLES: + case SQLCOM_SHOW_TABLE_STATUS: + case SQLCOM_SHOW_VARIABLES: + case SQLCOM_SHOW_WARNS: + *operation = QUERY_OP_SHOW; + break; - default: - *operation = QUERY_OP_UNDEFINED; + case SQLCOM_EXECUTE: + *operation = QUERY_OP_EXECUTE; + break; + + default: + *operation = QUERY_OP_UNDEFINED; + } } } } @@ -1769,15 +2009,18 @@ int32_t qc_mysql_get_prepare_name(GWBUF* stmt, char** namep) { LEX* lex = get_lex(stmt); - if ((lex->sql_command == SQLCOM_PREPARE) || - (lex->sql_command == SQLCOM_EXECUTE) || - (lex->sql_command == SQLCOM_DEALLOCATE_PREPARE)) + if (!lex->describe) { - name = (char*)malloc(lex->prepared_stmt_name.length + 1); - if (name) + if ((lex->sql_command == SQLCOM_PREPARE) || + (lex->sql_command == SQLCOM_EXECUTE) || + (lex->sql_command == SQLCOM_DEALLOCATE_PREPARE)) { - memcpy(name, lex->prepared_stmt_name.str, lex->prepared_stmt_name.length); - name[lex->prepared_stmt_name.length] = 0; + name = (char*)malloc(lex->prepared_stmt_name.length + 1); + if (name) + { + memcpy(name, lex->prepared_stmt_name.str, lex->prepared_stmt_name.length); + name[lex->prepared_stmt_name.length] = 0; + } } } } @@ -1796,14 +2039,21 @@ int32_t qc_mysql_get_preparable_stmt(GWBUF* stmt, GWBUF** preparable_stmt) { LEX* lex = get_lex(stmt); - if (lex->sql_command == SQLCOM_PREPARE) + if ((lex->sql_command == SQLCOM_PREPARE) && !lex->describe) { parsing_info_t* pi = get_pinfo(stmt); if (!pi->preparable_stmt) { - const char* preparable_stmt = lex->prepared_stmt_code.str; - size_t preparable_stmt_len = lex->prepared_stmt_code.length; + const char* preparable_stmt; + size_t preparable_stmt_len; +#if MYSQL_VERSION_MINOR >= 3 + preparable_stmt = lex->prepared_stmt_code->name.str; + preparable_stmt_len = lex->prepared_stmt_code->name.length; +#else + preparable_stmt = lex->prepared_stmt_code.str; + preparable_stmt_len = lex->prepared_stmt_code.length; +#endif size_t payload_len = preparable_stmt_len + 1; size_t packet_len = MYSQL_HEADER_LEN + payload_len; @@ -1861,9 +2111,13 @@ static bool should_exclude(const char* name, List* excludep) while (!exclude && (exclude_item = ilist++)) { - const char* exclude_name = exclude_item->name; + const char* exclude_name; + size_t length; + get_string_and_length(exclude_item->name, &exclude_name, &length); - if (exclude_name && (strcasecmp(name, exclude_name) == 0)) + if (exclude_name && + (strlen(name) == length) && + (strcasecmp(name, exclude_name) == 0)) { exclude = true; } @@ -1981,6 +2235,8 @@ static void add_function_info(parsing_info_t* info, { ss_dassert(name); + name = map_function_name(info->function_name_mappings, name); + QC_FUNCTION_INFO item = { (char*)name, usage }; size_t i; @@ -2036,7 +2292,12 @@ static void add_field_info(parsing_info_t* pi, Item_field* item, uint32_t usage, { const char* database = item->db_name; const char* table = item->table_name; - const char* column = item->field_name; + const char* s; + size_t l; + get_string_and_length(item->field_name, &s, &l); + char column[l + 1]; + strncpy(column, s, l); + column[l] = 0; LEX* lex = get_lex(pi); @@ -2125,7 +2386,12 @@ static void add_field_info(parsing_info_t* pi, Item* item, uint32_t usage, List< { const char* database = NULL; const char* table = NULL; - const char* column = item->name; + const char* s; + size_t l; + get_string_and_length(item->name, &s, &l); + char column[l + 1]; + strncpy(column, s, l); + column[l] = 0; add_field_info(pi, database, table, column, usage, excludep); } @@ -2161,6 +2427,43 @@ static void remove_surrounding_back_ticks(char* s) } } +static bool should_function_be_ignored(parsing_info_t* pi, const char* func_name) +{ + bool rv = false; + + // We want to ignore functions that do not really appear as such in an + // actual SQL statement. E.g. "SELECT @a" appears as a function "get_user_var". + if ((strcasecmp(func_name, "decimal_typecast") == 0) || + (strcasecmp(func_name, "cast_as_char") == 0) || + (strcasecmp(func_name, "cast_as_date") == 0) || + (strcasecmp(func_name, "cast_as_datetime") == 0) || + (strcasecmp(func_name, "cast_as_time") == 0) || + (strcasecmp(func_name, "cast_as_signed") == 0) || + (strcasecmp(func_name, "cast_as_unsigned") == 0) || + (strcasecmp(func_name, "get_user_var") == 0) || + (strcasecmp(func_name, "get_system_var") == 0) || + (strcasecmp(func_name, "set_user_var") == 0) || + (strcasecmp(func_name, "set_system_var") == 0)) + { + rv = true; + } + + // Any sequence related functions should be ignored as well. +#if MYSQL_VERSION_MAJOR >= 10 && MYSQL_VERSION_MINOR >= 3 + if (!rv) + { + if ((strcasecmp(func_name, "lastval") == 0) || + (strcasecmp(func_name, "nextval") == 0)) + { + pi->type_mask |= QUERY_TYPE_WRITE; + rv = true; + } + } +#endif + + return rv; +} + static void update_field_infos(parsing_info_t* pi, collect_source_t source, Item* item, @@ -2278,24 +2581,17 @@ static void update_field_infos(parsing_info_t* pi, // We want to ignore functions that do not really appear as such in an // actual SQL statement. E.g. "SELECT @a" appears as a function "get_user_var". - if ((strcasecmp(func_name, "decimal_typecast") != 0) && - (strcasecmp(func_name, "cast_as_char") != 0) && - (strcasecmp(func_name, "cast_as_date") != 0) && - (strcasecmp(func_name, "cast_as_datetime") != 0) && - (strcasecmp(func_name, "cast_as_time") != 0) && - (strcasecmp(func_name, "cast_as_signed") != 0) && - (strcasecmp(func_name, "cast_as_unsigned") != 0) && - (strcasecmp(func_name, "get_user_var") != 0) && - (strcasecmp(func_name, "get_system_var") != 0) && - (strcasecmp(func_name, "set_user_var") != 0) && - (strcasecmp(func_name, "set_system_var") != 0)) + if (!should_function_be_ignored(pi, func_name)) { if (strcmp(func_name, "%") == 0) { // Embedded library silently changes "mod" into "%". We need to check // what it originally was, so that the result agrees with that of // qc_sqlite. - if (func_item->name && (strncasecmp(func_item->name, "mod", 3) == 0)) + const char* s; + size_t l; + get_string_and_length(func_item->name, &s, &l); + if (s && (strncasecmp(s, "mod", 3) == 0)) { strcpy(func_name, "mod"); } @@ -2311,7 +2607,10 @@ static void update_field_infos(parsing_info_t* pi, // Embedded library silently changes "substring" into "substr". We need // to check what it originally was, so that the result agrees with // that of qc_sqlite. We reserved space for this above. - if (func_item->name && (strncasecmp(func_item->name, "substring", 9) == 0)) + const char* s; + size_t l; + get_string_and_length(func_item->name, &s, &l); + if (s && (strncasecmp(s, "substring", 9) == 0)) { strcpy(func_name, "substring"); } @@ -2472,6 +2771,9 @@ static void update_field_infos(parsing_info_t* pi, int32_t qc_mysql_get_field_info(GWBUF* buf, const QC_FIELD_INFO** infos, uint32_t* n_infos) { + *infos = NULL; + *n_infos = 0; + if (!buf) { return QC_RESULT_OK; @@ -2479,7 +2781,7 @@ int32_t qc_mysql_get_field_info(GWBUF* buf, const QC_FIELD_INFO** infos, uint32_ if (!ensure_query_is_parsed(buf)) { - return QC_RESULT_ERROR;; + return QC_RESULT_ERROR; } parsing_info_t* pi = get_pinfo(buf); @@ -2495,6 +2797,13 @@ int32_t qc_mysql_get_field_info(GWBUF* buf, const QC_FIELD_INFO** infos, uint32_ return QC_RESULT_ERROR; } + if (lex->describe || is_show_command(lex->sql_command)) + { + *infos = NULL; + *n_infos = 0; + return QC_RESULT_OK; + } + uint32_t usage = 0; switch (lex->sql_command) @@ -2587,19 +2896,27 @@ int32_t qc_mysql_get_function_info(GWBUF* buf, *function_infos = NULL; *n_function_infos = 0; - const QC_FIELD_INFO* field_infos; - uint32_t n_field_infos; + int32_t rv = QC_RESULT_OK; - // We ensure the information has been collected by querying the fields first. - qc_mysql_get_field_info(buf, &field_infos, &n_field_infos); + if (buf) + { + const QC_FIELD_INFO* field_infos; + uint32_t n_field_infos; - parsing_info_t* pi = get_pinfo(buf); - ss_dassert(pi); + // We ensure the information has been collected by querying the fields first. + rv = qc_mysql_get_field_info(buf, &field_infos, &n_field_infos); - *function_infos = pi->function_infos; - *n_function_infos = pi->function_infos_len; + if (rv == QC_RESULT_OK) + { + parsing_info_t* pi = get_pinfo(buf); + ss_dassert(pi); - return QC_RESULT_OK; + *function_infos = pi->function_infos; + *n_function_infos = pi->function_infos_len; + } + } + + return rv; } void qc_mysql_set_server_version(uint64_t version) @@ -2622,7 +2939,10 @@ const char* server_options[] = "--no-defaults", "--datadir=", "--language=", +#if MYSQL_VERSION_MINOR < 3 + // TODO: 10.3 understands neither "--skip-innodb" or "--innodb=OFF", although it should. "--skip-innodb", +#endif "--default-storage-engine=myisam", NULL }; @@ -2668,12 +2988,19 @@ void configure_options(const char* datadir, const char* langdir) } -int32_t qc_mysql_setup(const char* args) +int32_t qc_mysql_setup(qc_sql_mode_t sql_mode, const char* zArgs) { - if (args) + this_unit.sql_mode = sql_mode; + + if (sql_mode == QC_SQL_MODE_ORACLE) + { + this_unit.function_name_mappings = function_name_mappings_oracle; + } + + if (zArgs) { MXS_WARNING("'%s' provided as arguments, " - "even though no arguments are supported.", args); + "even though no arguments are supported.", zArgs); } return QC_RESULT_OK; @@ -2699,6 +3026,10 @@ int32_t qc_mysql_process_init(void) if (rc != 0) { + this_thread.sql_mode = this_unit.sql_mode; + ss_dassert(this_unit.function_name_mappings); + this_thread.function_name_mappings = this_unit.function_name_mappings; + MXS_ERROR("mysql_library_init() failed. Error code: %d", rc); } else @@ -2721,6 +3052,10 @@ void qc_mysql_process_end(void) int32_t qc_mysql_thread_init(void) { + this_thread.sql_mode = this_unit.sql_mode; + ss_dassert(this_unit.function_name_mappings); + this_thread.function_name_mappings = this_unit.function_name_mappings; + bool inited = (mysql_thread_init() == 0); if (!inited) @@ -2736,6 +3071,35 @@ void qc_mysql_thread_end(void) mysql_thread_end(); } +int32_t qc_mysql_get_sql_mode(qc_sql_mode_t* sql_mode) +{ + *sql_mode = this_thread.sql_mode; + return QC_RESULT_OK; +} + +int32_t qc_mysql_set_sql_mode(qc_sql_mode_t sql_mode) +{ + int32_t rv = QC_RESULT_OK; + + switch (sql_mode) + { + case QC_SQL_MODE_DEFAULT: + this_thread.sql_mode = sql_mode; + this_thread.function_name_mappings = function_name_mappings_default; + break; + + case QC_SQL_MODE_ORACLE: + this_thread.sql_mode = sql_mode; + this_thread.function_name_mappings = function_name_mappings_oracle; + break; + + default: + rv = QC_RESULT_ERROR; + } + + return rv; +} + /** * EXPORTS */ @@ -2767,6 +3131,8 @@ extern "C" qc_mysql_get_preparable_stmt, qc_mysql_set_server_version, qc_mysql_get_server_version, + qc_mysql_get_sql_mode, + qc_mysql_set_sql_mode, }; static MXS_MODULE info = diff --git a/query_classifier/qc_sqlite/builtin_functions.c b/query_classifier/qc_sqlite/builtin_functions.c index b157c31e9..ec4dae9da 100644 --- a/query_classifier/qc_sqlite/builtin_functions.c +++ b/query_classifier/qc_sqlite/builtin_functions.c @@ -413,6 +413,14 @@ static const char* BUILTIN_10_2_3_FUNCTIONS[] = const size_t N_BUILTIN_10_2_3_FUNCTIONS = sizeof(BUILTIN_10_2_3_FUNCTIONS) / sizeof(BUILTIN_10_2_3_FUNCTIONS[0]); +static const char* ORACLE_FUNCTIONS[] = +{ + "nvl", + "nvl2" +}; + +const size_t N_ORACLE_FUNCTIONS = sizeof(ORACLE_FUNCTIONS) / sizeof(ORACLE_FUNCTIONS[0]); + // NOTE: sort_compare and search_compare are not identical, so don't // NOTE: optimize either of them away. static int sort_compare(const void* key, const void* value) @@ -435,6 +443,7 @@ void init_builtin_functions() qsort(BUILTIN_FUNCTIONS, N_BUILTIN_FUNCTIONS, sizeof(char*), sort_compare); qsort(BUILTIN_10_2_3_FUNCTIONS, N_BUILTIN_10_2_3_FUNCTIONS, sizeof(char*), sort_compare); + qsort(ORACLE_FUNCTIONS, N_ORACLE_FUNCTIONS, sizeof(char*), sort_compare); unit.inited = true; } @@ -445,7 +454,9 @@ void finish_builtin_functions() unit.inited = false; } -bool is_builtin_readonly_function(const char* key, uint32_t major, uint32_t minor, uint32_t patch) +bool is_builtin_readonly_function(const char* key, + uint32_t major, uint32_t minor, uint32_t patch, + bool check_oracle) { ss_dassert(unit.inited); @@ -462,5 +473,10 @@ bool is_builtin_readonly_function(const char* key, uint32_t major, uint32_t mino } } + if (!value && check_oracle) + { + value = bsearch(key, ORACLE_FUNCTIONS, N_ORACLE_FUNCTIONS, sizeof(char*), search_compare); + } + return value ? true : false; } diff --git a/query_classifier/qc_sqlite/builtin_functions.h b/query_classifier/qc_sqlite/builtin_functions.h index 49768aee8..573bf1560 100644 --- a/query_classifier/qc_sqlite/builtin_functions.h +++ b/query_classifier/qc_sqlite/builtin_functions.h @@ -22,7 +22,9 @@ extern "C" { void init_builtin_functions(); void finish_builtin_functions(); -bool is_builtin_readonly_function(const char* zToken, uint32_t major, uint32_t minor, uint32_t patch); +bool is_builtin_readonly_function(const char* zToken, + uint32_t major, uint32_t minor, uint32_t patch, + bool check_oracle); #ifdef __cplusplus } diff --git a/query_classifier/qc_sqlite/qc_sqlite.c b/query_classifier/qc_sqlite/qc_sqlite.c index f9dfa289a..4bac109f1 100644 --- a/query_classifier/qc_sqlite/qc_sqlite.c +++ b/query_classifier/qc_sqlite/qc_sqlite.c @@ -49,44 +49,6 @@ static inline bool qc_info_was_parsed(qc_parse_result_t status) return status == QC_QUERY_PARSED; } -/** - * Contains information about a particular query. - */ -typedef struct qc_sqlite_info -{ - qc_parse_result_t status; // The validity of the information in this structure. - uint32_t collect; // What information should be collected. - uint32_t collected; // What information has been collected. - const char* query; // The query passed to sqlite. - size_t query_len; // The length of the query. - - uint32_t type_mask; // The type mask of the query. - qc_query_op_t operation; // The operation in question. - bool has_clause; // Has WHERE or HAVING. - char** table_names; // Array of table names used in the query. - size_t table_names_len; // The used entries in table_names. - size_t table_names_capacity; // The capacity of table_names. - char** table_fullnames; // Array of full (i.e. qualified) table names used in the query. - size_t table_fullnames_len; // The used entries in table_fullnames. - size_t table_fullnames_capacity; // The capacity of table_fullnames. - char* created_table_name; // The name of a created table. - bool is_drop_table; // Is the query a DROP TABLE. - char** database_names; // Array of database names used in the query. - size_t database_names_len; // The used entries in database_names. - size_t database_names_capacity; // The capacity of database_names. - int keyword_1; // The first encountered keyword. - int keyword_2; // The second encountered keyword. - char* prepare_name; // The name of a prepared statement. - GWBUF* preparable_stmt; // The preparable statement. - QC_FIELD_INFO *field_infos; // Pointer to array of QC_FIELD_INFOs. - size_t field_infos_len; // The used entries in field_infos. - size_t field_infos_capacity; // The capacity of the field_infos array. - QC_FUNCTION_INFO *function_infos;// Pointer to array of QC_FUNCTION_INFOs. - size_t function_infos_len; // The used entries in function_infos. - size_t function_infos_capacity; // The capacity of the function_infos array. - bool initializing; // Whether we are initializing sqlite3. -} QC_SQLITE_INFO; - typedef enum qc_log_level { QC_LOG_NOTHING = 0, @@ -95,6 +57,79 @@ typedef enum qc_log_level QC_LOG_NON_TOKENIZED, } qc_log_level_t; +typedef enum qc_parse_as +{ + QC_PARSE_AS_DEFAULT, // Parse as embedded lib does before 10.3 + QC_PARSE_AS_103 // Parse as embedded lib does in 10.3 +} qc_parse_as_t; + +/** + * Defines what a particular name should be mapped to. + */ +typedef struct qc_name_mapping +{ + const char* from; + const char* to; +} QC_NAME_MAPPING; + +static QC_NAME_MAPPING function_name_mappings_default[] = +{ + { NULL, NULL } +}; + +static QC_NAME_MAPPING function_name_mappings_103[] = +{ + { "now", "current_timestamp" }, + { NULL, NULL } +}; + +// NOTE: Duplicate the information from function_name_mappings_103 here. +static QC_NAME_MAPPING function_name_mappings_oracle[] = +{ + { "now", "current_timestamp" }, + { "nvl", "ifnull" }, + { NULL, NULL } +}; + +/** + * Contains information about a particular query. + */ +typedef struct qc_sqlite_info +{ + qc_parse_result_t status; // The validity of the information in this structure. + uint32_t collect; // What information should be collected. + uint32_t collected; // What information has been collected. + const char* query; // The query passed to sqlite. + size_t query_len; // The length of the query. + + uint32_t type_mask; // The type mask of the query. + qc_query_op_t operation; // The operation in question. + bool has_clause; // Has WHERE or HAVING. + char** table_names; // Array of table names used in the query. + size_t table_names_len; // The used entries in table_names. + size_t table_names_capacity; // The capacity of table_names. + char** table_fullnames; // Array of full (i.e. qualified) table names used in the query. + size_t table_fullnames_len; // The used entries in table_fullnames. + size_t table_fullnames_capacity; // The capacity of table_fullnames. + char* created_table_name; // The name of a created table. + bool is_drop_table; // Is the query a DROP TABLE. + char** database_names; // Array of database names used in the query. + size_t database_names_len; // The used entries in database_names. + size_t database_names_capacity; // The capacity of database_names. + int keyword_1; // The first encountered keyword. + int keyword_2; // The second encountered keyword. + char* prepare_name; // The name of a prepared statement. + GWBUF* preparable_stmt; // The preparable statement. + QC_FIELD_INFO *field_infos; // Pointer to array of QC_FIELD_INFOs. + size_t field_infos_len; // The used entries in field_infos. + size_t field_infos_capacity; // The capacity of the field_infos array. + QC_FUNCTION_INFO *function_infos; // Pointer to array of QC_FUNCTION_INFOs. + size_t function_infos_len; // The used entries in function_infos. + size_t function_infos_capacity; // The capacity of the function_infos array. + bool initializing; // Whether we are initializing sqlite3. + qc_sql_mode_t sql_mode; // The current sql_mode. + QC_NAME_MAPPING* function_name_mappings; // How function names should be mapped. +} QC_SQLITE_INFO; /** * The state of qc_sqlite. @@ -104,6 +139,9 @@ static struct bool initialized; bool setup; qc_log_level_t log_level; + qc_sql_mode_t sql_mode; + qc_parse_as_t parse_as; + QC_NAME_MAPPING* function_name_mappings; } this_unit; /** @@ -111,12 +149,14 @@ static struct */ static thread_local struct { - bool initialized; - sqlite3* db; // Thread specific database handle. - QC_SQLITE_INFO* info; + bool initialized; // Whether the thread specific data has been initialized. + sqlite3* db; // Thread specific database handle. + qc_sql_mode_t sql_mode; // What sql_mode is used. + QC_SQLITE_INFO* info; // The information for the current statement being classified. uint32_t version_major; uint32_t version_minor; uint32_t version_patch; + QC_NAME_MAPPING* function_name_mappings; // How function names should be mapped. } this_thread; /** @@ -141,7 +181,13 @@ static QC_SQLITE_INFO* info_alloc(uint32_t collect); static void info_finish(QC_SQLITE_INFO* info); static void info_free(QC_SQLITE_INFO* info); static QC_SQLITE_INFO* info_init(QC_SQLITE_INFO* info, uint32_t collect); +static bool is_sequence_related_field(QC_SQLITE_INFO* info, + const char* database, + const char* table, + const char* column); +static bool is_sequence_related_function(QC_SQLITE_INFO* info, const char* func_name); static void log_invalid_data(GWBUF* query, const char* message); +static const char* map_function_name(QC_NAME_MAPPING* function_name_mappings, const char* name); static bool parse_query(GWBUF* query, uint32_t collect); static void parse_query_string(const char* query, size_t len); static bool query_is_parsed(GWBUF* query, uint32_t collect); @@ -397,6 +443,8 @@ static QC_SQLITE_INFO* info_init(QC_SQLITE_INFO* info, uint32_t collect) info->function_infos_len = 0; info->function_infos_capacity = 0; info->initializing = false; + info->sql_mode = this_thread.sql_mode; + info->function_name_mappings = this_thread.function_name_mappings; return info; } @@ -414,6 +462,11 @@ static void parse_query_string(const char* query, size_t len) const char* suffix = (len > max_len ? "..." : ""); const char* format; + if (this_thread.info->operation == QUERY_OP_EXPLAIN) + { + this_thread.info->status = QC_QUERY_PARSED; + } + if (rc != SQLITE_OK) { if (qc_info_was_tokenized(this_thread.info->status)) @@ -527,9 +580,15 @@ static bool parse_query(GWBUF* query, uint32_t collect) ss_dassert((~info->collected & collect) != 0); // If we get here, then the statement has been parsed once, but - // not all needed was collected. Now we turn on all blinkelichts to + // not all needed was collected. Now we turn on all blinkenlichts to // ensure that a statement is parsed at most twice. info->collect = QC_COLLECT_ALL; + + // We also reset the collected keywords, so that code that behaves + // differently depending on whether keywords have been seem or not + // acts the same way on this second round. + info->keyword_1 = 0; + info->keyword_2 = 0; } else { @@ -613,6 +672,60 @@ static bool query_is_parsed(GWBUF* query, uint32_t collect) return rc; } +/** + * Returns whether a field is sequence related. + * + * @param info Current info object + * @param database The database/schema or NULL. + * @param table The table or NULL. + * @param column The column. + * + * @return True, if the field is sequence related, false otherwise. + */ +static bool is_sequence_related_field(QC_SQLITE_INFO* info, + const char* database, + const char* table, + const char* column) +{ + return is_sequence_related_function(info, column); +} + +/** + * Returns whether a function is sequence related. + * + * @param info Current info object + * @param func_name A function name. + * + * @return True, if the function is sequence related, false otherwise. + */ +static bool is_sequence_related_function(QC_SQLITE_INFO* info, const char* func_name) +{ + bool rv = false; + + if (info->sql_mode == QC_SQL_MODE_ORACLE) + { + // In Oracle mode we ignore the pseudocolumns "currval" and "nextval". + // We also exclude "lastval", the 10.3 equivalent of "currval". + if ((strcasecmp(func_name, "currval") == 0) || + (strcasecmp(func_name, "nextval") == 0) || + (strcasecmp(func_name, "lastval") == 0)) + { + rv = true; + } + } + + if (!rv && (this_unit.parse_as == QC_PARSE_AS_103)) + { + if ((strcasecmp(func_name, "lastval") == 0) || + (strcasecmp(func_name, "nextval") == 0)) + { + rv = true; + } + } + + return rv; +} + /** * Logs information about invalid data. * @@ -640,6 +753,34 @@ static void log_invalid_data(GWBUF* query, const char* message) } } +/** + * Map a function name to another. + * + * @param function_name_mappings The name mapping to use. + * @param from The function name to map. + * + * @param The mapped name, or @c from if the name is not mapped. + */ +static const char* map_function_name(QC_NAME_MAPPING* function_name_mappings, const char* from) +{ + QC_NAME_MAPPING* map = function_name_mappings; + const char* to = NULL; + + while (!to && map->from) + { + if (strcasecmp(from, map->from) == 0) + { + to = map->to; + } + else + { + ++map; + } + } + + return to ? to : from; +} + static bool should_exclude(const char* zName, const ExprList* pExclude) { int i; @@ -694,6 +835,12 @@ static void update_field_info(QC_SQLITE_INFO* info, { ss_dassert(column); + if (is_sequence_related_field(info, database, table, column)) + { + info->type_mask |= QUERY_TYPE_WRITE; + return; + } + if (!(info->collect & QC_COLLECT_FIELDS) || (info->collected & QC_COLLECT_FIELDS)) { // If field information should not be collected, or if field information @@ -793,6 +940,8 @@ static void update_function_info(QC_SQLITE_INFO* info, return; } + name = map_function_name(info->function_name_mappings, name); + QC_FUNCTION_INFO item = { (char*)name, usage }; int i; @@ -996,8 +1145,12 @@ static void update_field_infos(QC_SQLITE_INFO* info, qc_token_position_t pos, const ExprList* pExclude) { + const Expr* pLeft = pExpr->pLeft; + const Expr* pRight = pExpr->pRight; const char* zToken = pExpr->u.zToken; + bool ignore_exprlist = false; + switch (pExpr->op) { case TK_ASTERISK: // select * @@ -1091,13 +1244,54 @@ static void update_field_infos(QC_SQLITE_INFO* info, case TK_MINUS: case TK_NOTNULL: case TK_PLUS: - case TK_REM: case TK_SLASH: case TK_STAR: - case TK_UMINUS: update_function_info(info, get_token_symbol(pExpr->op), usage); break; + case TK_REM: + if (info->sql_mode == QC_SQL_MODE_ORACLE) + { + if ((pLeft && (pLeft->op == TK_ID)) && + (pRight && (pRight->op == TK_ID)) && + (strcasecmp(pLeft->u.zToken, "sql") == 0) && + (strcasecmp(pRight->u.zToken, "rowcount") == 0)) + { + char sqlrowcount[13]; // strlen("sql") + strlen("%") + strlen("rowcount") + 1 + sprintf(sqlrowcount, "%s%%%s", pLeft->u.zToken, pRight->u.zToken); + + update_function_info(info, sqlrowcount, usage); + + pLeft = NULL; + pRight = NULL; + } + else + { + update_function_info(info, get_token_symbol(pExpr->op), usage); + } + } + else + { + update_function_info(info, get_token_symbol(pExpr->op), usage); + } + break; + + case TK_UMINUS: + switch (this_unit.parse_as) + { + case QC_PARSE_AS_DEFAULT: + update_function_info(info, get_token_symbol(pExpr->op), usage); + break; + + case QC_PARSE_AS_103: + // In MariaDB 10.3 a unary minus is not considered a function. + break; + + default: + ss_dassert(!true); + } + break; + case TK_FUNCTION: if (zToken) { @@ -1105,17 +1299,23 @@ static void update_field_infos(QC_SQLITE_INFO* info, { info->type_mask |= (QUERY_TYPE_READ | QUERY_TYPE_MASTER_READ); } + else if (is_sequence_related_function(info, zToken)) + { + info->type_mask |= QUERY_TYPE_WRITE; + ignore_exprlist = true; + } else if (!is_builtin_readonly_function(zToken, this_thread.version_major, this_thread.version_minor, - this_thread.version_patch)) + this_thread.version_patch, + info->sql_mode == QC_SQL_MODE_ORACLE)) { info->type_mask |= QUERY_TYPE_WRITE; } // We exclude "row", because we cannot detect all rows the same // way qc_mysqlembedded does. - if (strcasecmp(zToken, "row") != 0) + if (!ignore_exprlist && (strcasecmp(zToken, "row") != 0)) { update_function_info(info, zToken, usage); } @@ -1126,12 +1326,12 @@ static void update_field_infos(QC_SQLITE_INFO* info, break; } - if (pExpr->pLeft) + if (pLeft) { update_field_infos(info, pExpr->op, pExpr->pLeft, usage, QC_TOKEN_LEFT, pExclude); } - if (pExpr->pRight) + if (pRight) { if (usage & QC_USED_IN_SET) { @@ -1148,7 +1348,10 @@ static void update_field_infos(QC_SQLITE_INFO* info, case TK_BETWEEN: case TK_CASE: case TK_FUNCTION: - update_field_infos_from_exprlist(info, pExpr->x.pList, usage, pExclude); + if (!ignore_exprlist) + { + update_field_infos_from_exprlist(info, pExpr->x.pList, usage, pExclude); + } break; case TK_EXISTS: @@ -1281,35 +1484,38 @@ static void update_names(QC_SQLITE_INFO* info, const char* zDatabase, const char { if ((info->collect & QC_COLLECT_TABLES) && !(info->collected & QC_COLLECT_TABLES)) { - char* zCopy = MXS_STRDUP(zTable); - MXS_ABORT_IF_NULL(zCopy); - // TODO: Is this call really needed. Check also sqlite3Dequote. - exposed_sqlite3Dequote(zCopy); - - enlarge_string_array(1, info->table_names_len, &info->table_names, &info->table_names_capacity); - info->table_names[info->table_names_len++] = zCopy; - info->table_names[info->table_names_len] = NULL; - - if (zDatabase) + if (strcasecmp(zTable, "DUAL") != 0) { - zCopy = MXS_MALLOC(strlen(zDatabase) + 1 + strlen(zTable) + 1); + char* zCopy = MXS_STRDUP(zTable); MXS_ABORT_IF_NULL(zCopy); - - strcpy(zCopy, zDatabase); - strcat(zCopy, "."); - strcat(zCopy, zTable); + // TODO: Is this call really needed. Check also sqlite3Dequote. exposed_sqlite3Dequote(zCopy); - } - else - { - zCopy = MXS_STRDUP(zCopy); - MXS_ABORT_IF_NULL(zCopy); - } - enlarge_string_array(1, info->table_fullnames_len, - &info->table_fullnames, &info->table_fullnames_capacity); - info->table_fullnames[info->table_fullnames_len++] = zCopy; - info->table_fullnames[info->table_fullnames_len] = NULL; + enlarge_string_array(1, info->table_names_len, &info->table_names, &info->table_names_capacity); + info->table_names[info->table_names_len++] = zCopy; + info->table_names[info->table_names_len] = NULL; + + if (zDatabase) + { + zCopy = MXS_MALLOC(strlen(zDatabase) + 1 + strlen(zTable) + 1); + MXS_ABORT_IF_NULL(zCopy); + + strcpy(zCopy, zDatabase); + strcat(zCopy, "."); + strcat(zCopy, zTable); + exposed_sqlite3Dequote(zCopy); + } + else + { + zCopy = MXS_STRDUP(zCopy); + MXS_ABORT_IF_NULL(zCopy); + } + + enlarge_string_array(1, info->table_fullnames_len, + &info->table_fullnames, &info->table_fullnames_capacity); + info->table_fullnames[info->table_fullnames_len++] = zCopy; + info->table_fullnames[info->table_fullnames_len] = NULL; + } } if ((info->collect & QC_COLLECT_DATABASES) && !(info->collected & QC_COLLECT_DATABASES)) @@ -1383,15 +1589,18 @@ void mxs_sqlite3Analyze(Parse* pParse, SrcList* pSrcList) exposed_sqlite3SrcListDelete(pParse->db, pSrcList); } -void mxs_sqlite3BeginTransaction(Parse* pParse, int type) +void mxs_sqlite3BeginTransaction(Parse* pParse, int token, int type) { QC_TRACE(); QC_SQLITE_INFO* info = this_thread.info; ss_dassert(info); - info->status = QC_QUERY_PARSED; - info->type_mask = QUERY_TYPE_BEGIN_TRX | type; + if ((info->sql_mode != QC_SQL_MODE_ORACLE) || (token == TK_START)) + { + info->status = QC_QUERY_PARSED; + info->type_mask = QUERY_TYPE_BEGIN_TRX | type; + } } void mxs_sqlite3BeginTrigger(Parse *pParse, /* The parse context of the CREATE TRIGGER statement */ @@ -1530,60 +1739,64 @@ void mxs_sqlite3DeleteFrom(Parse* pParse, SrcList* pTabList, Expr* pWhere, SrcLi ss_dassert(info); info->status = QC_QUERY_PARSED; - info->type_mask = QUERY_TYPE_WRITE; - info->operation = QUERY_OP_DELETE; - info->has_clause = pWhere ? true : false; - if (pUsing) + if (info->operation != QUERY_OP_EXPLAIN) { - // Walk through the using declaration and update - // table and database names. - for (int i = 0; i < pUsing->nSrc; ++i) + info->type_mask = QUERY_TYPE_WRITE; + info->operation = QUERY_OP_DELETE; + info->has_clause = pWhere ? true : false; + + if (pUsing) { - struct SrcList_item* pItem = &pUsing->a[i]; - - update_names(info, pItem->zDatabase, pItem->zName); - } - - // Walk through the tablenames while excluding alias - // names from the using declaration. - for (int i = 0; i < pTabList->nSrc; ++i) - { - const struct SrcList_item* pTable = &pTabList->a[i]; - ss_dassert(pTable->zName); - int j = 0; - bool isSame = false; - - do + // Walk through the using declaration and update + // table and database names. + for (int i = 0; i < pUsing->nSrc; ++i) { - struct SrcList_item* pItem = &pUsing->a[j++]; + struct SrcList_item* pItem = &pUsing->a[i]; - if (strcasecmp(pTable->zName, pItem->zName) == 0) + update_names(info, pItem->zDatabase, pItem->zName); + } + + // Walk through the tablenames while excluding alias + // names from the using declaration. + for (int i = 0; i < pTabList->nSrc; ++i) + { + const struct SrcList_item* pTable = &pTabList->a[i]; + ss_dassert(pTable->zName); + int j = 0; + bool isSame = false; + + do { - isSame = true; + struct SrcList_item* pItem = &pUsing->a[j++]; + + if (strcasecmp(pTable->zName, pItem->zName) == 0) + { + isSame = true; + } + else if (pItem->zAlias && (strcasecmp(pTable->zName, pItem->zAlias) == 0)) + { + isSame = true; + } } - else if (pItem->zAlias && (strcasecmp(pTable->zName, pItem->zAlias) == 0)) + while (!isSame && (j < pUsing->nSrc)); + + if (!isSame) { - isSame = true; + // No alias name, update the table name. + update_names(info, pTable->zDatabase, pTable->zName); } } - while (!isSame && (j < pUsing->nSrc)); - - if (!isSame) - { - // No alias name, update the table name. - update_names(info, pTable->zDatabase, pTable->zName); - } } - } - else - { - update_names_from_srclist(info, pTabList); - } + else + { + update_names_from_srclist(info, pTabList); + } - if (pWhere) - { - update_field_infos(info, 0, pWhere, QC_USED_IN_WHERE, QC_TOKEN_MIDDLE, 0); + if (pWhere) + { + update_field_infos(info, 0, pWhere, QC_USED_IN_WHERE, QC_TOKEN_MIDDLE, 0); + } } exposed_sqlite3ExprDelete(pParse->db, pWhere); @@ -1683,36 +1896,40 @@ void mxs_sqlite3Insert(Parse* pParse, ss_dassert(info); info->status = QC_QUERY_PARSED; - info->type_mask = QUERY_TYPE_WRITE; - info->operation = QUERY_OP_INSERT; - ss_dassert(pTabList); - ss_dassert(pTabList->nSrc >= 1); - update_names_from_srclist(info, pTabList); - if (pColumns) + if (info->operation != QUERY_OP_EXPLAIN) { - update_field_infos_from_idlist(info, pColumns, 0, NULL); - } + info->type_mask = QUERY_TYPE_WRITE; + info->operation = QUERY_OP_INSERT; + ss_dassert(pTabList); + ss_dassert(pTabList->nSrc >= 1); + update_names_from_srclist(info, pTabList); - if (pSelect) - { - uint32_t usage; - - if (pSelect->selFlags & SF_Values) // Synthesized from VALUES clause + if (pColumns) { - usage = 0; - } - else - { - usage = QC_USED_IN_SELECT; + update_field_infos_from_idlist(info, pColumns, 0, NULL); } - update_field_infos_from_select(info, pSelect, usage, NULL); - } + if (pSelect) + { + uint32_t usage; - if (pSet) - { - update_field_infos_from_exprlist(info, pSet, 0, NULL); + if (pSelect->selFlags & SF_Values) // Synthesized from VALUES clause + { + usage = 0; + } + else + { + usage = QC_USED_IN_SELECT; + } + + update_field_infos_from_select(info, pSelect, usage, NULL); + } + + if (pSet) + { + update_field_infos_from_exprlist(info, pSet, 0, NULL); + } } exposed_sqlite3SrcListDelete(pParse->db, pTabList); @@ -1743,9 +1960,13 @@ int mxs_sqlite3Select(Parse* pParse, Select* p, SelectDest* pDest) if (!info->initializing) { info->status = QC_QUERY_PARSED; - info->operation = QUERY_OP_SELECT; - maxscaleCollectInfoFromSelect(pParse, p, 0); + if (info->operation != QUERY_OP_EXPLAIN) + { + info->operation = QUERY_OP_SELECT; + + maxscaleCollectInfoFromSelect(pParse, p, 0); + } // NOTE: By convention, the select is deleted in parse.y. } else @@ -1834,24 +2055,28 @@ void mxs_sqlite3Update(Parse* pParse, SrcList* pTabList, ExprList* pChanges, Exp ss_dassert(info); info->status = QC_QUERY_PARSED; - info->type_mask = QUERY_TYPE_WRITE; - info->operation = QUERY_OP_UPDATE; - update_names_from_srclist(info, pTabList); - info->has_clause = (pWhere ? true : false); - if (pChanges) + if (info->operation != QUERY_OP_EXPLAIN) { - for (int i = 0; i < pChanges->nExpr; ++i) + info->type_mask = QUERY_TYPE_WRITE; + info->operation = QUERY_OP_UPDATE; + update_names_from_srclist(info, pTabList); + info->has_clause = (pWhere ? true : false); + + if (pChanges) { - struct ExprList_item* pItem = &pChanges->a[i]; + for (int i = 0; i < pChanges->nExpr; ++i) + { + struct ExprList_item* pItem = &pChanges->a[i]; - update_field_infos(info, 0, pItem->pExpr, QC_USED_IN_SET, QC_TOKEN_MIDDLE, NULL); + update_field_infos(info, 0, pItem->pExpr, QC_USED_IN_SET, QC_TOKEN_MIDDLE, NULL); + } } - } - if (pWhere) - { - update_field_infos(info, 0, pWhere, QC_USED_IN_WHERE, QC_TOKEN_MIDDLE, pChanges); + if (pWhere) + { + update_field_infos(info, 0, pWhere, QC_USED_IN_WHERE, QC_TOKEN_MIDDLE, pChanges); + } } exposed_sqlite3SrcListDelete(pParse->db, pTabList); @@ -1928,7 +2153,7 @@ void maxscaleAlterTable(Parse *pParse, /* Parser context. */ exposed_sqlite3SrcListDelete(pParse->db, pSrc); } -void maxscaleCall(Parse* pParse, SrcList* pName) +void maxscaleCall(Parse* pParse, SrcList* pName, ExprList* pExprList) { QC_TRACE(); @@ -1938,7 +2163,13 @@ void maxscaleCall(Parse* pParse, SrcList* pName) info->status = QC_QUERY_PARSED; info->type_mask = QUERY_TYPE_WRITE; + if (pExprList) + { + update_field_infos_from_exprlist(info, pExprList, 0, NULL); + } + exposed_sqlite3SrcListDelete(pParse->db, pName); + exposed_sqlite3ExprListDelete(pParse->db, pExprList); } void maxscaleCheckTable(Parse* pParse, SrcList* pTables) @@ -1956,6 +2187,33 @@ void maxscaleCheckTable(Parse* pParse, SrcList* pTables) exposed_sqlite3SrcListDelete(pParse->db, pTables); } +void maxscaleCreateSequence(Parse* pParse, Token* pDatabase, Token* pTable) +{ + QC_TRACE(); + + QC_SQLITE_INFO* info = this_thread.info; + ss_dassert(info); + + info->status = QC_QUERY_PARSED; + + const char* zDatabase = NULL; + char database[pDatabase ? pDatabase->n + 1 : 1]; + + if (pDatabase) + { + strncpy(database, pDatabase->z, pDatabase->n); + database[pDatabase->n] = 0; + + zDatabase = database; + } + + char table[pTable->n + 1]; + strncpy(table, pTable->z, pTable->n); + table[pTable->n] = 0; + + update_names(info, zDatabase, table); +} + void maxscaleComment() { QC_TRACE(); @@ -1970,6 +2228,19 @@ void maxscaleComment() } } +void maxscaleDeclare(Parse* pParse) +{ + QC_TRACE(); + + QC_SQLITE_INFO* info = this_thread.info; + ss_dassert(info); + + if (info->sql_mode != QC_SQL_MODE_ORACLE) + { + info->status = QC_QUERY_INVALID; + } +} + void maxscaleDeallocate(Parse* pParse, Token* pName) { QC_TRACE(); @@ -2011,7 +2282,7 @@ void maxscaleDo(Parse* pParse, ExprList* pEList) exposed_sqlite3ExprListDelete(pParse->db, pEList); } -void maxscaleDrop(Parse* pParse, MxsDrop* pDrop) +void maxscaleDrop(Parse* pParse, int what, Token* pDatabase, Token* pName) { QC_TRACE(); @@ -2021,9 +2292,29 @@ void maxscaleDrop(Parse* pParse, MxsDrop* pDrop) info->status = QC_QUERY_PARSED; info->type_mask = (QUERY_TYPE_WRITE | QUERY_TYPE_COMMIT); info->operation = QUERY_OP_DROP; + + if (what == MXS_DROP_SEQUENCE) + { + const char* zDatabase = NULL; + char database[pDatabase ? pDatabase->n + 1 : 1]; + + if (pDatabase) + { + strncpy(database, pDatabase->z, pDatabase->n); + database[pDatabase->n] = 0; + + zDatabase = database; + } + + char table[pName->n + 1]; + strncpy(table, pName->z, pName->n); + table[pName->n] = 0; + + update_names(info, zDatabase, table); + } } -void maxscaleExecute(Parse* pParse, Token* pName) +void maxscaleExecute(Parse* pParse, Token* pName, int type_mask) { QC_TRACE(); @@ -2031,7 +2322,7 @@ void maxscaleExecute(Parse* pParse, Token* pName) ss_dassert(info); info->status = QC_QUERY_PARSED; - info->type_mask = QUERY_TYPE_WRITE; + info->type_mask = (QUERY_TYPE_WRITE | type_mask); info->operation = QUERY_OP_EXECUTE; // If information is collected in several passes, then we may @@ -2052,7 +2343,79 @@ void maxscaleExecute(Parse* pParse, Token* pName) } } -void maxscaleExplain(Parse* pParse, SrcList* pName) +static int32_t type_check_dynamic_string(const Expr* pExpr) +{ + int32_t type_mask = 0; + + if (pExpr) + { + switch (pExpr->op) + { + case TK_CONCAT: + type_mask |= type_check_dynamic_string(pExpr->pLeft); + type_mask |= type_check_dynamic_string(pExpr->pRight); + break; + + case TK_VARIABLE: + ss_dassert(pExpr->u.zToken); + { + const char* zToken = pExpr->u.zToken; + if (zToken[0] == '@') + { + if (zToken[1] == '@') + { + type_mask |= QUERY_TYPE_SYSVAR_READ; + } + else + { + type_mask |= QUERY_TYPE_USERVAR_READ; + } + } + } + break; + + default: + break; + } + } + + return type_mask; +} + +void maxscaleExecuteImmediate(Parse* pParse, Token* pName, ExprSpan* pExprSpan, int type_mask) +{ + QC_TRACE(); + + QC_SQLITE_INFO* info = this_thread.info; + ss_dassert(info); + + if (info->sql_mode == QC_SQL_MODE_ORACLE) + { + // This should be "EXECUTE IMMEDIATE ...", but as "IMMEDIATE" is not + // checked by the parser we do it here. + + static const char IMMEDIATE[] = "IMMEDIATE"; + + if ((pName->n == sizeof(IMMEDIATE) - 1) && (strncasecmp(pName->z, IMMEDIATE, pName->n)) == 0) + { + info->status = QC_QUERY_PARSED; + info->type_mask = (QUERY_TYPE_WRITE | type_mask); + info->type_mask |= type_check_dynamic_string(pExprSpan->pExpr); + } + else + { + info->status = QC_QUERY_INVALID; + } + } + else + { + info->status = QC_QUERY_INVALID; + } + + exposed_sqlite3ExprDelete(pParse->db, pExprSpan->pExpr); +} + +void maxscaleExplain(Parse* pParse, Token* pNext) { QC_TRACE(); @@ -2061,16 +2424,28 @@ void maxscaleExplain(Parse* pParse, SrcList* pName) info->status = QC_QUERY_PARSED; info->type_mask = QUERY_TYPE_READ; - update_names(info, pName->a[0].zDatabase, pName->a[0].zName); - uint32_t u = QC_USED_IN_SELECT; - update_field_info(info, "information_schema", "COLUMNS", "COLUMN_DEFAULT", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "COLUMN_KEY", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "COLUMN_NAME", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "COLUMN_TYPE", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "EXTRA", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "IS_NULLABLE", u, NULL); + info->operation = QUERY_OP_SHOW; - exposed_sqlite3SrcListDelete(pParse->db, pName); + if (pNext) + { + if (pNext->z) + { + const char EXTENDED[] = "EXTENDED"; + const char PARTITIONS[] = "PARTITIONS"; + const char FORMAT[] = "FORMAT"; + const char FOR[] = "FOR"; + +#define MATCHES_KEYWORD(t, k) ((t->n == sizeof(k) - 1) && (strncasecmp(t->z, k, t->n) == 0)) + + if (MATCHES_KEYWORD(pNext, EXTENDED) || + MATCHES_KEYWORD(pNext, PARTITIONS) || + MATCHES_KEYWORD(pNext, FORMAT) || + MATCHES_KEYWORD(pNext, FOR)) + { + info->operation = QUERY_OP_EXPLAIN; + } + } + } } void maxscaleFlush(Parse* pParse, Token* pWhat) @@ -2162,10 +2537,47 @@ void maxscaleLock(Parse* pParse, mxs_lock_t type, SrcList* pTables) } } -void maxscaleKeyword(int token) +int maxscaleTranslateKeyword(int token) +{ + QC_SQLITE_INFO* info = this_thread.info; + ss_dassert(info); + + switch (token) + { + case TK_CHARSET: + case TK_DO: + case TK_HANDLER: + if (info->sql_mode == QC_SQL_MODE_ORACLE) + { + // The keyword is translated, but only if it not used + // as the first keyword. Matters for DO and HANDLER. + if (info->keyword_1) + { + token = TK_ID; + } + } + break; + + default: + break; + } + + return token; +} + +/** + * Register the tokenization of a keyword. + * + * @param token A keyword code (check generated parse.h) + * + * @return Non-zero if all input should be consumed, 0 otherwise. + */ +int maxscaleKeyword(int token) { QC_TRACE(); + int rv = 0; + QC_SQLITE_INFO* info = this_thread.info; ss_dassert(info); @@ -2190,6 +2602,20 @@ void maxscaleKeyword(int token) info->operation = QUERY_OP_ALTER; break; + case TK_BEGIN: + case TK_DECLARE: + case TK_FOR: + if (info->sql_mode == QC_SQL_MODE_ORACLE) + { + // The beginning of a BLOCK. We'll assume it is in a single + // COM_QUERY packet and hence one GWBUF. + info->status = QC_QUERY_TOKENIZED; + info->type_mask = QUERY_TYPE_WRITE; + // Return non-0 to cause the entire input to be consumed. + rv = 1; + } + break; + case TK_CALL: info->status = QC_QUERY_TOKENIZED; info->type_mask = QUERY_TYPE_WRITE; @@ -2210,6 +2636,7 @@ void maxscaleKeyword(int token) case TK_DESC: info->status = QC_QUERY_TOKENIZED; info->type_mask = QUERY_TYPE_READ; + info->operation = QUERY_OP_EXPLAIN; break; case TK_DROP: @@ -2226,6 +2653,7 @@ void maxscaleKeyword(int token) case TK_EXPLAIN: info->status = QC_QUERY_TOKENIZED; info->type_mask = QUERY_TYPE_READ; + info->operation = QUERY_OP_EXPLAIN; break; case TK_GRANT: @@ -2279,7 +2707,8 @@ void maxscaleKeyword(int token) case TK_SHOW: info->status = QC_QUERY_TOKENIZED; - info->type_mask = QUERY_TYPE_WRITE; + info->type_mask = QUERY_TYPE_READ; + info->operation = QUERY_OP_SHOW; break; case TK_START: @@ -2378,6 +2807,8 @@ void maxscaleKeyword(int token) } } } + + return rv; } void maxscaleRenameTable(Parse* pParse, SrcList* pTables) @@ -2404,14 +2835,72 @@ void maxscaleRenameTable(Parse* pParse, SrcList* pTables) exposed_sqlite3SrcListDelete(pParse->db, pTables); } -void maxscalePrepare(Parse* pParse, Token* pName, Token* pStmt) +/** + * Returns some string from an expression. + * + * @param pExpr An expression. + * + * @return Some string referred to in pExpr. + */ +static const char* find_one_string(Expr* pExpr) +{ + const char* z = NULL; + + if (pExpr->op == TK_STRING) + { + ss_dassert(pExpr->u.zToken); + z = pExpr->u.zToken; + } + + if (!z && pExpr->pLeft) + { + z = find_one_string(pExpr->pLeft); + } + + if (!z && pExpr->pRight) + { + z = find_one_string(pExpr->pRight); + } + + return z; +} + +void maxscalePrepare(Parse* pParse, Token* pName, Expr* pStmt) { QC_TRACE(); QC_SQLITE_INFO* info = this_thread.info; ss_dassert(info); - info->status = QC_QUERY_PARSED; + // If the mode is MODE_ORACLE then if expression contains simply a string + // we can conclude that the statement has been fully parsed, because it will + // be sensible to parse the preparable statement. Otherwise we mark the + // statement as having been partially parsed, since the preparable statement + // will not contain the full statement. + if (info->sql_mode == QC_SQL_MODE_ORACLE) + { + if (pStmt->op == TK_STRING) + { + info->status = QC_QUERY_PARSED; + } + else + { + info->status = QC_QUERY_PARTIALLY_PARSED; + } + } + else + { + // If the mode is not MODE_ORACLE, then only a string is acceptable. + if (pStmt->op == TK_STRING) + { + info->status = QC_QUERY_PARSED; + } + else + { + info->status = QC_QUERY_INVALID; + } + } + info->type_mask = QUERY_TYPE_PREPARE_NAMED_STMT; // If information is collected in several passes, then we may @@ -2425,25 +2914,38 @@ void maxscalePrepare(Parse* pParse, Token* pName, Token* pStmt) info->prepare_name[pName->n] = 0; } - size_t preparable_stmt_len = pStmt->n - 2; - size_t payload_len = 1 + preparable_stmt_len; - size_t packet_len = MYSQL_HEADER_LEN + payload_len; + // If the expression just contains a string, then zStmt will + // be that string. Otherwise it will be _some_ string from the + // expression. In the latter case we've already marked the result + // to have been partially parsed. + const char* zStmt = find_one_string(pStmt); - info->preparable_stmt = gwbuf_alloc(packet_len); - - if (info->preparable_stmt) + if (zStmt) { - uint8_t* ptr = GWBUF_DATA(info->preparable_stmt); - // Payload length - *ptr++ = payload_len; - *ptr++ = (payload_len >> 8); - *ptr++ = (payload_len >> 16); - // Sequence id - *ptr++ = 0x00; - // Command - *ptr++ = MYSQL_COM_QUERY; + size_t preparable_stmt_len = zStmt ? strlen(zStmt) : 0; + size_t payload_len = 1 + preparable_stmt_len; + size_t packet_len = MYSQL_HEADER_LEN + payload_len; - memcpy(ptr, pStmt->z + 1, pStmt->n - 2); + info->preparable_stmt = gwbuf_alloc(packet_len); + + if (info->preparable_stmt) + { + uint8_t* ptr = GWBUF_DATA(info->preparable_stmt); + // Payload length + *ptr++ = payload_len; + *ptr++ = (payload_len >> 8); + *ptr++ = (payload_len >> 16); + // Sequence id + *ptr++ = 0x00; + // Command + *ptr++ = MYSQL_COM_QUERY; + + memcpy(ptr, zStmt, preparable_stmt_len); + } + } + else + { + info->status = QC_QUERY_INVALID; } } else @@ -2451,6 +2953,8 @@ void maxscalePrepare(Parse* pParse, Token* pName, Token* pStmt) ss_dassert(info->collect != info->collected); ss_dassert(strncmp(info->prepare_name, pName->z, pName->n) == 0); } + + exposed_sqlite3ExprDelete(pParse->db, pStmt); } void maxscalePrivileges(Parse* pParse, int kind) @@ -2663,188 +3167,87 @@ extern void maxscaleShow(Parse* pParse, MxsShow* pShow) ss_dassert(info); info->status = QC_QUERY_PARSED; - - char* zDatabase = NULL; - char* zName = NULL; - - char database[pShow->pDatabase ? pShow->pDatabase->n + 1 : 0]; - if (pShow->pDatabase) - { - strncpy(database, pShow->pDatabase->z, pShow->pDatabase->n); - database[pShow->pDatabase->n] = 0; - zDatabase = database; - } - - char name[pShow->pName ? pShow->pName->n + 1 : 0]; - if (pShow->pName) - { - strncpy(name, pShow->pName->z, pShow->pName->n); - name[pShow->pName->n] = 0; - zName = name; - } + info->operation = QUERY_OP_SHOW; uint32_t u = QC_USED_IN_SELECT; switch (pShow->what) { case MXS_SHOW_COLUMNS: - { - info->type_mask = QUERY_TYPE_READ; - update_names(info, zDatabase, zName); - if (pShow->data == MXS_SHOW_COLUMNS_FULL) - { - update_field_info(info, "information_schema", "COLUMNS", "COLLATION_NAME", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "COLUMN_COMMENT", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "COLUMN_DEFAULT", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "COLUMN_KEY", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "COLUMN_NAME", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "COLUMN_TYPE", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "EXTRA", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "IS_NULLABLE", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "PRIVILEGES", u, NULL); - } - else - { - update_field_info(info, "information_schema", "COLUMNS", "COLUMN_DEFAULT", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "COLUMN_KEY", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "COLUMN_NAME", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "COLUMN_TYPE", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "EXTRA", u, NULL); - update_field_info(info, "information_schema", "COLUMNS", "IS_NULLABLE", u, NULL); - } - } + info->type_mask = QUERY_TYPE_READ; + break; + + case MXS_SHOW_CREATE_SEQUENCE: + info->type_mask = QUERY_TYPE_READ; break; case MXS_SHOW_CREATE_VIEW: - { - info->type_mask = QUERY_TYPE_WRITE; - update_names(info, zDatabase, zName); - } + info->type_mask = QUERY_TYPE_READ; break; case MXS_SHOW_CREATE_TABLE: - { - info->type_mask = QUERY_TYPE_WRITE; - update_names(info, zDatabase, zName); - } + info->type_mask = QUERY_TYPE_READ; break; case MXS_SHOW_DATABASES: - { - info->type_mask = QUERY_TYPE_SHOW_DATABASES; - update_names(info, "information_schema", "SCHEMATA"); - update_field_info(info, "information_schema", "SCHEMATA", "SCHEMA_NAME", u, NULL); - } + info->type_mask = QUERY_TYPE_SHOW_DATABASES; break; case MXS_SHOW_INDEX: case MXS_SHOW_INDEXES: case MXS_SHOW_KEYS: - { - info->type_mask = QUERY_TYPE_WRITE; - update_names(info, "information_schema", "STATISTICS"); - update_field_info(info, "information_schema", "STATISTICS", "CARDINALITY", u, NULL); - update_field_info(info, "information_schema", "STATISTICS", "COLLATION", u, NULL); - update_field_info(info, "information_schema", "STATISTICS", "COLUMN_NAME", u, NULL); - update_field_info(info, "information_schema", "STATISTICS", "COMMENT", u, NULL); - update_field_info(info, "information_schema", "STATISTICS", "INDEX_COMMENT", u, NULL); - update_field_info(info, "information_schema", "STATISTICS", "INDEX_NAME", u, NULL); - update_field_info(info, "information_schema", "STATISTICS", "INDEX_TYPE", u, NULL); - update_field_info(info, "information_schema", "STATISTICS", "NON_UNIQUE", u, NULL); - update_field_info(info, "information_schema", "STATISTICS", "NULLABLE", u, NULL); - update_field_info(info, "information_schema", "STATISTICS", "PACKED", u, NULL); - update_field_info(info, "information_schema", "STATISTICS", "SEQ_IN_INDEX", u, NULL); - update_field_info(info, "information_schema", "STATISTICS", "SUB_PART", u, NULL); - update_field_info(info, "information_schema", "STATISTICS", "TABLE_NAME", u, NULL); - } + info->type_mask = QUERY_TYPE_WRITE; break; case MXS_SHOW_TABLE_STATUS: - { - info->type_mask = QUERY_TYPE_WRITE; - update_names(info, "information_schema", "TABLES"); - update_field_info(info, "information_schema", "TABLES", "AUTO_INCREMENT", u, NULL); - update_field_info(info, "information_schema", "TABLES", "AVG_ROW_LENGTH", u, NULL); - update_field_info(info, "information_schema", "TABLES", "CHECKSUM", u, NULL); - update_field_info(info, "information_schema", "TABLES", "CHECK_TIME", u, NULL); - update_field_info(info, "information_schema", "TABLES", "CREATE_OPTIONS", u, NULL); - update_field_info(info, "information_schema", "TABLES", "CREATE_TIME", u, NULL); - update_field_info(info, "information_schema", "TABLES", "DATA_FREE", u, NULL); - update_field_info(info, "information_schema", "TABLES", "DATA_LENGTH", u, NULL); - update_field_info(info, "information_schema", "TABLES", "ENGINE", u, NULL); - update_field_info(info, "information_schema", "TABLES", "INDEX_LENGTH", u, NULL); - update_field_info(info, "information_schema", "TABLES", "MAX_DATA_LENGTH", u, NULL); - update_field_info(info, "information_schema", "TABLES", "ROW_FORMAT", u, NULL); - update_field_info(info, "information_schema", "TABLES", "TABLE_COLLATION", u, NULL); - update_field_info(info, "information_schema", "TABLES", "TABLE_COMMENT", u, NULL); - update_field_info(info, "information_schema", "TABLES", "TABLE_NAME", u, NULL); - update_field_info(info, "information_schema", "TABLES", "TABLE_ROWS", u, NULL); - update_field_info(info, "information_schema", "TABLES", "UPDATE_TIME", u, NULL); - update_field_info(info, "information_schema", "TABLES", "VERSION", u, NULL); - } + info->type_mask = QUERY_TYPE_WRITE; break; case MXS_SHOW_STATUS: + switch (pShow->data) { - switch (pShow->data) - { - case MXS_SHOW_VARIABLES_GLOBAL: - case MXS_SHOW_VARIABLES_SESSION: - case MXS_SHOW_VARIABLES_UNSPECIFIED: - // TODO: qc_mysqlembedded does not set the type bit. - info->type_mask = QUERY_TYPE_UNKNOWN; - update_names(info, "information_schema", "SESSION_STATUS"); - update_field_info(info, "information_schema", "SESSION_STATUS", "VARIABLE_NAME", u, NULL); - update_field_info(info, "information_schema", "SESSION_STATUS", "VARIABLE_VALUE", u, NULL); - break; + case MXS_SHOW_VARIABLES_GLOBAL: + case MXS_SHOW_VARIABLES_SESSION: + case MXS_SHOW_VARIABLES_UNSPECIFIED: + info->type_mask = QUERY_TYPE_READ; + break; - case MXS_SHOW_STATUS_MASTER: - info->type_mask = QUERY_TYPE_WRITE; - break; + case MXS_SHOW_STATUS_MASTER: + info->type_mask = QUERY_TYPE_WRITE; + break; - case MXS_SHOW_STATUS_SLAVE: - info->type_mask = QUERY_TYPE_READ; - break; + case MXS_SHOW_STATUS_SLAVE: + info->type_mask = QUERY_TYPE_READ; + break; - case MXS_SHOW_STATUS_ALL_SLAVES: - info->type_mask = QUERY_TYPE_READ; - break; + case MXS_SHOW_STATUS_ALL_SLAVES: + info->type_mask = QUERY_TYPE_READ; + break; - default: - break; - } + default: + info->type_mask = QUERY_TYPE_READ; + break; } break; case MXS_SHOW_TABLES: - { - info->type_mask = QUERY_TYPE_SHOW_TABLES; - update_names(info, "information_schema", "TABLE_NAMES"); - update_field_info(info, "information_schema", "TABLE_NAMES", "TABLE_NAME", u, NULL); - } + info->type_mask = QUERY_TYPE_SHOW_TABLES; break; case MXS_SHOW_VARIABLES: + if (pShow->data == MXS_SHOW_VARIABLES_GLOBAL) { - if (pShow->data == MXS_SHOW_VARIABLES_GLOBAL) - { - info->type_mask = QUERY_TYPE_GSYSVAR_READ; - } - else - { - info->type_mask = QUERY_TYPE_SYSVAR_READ; - } - update_names(info, "information_schema", "SESSION_VARIABLES"); - update_field_info(info, "information_schema", "SESSION_STATUS", "VARIABLE_NAME", u, NULL); - update_field_info(info, "information_schema", "SESSION_STATUS", "VARIABLE_VALUE", u, NULL); + info->type_mask = QUERY_TYPE_GSYSVAR_READ; + } + else + { + info->type_mask = QUERY_TYPE_SYSVAR_READ; } break; case MXS_SHOW_WARNINGS: - { - // qc_mysqliembedded claims this. - info->type_mask = QUERY_TYPE_WRITE; - } + // qc_mysqliembedded claims this. + info->type_mask = QUERY_TYPE_WRITE; break; default: @@ -2899,7 +3302,7 @@ void maxscaleUse(Parse* pParse, Token* pToken) /** * API */ -static int32_t qc_sqlite_setup(const char* args); +static int32_t qc_sqlite_setup(qc_sql_mode_t sql_mode, const char* args); static int32_t qc_sqlite_process_init(void); static void qc_sqlite_process_end(void); static int32_t qc_sqlite_thread_init(void); @@ -2916,6 +3319,8 @@ static int32_t qc_sqlite_get_database_names(GWBUF* query, char*** names, int* si static int32_t qc_sqlite_get_preparable_stmt(GWBUF* stmt, GWBUF** preparable_stmt); static void qc_sqlite_set_server_version(uint64_t version); static void qc_sqlite_get_server_version(uint64_t* version); +static int32_t qc_sqlite_get_sql_mode(qc_sql_mode_t* sql_mode); +static int32_t qc_sqlite_set_sql_mode(qc_sql_mode_t sql_mode); static bool get_key_and_value(char* arg, const char** pkey, const char** pvalue) { @@ -2932,54 +3337,90 @@ static bool get_key_and_value(char* arg, const char** pkey, const char** pvalue) return p != NULL; } -static char ARG_LOG_UNRECOGNIZED_STATEMENTS[] = "log_unrecognized_statements"; +static const char ARG_LOG_UNRECOGNIZED_STATEMENTS[] = "log_unrecognized_statements"; +static const char ARG_PARSE_AS[] = "parse_as"; -static int32_t qc_sqlite_setup(const char* args) +static int32_t qc_sqlite_setup(qc_sql_mode_t sql_mode, const char* cargs) { QC_TRACE(); assert(!this_unit.setup); qc_log_level_t log_level = QC_LOG_NOTHING; + qc_parse_as_t parse_as = (sql_mode == QC_SQL_MODE_ORACLE) ? QC_PARSE_AS_103 : QC_PARSE_AS_DEFAULT; + QC_NAME_MAPPING* function_name_mappings = function_name_mappings_default; - if (args) + if (cargs) { - char arg[strlen(args) + 1]; - strcpy(arg, args); + char args[strlen(cargs) + 1]; + strcpy(args, cargs); - const char* key; - const char* value; + char *p1; + char *token = strtok_r(args, ",", &p1); - if (get_key_and_value(arg, &key, &value)) + while (token) { - if (strcmp(key, ARG_LOG_UNRECOGNIZED_STATEMENTS) == 0) + const char* key; + const char* value; + + if (get_key_and_value(token, &key, &value)) { - char *end; - - long l = strtol(value, &end, 0); - - if ((*end == 0) && (l >= QC_LOG_NOTHING) && (l <= QC_LOG_NON_TOKENIZED)) + if (strcmp(key, ARG_LOG_UNRECOGNIZED_STATEMENTS) == 0) { - log_level = l; + char *end; + + long l = strtol(value, &end, 0); + + if ((*end == 0) && (l >= QC_LOG_NOTHING) && (l <= QC_LOG_NON_TOKENIZED)) + { + log_level = l; + } + else + { + MXS_WARNING("'%s' is not a number between %d and %d.", + value, QC_LOG_NOTHING, QC_LOG_NON_TOKENIZED); + } + } + else if (strcmp(key, ARG_PARSE_AS) == 0) + { + if (strcmp(value, "10.3") == 0) + { + parse_as = QC_PARSE_AS_103; + MXS_NOTICE("Parsing as 10.3."); + } + else + { + MXS_WARNING("'%s' is not a recognized value for '%s'. " + "Parsing as pre-10.3.", value, key); + } } else { - MXS_WARNING("'%s' is not a number between %d and %d.", - value, QC_LOG_NOTHING, QC_LOG_NON_TOKENIZED); + MXS_WARNING("'%s' is not a recognized argument.", key); } } else { - MXS_WARNING("'%s' is not a recognized argument.", key); + MXS_WARNING("'%s' is not a recognized argument string.", args); } + + token = strtok_r(NULL, ",", &p1); } - else - { - MXS_WARNING("'%s' is not a recognized argument string.", args); - } + } + + if (sql_mode == QC_SQL_MODE_ORACLE) + { + function_name_mappings = function_name_mappings_oracle; + } + else if (parse_as == QC_PARSE_AS_103) + { + function_name_mappings = function_name_mappings_103; } this_unit.setup = true; this_unit.log_level = log_level; + this_unit.sql_mode = sql_mode; + this_unit.parse_as = parse_as; + this_unit.function_name_mappings = function_name_mappings; return this_unit.setup ? QC_RESULT_OK : QC_RESULT_ERROR; } @@ -3061,6 +3502,8 @@ static int32_t qc_sqlite_thread_init(void) int rc = sqlite3_open(":memory:", &this_thread.db); if (rc == SQLITE_OK) { + this_thread.sql_mode = this_unit.sql_mode; + this_thread.function_name_mappings = this_unit.function_name_mappings; this_thread.initialized = true; MXS_INFO("In-memory sqlite database successfully opened for thread %lu.", @@ -3074,7 +3517,7 @@ static int32_t qc_sqlite_thread_init(void) // With this statement we cause sqlite3 to initialize itself, so that it // is not done as part of the actual classification of data. - const char* s = "CREATE TABLE __maxscale__internal__ (int field UNIQUE)"; + const char* s = "CREATE TABLE __maxscale__internal__ (field int UNIQUE)"; size_t len = strlen(s); this_thread.info->query = s; @@ -3554,6 +3997,43 @@ static void qc_sqlite_get_server_version(uint64_t* version) *version = this_thread.version_major * 10000 + this_thread.version_minor * 100 + this_thread.version_patch; } + +int32_t qc_sqlite_get_sql_mode(qc_sql_mode_t* sql_mode) +{ + *sql_mode = this_thread.sql_mode; + return QC_RESULT_OK; +} + +int32_t qc_sqlite_set_sql_mode(qc_sql_mode_t sql_mode) +{ + int32_t rv = QC_RESULT_OK; + + switch (sql_mode) + { + case QC_SQL_MODE_DEFAULT: + this_thread.sql_mode = sql_mode; + if (this_unit.parse_as == QC_PARSE_AS_103) + { + this_thread.function_name_mappings = function_name_mappings_103; + } + else + { + this_thread.function_name_mappings = function_name_mappings_default; + } + break; + + case QC_SQL_MODE_ORACLE: + this_thread.sql_mode = sql_mode; + this_thread.function_name_mappings = function_name_mappings_oracle; + break; + + default: + rv = QC_RESULT_ERROR; + } + + return rv; +} + /** * EXPORTS */ @@ -3582,6 +4062,8 @@ MXS_MODULE* MXS_CREATE_MODULE() qc_sqlite_get_preparable_stmt, qc_sqlite_set_server_version, qc_sqlite_get_server_version, + qc_sqlite_get_sql_mode, + qc_sqlite_set_sql_mode, }; static MXS_MODULE info = diff --git a/query_classifier/qc_sqlite/sqlite-src-3110100/src/parse.y b/query_classifier/qc_sqlite/sqlite-src-3110100/src/parse.y index 1f24bbfde..28b331bd4 100644 --- a/query_classifier/qc_sqlite/sqlite-src-3110100/src/parse.y +++ b/query_classifier/qc_sqlite/sqlite-src-3110100/src/parse.y @@ -64,6 +64,7 @@ enum { QUERY_TYPE_READ = 0x000002, /*< Read database data:any */ QUERY_TYPE_WRITE = 0x000004, /*< Master data will be modified:master */ + QUERY_TYPE_USERVAR_READ = 0x000040, /*< Read a user variable:master or any */ }; typedef enum qc_field_usage @@ -85,7 +86,7 @@ typedef enum qc_field_usage extern void mxs_sqlite3AlterFinishAddColumn(Parse *, Token *); extern void mxs_sqlite3AlterBeginAddColumn(Parse *, SrcList *); extern void mxs_sqlite3Analyze(Parse *, SrcList *); -extern void mxs_sqlite3BeginTransaction(Parse*, int); +extern void mxs_sqlite3BeginTransaction(Parse*, int token, int type); extern void mxs_sqlite3CommitTransaction(Parse*); extern void mxs_sqlite3CreateIndex(Parse*,Token*,Token*,SrcList*,ExprList*,int,Token*, Expr*, int, int); @@ -107,18 +108,21 @@ extern void mxs_sqlite3Update(Parse*, SrcList*, ExprList*, Expr*, int); extern void maxscaleCollectInfoFromSelect(Parse*, Select*, int); extern void maxscaleAlterTable(Parse*, mxs_alter_t command, SrcList*, Token*); -extern void maxscaleCall(Parse*, SrcList* pName); +extern void maxscaleCall(Parse*, SrcList* pName, ExprList* pExprList); extern void maxscaleCheckTable(Parse*, SrcList* pTables); +extern void maxscaleCreateSequence(Parse*, Token* pDatabase, Token* pTable); +extern void maxscaleDeclare(Parse* pParse); extern void maxscaleDeallocate(Parse*, Token* pName); extern void maxscaleDo(Parse*, ExprList* pEList); -extern void maxscaleDrop(Parse*, MxsDrop* pDrop); -extern void maxscaleExecute(Parse*, Token* pName); -extern void maxscaleExplain(Parse*, SrcList* pName); +extern void maxscaleDrop(Parse*, int what, Token* pDatabase, Token* pName); +extern void maxscaleExecute(Parse*, Token* pName, int type_mask); +extern void maxscaleExecuteImmediate(Parse*, Token* pName, ExprSpan* pExprSpan, int type_mask); +extern void maxscaleExplain(Parse*, Token* pNext); extern void maxscaleFlush(Parse*, Token* pWhat); extern void maxscaleHandler(Parse*, mxs_handler_t, SrcList* pFullName, Token* pName); extern void maxscaleLoadData(Parse*, SrcList* pFullName); extern void maxscaleLock(Parse*, mxs_lock_t, SrcList*); -extern void maxscalePrepare(Parse*, Token* pName, Token* pStmt); +extern void maxscalePrepare(Parse*, Token* pName, Expr* pStmt); extern void maxscalePrivileges(Parse*, int kind); extern void maxscaleRenameTable(Parse*, SrcList* pTables); extern void maxscaleSet(Parse*, int scope, mxs_set_t kind, ExprList*); @@ -276,33 +280,23 @@ input ::= cmdlist. cmdlist ::= cmdlist ecmd. cmdlist ::= ecmd. ecmd ::= SEMI. -ecmd ::= explain cmdx SEMI. +ecmd ::= explain SEMI. +ecmd ::= cmdx SEMI. +ecmd ::= oracle_assignment SEMI. %ifdef MAXSCALE explain_kw ::= EXPLAIN. // Also covers DESCRIBE explain_kw ::= DESC. -ecmd ::= explain_kw fullname(X) SEMI. { - pParse->explain = 1; - maxscaleExplain(pParse, X); -} +explain ::= explain_kw. { pParse->explain = 1; } // deferred_id is defined later, after the id token_class has been defined. -ecmd ::= explain FOR deferred_id INTEGER SEMI. { // FOR CONNECTION connection_id +explain ::= explain_kw deferred_id(A). { maxscaleExplain(pParse, &A); } +explain ::= explain_kw deferred_id(A) DOT deferred_id. { maxscaleExplain(pParse, &A); } +ecmd ::= explain FOR(A) deferred_id INTEGER SEMI. { // FOR CONNECTION connection_id pParse->explain = 1; - maxscaleExplain(pParse, 0); + maxscaleExplain(pParse, &A); } %endif -explain ::= . %ifndef SQLITE_OMIT_EXPLAIN -%ifdef MAXSCALE -explain_type_opt ::= . -explain_type_opt ::= deferred_id. // EXTENDED | PARTITIONS -explain_type_opt ::= deferred_id eq deferred_id. // FORMAT = {TRADITIONAL|JSON} - -explain ::= explain_kw explain_type_opt. { pParse->explain = 1; } -%endif -%ifndef MAXSCALE -explain ::= EXPLAIN. { pParse->explain = 1; } -%endif %ifndef MAXSCALE explain ::= EXPLAIN QUERY PLAN. { pParse->explain = 2; } %endif @@ -315,7 +309,7 @@ cmdx ::= cmd. { sqlite3FinishCoding(pParse); } %ifdef MAXSCALE work_opt ::= WORK. work_opt ::= . -cmd ::= BEGIN work_opt. {mxs_sqlite3BeginTransaction(pParse, 0);} // BEGIN [WORK] +cmd ::= BEGIN work_opt. {mxs_sqlite3BeginTransaction(pParse, TK_BEGIN, 0);} // BEGIN [WORK] %endif %ifndef MAXSCALE cmd ::= BEGIN transtype(Y) trans_opt. {sqlite3BeginTransaction(pParse, Y);} @@ -590,14 +584,14 @@ columnid(A) ::= nm(X). { %endif %ifdef MAXSCALE /*ABORT*/ ACTION AFTER ALGORITHM /*ANALYZE*/ /*ASC*/ /*ATTACH*/ - /*BEFORE*/ BEGIN BY + /*BEFORE*/ /*BEGIN*/ BY // TODO: BINARY is a reserved word and should not automatically convert into an identifer. // TODO: However, if not here then rules such as CAST need to be modified. BINARY /*CASCADE*/ CAST CLOSE COLUMNKW COLUMNS COMMENT CONCURRENT /*CONFLICT*/ DATA /*DATABASE*/ DEALLOCATE DEFERRED /*DESC*/ /*DETACH*/ DUMPFILE /*EACH*/ END ENUM EXCLUSIVE /*EXPLAIN*/ - FIRST FLUSH /*FOR*/ + FIRST FLUSH /*FOR*/ FORMAT GLOBAL // TODO: IF is a reserved word and should not automatically convert into an identifer. IF IMMEDIATE INITIALLY INSTEAD @@ -607,10 +601,10 @@ columnid(A) ::= nm(X). { NO OF OFFSET OPEN QUICK - RAISE RECURSIVE /*REINDEX*/ RELEASE /*RENAME*/ REPLACE RESTRICT ROLLBACK ROLLUP ROW - SAVEPOINT SELECT_OPTIONS_KW SLAVE START STATUS + RAISE RECURSIVE /*REINDEX*/ RELEASE /*RENAME*/ /*REPLACE*/ RESTRICT ROLLBACK ROLLUP ROW + SAVEPOINT SELECT_OPTIONS_KW /*SEQUENCE*/ SLAVE /*START*/ STATUS TABLES TEMP TEMPTABLE /*TRIGGER*/ - TRUNCATE + /*TRUNCATE*/ // TODO: UNSIGNED is a reserved word and should not automatically convert into an identifer. // TODO: However, if not here then rules such as CAST need to be modified. UNSIGNED @@ -663,6 +657,10 @@ eq ::= EQ. nm(A) ::= id(X). {A = X;} nm(A) ::= STRING(X). {A = X;} nm(A) ::= JOIN_KW(X). {A = X;} +nm(A) ::= START(X). {A = X;} +nm(A) ::= TRUNCATE(X). {A = X;} +nm(A) ::= BEGIN(X). {A = X;} +nm(A) ::= REPLACE(X). {A = X;} // A typetoken is really one or more tokens that form a type name such // as can be found after the column name in a CREATE TABLE statement. @@ -1122,6 +1120,7 @@ select_into(A) ::= INTO OUTFILE STRING. {A = sqlite3ExprListAppend(pParse, 0, 0) %type select_options {int} select_options(A) ::= . {A = 0;} select_options(A) ::= select_options DISTINCT. {A = SF_Distinct;} +select_options(A) ::= select_options UNIQUE. {A = SF_Distinct;} select_options(A) ::= select_options ALL. {A = SF_All;} select_options(A) ::= select_options(X) HIGH_PRIORITY. {A = X;} select_options(A) ::= select_options(X) SELECT_OPTIONS_KW. {A = X;} @@ -1131,6 +1130,9 @@ select_options(A) ::= select_options(X) STRAIGHT_JOIN. {A = X;} // present and false (0) if it is not. // %type distinct {int} +%ifdef MAXSCALE +distinct(A) ::= UNIQUE. {A = SF_Distinct;} +%endif distinct(A) ::= DISTINCT. {A = SF_Distinct;} distinct(A) ::= ALL. {A = SF_All;} distinct(A) ::= . {A = 0;} @@ -1771,13 +1773,18 @@ term(A) ::= DEFAULT(X). {spanExpr(&A, pParse, @X, &X);} %endif term(A) ::= NULL(X). {spanExpr(&A, pParse, @X, &X);} expr(A) ::= id(X). {spanExpr(&A, pParse, TK_ID, &X);} -expr(A) ::= JOIN_KW(X). {spanExpr(&A, pParse, TK_ID, &X);} expr(A) ::= nm(X) DOT nm(Y). { Expr *temp1 = sqlite3PExpr(pParse, TK_ID, 0, 0, &X); Expr *temp2 = sqlite3PExpr(pParse, TK_ID, 0, 0, &Y); A.pExpr = sqlite3PExpr(pParse, TK_DOT, temp1, temp2, 0); spanSet(&A,&X,&Y); } +expr(A) ::= DOT nm(X) DOT nm(Y). { + Expr *temp1 = sqlite3PExpr(pParse, TK_ID, 0, 0, &X); + Expr *temp2 = sqlite3PExpr(pParse, TK_ID, 0, 0, &Y); + A.pExpr = sqlite3PExpr(pParse, TK_DOT, temp1, temp2, 0); + spanSet(&A,&X,&Y); +} expr(A) ::= nm(X) DOT nm(Y) DOT nm(Z). { Expr *temp1 = sqlite3PExpr(pParse, TK_ID, 0, 0, &X); Expr *temp2 = sqlite3PExpr(pParse, TK_ID, 0, 0, &Y); @@ -2675,7 +2682,7 @@ type_options(A) ::= type_options CHARACTER SET ids. {A|=8;} type_options(A) ::= type_options CHARSET ids. {A|=8;} // deferred_id is used instead of id before the token_class id has been defined. -deferred_id ::= id. +deferred_id(A) ::= id(X). {A=X;} as_opt ::= . as_opt ::= AS. @@ -2690,31 +2697,18 @@ default_opt ::= DEFAULT. // cmd ::= call. -call_arg ::= INTEGER. -call_arg ::= FLOAT. -call_arg ::= STRING. -call_arg ::= id. -call_arg ::= VARIABLE. +%type call_args_opt {ExprList*} +call_args_opt(A) ::= . {A=0;} +call_args_opt(A) ::= LP exprlist(X) RP. {A=X;} -call_args ::= call_arg. -call_args ::= call_args COMMA call_arg. - -call_args_opt ::= . -call_args_opt ::= LP RP. -call_args_opt ::= LP call_args RP. - -call ::= CALL fullname(X) call_args_opt. { - maxscaleCall(pParse, X); +call ::= CALL fullname(X) call_args_opt(Y). { + maxscaleCall(pParse, X, Y); } //////////////////////// DROP FUNCTION statement //////////////////////////////////// // cmd ::= DROP FUNCTION_KW ifexists nm(X). { - MxsDrop drop; - drop.what = MXS_DROP_FUNCTION; - drop.token = X; - - maxscaleDrop(pParse, &drop); + maxscaleDrop(pParse, MXS_DROP_FUNCTION, NULL, &X); } //////////////////////// The CHECK TABLE statement //////////////////////////////////// @@ -2845,19 +2839,32 @@ cmd ::= prepare. cmd ::= execute. cmd ::= deallocate. -prepare ::= PREPARE nm(X) FROM STRING(Y). +prepare ::= PREPARE nm(X) FROM expr(Y). { - maxscalePrepare(pParse, &X, &Y); + maxscalePrepare(pParse, &X, Y.pExpr); } -execute_variables ::= VARIABLE. -execute_variables ::= execute_variables COMMA VARIABLE. +%type execute_variable {int} +execute_variable(A) ::= INTEGER. {A=0;} // For Oracle +execute_variable(A) ::= VARIABLE. {A=QUERY_TYPE_USERVAR_READ;} -execute_variables_opt ::= . -execute_variables_opt ::= USING execute_variables. +%type execute_variables {int} +execute_variables(A) ::= execute_variable(X). {A=X;} +execute_variables(A) ::= execute_variables(X) COMMA execute_variable(Y). { + A = X|Y; +} -execute ::= EXECUTE nm(X) execute_variables_opt. { - maxscaleExecute(pParse, &X); +%type execute_variables_opt {int} + +execute_variables_opt(A) ::= . {A=0;} +execute_variables_opt(A) ::= USING execute_variables(X). {A=X;} + +execute ::= EXECUTE nm(X) execute_variables_opt(Y). { + maxscaleExecute(pParse, &X, Y); +} + +execute ::= EXECUTE id(X) expr(Y) execute_variables_opt(Z). { + maxscaleExecuteImmediate(pParse, &X, &Y, Z); } dod ::= DEALLOCATE. @@ -3092,6 +3099,19 @@ show(A) ::= SHOW CREATE VIEW nm(X) dbnm(Y). { } } +show(A) ::= SHOW CREATE SEQUENCE nm(X) dbnm(Y). { + A.what = MXS_SHOW_CREATE_SEQUENCE; + A.data = 0; + if (Y.z) { + A.pName = &Y; + A.pDatabase = &X; + } + else { + A.pName = &X; + A.pDatabase = NULL; + } +} + show(A) ::= SHOW DATABASES_KW like_or_where_opt. { A.what = MXS_SHOW_DATABASES; A.data = 0; @@ -3219,7 +3239,7 @@ start_transaction_characteristics(A) ::= } cmd ::= START TRANSACTION start_transaction_characteristics(X). { - mxs_sqlite3BeginTransaction(pParse, X); + mxs_sqlite3BeginTransaction(pParse, TK_START, X); } //////////////////////// The TRUNCATE statement //////////////////////////////////// @@ -3243,4 +3263,55 @@ cmd ::= TRUNCATE table_opt nm(X) dbnm(Y). { maxscaleTruncate(pParse, pDatabase, pName); } +//////////////////////// ORACLE Assignment //////////////////////////////////// +// +oracle_assignment ::= id(X) EQ expr(Y). { + Expr* pX = sqlite3PExpr(pParse, TK_ID, 0, 0, &X); + Expr* pExpr = sqlite3PExpr(pParse, TK_EQ, pX, Y.pExpr, 0); + ExprList* pExprList = sqlite3ExprListAppend(pParse, 0, pExpr); + maxscaleSet(pParse, 0, MXS_SET_VARIABLES, pExprList); +} + +//////////////////////// ORACLE CREATE SEQUENCE //////////////////////////////////// +// +cmd ::= CREATE SEQUENCE nm(X) dbnm(Y).{ // CREATE SEQUENCE db + Token* pDatabase; + Token* pTable; + if (Y.z) + { + pDatabase = &X; + pTable = &Y; + } + else + { + pDatabase = NULL; + pTable = &X; + } + maxscaleCreateSequence(pParse, pDatabase, pTable); +} + +//////////////////////// ORACLE CREATE SEQUENCE //////////////////////////////////// +// +cmd ::= DROP SEQUENCE nm(X) dbnm(Y).{ // CREATE SEQUENCE db + Token* pDatabase; + Token* pTable; + if (Y.z) + { + pDatabase = &X; + pTable = &Y; + } + else + { + pDatabase = NULL; + pTable = &X; + } + maxscaleDrop(pParse, MXS_DROP_SEQUENCE, pDatabase, pTable); +} + +//////////////////////// ORACLE DECLARE //////////////////////////////////// +// +cmd ::= DECLARE. { + maxscaleDeclare(pParse); +} + %endif diff --git a/query_classifier/qc_sqlite/sqlite-src-3110100/src/sqliteInt.h b/query_classifier/qc_sqlite/sqlite-src-3110100/src/sqliteInt.h index f72fe988a..a23fc7090 100644 --- a/query_classifier/qc_sqlite/sqlite-src-3110100/src/sqliteInt.h +++ b/query_classifier/qc_sqlite/sqlite-src-3110100/src/sqliteInt.h @@ -4092,14 +4092,9 @@ int sqlite3DbstatRegister(sqlite3*); typedef enum mxs_drop { MXS_DROP_FUNCTION, + MXS_DROP_SEQUENCE, } mxs_drop_t; -typedef struct MxsDrop -{ - mxs_drop_t what; - Token token; -} MxsDrop; - typedef enum mxs_set { MXS_SET_VARIABLES, @@ -4109,6 +4104,7 @@ typedef enum mxs_set typedef enum mxs_show { MXS_SHOW_COLUMNS, + MXS_SHOW_CREATE_SEQUENCE, MXS_SHOW_CREATE_TABLE, MXS_SHOW_CREATE_VIEW, MXS_SHOW_DATABASES, diff --git a/query_classifier/qc_sqlite/sqlite-src-3110100/src/tokenize.c b/query_classifier/qc_sqlite/sqlite-src-3110100/src/tokenize.c index 055dee4dc..fd875b64e 100644 --- a/query_classifier/qc_sqlite/sqlite-src-3110100/src/tokenize.c +++ b/query_classifier/qc_sqlite/sqlite-src-3110100/src/tokenize.c @@ -496,6 +496,20 @@ int sqlite3GetToken(const unsigned char *z, int *tokenType){ break; }else if( c==':' && z[i+1]==':' ){ i++; +#endif +#ifdef MAXSCALE + }else if ( c=='\'' || c=='"' || c=='`' ){ + int q=c; + ++i; + while ( IdChar(z[i]) ) { + ++i; + ++n; + } + if ( z[i]==q ) + { + ++i; + break; + } #endif }else{ break; @@ -554,8 +568,20 @@ int sqlite3GetToken(const unsigned char *z, int *tokenType){ } if (*tokenType != TK_ID) { - extern void maxscaleKeyword(int); - maxscaleKeyword(*tokenType); + extern int maxscaleKeyword(int); + extern int maxscaleTranslateKeyword(int); + + *tokenType = maxscaleTranslateKeyword(*tokenType); + + if (*tokenType != TK_ID) { + if (maxscaleKeyword(*tokenType) != 0) + { + /* Consume the entire string. */ + while ( z[i] ) { + ++i; + } + } + } } } return i; diff --git a/query_classifier/qc_sqlite/sqlite-src-3110100/tool/mkkeywordhash.c b/query_classifier/qc_sqlite/sqlite-src-3110100/tool/mkkeywordhash.c index 7a6ce5782..d496ec6be 100644 --- a/query_classifier/qc_sqlite/sqlite-src-3110100/tool/mkkeywordhash.c +++ b/query_classifier/qc_sqlite/sqlite-src-3110100/tool/mkkeywordhash.c @@ -218,6 +218,7 @@ static Keyword aKeywordTable[] = { #ifdef MAXSCALE { "DATABASES", "TK_DATABASES_KW", ALWAYS }, { "DEALLOCATE", "TK_DEALLOCATE", ALWAYS }, + { "DECLARE", "TK_DECLARE", ALWAYS }, #endif { "DEFAULT", "TK_DEFAULT", ALWAYS }, { "DEFERRED", "TK_DEFERRED", ALWAYS }, @@ -268,6 +269,9 @@ static Keyword aKeywordTable[] = { { "FORCE", "TK_FORCE", ALWAYS }, #endif { "FOREIGN", "TK_FOREIGN", FKEY }, +#ifdef MAXSCALE + { "FORMAT", "TK_FORMAT", ALWAYS }, +#endif { "FROM", "TK_FROM", ALWAYS }, { "FULL", "TK_JOIN_KW", ALWAYS }, #ifdef MAXSCALE @@ -396,6 +400,7 @@ static Keyword aKeywordTable[] = { { "SAVEPOINT", "TK_SAVEPOINT", ALWAYS }, #ifdef MAXSCALE { "SCHEMAS", "TK_DATABASES_KW", ALWAYS }, + { "SEQUENCE", "TK_SEQUENCE", ALWAYS }, #endif { "SELECT", "TK_SELECT", ALWAYS }, #ifdef MAXSCALE diff --git a/query_classifier/test/CMakeLists.txt b/query_classifier/test/CMakeLists.txt index a67ea4da6..e9ad22ec0 100644 --- a/query_classifier/test/CMakeLists.txt +++ b/query_classifier/test/CMakeLists.txt @@ -1,5 +1,7 @@ # Include the embedded library headers if (BUILD_QC_MYSQLEMBEDDED) + find_package(MySQL) + subdirs(MYSQL_INCLUDE_DIR_ALL ${MYSQL_EMBEDDED_INCLUDE_DIR}) foreach(DIR ${MYSQL_INCLUDE_DIR_ALL}) include_directories(${DIR}) @@ -44,6 +46,42 @@ if (BUILD_QC_MYSQLEMBEDDED) add_test(TestQC_CompareWhiteSpace compare -v 2 -S -s "select user from mysql.user; ") add_test(TestQC_version_sensitivity version_sensitivity) + + if(NOT (MYSQL_EMBEDDED_VERSION VERSION_LESS 10.3)) + add_test(TestQC_Oracle-binlog_stm_ps compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/binlog_stm_ps.test) + add_test(TestQC_Oracle-binlog_stm_sp compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/binlog_stm_sp.test) + add_test(TestQC_Oracle-exception compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/exception.test) + add_test(TestQC_Oracle-func_case compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/func_case.test) + add_test(TestQC_Oracle-func_concat compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/func_concat.test) + add_test(TestQC_Oracle-func_decode compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/func_decode.test) + add_test(TestQC_Oracle-func_length compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/func_length.test) + add_test(TestQC_Oracle-func_misc compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/func_misc.test) + add_test(TestQC_Oracle-misc compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/misc.test) + add_test(TestQC_Oracle-ps compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/ps.test) + add_test(TestQC_Oracle-sequence compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/sequence.test) + add_test(TestQC_Oracle-sp-anonymous compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/sp-anonymous.test) + add_test(TestQC_Oracle-sp-code compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/sp-code.test) + add_test(TestQC_Oracle-sp-cursor-decl compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/sp-cursor-decl.test) + add_test(TestQC_Oracle-sp-cursor-rowtype compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/sp-cursor-rowtype.test) + add_test(TestQC_Oracle-sp-cursor compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/sp-cursor.test) + add_test(TestQC_Oracle-sp-goto compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/sp-goto.test) + add_test(TestQC_Oracle-sp-param_inc compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/sp-param.inc) + add_test(TestQC_Oracle-sp-param compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/sp-param.test) + add_test(TestQC_Oracle-sp-row compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/sp-row.test) + add_test(TestQC_Oracle-sp-row-vs-var_inc compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/sp-row-vs-var.inc) + add_test(TestQC_Oracle-sp-security compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/sp-security.test) + add_test(TestQC_Oracle-sp compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/sp.test) + add_test(TestQC_Oracle-trigger compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/trigger.test) + add_test(TestQC_Oracle-truncate compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/truncate.test) + add_test(TestQC_Oracle-type_blob compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/type_blob.test) + add_test(TestQC_Oracle-type_clob compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/type_clob.test) + add_test(TestQC_Oracle-type_date compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/type_date.test) + add_test(TestQC_Oracle-type_number compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/type_number.test) + add_test(TestQC_Oracle-type_raw compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/type_raw.test) + add_test(TestQC_Oracle-type_varchar compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/type_varchar.test) + add_test(TestQC_Oracle-type_varchar2 compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/type_varchar2.test) + add_test(TestQC_Oracle-type_variables compare -v 2 ${CMAKE_CURRENT_SOURCE_DIR}/oracle/variables.test) + endif() endif() add_subdirectory(canonical_tests) diff --git a/query_classifier/test/canonical_tests/canonizer.c b/query_classifier/test/canonical_tests/canonizer.c index 7084d559c..0194610bb 100644 --- a/query_classifier/test/canonical_tests/canonizer.c +++ b/query_classifier/test/canonical_tests/canonizer.c @@ -46,7 +46,7 @@ int main(int argc, char** argv) set_langdir(strdup(".")); set_process_datadir(strdup("/tmp")); - qc_setup("qc_sqlite", NULL); + qc_setup("qc_sqlite", QC_SQL_MODE_DEFAULT, NULL); qc_process_init(QC_INIT_BOTH); infile = fopen(argv[1], "rb"); diff --git a/query_classifier/test/classify.c b/query_classifier/test/classify.c index 79bfeeb50..39ac87aa9 100644 --- a/query_classifier/test/classify.c +++ b/query_classifier/test/classify.c @@ -313,7 +313,7 @@ int main(int argc, char** argv) if (mxs_log_init(NULL, ".", MXS_LOG_TARGET_DEFAULT)) { - if (qc_setup(lib, NULL) && qc_process_init(QC_INIT_BOTH)) + if (qc_setup(lib, QC_SQL_MODE_DEFAULT, NULL) && qc_process_init(QC_INIT_BOTH)) { rc = run(input_name, expected_name); qc_process_end(QC_INIT_BOTH); diff --git a/query_classifier/test/compare.cc b/query_classifier/test/compare.cc index 899739cf0..3f6536612 100644 --- a/query_classifier/test/compare.cc +++ b/query_classifier/test/compare.cc @@ -20,6 +20,8 @@ #include #include #include +#include +#define MYSQL_COM_QUERY COM_QUERY #define MYSQL_COM_QUIT COM_QUIT #define MYSQL_COM_INIT_DB COM_INIT_DB #define MYSQL_COM_CHANGE_USER COM_CHANGE_USER @@ -27,6 +29,7 @@ #include #include #include +#include "../../server/modules/protocol/MySQL/MySQLClient/setsqlmodeparser.hh" #include "testreader.hh" using std::cerr; using std::cin; @@ -39,20 +42,29 @@ using std::ostream; using std::string; using std::stringstream; +#if MYSQL_VERSION_MAJOR == 10 && MYSQL_VERSION_MINOR == 3 +#define USING_MARIADB_103 +#else +#undef USING_MARIADB_103 +#endif + namespace { char USAGE[] = "usage: compare [-r count] [-d] [-1 classfier1] [-2 classifier2] " - "[-A args] [-B args] [-v [0..2]] [-s statement]|[file]]\n\n" + "[-A args] [-B args] [-C args] [-m [default|oracle]] [-v [0..2]] [-s statement]|[file]]\n\n" "-r redo the test the specified number of times; 0 means forever, default is 1\n" "-d don't stop after first failed query\n" - "-1 the first classifier, default qc_mysqlembedded\n" - "-2 the second classifier, default qc_sqlite\n" + "-1 the first classifier, default 'qc_mysqlembedded'\n" + "-2 the second classifier, default 'qc_sqlite'\n" "-A arguments for the first classifier\n" "-B arguments for the second classifier\n" + "-C arguments for both classifiers\n" + "-m initial sql mode, 'default' or 'oracle', default is 'default'\n" "-s compare single statement\n" "-S strict, also require that the parse result is identical\n" + "-R strict reporting, report if parse result is different\n" "-v 0, only return code\n" " 1, query and result for failed cases\n" " 2, all queries, and result for failed cases\n" @@ -75,6 +87,7 @@ struct State bool result_printed; bool stop_at_error; bool strict; + bool strict_reporting; size_t line; size_t n_statements; size_t n_errors; @@ -87,6 +100,7 @@ struct State false, // result_printed true, // stop_at_error false, // strict + false, // strict reporting 0, // line 0, // n_statements 0, // n_errors @@ -160,13 +174,13 @@ QUERY_CLASSIFIER* load_classifier(const char* name) return pClassifier; } -QUERY_CLASSIFIER* get_classifier(const char* zName, const char* zArgs) +QUERY_CLASSIFIER* get_classifier(const char* zName, qc_sql_mode_t sql_mode, const char* zArgs) { QUERY_CLASSIFIER* pClassifier = load_classifier(zName); if (pClassifier) { - if ((pClassifier->qc_setup(zArgs) != QC_RESULT_OK) || + if ((pClassifier->qc_setup(sql_mode, zArgs) != QC_RESULT_OK) || ((pClassifier->qc_process_init() != QC_RESULT_OK))) { cerr << "error: Could not setup or init classifier " << zName << "." << endl; @@ -187,16 +201,17 @@ void put_classifier(QUERY_CLASSIFIER* pClassifier) } } -bool get_classifiers(const char* zName1, const char* zArgs1, QUERY_CLASSIFIER** ppClassifier1, +bool get_classifiers(qc_sql_mode_t sql_mode, + const char* zName1, const char* zArgs1, QUERY_CLASSIFIER** ppClassifier1, const char* zName2, const char* zArgs2, QUERY_CLASSIFIER** ppClassifier2) { bool rc = false; - QUERY_CLASSIFIER* pClassifier1 = get_classifier(zName1, zArgs1); + QUERY_CLASSIFIER* pClassifier1 = get_classifier(zName1, sql_mode, zArgs1); if (pClassifier1) { - QUERY_CLASSIFIER* pClassifier2 = get_classifier(zName2, zArgs2); + QUERY_CLASSIFIER* pClassifier2 = get_classifier(zName2, sql_mode, zArgs2); if (pClassifier2) { @@ -339,7 +354,10 @@ bool compare_parse(QUERY_CLASSIFIER* pClassifier1, GWBUF* pCopy1, else { ss << "INF: "; - success = true; + if (!global.strict_reporting) + { + success = true; + } } ss << static_cast(rv1) << " != " << static_cast(rv2); @@ -1221,6 +1239,33 @@ bool compare(QUERY_CLASSIFIER* pClassifier1, QUERY_CLASSIFIER* pClassifier2, con bool success = compare(pClassifier1, pCopy1, pClassifier2, pCopy2); + if (success) + { + SetSqlModeParser::sql_mode_t sql_mode; + SetSqlModeParser parser; + + if (parser.get_sql_mode(&pCopy1, &sql_mode) == SetSqlModeParser::IS_SET_SQL_MODE) + { + switch (sql_mode) + { + case SetSqlModeParser::DEFAULT: + pClassifier1->qc_set_sql_mode(QC_SQL_MODE_DEFAULT); + pClassifier2->qc_set_sql_mode(QC_SQL_MODE_DEFAULT); + break; + + case SetSqlModeParser::ORACLE: + pClassifier1->qc_set_sql_mode(QC_SQL_MODE_ORACLE); + pClassifier2->qc_set_sql_mode(QC_SQL_MODE_ORACLE); + break; + + default: + ss_dassert(!true); + case SetSqlModeParser::SOMETHING: + break; + }; + } + } + gwbuf_free(pCopy1); gwbuf_free(pCopy2); @@ -1302,6 +1347,15 @@ int run(QUERY_CLASSIFIER* pClassifier1, QUERY_CLASSIFIER* pClassifier2, const st return global.n_errors == 0 ? EXIT_SUCCESS : EXIT_FAILURE; } +void append_arg(string& args, const string& arg) +{ + if (!args.empty()) + { + args += ","; + } + args += arg; +} + } int main(int argc, char* argv[]) @@ -1310,14 +1364,19 @@ int main(int argc, char* argv[]) const char* zClassifier1 = "qc_mysqlembedded"; const char* zClassifier2 = "qc_sqlite"; - const char* zClassifier1Args = NULL; - const char* zClassifier2Args = "log_unrecognized_statements=1"; + string classifier1Args; +#if defined(USING_MARIADB_103) + string classifier2Args("parse_as=10.3,log_unrecognized_statements=1"); +#else + string classifier2Args("log_unrecognized_statements=1"); +#endif const char* zStatement = NULL; + qc_sql_mode_t sql_mode = QC_SQL_MODE_DEFAULT; size_t rounds = 1; int v = VERBOSITY_NORMAL; int c; - while ((c = getopt(argc, argv, "r:d1:2:v:A:B:s:S")) != -1) + while ((c = getopt(argc, argv, "r:d1:2:v:A:B:C:m:s:SR")) != -1) { switch (c) { @@ -1338,11 +1397,16 @@ int main(int argc, char* argv[]) break; case 'A': - zClassifier1Args = optarg; + append_arg(classifier1Args, optarg); break; case 'B': - zClassifier2Args = optarg; + append_arg(classifier2Args, optarg); + break; + + case 'C': + append_arg(classifier1Args, optarg); + append_arg(classifier2Args, optarg); break; case 'd': @@ -1353,10 +1417,30 @@ int main(int argc, char* argv[]) zStatement = optarg; break; + case 'm': + if (strcasecmp(optarg, "default") == 0) + { + sql_mode = QC_SQL_MODE_DEFAULT; + } + else if (strcasecmp(optarg, "oracle") == 0) + { + sql_mode = QC_SQL_MODE_ORACLE; + } + else + { + rc = EXIT_FAILURE; + break; + } + break; + case 'S': global.strict = true; break; + case 'R': + global.strict_reporting = true; + break; + default: rc = EXIT_FAILURE; break; @@ -1378,10 +1462,14 @@ int main(int argc, char* argv[]) if (mxs_log_init(NULL, ".", MXS_LOG_TARGET_DEFAULT)) { + const char* zClassifier1Args = classifier1Args.c_str(); + const char* zClassifier2Args = classifier2Args.c_str(); + QUERY_CLASSIFIER* pClassifier1; QUERY_CLASSIFIER* pClassifier2; - if (get_classifiers(zClassifier1, zClassifier1Args, &pClassifier1, + if (get_classifiers(sql_mode, + zClassifier1, zClassifier1Args, &pClassifier1, zClassifier2, zClassifier2Args, &pClassifier2)) { size_t round = 0; diff --git a/query_classifier/test/crash_qc_sqlite.c b/query_classifier/test/crash_qc_sqlite.c index f54fb7add..23930f66c 100644 --- a/query_classifier/test/crash_qc_sqlite.c +++ b/query_classifier/test/crash_qc_sqlite.c @@ -41,7 +41,7 @@ int main() set_libdir(strdup("../qc_sqlite")); - if (qc_setup("qc_sqlite", NULL) && qc_process_init(QC_INIT_BOTH)) + if (qc_setup("qc_sqlite", QC_SQL_MODE_DEFAULT, NULL) && qc_process_init(QC_INIT_BOTH)) { const char s[] = "SELECT @@global.max_allowed_packet"; diff --git a/query_classifier/test/oracle.test b/query_classifier/test/oracle.test new file mode 100644 index 000000000..e436ce510 --- /dev/null +++ b/query_classifier/test/oracle.test @@ -0,0 +1,11 @@ +# +# This file contains PL/SQL statements that in some way are not handled +# completely or correctly. +# + +PREPARE stmt FROM 'SELECT 1 AS a FROM ' || @table_name; + +# qc_sqlite parses this correctly. However, currently there is no way +# that the "'SELECT 1 AS a FROM ' || @table_name" can be expressed as +# a statement that separately can be analyzed. Consequently, statements +# like this will not pass through the database firewall filter. diff --git a/query_classifier/test/oracle/binlog_stm_ps.test b/query_classifier/test/oracle/binlog_stm_ps.test new file mode 100644 index 000000000..42a54553b --- /dev/null +++ b/query_classifier/test/oracle/binlog_stm_ps.test @@ -0,0 +1,37 @@ +--source include/not_embedded.inc +--source include/have_binlog_format_statement.inc + +--disable_query_log +#qc_sqlite: reset master; # get rid of previous tests binlog +--enable_query_log + +SET sql_mode=ORACLE; + +--echo # +--echo # MDEV-10801 sql_mode: dynamic SQL placeholders +--echo # + +CREATE TABLE t1 (a INT, b INT); +SET @a=10, @b=20; +PREPARE stmt FROM 'INSERT INTO t1 VALUES (?,?)'; +EXECUTE stmt USING @a, @b; +PREPARE stmt FROM 'INSERT INTO t1 VALUES (:a,:b)'; +EXECUTE stmt USING @a, @b; +PREPARE stmt FROM 'INSERT INTO t1 VALUES (:aaa,:bbb)'; +EXECUTE stmt USING @a, @b; +PREPARE stmt FROM 'INSERT INTO t1 VALUES (:"a",:"b")'; +EXECUTE stmt USING @a, @b; +PREPARE stmt FROM 'INSERT INTO t1 VALUES (:"aaa",:"bbb")'; +EXECUTE stmt USING @a, @b; +PREPARE stmt FROM 'INSERT INTO t1 VALUES (:1,:2)'; +EXECUTE stmt USING @a, @b; +PREPARE stmt FROM 'INSERT INTO t1 VALUES (:222,:111)'; +EXECUTE stmt USING @a, @b; +PREPARE stmt FROM 'INSERT INTO t1 VALUES (:0,:65535)'; +EXECUTE stmt USING @a, @b; +PREPARE stmt FROM 'INSERT INTO t1 VALUES (:65535,:0)'; +EXECUTE stmt USING @a, @b; +SELECT * FROM t1; +--let $binlog_file = LAST +source include/show_binlog_events.inc; +DROP TABLE t1; diff --git a/query_classifier/test/oracle/binlog_stm_sp.test b/query_classifier/test/oracle/binlog_stm_sp.test new file mode 100644 index 000000000..71e448b2c --- /dev/null +++ b/query_classifier/test/oracle/binlog_stm_sp.test @@ -0,0 +1,196 @@ +--source include/not_embedded.inc +--source include/have_binlog_format_statement.inc + +--disable_query_log +call mtr.add_suppression("Unsafe statement written to the binary log using statement format since BINLOG_FORMAT = STATEMENT"); +#qc_sqlite: reset master; # get rid of previous tests binlog +--enable_query_log + + +SET sql_mode=ORACLE; + +--echo # +--echo # MDEV-10914 ROW data type for stored routine variables +--echo # + +CREATE TABLE t1 (a INT, b INT); +DELIMITER $$; +CREATE PROCEDURE p1 +AS + rec ROW(a INT,b INT); +BEGIN + rec.a:=100; + rec.b:=200; + INSERT INTO t1 VALUES (rec.a,rec.b); + INSERT INTO t1 VALUES (10, rec=ROW(100,200)); + INSERT INTO t1 VALUES (10, ROW(100,200)=rec); + INSERT INTO t1 SELECT 10, 20 FROM DUAL WHERE rec=ROW(100,200); + INSERT INTO t1 SELECT 10, 21 FROM DUAL WHERE ROW(100,200)=rec; + rec.a:=NULL; + INSERT INTO t1 VALUES (11, rec=ROW(100,200)); + INSERT INTO t1 VALUES (11, rec=ROW(100,201)); + INSERT INTO t1 VALUES (11, ROW(100,200)=rec); + INSERT INTO t1 VALUES (11, ROW(100,201)=rec); + INSERT INTO t1 SELECT 11, 20 FROM DUAL WHERE rec=ROW(100,200); + INSERT INTO t1 SELECT 11, 21 FROM DUAL WHERE ROW(100,200)=rec; + rec.b:=NULL; + INSERT INTO t1 VALUES (12, rec=ROW(100,200)); + INSERT INTO t1 VALUES (12, ROW(100,200)=rec); + INSERT INTO t1 SELECT 12, 20 FROM DUAL WHERE rec=ROW(100,200); + INSERT INTO t1 SELECT 12, 21 FROM DUAL WHERE ROW(100,200)=rec; +END; +$$ +DELIMITER ;$$ +CALL p1(); +SELECT * FROM t1; +DROP TABLE t1; +DROP PROCEDURE p1; +--let $binlog_file = LAST +source include/show_binlog_events.inc; + + +--echo # +--echo # Testing ROW fields in LIMIT +--echo # + +FLUSH LOGS; +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10),(10); +CREATE TABLE t2 (a INT); +DELIMITER $$; +CREATE PROCEDURE p1() +AS + a INT:= 1; + rec ROW(a INT); +BEGIN + rec.a:= 1; + INSERT INTO t2 SELECT 1 FROM t1 LIMIT a; + INSERT INTO t2 SELECT 2 FROM t1 LIMIT rec.a; +END; +$$ +DELIMITER ;$$ +CALL p1(); +DROP TABLE t1,t2; +DROP PROCEDURE p1; +--let $binlog_file = LAST +source include/show_binlog_events.inc; + + +--echo # +--echo # End of MDEV-10914 ROW data type for stored routine variables +--echo # + + +--echo # +--echo # MDEV-12133 sql_mode=ORACLE: table%ROWTYPE in variable declarations +--echo # + +CREATE TABLE t1 (a INT, b INT); +DELIMITER $$; +CREATE PROCEDURE p1 +AS + rec t1%ROWTYPE; +BEGIN + rec.a:=100; + rec.b:=200; + SELECT rec=ROW(100,200) AS true1, ROW(100,200)=rec AS true2; + INSERT INTO t1 VALUES (rec.a,rec.b); + INSERT INTO t1 VALUES (10, rec=ROW(100,200)); + INSERT INTO t1 VALUES (10, ROW(100,200)=rec); + INSERT INTO t1 SELECT 10, 20 FROM DUAL WHERE rec=ROW(100,200); + INSERT INTO t1 SELECT 10, 21 FROM DUAL WHERE ROW(100,200)=rec; + rec.a:=NULL; + INSERT INTO t1 VALUES (11, rec=ROW(100,200)); + INSERT INTO t1 VALUES (11, rec=ROW(100,201)); + INSERT INTO t1 VALUES (11, ROW(100,200)=rec); + INSERT INTO t1 VALUES (11, ROW(100,201)=rec); + INSERT INTO t1 SELECT 11, 20 FROM DUAL WHERE rec=ROW(100,200); + INSERT INTO t1 SELECT 11, 21 FROM DUAL WHERE ROW(100,200)=rec; + rec.b:=NULL; + INSERT INTO t1 VALUES (12, rec=ROW(100,200)); + INSERT INTO t1 VALUES (12, ROW(100,200)=rec); + INSERT INTO t1 SELECT 12, 20 FROM DUAL WHERE rec=ROW(100,200); + INSERT INTO t1 SELECT 12, 21 FROM DUAL WHERE ROW(100,200)=rec; +END; +$$ +DELIMITER ;$$ +CALL p1(); +SELECT * FROM t1; +DROP TABLE t1; +DROP PROCEDURE p1; +--let $binlog_file = LAST +source include/show_binlog_events.inc; + + +--echo # +--echo # MDEV-12291 Allow ROW variables as SELECT INTO targets +--echo # + +FLUSH LOGS; +CREATE TABLE t1 (a INT, b VARCHAR(32)); +INSERT INTO t1 VALUES (10, 'b10'); +CREATE TABLE t2 LIKE t1; +DELIMITER $$; +CREATE PROCEDURE p1 +AS + rec1 ROW(a INT, b VARCHAR(32)); +BEGIN + SELECT * INTO rec1 FROM t1; + INSERT INTO t2 VALUES (rec1.a, rec1.b); +END; +$$ +DELIMITER ;$$ +CALL p1(); +SELECT * FROM t1; +DROP TABLE t1; +DROP TABLE t2; +DROP PROCEDURE p1; +--let $binlog_file = LAST +source include/show_binlog_events.inc; + + +FLUSH LOGS; +CREATE TABLE t1 (a INT, b VARCHAR(32)); +INSERT INTO t1 VALUES (10, 'b10'); +CREATE TABLE t2 LIKE t1; +DELIMITER $$; +CREATE PROCEDURE p1 +AS + rec1 t1%ROWTYPE; +BEGIN + SELECT * INTO rec1 FROM t1; + INSERT INTO t2 VALUES (rec1.a, rec1.b); +END; +$$ +DELIMITER ;$$ +CALL p1(); +SELECT * FROM t1; +DROP TABLE t1; +DROP TABLE t2; +DROP PROCEDURE p1; +--let $binlog_file = LAST +source include/show_binlog_events.inc; + + +FLUSH LOGS; +CREATE TABLE t1 (a INT, b VARCHAR(32)); +INSERT INTO t1 VALUES (10, 'b10'); +CREATE TABLE t2 LIKE t1; +DELIMITER $$; +CREATE PROCEDURE p1 +AS + CURSOR cur1 IS SELECT * FROM t1; + rec1 cur1%ROWTYPE; +BEGIN + SELECT * INTO rec1 FROM t1; + INSERT INTO t2 VALUES (rec1.a, rec1.b); +END; +$$ +DELIMITER ;$$ +CALL p1(); +SELECT * FROM t1; +DROP TABLE t1; +DROP TABLE t2; +DROP PROCEDURE p1; +--let $binlog_file = LAST +source include/show_binlog_events.inc; diff --git a/query_classifier/test/oracle/exception.test b/query_classifier/test/oracle/exception.test new file mode 100644 index 000000000..6448a6ef6 --- /dev/null +++ b/query_classifier/test/oracle/exception.test @@ -0,0 +1,457 @@ +SET sql_mode=ORACLE; + +--echo # +--echo # sql_mode=ORACLE: Predefined exceptions: TOO_MANY_ROWS, NO_DATA_FOUND, DUP_VAL_ON_INDEX +--echo # + +--echo # +--echo # Testing NO_DATA_FOUND and TOO_MANY_ROWS +--echo # + +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10),(20); +DELIMITER $$; +CREATE PROCEDURE p1(lim INT, res OUT VARCHAR) +AS + a INT; +BEGIN + SELECT a INTO a FROM t1 LIMIT lim; +EXCEPTION + WHEN TOO_MANY_ROWS THEN res:='--- too_many_rows cought ---'; + WHEN NO_DATA_FOUND THEN res:='--- no_data_found cought ---'; +END; +$$ +DELIMITER ;$$ +SET @res=''; +CALL p1(0, @res); +SELECT @res; +CALL p1(2, @res); +SELECT @res; +DROP PROCEDURE p1; +DROP TABLE t1; + +--echo # +--echo # Testing DUP_VAL_ON_INDEX +--echo # + +CREATE TABLE t1 (a INT PRIMARY KEY); +DELIMITER $$; +CREATE PROCEDURE p1(res OUT VARCHAR) +AS +BEGIN + INSERT INTO t1 VALUES (10); + INSERT INTO t1 VALUES (10); +EXCEPTION + WHEN DUP_VAL_ON_INDEX THEN res:='--- dup_val_on_index cought ---'; +END; +$$ +DELIMITER ;$$ +SET @res=''; +CALL p1(@res); +SELECT @res; +SELECT * FROM t1; +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # +--echo # MDEV-10840 sql_mode=ORACLE: RAISE statement for predefined exceptions +--echo # + +--echo # +--echo # RAISE outside of an SP context +--echo # + +--error ER_SP_COND_MISMATCH +RAISE NO_DATA_FOUND; +--error ER_SP_COND_MISMATCH +RAISE INVALID_CURSOR; +--error ER_SP_COND_MISMATCH +RAISE DUP_VAL_ON_INDEX; +--error ER_SP_COND_MISMATCH +RAISE TOO_MANY_ROWS; + +--error ER_RESIGNAL_WITHOUT_ACTIVE_HANDLER +RAISE; + + +--echo # +--echo # RAISE for an undefinite exception +--echo # + +DELIMITER $$; +--error ER_SP_COND_MISMATCH +CREATE PROCEDURE p1 +AS +BEGIN + RAISE xxx; +END; +$$ +DELIMITER ;$$ + + +--echo # +--echo # RAISE for predefined exceptions +--echo # + +DELIMITER $$; +CREATE PROCEDURE p1 +AS +BEGIN + RAISE no_data_found; +END; +$$ +DELIMITER ;$$ +CALL p1(); +DROP PROCEDURE p1; + +DELIMITER $$; +CREATE PROCEDURE p1 +AS +BEGIN + RAISE invalid_cursor; +END; +$$ +DELIMITER ;$$ +--error ER_SP_CURSOR_NOT_OPEN +CALL p1(); +DROP PROCEDURE p1; + +DELIMITER $$; +CREATE PROCEDURE p1 +AS +BEGIN + RAISE dup_val_on_index; +END; +$$ +DELIMITER ;$$ +--error ER_DUP_ENTRY +CALL p1(); +DROP PROCEDURE p1; + +DELIMITER $$; +CREATE PROCEDURE p1 +AS +BEGIN + raise too_many_rows; +END; +$$ +DELIMITER ;$$ +--error ER_TOO_MANY_ROWS +CALL p1(); +DROP PROCEDURE p1; + + +--echo # +--echo # RAISE with no exception name (resignal) +--echo # + +DELIMITER $$; +CREATE PROCEDURE p1() +AS +BEGIN + RAISE; +END; +$$ +DELIMITER ;$$ +--error ER_RESIGNAL_WITHOUT_ACTIVE_HANDLER +CALL p1(); +DROP PROCEDURE p1; + + +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10),(20); +DELIMITER $$; +CREATE PROCEDURE p1(lim INT) +AS + a INT; +BEGIN + SELECT a INTO a FROM t1 LIMIT lim; +EXCEPTION + WHEN TOO_MANY_ROWS THEN RAISE; + WHEN NO_DATA_FOUND THEN RAISE; +END; +$$ +DELIMITER ;$$ +CALL p1(0); +--error ER_TOO_MANY_ROWS +CALL p1(2); +DROP PROCEDURE p1; +DROP TABLE t1; + + +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10),(20); +DELIMITER $$; +CREATE PROCEDURE p1(lim INT) +AS + a INT; +BEGIN + SELECT a INTO a FROM t1 LIMIT lim; +EXCEPTION + WHEN OTHERS THEN RAISE; +END; +$$ +DELIMITER ;$$ +CALL p1(0); +--error ER_TOO_MANY_ROWS +CALL p1(2); +DROP PROCEDURE p1; +DROP TABLE t1; + + +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10),(20); +DELIMITER $$; +CREATE PROCEDURE p1() +AS + a INT; + CURSOR c IS SELECT a FROM t1; +BEGIN + FETCH c INTO a; +EXCEPTION + WHEN INVALID_CURSOR THEN RAISE; +END; +$$ +DELIMITER ;$$ +--error ER_SP_CURSOR_NOT_OPEN +CALL p1(); +DROP PROCEDURE p1; +DROP TABLE t1; + + +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10),(20); +DELIMITER $$; +CREATE PROCEDURE p1() +AS + a INT; + CURSOR c IS SELECT a FROM t1; +BEGIN + FETCH c INTO a; +EXCEPTION + WHEN OTHERS THEN RAISE; +END; +$$ +DELIMITER ;$$ +--error ER_SP_CURSOR_NOT_OPEN +CALL p1(); +DROP PROCEDURE p1; +DROP TABLE t1; + +--echo # +--echo # Testing that warning-alike errors are caught by OTHERS +--echo # + +CREATE TABLE t1 (a INT); +DELIMITER $$; +CREATE FUNCTION f1 RETURN VARCHAR +AS + a INT:=10; +BEGIN + SELECT a INTO a FROM t1; + RETURN 'OK'; +EXCEPTION + WHEN OTHERS THEN RETURN 'Exception'; +END; +$$ +DELIMITER ;$$ +SELECT f1() FROM DUAL; +DROP FUNCTION f1; +DROP TABLE t1; + + +--echo # +--echo # End of MDEV-10840 sql_mode=ORACLE: RAISE statement for predefined exceptions +--echo # + + +--echo # +--echo # MDEV-10587 sql_mode=ORACLE: User defined exceptions +--echo # + +--echo # +--echo # Checking that duplicate WHEN clause is not allowed +--echo # + +DELIMITER $$; +--error ER_SP_DUP_HANDLER +CREATE FUNCTION f1() RETURN VARCHAR +AS + e EXCEPTION; +BEGIN + RETURN 'Got no exceptions'; +EXCEPTION + WHEN e THEN RETURN 'Got exception e'; + WHEN e THEN RETURN 'Got exception e'; +END; +$$ +DELIMITER ;$$ + + +--echo # +--echo # Checking that raised user exceptions are further caught by name +--echo # + +DELIMITER $$; +CREATE FUNCTION f1(c VARCHAR) RETURN VARCHAR +AS + e EXCEPTION; + f EXCEPTION; +BEGIN + IF c = 'e' THEN RAISE e; END IF; + IF c = 'f' THEN RAISE f; END IF; + RETURN 'Got no exceptions'; +EXCEPTION + WHEN e THEN RETURN 'Got exception e'; +END; +$$ +DELIMITER ;$$ +SELECT f1(''); +SELECT f1('e'); +--error ER_SIGNAL_EXCEPTION +SELECT f1('f'); +DROP FUNCTION f1; + + +--echo # +--echo # Checking that raised user exceptions are further caught by OTHERS +--echo # + +DELIMITER $$; +CREATE FUNCTION f1(c VARCHAR) RETURN VARCHAR +AS + e EXCEPTION; + f EXCEPTION; +BEGIN + IF c = 'e' THEN RAISE e; END IF; + IF c = 'f' THEN RAISE f; END IF; + RETURN 'Got no exceptions'; +EXCEPTION + WHEN OTHERS THEN RETURN 'Got some exception'; +END; +$$ +DELIMITER ;$$ +SELECT f1(''); +SELECT f1('e'); +SELECT f1('f'); +DROP FUNCTION f1; + + +--echo # +--echo # Checking that 'WHEN e .. WHEN f' does not produce ER_SP_DUP_HANDLER +--echo # + +DELIMITER $$; +CREATE FUNCTION f1(c VARCHAR) RETURN VARCHAR +AS + e EXCEPTION; + f EXCEPTION; + a VARCHAR(64):=''; +BEGIN + BEGIN + IF c = 'e' THEN RAISE e; END IF; + IF c = 'f' THEN RAISE f; END IF; + EXCEPTION + WHEN e THEN BEGIN a:='Got EXCEPTION1/e; '; RAISE e; END; + WHEN f THEN BEGIN a:='Got EXCEPTION1/f; '; RAISE f; END; + END; + RETURN 'Got no exceptions'; +EXCEPTION + WHEN OTHERS THEN RETURN a || 'Got EXCEPTION2/OTHERS;'; +END; +$$ +DELIMITER ;$$ +SELECT f1(''); +SELECT f1('e'); +SELECT f1('f'); +DROP FUNCTION f1; + + +--echo # +--echo # Checking that resignaled user exceptions are further caught by name +--echo # +DELIMITER $$; +CREATE FUNCTION f1(c VARCHAR) RETURN VARCHAR +AS + e EXCEPTION; + f EXCEPTION; + a VARCHAR(64):=''; +BEGIN + BEGIN + IF c = 'e' THEN RAISE e; END IF; + IF c = 'f' THEN RAISE f; END IF; + EXCEPTION + WHEN e THEN BEGIN a:='Got EXCEPTION1/e; '; RAISE; END; + WHEN f THEN BEGIN a:='Got EXCEPTION1/f; '; RAISE; END; + END; + RETURN 'Got no exceptions'; +EXCEPTION + WHEN e THEN RETURN a || 'Got EXCEPTION2/e;'; +END; +$$ +DELIMITER ;$$ +SELECT f1(''); +SELECT f1('e'); +--error ER_SIGNAL_EXCEPTION +SELECT f1('f'); +DROP FUNCTION f1; + + +--echo # +--echo # Checking that resignaled user exceptions are further caught by OTHERS +--echo # + +DELIMITER $$; +CREATE FUNCTION f1(c VARCHAR) RETURN VARCHAR +AS + e EXCEPTION; + f EXCEPTION; + a VARCHAR(64):=''; +BEGIN + BEGIN + IF c = 'e' THEN RAISE e; END IF; + IF c = 'f' THEN RAISE f; END IF; + EXCEPTION + WHEN e THEN BEGIN a:='Got EXCEPTION1/e; '; RAISE; END; + WHEN f THEN BEGIN a:='Got EXCEPTION1/f; '; RAISE; END; + END; + RETURN 'Got no exceptions'; +EXCEPTION + WHEN OTHERS THEN RETURN a || 'Got EXCEPTION2/OTHERS;'; +END; +$$ +DELIMITER ;$$ +SELECT f1(''); +SELECT f1('e'); +SELECT f1('f'); +DROP FUNCTION f1; + + +--echo # +--echo # End of MDEV-10587 sql_mode=ORACLE: User defined exceptions +--echo # + +--echo # +--echo # MDEV-12088 sql_mode=ORACLE: Do not require BEGIN..END in multi-statement exception handlers in THEN clause +--echo # +CREATE TABLE t1 (a INT PRIMARY KEY); +INSERT INTO t1 VALUES (10),(20),(30); +DELIMITER $$; +CREATE PROCEDURE p1(a INT) AS +BEGIN + INSERT INTO t1 (a) VALUES (a); +EXCEPTION + WHEN DUP_VAL_ON_INDEX THEN + a:= a+1; + INSERT INTO t1 VALUES (a); + WHEN OTHERS THEN + NULL; + NULL; +END; +$$ +DELIMITER ;$$ +CALL p1(30); +SELECT * FROM t1; +DROP PROCEDURE p1; +DROP TABLE t1; diff --git a/query_classifier/test/oracle/func_case.test b/query_classifier/test/oracle/func_case.test new file mode 100644 index 000000000..d5e0d6509 --- /dev/null +++ b/query_classifier/test/oracle/func_case.test @@ -0,0 +1,9 @@ +# +# Testing CASE and its abbreviations +# + +SET sql_mode=ORACLE; + +SELECT NVL(NULL, 'a'), NVL('a', 'b'); + +SELECT NVL2(NULL, 'a', 'b'), NVL2('a', 'b', 'c'); diff --git a/query_classifier/test/oracle/func_concat.test b/query_classifier/test/oracle/func_concat.test new file mode 100644 index 000000000..e1d8a5c47 --- /dev/null +++ b/query_classifier/test/oracle/func_concat.test @@ -0,0 +1,116 @@ +# +# Testing CONCAT with null values +# + +SET sql_mode=ORACLE; + +EXPLAIN EXTENDED SELECT 'a'||'b'||'c'; +EXPLAIN EXTENDED SELECT CONCAT('a'||'b'||'c'); + +SELECT '' || ''; +SELECT '' || 'b'; +SELECT '' || NULL; +SELECT 'a' || ''; +SELECT 'a' || 'b'; +SELECT 'a' || NULL; +SELECT NULL || ''; +SELECT NULL || 'b'; +SELECT NULL || NULL; + +SELECT '' || '' || ''; +SELECT '' || '' || 'c'; +SELECT '' || '' || NULL; +SELECT '' || 'b' || ''; +SELECT '' || 'b' || 'c'; +SELECT '' || 'b' || NULL; +SELECT '' || NULL || ''; +SELECT '' || NULL || 'c'; +SELECT '' || NULL || NULL; + +SELECT 'a' || '' || ''; +SELECT 'a' || '' || 'c'; +SELECT 'a' || '' || NULL; +SELECT 'a' || 'b' || ''; +SELECT 'a' || 'b' || 'c'; +SELECT 'a' || 'b' || NULL; +SELECT 'a' || NULL || ''; +SELECT 'a' || NULL || 'c'; +SELECT 'a' || NULL || NULL; + +SELECT NULL || '' || ''; +SELECT NULL || '' || 'c'; +SELECT NULL || '' || NULL; +SELECT NULL || 'b' || ''; +SELECT NULL || 'b' || 'c'; +SELECT NULL || 'b' || NULL; +SELECT NULL || NULL || ''; +SELECT NULL || NULL || 'c'; +SELECT NULL || NULL || NULL; + +CREATE TABLE t1 (a VARCHAR(10), b VARCHAR(10), c VARCHAR(10)); + +INSERT INTO t1 VALUES ('', '', ''); +INSERT INTO t1 VALUES ('', '', 'c'); +INSERT INTO t1 VALUES ('', '', NULL); +INSERT INTO t1 VALUES ('', 'b', ''); +INSERT INTO t1 VALUES ('', 'b', 'c'); +INSERT INTO t1 VALUES ('', 'b', NULL); +INSERT INTO t1 VALUES ('', NULL, ''); +INSERT INTO t1 VALUES ('', NULL, 'c'); +INSERT INTO t1 VALUES ('', NULL, NULL); + +INSERT INTO t1 VALUES ('a', '', ''); +INSERT INTO t1 VALUES ('a', '', 'c'); +INSERT INTO t1 VALUES ('a', '', NULL); +INSERT INTO t1 VALUES ('a', 'b', ''); +INSERT INTO t1 VALUES ('a', 'b', 'c'); +INSERT INTO t1 VALUES ('a', 'b', NULL); +INSERT INTO t1 VALUES ('a', NULL, ''); +INSERT INTO t1 VALUES ('a', NULL, 'c'); +INSERT INTO t1 VALUES ('a', NULL, NULL); + +INSERT INTO t1 VALUES (NULL, '', ''); +INSERT INTO t1 VALUES (NULL, '', 'c'); +INSERT INTO t1 VALUES (NULL, '', NULL); +INSERT INTO t1 VALUES (NULL, 'b', ''); +INSERT INTO t1 VALUES (NULL, 'b', 'c'); +INSERT INTO t1 VALUES (NULL, 'b', NULL); +INSERT INTO t1 VALUES (NULL, NULL, ''); +INSERT INTO t1 VALUES (NULL, NULL, 'c'); +INSERT INTO t1 VALUES (NULL, NULL, NULL); + +SELECT LENGTH(a||b||c), a||b||c FROM t1 ORDER BY a,b,c; +SELECT LENGTH(CONCAT(a||b||c)), CONCAT(a||b||c) FROM t1 ORDER BY a,b,c; + +DROP TABLE t1; + +--echo # +--echo # MDEV-12478 CONCAT function inside view casts values incorrectly with Oracle sql_mode +--echo # + +SET sql_mode=ORACLE; +CREATE VIEW v1 AS SELECT 'foo'||NULL||'bar' AS test; +SHOW CREATE VIEW v1; +SELECT * FROM v1; +SET sql_mode=DEFAULT; +SHOW CREATE VIEW v1; +SELECT * FROM v1; +DROP VIEW v1; + +SET sql_mode=DEFAULT; +CREATE VIEW v1 AS SELECT CONCAT('foo',NULL,'bar') AS test; +SHOW CREATE VIEW v1; +SELECT * FROM v1; +SET sql_mode=ORACLE; +SHOW CREATE VIEW v1; +SELECT * FROM v1; +DROP VIEW v1; + +SET sql_mode=DEFAULT; +CREATE VIEW v1 AS SELECT '0'||'1' AS test; +SHOW CREATE VIEW v1; +SELECT * FROM v1; +SET sql_mode=ORACLE; +SHOW CREATE VIEW v1; +SELECT * FROM v1; +DROP VIEW v1; diff --git a/query_classifier/test/oracle/func_decode.test b/query_classifier/test/oracle/func_decode.test new file mode 100644 index 000000000..ae05cb2c3 --- /dev/null +++ b/query_classifier/test/oracle/func_decode.test @@ -0,0 +1,21 @@ +SET sql_mode=ORACLE; + +--error ER_PARSE_ERROR +SELECT DECODE(10); +--error ER_PARSE_ERROR +SELECT DECODE(10,10); + +SELECT DECODE(10,10,'x10'); +SELECT DECODE(11,10,'x10'); + +SELECT DECODE(10,10,'x10','def'); +SELECT DECODE(11,10,'x10','def'); + +SELECT DECODE(10,10,'x10',11,'x11','def'); +SELECT DECODE(11,10,'x10',11,'x11','def'); +SELECT DECODE(12,10,'x10',11,'x11','def'); + +EXPLAIN EXTENDED SELECT DECODE(12,10,'x10',11,'x11','def'); + +CREATE TABLE decode (decode int); +DROP TABLE decode; diff --git a/query_classifier/test/oracle/func_length.test b/query_classifier/test/oracle/func_length.test new file mode 100644 index 000000000..9a5056e6a --- /dev/null +++ b/query_classifier/test/oracle/func_length.test @@ -0,0 +1,20 @@ +SET sql_mode=ORACLE; + +--echo # +--echo # MDEV-12783 sql_mode=ORACLE: Functions LENGTH() and LENGTHB() +--echo # +# +# Testing LENGTH / LENGTHB +# +# LENGTH : return the length of char +# LENGTHB : return the length of byte + + +SELECT LENGTH(null), LENGTH('a'), LENGTH(123); +SELECT LENGTHB(null), LENGTHB('a'), LENGTHB(123); + +# qc_sqlite: SELECT LENGTH(_utf8 0xC39F), LENGTH(CHAR(14844588 USING utf8)); +# Sqlite3 error: SQL logic error or missing database, near "0xC39F": syntax error +# qc_sqlite: SELECT LENGTHB(_utf8 0xC39F), LENGTHB(CHAR(14844588 USING utf8)); +# Sqlite3 error: SQL logic error or missing database, near "0xC39F": syntax error +EXPLAIN EXTENDED SELECT LENGTH('a'), LENGTHB('a'); diff --git a/query_classifier/test/oracle/func_misc.test b/query_classifier/test/oracle/func_misc.test new file mode 100644 index 000000000..c5b42134f --- /dev/null +++ b/query_classifier/test/oracle/func_misc.test @@ -0,0 +1,346 @@ +SET sql_mode=ORACLE; + +--echo # +--echo # MDEV-10578 sql_mode=ORACLE: SP control functions SQLCODE, SQLERRM +--echo # + +--echo # +--echo # Using SQLCODE and SQLERRM outside of an SP +--echo # + +--error ER_BAD_FIELD_ERROR +SELECT SQLCODE; + +--error ER_BAD_FIELD_ERROR +SELECT SQLERRM; + +CREATE TABLE t1 (SQLCODE INT, SQLERRM VARCHAR(10)); +INSERT INTO t1 VALUES (10, 'test'); +SELECT SQLCODE, SQLERRM FROM t1; +DROP TABLE t1; + +--echo # +--echo # Normal SQLCODE and SQLERRM usage +--echo # + +DELIMITER $$; +CREATE PROCEDURE p1(stmt VARCHAR) +AS +BEGIN + EXECUTE IMMEDIATE stmt; + SELECT 'Error1: ' || SQLCODE || ' ' || SQLERRM; +EXCEPTION + WHEN OTHERS THEN + SELECT 'Error2: ' || SQLCODE || ' ' || SQLERRM; +END; +$$ +DELIMITER ;$$ +CALL p1('SELECT 1'); +CALL p1('xxx'); +CALL p1('SELECT 1'); +DROP PROCEDURE p1; + +--echo # +--echo # SQLCODE and SQLERRM hidden by local variables +--echo # + +DELIMITER $$; +CREATE PROCEDURE p1() +AS + sqlcode INT:= 10; + sqlerrm VARCHAR(64) := 'test'; +BEGIN + SELECT 'Error: ' || SQLCODE || ' ' || SQLERRM; +END; +$$ +DELIMITER ;$$ +CALL p1; +DROP PROCEDURE p1; + +DELIMITER $$; +CREATE PROCEDURE p1() +AS + sqlcode INT; + sqlerrm VARCHAR(64); +BEGIN + SQLCODE:= 10; + sqlerrm:= 'test'; + SELECT 'Error: ' || SQLCODE || ' ' || SQLERRM; +END; +$$ +DELIMITER ;$$ +CALL p1; +DROP PROCEDURE p1; + + +--echo # +--echo # SQLCODE and SQLERRM hidden by parameters +--echo # + +DELIMITER $$; +CREATE PROCEDURE p1(sqlcode INT, sqlerrm VARCHAR) +AS +BEGIN + SELECT 'Error: ' || SQLCODE || ' ' || SQLERRM; +END; +$$ +DELIMITER ;$$ +CALL p1(10, 'test'); +DROP PROCEDURE p1; + + +--echo # +--echo # SQLCODE and SQLERRM in CREATE..SELECT +--echo # + +DELIMITER $$; +CREATE PROCEDURE p1 +AS +BEGIN + CREATE TABLE t1 AS SELECT SQLCODE, SQLERRM; +END; +$$ +DELIMITER ;$$ +CALL p1; +SHOW CREATE TABLE t1; +DROP TABLE t1; +DROP PROCEDURE p1; + + +--echo # +--echo # SQLCODE and SQLERRM in EXPLAIN EXTENDED SELECT +--echo # + +DELIMITER $$; +CREATE PROCEDURE p1 +AS +BEGIN + EXPLAIN EXTENDED SELECT SQLCode, SQLErrm; +END; +$$ +DELIMITER ;$$ +CALL p1; +DROP PROCEDURE p1; + + + +--echo # +--echo # Warning-alike errors in stored functions +--echo # + +CREATE TABLE t1 (a INT); +DELIMITER $$; +CREATE FUNCTION f1 RETURN VARCHAR +AS + a INT; +BEGIN + SELECT a INTO a FROM t1; + RETURN 'No exception ' || SQLCODE || ' ' || SQLERRM; +EXCEPTION + WHEN NO_DATA_FOUND THEN + RETURN 'Exception ' || SQLCODE || ' ' || SQLERRM; +END; +$$ +DELIMITER ;$$ +SELECT f1() FROM DUAL; +DROP FUNCTION f1; +DROP TABLE t1; + + +CREATE TABLE t1 (a INT); +DELIMITER $$; +CREATE FUNCTION f1 RETURN VARCHAR +AS + a INT; +BEGIN + SELECT a INTO a FROM t1; + RETURN 'No exception ' || SQLCODE || ' ' || SQLERRM; +EXCEPTION + WHEN OTHERS THEN + RETURN 'Exception ' || SQLCODE || ' ' || SQLERRM; +END; +$$ +DELIMITER ;$$ +SELECT f1() FROM DUAL; +DROP FUNCTION f1; +DROP TABLE t1; + + +--echo # +--echo # Warning-alike errors in stored procedures +--echo # + +CREATE TABLE t1 (a INT); +DELIMITER $$; +CREATE PROCEDURE p1(res OUT VARCHAR) +AS + a INT; +BEGIN + SELECT a INTO a FROM t1; + res:= 'No exception ' || SQLCODE || ' ' || SQLERRM; +EXCEPTION + WHEN NO_DATA_FOUND THEN + res:= 'Exception ' || SQLCODE || ' ' || SQLERRM; +END; +$$ +DELIMITER ;$$ +CALL p1(@a); +SELECT @a; +DROP PROCEDURE p1; +DROP TABLE t1; + + +CREATE TABLE t1 (a INT); +DELIMITER $$; +CREATE PROCEDURE p1(res OUT VARCHAR) +AS + a INT; +BEGIN + SELECT a INTO a FROM t1; + res:= 'No exception ' || SQLCODE || ' ' || SQLERRM; +EXCEPTION + WHEN OTHERS THEN + res:= 'Exception ' || SQLCODE || ' ' || SQLERRM; +END; +$$ +DELIMITER ;$$ +CALL p1(@a); +SELECT @a; +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # +--echo # SQLCODE and SQLERRM are cleared on RETURN +--echo # + +CREATE TABLE t1 (a INT); +DELIMITER $$; +CREATE FUNCTION f1 RETURN VARCHAR +AS + a INT:=10; +BEGIN + SELECT a INTO a FROM t1; + RETURN 'Value=' || a; +EXCEPTION + WHEN NO_DATA_FOUND THEN RETURN 'Exception|' || SQLCODE || ' ' || SQLERRM; +END; +$$ +CREATE FUNCTION f2 RETURN VARCHAR +AS + a VARCHAR(128); +BEGIN + RETURN f1() || '|' || SQLCODE || ' ' || SQLERRM; +END; +$$ +DELIMITER ;$$ +SELECT f1() FROM DUAL; +SELECT f2() FROM DUAL; +DROP TABLE t1; +DROP FUNCTION f2; +DROP FUNCTION f1; + + +CREATE TABLE t1 (a INT); +DELIMITER $$; +CREATE FUNCTION f1 RETURN VARCHAR +AS + a INT:=10; +BEGIN + SELECT a INTO a FROM t1; + RETURN 'Value=' || a; +EXCEPTION + WHEN OTHERS THEN RETURN 'Exception|' || SQLCODE || ' ' || SQLERRM; +END; +$$ +CREATE FUNCTION f2 RETURN VARCHAR +AS + a VARCHAR(128); +BEGIN + RETURN f1() || '|' || SQLCODE || ' ' || SQLERRM; +END; +$$ +DELIMITER ;$$ +SELECT f1() FROM DUAL; +SELECT f2() FROM DUAL; +DROP TABLE t1; +DROP FUNCTION f2; +DROP FUNCTION f1; + + +--echo # +--echo # SQLCODE and SQLERRM are cleared on a return from a PROCEDURE +--echo # + +CREATE TABLE t1 (a INT); +DELIMITER $$; +CREATE PROCEDURE p1(res OUT VARCHAR) +AS + a INT:=10; +BEGIN + SELECT a INTO a FROM t1; + res:='Value=' || a; +EXCEPTION + WHEN NO_DATA_FOUND THEN res:='Exception|' || SQLCODE || ' ' || SQLERRM; +END; +$$ +CREATE FUNCTION f2 RETURN VARCHAR +AS + res VARCHAR(128); +BEGIN + CALL p1(res); + RETURN res || '|' || SQLCODE || ' ' || SQLERRM; +END; +$$ +DELIMITER ;$$ +SELECT f2() FROM DUAL; +DROP FUNCTION f2; +DROP PROCEDURE p1; +DROP TABLE t1; + + +CREATE TABLE t1 (a INT); +DELIMITER $$; +CREATE PROCEDURE p1(res OUT VARCHAR) +AS + a INT:=10; +BEGIN + SELECT a INTO a FROM t1; + res:='Value=' || a; +EXCEPTION + WHEN OTHERS THEN res:='Exception|' || SQLCODE || ' ' || SQLERRM; +END; +$$ +CREATE FUNCTION f2 RETURN VARCHAR +AS + res VARCHAR(128); +BEGIN + CALL p1(res); + RETURN res || '|' || SQLCODE || ' ' || SQLERRM; +END; +$$ +DELIMITER ;$$ +SELECT f2() FROM DUAL; +DROP FUNCTION f2; +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # +--echo # End of MDEV-10578 sql_mode=ORACLE: SP control functions SQLCODE, SQLERRM +--echo # + +--echo # +--echo # MDEV-12854 Synchronize CREATE..SELECT data type and result set metadata data type for INT functions +--echo # + +--enable_metadata +--disable_ps_protocol +DELIMITER $$; +BEGIN + SELECT SQLCODE; +END +$$ +DELIMITER ;$$ +--enable_ps_protocol +--disable_metadata diff --git a/query_classifier/test/oracle/misc.test b/query_classifier/test/oracle/misc.test new file mode 100644 index 000000000..d939b20f8 --- /dev/null +++ b/query_classifier/test/oracle/misc.test @@ -0,0 +1,10 @@ +SET sql_mode=ORACLE; + +--echo # +--echo # MDEV-12086 sql_mode=ORACLE: allow SELECT UNIQUE as a synonym for SELECT DISTINCT +--echo # + +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10),(20),(20),(30),(30),(30); +SELECT UNIQUE a FROM t1; +DROP TABLE t1; diff --git a/query_classifier/test/oracle/ps.test b/query_classifier/test/oracle/ps.test new file mode 100644 index 000000000..9cad48f79 --- /dev/null +++ b/query_classifier/test/oracle/ps.test @@ -0,0 +1,266 @@ +SET sql_mode=ORACLE; + +--echo # +--echo # MDEV-10801 sql_mode: dynamic SQL placeholders +--echo # + +SET @a=10, @b=20; +PREPARE stmt FROM 'SELECT ?,?'; +EXECUTE stmt USING @a, @b; +PREPARE stmt FROM 'SELECT :a,:b'; +EXECUTE stmt USING @a, @b; +PREPARE stmt FROM 'SELECT :aaa,:bbb'; +EXECUTE stmt USING @a, @b; +#qc_mysqlembedded: PREPARE stmt FROM 'SELECT :"a",:"b"'; +EXECUTE stmt USING @a, @b; +#qc_mysqlembedded: PREPARE stmt FROM 'SELECT :"aaa",:"bbb"'; +EXECUTE stmt USING @a, @b; +PREPARE stmt FROM 'SELECT :1,:2'; +EXECUTE stmt USING @a, @b; +PREPARE stmt FROM 'SELECT :222,:111'; +EXECUTE stmt USING @a, @b; +PREPARE stmt FROM 'SELECT :0,:65535'; +EXECUTE stmt USING @a, @b; +PREPARE stmt FROM 'SELECT :65535,:0'; +EXECUTE stmt USING @a, @b; + +--echo # +--echo # MDEV-10709 Expressions as parameters to Dynamic SQL +--echo # + +--echo # +--echo # Testing disallowed expressions in USING +--echo # + +PREPARE stmt FROM 'SELECT :1 FROM DUAL'; +--error ER_PARSE_ERROR +EXECUTE stmt USING (SELECT 1); +DEALLOCATE PREPARE stmt; + +DELIMITER $$; +CREATE FUNCTION f1() RETURN VARCHAR +AS +BEGIN + RETURN 'test'; +END; +$$ +DELIMITER ;$$ +PREPARE stmt FROM 'SELECT ? FROM DUAL'; +--error ER_SUBQUERIES_NOT_SUPPORTED +EXECUTE stmt USING f1(); +DEALLOCATE PREPARE stmt; +DROP FUNCTION f1; + +--echo # +--echo # Using a user variable as a EXECUTE..USING out parameter +--echo # + +DELIMITER /; +CREATE PROCEDURE p1(a OUT INT) +AS +BEGIN + a:= 10; +END; +/ +DELIMITER ;/ +SET @a=1; +CALL p1(@a); +SELECT @a; +SET @a=2; +PREPARE stmt FROM 'CALL p1(?)'; +EXECUTE stmt USING @a; +SELECT @a; +DROP PROCEDURE p1; + + +--echo # +--echo # Using an SP variable as a EXECUTE..USING out parameter +--echo # + +DELIMITER /; +CREATE PROCEDURE p1 (a OUT INT) +AS +BEGIN + a:=10; +END; +/ +CREATE PROCEDURE p2 (a OUT INT) +AS +BEGIN + PREPARE stmt FROM 'CALL p1(?)'; + EXECUTE stmt USING a; +END; +/ +DELIMITER ;/ +SET @a= 1; +CALL p2(@a); +SELECT @a; +DROP PROCEDURE p2; +DROP PROCEDURE p1; + + +--echo # +--echo # Using a trigger field as a EXECUTE..USING out parameter +--echo # +DELIMITER /; +CREATE PROCEDURE p1 (a OUT INT) +AS +BEGIN + a:= 10; +END; +/ +DELIMITER ;/ +CREATE TABLE t1 (a INT); +CREATE TRIGGER tr1 BEFORE INSERT ON t1 FOR EACH ROW CALL p1(:NEW.a); +INSERT INTO t1 VALUES (1); +SELECT * FROM t1; +DROP TABLE t1; +DROP PROCEDURE p1; + + +--echo # +--echo # Testing re-prepare on a table metadata update between PREPARE and EXECUTE +--echo # + +CREATE TABLE t1 (a INT); +DELIMITER /; +CREATE PROCEDURE p1(a IN INT) +AS +BEGIN + INSERT INTO t1 VALUES (a); +END; +/ +DELIMITER ;/ +PREPARE stmt FROM 'CALL p1(?)'; +EXECUTE stmt USING 10; +SELECT * FROM t1; +CREATE TRIGGER tr1 BEFORE INSERT ON t1 FOR EACH ROW NEW.a:=NEW.a+1; +EXECUTE stmt USING 20; +SELECT * FROM t1; +DEALLOCATE PREPARE stmt; +DROP PROCEDURE p1; +DROP TABLE t1; + +--echo # +--echo # End of MDEV-10709 Expressions as parameters to Dynamic SQL +--echo # + +--echo # +--echo # MDEV-10585 EXECUTE IMMEDIATE statement +--echo # + +--echo # +--echo # Testing disallowed expressions in USING +--echo # + +--error ER_PARSE_ERROR +EXECUTE IMMEDIATE 'SELECT :1 FROM DUAL' USING (SELECT 1); + +DELIMITER $$; +CREATE FUNCTION f1() RETURN VARCHAR +AS +BEGIN + RETURN 'test'; +END; +$$ +DELIMITER ;$$ +--error ER_SUBQUERIES_NOT_SUPPORTED +EXECUTE IMMEDIATE 'SELECT ? FROM DUAL' USING f1(); +DROP FUNCTION f1; + + +--echo # +--echo # Testing simple expressions +--echo # + +EXECUTE IMMEDIATE 'SELECT :1 FROM DUAL' USING 10; + + +--echo # +--echo # MDEV-10866 Extend PREPARE and EXECUTE IMMEDIATE to understand expressions +--echo # + +--echo # +--echo # Testing erroneous and diallowed prepare source +--echo # + +--error ER_CANT_AGGREGATE_2COLLATIONS +EXECUTE IMMEDIATE _latin1'SELECT 1 AS c FROM ' || _latin2 'DUAL'; +--error ER_CANT_AGGREGATE_2COLLATIONS +PREPARE stmt FROM _latin1'SELECT 1 AS c FROM ' || _latin2 'DUAL'; + +--error ER_PARSE_ERROR +EXECUTE IMMEDIATE (SELECT 'SELECT 1'); +--error ER_PARSE_ERROR +PREPARE stmt FROM (SELECT 'SELECT 1'); + +--error ER_BAD_FIELD_ERROR +EXECUTE IMMEDIATE a; +--error ER_BAD_FIELD_ERROR +PREPARE stmt FROM a; + +--error ER_PARSE_ERROR +EXECUTE IMMEDIATE NULL; +--error ER_PARSE_ERROR +PREPARE stmt FROM NULL; + +--error ER_PARSE_ERROR +EXECUTE IMMEDIATE COALESCE(NULL); +--error ER_PARSE_ERROR +PREPARE stmt FROM COALESCE(NULL); + +DELIMITER $$; +CREATE FUNCTION f1() RETURN VARCHAR +AS +BEGIN + RETURN 't1'; +END; +$$ +DELIMITER ;$$ +--error ER_SUBQUERIES_NOT_SUPPORTED +EXECUTE IMMEDIATE f1(); +--error ER_SUBQUERIES_NOT_SUPPORTED +PREPARE stmt FROM f1(); +DROP FUNCTION f1; + +--echo # +--echo # Testing user variables in prepare source +--echo # + +SET @table_name='DUAL'; +EXECUTE IMMEDIATE 'SELECT 1 AS a FROM ' || @table_name; +#qc: PREPARE stmt FROM 'SELECT 1 AS a FROM ' || @table_name; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +--echo # +--echo # Testing SP parameters and variables in prepare source +--echo # + +DELIMITER $$; +CREATE PROCEDURE p1(table_name VARCHAR) +AS +BEGIN + EXECUTE IMMEDIATE 'SELECT 1 AS c FROM '|| table_name; +END; +$$ +DELIMITER ;$$ +CALL p1('DUAL'); +DROP PROCEDURE p1; + +DELIMITER $$; +CREATE PROCEDURE p1() +AS + table_name VARCHAR(64):='DUAL'; +BEGIN + EXECUTE IMMEDIATE 'SELECT 1 AS c FROM ' || table_name; +END; +$$ +DELIMITER ;$$ +CALL p1(); +DROP PROCEDURE p1; + + +--echo # +--echo # End of MDEV-10866 Extend PREPARE and EXECUTE IMMEDIATE to understand expressions +--echo # diff --git a/query_classifier/test/oracle/sequence.test b/query_classifier/test/oracle/sequence.test new file mode 100644 index 000000000..719c4bcd4 --- /dev/null +++ b/query_classifier/test/oracle/sequence.test @@ -0,0 +1,43 @@ +--source include/have_binlog_format_row.inc + +SET sql_mode=ORACLE; + +CREATE SEQUENCE s1; +SHOW CREATE SEQUENCE s1; +SELECT s1.currval; +SELECT s1.nextval; +SELECT s1.nextval; +SELECT s1.nextval; +EXPLAIN EXTENDED SELECT s1.nextval; +SELECT nextval(s1); +EXPLAIN EXTENDED SELECT s1.currval; +SELECT lastval(s1); +DROP SEQUENCE s1; + + +CREATE SEQUENCE s1; +CREATE VIEW v1 AS SELECT s1.nextval AS a; +SELECT VIEW_DEFINITION FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME='v1'; +SELECT * FROM v1; +SHOW CREATE VIEW v1; +DROP VIEW v1; +DROP SEQUENCE s1; + + +CREATE SEQUENCE s1; +CREATE VIEW v1 AS SELECT s1.currval AS a; +SELECT VIEW_DEFINITION FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME='v1'; +SELECT * FROM v1; +SHOW CREATE VIEW v1; +DROP VIEW v1; +DROP SEQUENCE s1; + +--echo # +--echo # MDEV-12533 sql_mode=ORACLE: Add support for database qualified sequence names in NEXTVAL and CURRVAL +--echo # +CREATE SEQUENCE s1; +SELECT test.s1.nextval; +SELECT test.s1.currval; +SELECT .s1.nextval; +SELECT .s1.currval; +DROP SEQUENCE s1; diff --git a/query_classifier/test/oracle/sp-anonymous.test b/query_classifier/test/oracle/sp-anonymous.test new file mode 100644 index 000000000..ac61e8ace --- /dev/null +++ b/query_classifier/test/oracle/sp-anonymous.test @@ -0,0 +1,244 @@ +--source include/have_innodb.inc + +SET sql_mode=ORACLE; + +--echo # +--echo # MDEV-10655 Anonymous blocks +--echo # + +--echo # Testing BEGIN NOT ATOMIC with no declarations +DELIMITER /; +BEGIN NOT ATOMIC + SELECT 1 AS a; +END +/ +DELIMITER ;/ + +--echo # Testing BEGIN NOT ATOMIC with declarations +--echo # DECLARE starts a new block and thus must be followed by BEGIN .. END +DELIMITER /; +BEGIN NOT ATOMIC + DECLARE + i INT DEFAULT 5; + x INT DEFAULT 10; + BEGIN + <