diff options
Diffstat (limited to 'client/mysqltest.cc')
-rw-r--r-- | client/mysqltest.cc | 12109 |
1 files changed, 12109 insertions, 0 deletions
diff --git a/client/mysqltest.cc b/client/mysqltest.cc new file mode 100644 index 00000000..87c5a62a --- /dev/null +++ b/client/mysqltest.cc @@ -0,0 +1,12109 @@ +/* Copyright (c) 2000, 2013, Oracle and/or its affiliates. + Copyright (c) 2009, 2022, MariaDB + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ + +/* + mysqltest + + Tool used for executing a .test file + + See the "MySQL Test framework manual" for more information + https://mariadb.com/kb/en/library/mysqltest/ + + Please keep the test framework tools identical in all versions! + + Written by: + Sasha Pachev <sasha@mysql.com> + Matt Wagner <matt@mysql.com> + Monty + Jani + Holyfoot + And many others +*/ + +#define MTEST_VERSION "3.5" + +#include "client_priv.h" +#include <mysql_version.h> +#include <mysqld_error.h> +#include <sql_common.h> +#include <m_ctype.h> +#include "client_metadata.h" +#include <my_dir.h> +#include <hash.h> +#include <stdarg.h> +#include <violite.h> +#define PCRE2_STATIC 1 /* Important on Windows */ +#include "pcre2posix.h" /* pcreposix regex library */ +#ifdef HAVE_SYS_WAIT_H +#include <sys/wait.h> +#endif +#ifdef _WIN32 +#include <direct.h> +#endif +#include <signal.h> +#include <my_stacktrace.h> + +#include <welcome_copyright_notice.h> // ORACLE_WELCOME_COPYRIGHT_NOTICE + +#ifdef _WIN32 +#include <crtdbg.h> +#define SIGNAL_FMT "exception 0x%x" +#else +#define SIGNAL_FMT "signal %d" +#endif + +static my_bool non_blocking_api_enabled= 0; +#if !defined(EMBEDDED_LIBRARY) +#define WRAP_NONBLOCK_ENABLED non_blocking_api_enabled +#include "../tests/nonblock-wrappers.h" +#endif + + +#define MAX_VAR_NAME_LENGTH 256 +#define MAX_COLUMNS 256 +#define MAX_EMBEDDED_SERVER_ARGS 64 +#define MAX_DELIMITER_LENGTH 16 +#define DEFAULT_MAX_CONN 64 + +#define DIE_BUFF_SIZE 256*1024 + +/* Flags controlling send and reap */ +#define QUERY_SEND_FLAG 1 +#define QUERY_REAP_FLAG 2 + +#define QUERY_PRINT_ORIGINAL_FLAG 4 + +#define CLOSED_CONNECTION "-closed_connection-" + +#ifndef HAVE_SETENV +static int setenv(const char *name, const char *value, int overwrite); +#endif + +C_MODE_START +static sig_handler signal_handler(int sig); +static my_bool get_one_option(const struct my_option *, const char *, + const char *); +C_MODE_END + +enum { + OPT_LOG_DIR=OPT_MAX_CLIENT_OPTION, OPT_RESULT_FORMAT_VERSION +}; + +static int record= 0, opt_sleep= -1; +static char *opt_db= 0, *opt_pass= 0; +const char *opt_user= 0, *opt_host= 0, *unix_sock= 0, *opt_basedir= "./"; +const char *opt_logdir= ""; +const char *opt_prologue= 0, *opt_charsets_dir; +static int opt_port= 0; +static int opt_max_connect_retries; +static int opt_result_format_version; +static int opt_max_connections= DEFAULT_MAX_CONN; +static int error_count= 0; +static my_bool opt_compress= 0, silent= 0, verbose= 0; +static my_bool debug_info_flag= 0, debug_check_flag= 0; +static my_bool tty_password= 0; +static my_bool opt_mark_progress= 0; +static my_bool ps_protocol= 0, ps_protocol_enabled= 0, ps2_protocol_enabled= 0; +static my_bool sp_protocol= 0, sp_protocol_enabled= 0; +static my_bool view_protocol= 0, view_protocol_enabled= 0; +static my_bool service_connection_enabled= 1; +static my_bool cursor_protocol= 0, cursor_protocol_enabled= 0; +static my_bool parsing_disabled= 0; +static my_bool display_result_vertically= FALSE, display_result_lower= FALSE, + display_metadata= FALSE, display_result_sorted= FALSE, + display_session_track_info= FALSE; +static my_bool disable_query_log= 0, disable_result_log= 0; +static my_bool disable_connect_log= 0; +static my_bool disable_warnings= 0, disable_column_names= 0; +static my_bool prepare_warnings_enabled= 0; +static my_bool disable_info= 1; +static my_bool abort_on_error= 1, opt_continue_on_error= 0; +static my_bool server_initialized= 0; +static my_bool is_windows= 0; +static my_bool optimizer_trace_active= 0; +static char **default_argv; +static const char *load_default_groups[]= +{ "mysqltest", "mariadb-test", "client", "client-server", "client-mariadb", + 0 }; +static char line_buffer[MAX_DELIMITER_LENGTH], *line_buffer_pos= line_buffer; + +/* Info on properties that can be set with --enable_X and --disable_X */ + +struct property { + my_bool *var; /* Actual variable */ + my_bool set; /* Has been set for ONE command */ + my_bool old; /* If set, thus is the old value */ + my_bool reverse; /* Variable is true if disabled */ + const char *env_name; /* Env. variable name */ +}; + +static struct property prop_list[] = { + { &abort_on_error, 0, 1, 0, "$ENABLED_ABORT_ON_ERROR" }, + { &disable_connect_log, 0, 1, 1, "$ENABLED_CONNECT_LOG" }, + { &disable_info, 0, 1, 1, "$ENABLED_INFO" }, + { &display_session_track_info, 0, 1, 1, "$ENABLED_STATE_CHANGE_INFO" }, + { &display_metadata, 0, 0, 0, "$ENABLED_METADATA" }, + { &ps_protocol_enabled, 0, 0, 0, "$ENABLED_PS_PROTOCOL" }, + { &ps2_protocol_enabled, 0, 0, 0, "$ENABLED_PS2_PROTOCOL" }, + { &view_protocol_enabled, 0, 0, 0, "$ENABLED_VIEW_PROTOCOL"}, + { &service_connection_enabled, 0, 1, 0, "$ENABLED_SERVICE_CONNECTION"}, + { &disable_query_log, 0, 0, 1, "$ENABLED_QUERY_LOG" }, + { &disable_result_log, 0, 0, 1, "$ENABLED_RESULT_LOG" }, + { &disable_warnings, 0, 0, 1, "$ENABLED_WARNINGS" } +}; + +static my_bool once_property= FALSE; + +enum enum_prop { + P_ABORT= 0, + P_CONNECT, + P_INFO, + P_SESSION_TRACK, + P_META, + P_PS, + P_PS2, + P_VIEW, + P_CONN, + P_QUERY, + P_RESULT, + P_WARN, + P_MAX +}; + +static uint start_lineno= 0; /* Start line of current command */ +static uint my_end_arg= 0; + +/* Number of lines of the result to include in failure report */ +static uint opt_tail_lines= 0; + +static uint opt_connect_timeout= 0; +static uint opt_wait_for_pos_timeout= 0; +static const uint default_wait_for_pos_timeout= 300; +static char delimiter[MAX_DELIMITER_LENGTH]= ";"; +static size_t delimiter_length= 1; + +static char TMPDIR[FN_REFLEN]; +static char global_subst_from[200]; +static char global_subst_to[200]; +static char *global_subst= NULL; +static char *read_command_buf= NULL; +static MEM_ROOT require_file_root; +static const my_bool my_true= 1; +static const my_bool my_false= 0; + +/* Block stack */ +enum block_cmd { + cmd_none, + cmd_if, + cmd_while +}; + +struct st_block +{ + int line; /* Start line of block */ + my_bool ok; /* Should block be executed */ + enum block_cmd cmd; /* Command owning the block */ + char delim[MAX_DELIMITER_LENGTH]; /* Delimiter before block */ +}; + +static struct st_block block_stack[32]; +static struct st_block *cur_block, *block_stack_end; + +/* Open file stack */ +struct st_test_file +{ + FILE* file; + char *file_name; + uint lineno; /* Current line in file */ +}; + +static struct st_test_file file_stack[16]; +static struct st_test_file* cur_file; +static struct st_test_file* file_stack_end; + +static CHARSET_INFO *charset_info= &my_charset_latin1; /* Default charset */ + +static const char *embedded_server_groups[]= +{ + "server", + "embedded", + "mysqltest_SERVER", + NullS +}; + +static int embedded_server_arg_count=0; +static char *embedded_server_args[MAX_EMBEDDED_SERVER_ARGS]; + +/* + Timer related variables + See the timer_output() definition for details +*/ +static char *timer_file = NULL; +static ulonglong timer_start; +static void timer_output(void); +static ulonglong timer_now(void); + + +static ulong connection_retry_sleep= 100000; /* Microseconds */ + +static const char *opt_plugin_dir; +static const char *opt_suite_dir, *opt_overlay_dir; +static size_t suite_dir_len, overlay_dir_len; + +/* Precompiled re's */ +static regex_t ps_re; /* the query can be run using PS protocol */ +static regex_t ps2_re; /* the query can be run using PS protocol with second execution*/ +static regex_t sp_re; /* the query can be run as a SP */ +static regex_t view_re; /* the query can be run as a view*/ + +static void init_re(void); +static int match_re(regex_t *, char *); +static void free_re(void); + +static char *get_string(char **to_ptr, char **from_ptr, + struct st_command *command); +static int replace(DYNAMIC_STRING *ds_str, + const char *search_str, size_t search_len, + const char *replace_str, size_t replace_len); + +static uint opt_protocol=0; + +DYNAMIC_ARRAY q_lines; + +#include "sslopt-vars.h" + +struct Parser +{ + int read_lines,current_line; +} parser; + +struct MasterPos +{ + char file[FN_REFLEN]; + ulong pos; +} master_pos; + +/* if set, all results are concated and compared against this file */ +const char *result_file_name= 0; + +typedef struct +{ + char *name; + size_t name_len; + char *str_val; + size_t str_val_len; + int int_val; + size_t alloced_len; + bool int_dirty; /* do not update string if int is updated until first read */ + bool is_int; + bool alloced; +} VAR; + +/*Perl/shell-like variable registers */ +VAR var_reg[10]; + +HASH var_hash; + +struct st_connection +{ + MYSQL *mysql; + /* Used when creating views and sp, to avoid implicit commit */ + MYSQL* util_mysql; + char *name; + size_t name_len; + MYSQL_STMT* stmt; + MYSQL_BIND *ps_params; + /* Set after send to disallow other queries before reap */ + my_bool pending; + +#ifdef EMBEDDED_LIBRARY + pthread_t tid; + const char *cur_query; + int cur_query_len; + int command, result; + pthread_mutex_t query_mutex; + pthread_cond_t query_cond; + pthread_mutex_t result_mutex; + pthread_cond_t result_cond; + int query_done; + my_bool has_thread; +#endif /*EMBEDDED_LIBRARY*/ +}; + +struct st_connection *connections= NULL; +struct st_connection* cur_con= NULL, *next_con, *connections_end; + +/* + List of commands in mysqltest + Must match the "command_names" array + Add new commands before Q_UNKNOWN! +*/ +enum enum_commands { + Q_CONNECTION=1, Q_QUERY, + Q_CONNECT, Q_SLEEP, Q_REAL_SLEEP, + Q_INC, Q_DEC, + Q_SOURCE, Q_DISCONNECT, + Q_LET, Q_ECHO, + Q_WHILE, Q_END_BLOCK, + Q_SYSTEM, Q_RESULT, + Q_REQUIRE, Q_SAVE_MASTER_POS, + Q_SYNC_WITH_MASTER, + Q_SYNC_SLAVE_WITH_MASTER, + Q_ERROR, + Q_SEND, Q_REAP, + Q_DIRTY_CLOSE, Q_REPLACE, Q_REPLACE_COLUMN, + Q_PING, Q_EVAL, + Q_EVALP, + Q_EVAL_RESULT, + Q_ENABLE_QUERY_LOG, Q_DISABLE_QUERY_LOG, + Q_ENABLE_RESULT_LOG, Q_DISABLE_RESULT_LOG, + Q_ENABLE_CONNECT_LOG, Q_DISABLE_CONNECT_LOG, + Q_WAIT_FOR_SLAVE_TO_STOP, + Q_ENABLE_WARNINGS, Q_DISABLE_WARNINGS, + Q_ENABLE_INFO, Q_DISABLE_INFO, + Q_ENABLE_SESSION_TRACK_INFO, Q_DISABLE_SESSION_TRACK_INFO, + Q_ENABLE_METADATA, Q_DISABLE_METADATA, + Q_ENABLE_COLUMN_NAMES, Q_DISABLE_COLUMN_NAMES, + Q_EXEC, Q_DELIMITER, + Q_DISABLE_ABORT_ON_ERROR, Q_ENABLE_ABORT_ON_ERROR, + Q_DISPLAY_VERTICAL_RESULTS, Q_DISPLAY_HORIZONTAL_RESULTS, + Q_QUERY_VERTICAL, Q_QUERY_HORIZONTAL, Q_SORTED_RESULT, + Q_LOWERCASE, + Q_START_TIMER, Q_END_TIMER, + Q_CHARACTER_SET, Q_DISABLE_PS_PROTOCOL, Q_ENABLE_PS_PROTOCOL, + Q_DISABLE_PS2_PROTOCOL, Q_ENABLE_PS2_PROTOCOL, + Q_DISABLE_VIEW_PROTOCOL, Q_ENABLE_VIEW_PROTOCOL, + Q_DISABLE_SERVICE_CONNECTION, Q_ENABLE_SERVICE_CONNECTION, + Q_ENABLE_NON_BLOCKING_API, Q_DISABLE_NON_BLOCKING_API, + Q_DISABLE_RECONNECT, Q_ENABLE_RECONNECT, + Q_IF, + Q_DISABLE_PARSING, Q_ENABLE_PARSING, + Q_REPLACE_REGEX, Q_REMOVE_FILE, Q_FILE_EXIST, + Q_WRITE_FILE, Q_COPY_FILE, Q_PERL, Q_DIE, Q_EXIT, Q_SKIP, + Q_CHMOD_FILE, Q_APPEND_FILE, Q_CAT_FILE, Q_DIFF_FILES, + Q_SEND_QUIT, Q_CHANGE_USER, Q_MKDIR, Q_RMDIR, + Q_LIST_FILES, Q_LIST_FILES_WRITE_FILE, Q_LIST_FILES_APPEND_FILE, + Q_SEND_SHUTDOWN, Q_SHUTDOWN_SERVER, + Q_RESULT_FORMAT_VERSION, + Q_MOVE_FILE, Q_REMOVE_FILES_WILDCARD, Q_SEND_EVAL, + Q_ENABLE_PREPARE_WARNINGS, Q_DISABLE_PREPARE_WARNINGS, + Q_RESET_CONNECTION, + Q_OPTIMIZER_TRACE, + Q_PS_PREPARE, + Q_PS_BIND, + Q_PS_EXECUTE, + Q_PS_CLOSE, + Q_UNKNOWN, /* Unknown command. */ + Q_COMMENT, /* Comments, ignored. */ + Q_COMMENT_WITH_COMMAND, + Q_EMPTY_LINE +}; + + +const char *command_names[]= +{ + "connection", + "query", + "connect", + "sleep", + "real_sleep", + "inc", + "dec", + "source", + "disconnect", + "let", + "echo", + "while", + "end", + "system", + "result", + "require", + "save_master_pos", + "sync_with_master", + "sync_slave_with_master", + "error", + "send", + "reap", + "dirty_close", + "replace_result", + "replace_column", + "ping", + "eval", + "evalp", + "eval_result", + /* Enable/disable that the _query_ is logged to result file */ + "enable_query_log", + "disable_query_log", + /* Enable/disable that the _result_ from a query is logged to result file */ + "enable_result_log", + "disable_result_log", + "enable_connect_log", + "disable_connect_log", + "wait_for_slave_to_stop", + "enable_warnings", + "disable_warnings", + "enable_info", + "disable_info", + "enable_session_track_info", + "disable_session_track_info", + "enable_metadata", + "disable_metadata", + "enable_column_names", + "disable_column_names", + "exec", + "delimiter", + "disable_abort_on_error", + "enable_abort_on_error", + "vertical_results", + "horizontal_results", + "query_vertical", + "query_horizontal", + "sorted_result", + "lowercase_result", + "start_timer", + "end_timer", + "character_set", + "disable_ps_protocol", + "enable_ps_protocol", + "disable_ps2_protocol", + "enable_ps2_protocol", + "disable_view_protocol", + "enable_view_protocol", + "disable_service_connection", + "enable_service_connection", + "enable_non_blocking_api", + "disable_non_blocking_api", + "disable_reconnect", + "enable_reconnect", + "if", + "disable_parsing", + "enable_parsing", + "replace_regex", + "remove_file", + "file_exists", + "write_file", + "copy_file", + "perl", + "die", + + /* Don't execute any more commands, compare result */ + "exit", + "skip", + "chmod", + "append_file", + "cat_file", + "diff_files", + "send_quit", + "change_user", + "mkdir", + "rmdir", + "list_files", + "list_files_write_file", + "list_files_append_file", + "send_shutdown", + "shutdown_server", + "result_format", + "move_file", + "remove_files_wildcard", + "send_eval", + "enable_prepare_warnings", + "disable_prepare_warnings", + "reset_connection", + "optimizer_trace", + "PS_prepare", + "PS_bind", + "PS_execute", + "PS_close", + 0 +}; + + +/* + The list of error codes to --error are stored in an internal array of + structs. This struct can hold numeric SQL error codes, error names or + SQLSTATE codes as strings. The element next to the last active element + in the list is set to type ERR_EMPTY. When an SQL statement returns an + error, we use this list to check if this is an expected error. +*/ +enum match_err_type +{ + ERR_EMPTY= 0, + ERR_ERRNO, + ERR_SQLSTATE +}; + +struct st_match_err +{ + enum match_err_type type; + union + { + uint errnum; + char sqlstate[SQLSTATE_LENGTH+1]; /* \0 terminated string */ + } code; +}; + +struct st_expected_errors +{ + struct st_match_err err[12]; + uint count; +}; +static struct st_expected_errors saved_expected_errors; + +struct st_command +{ + char *query, *query_buf,*first_argument,*last_argument,*end; + DYNAMIC_STRING content; + DYNAMIC_STRING eval_query; + int first_word_len, query_len; + my_bool abort_on_error, used_replace; + struct st_expected_errors expected_errors; + char *require_file; + enum enum_commands type; +}; + +TYPELIB command_typelib= {array_elements(command_names),"", + command_names, 0}; + +DYNAMIC_STRING ds_res; +/* Points to ds_warning in run_query, so it can be freed */ +DYNAMIC_STRING *ds_warn= 0; +struct st_command *curr_command= 0; + +char builtin_echo[FN_REFLEN]; + +struct st_replace_regex +{ +DYNAMIC_ARRAY regex_arr; /* stores a list of st_regex substitutions */ + +/* +Temporary storage areas for substitutions. To reduce unnecessary copying +and memory freeing/allocation, we pre-allocate two buffers, and alternate +their use, one for input/one for output, the roles changing on the next +st_regex substitution. At the end of substitutions buf points to the +one containing the final result. +*/ +char* buf; +char* even_buf; +char* odd_buf; +int even_buf_len; +int odd_buf_len; +}; + +struct st_replace_regex *glob_replace_regex= 0; + +struct st_replace; +struct st_replace *glob_replace= 0; +void replace_strings_append(struct st_replace *rep, DYNAMIC_STRING* ds, +const char *from); + +ATTRIBUTE_NORETURN +static void cleanup_and_exit(int exit_code); + +ATTRIBUTE_NORETURN +static void really_die(const char *msg); +void report_or_die(const char *fmt, ...); +ATTRIBUTE_NORETURN +static void die(const char *fmt, ...); +static void make_error_message(char *buf, size_t len, const char *fmt, va_list args); +ATTRIBUTE_NORETURN ATTRIBUTE_FORMAT(printf, 1, 2) +void abort_not_supported_test(const char *fmt, ...); +void verbose_msg(const char *fmt, ...) ATTRIBUTE_FORMAT(printf, 1, 2); +void log_msg(const char *fmt, ...) ATTRIBUTE_FORMAT(printf, 1, 2); + +VAR* var_from_env(const char *, const char *); +VAR* var_init(VAR* v, const char *name, size_t name_len, const char *val, size_t val_len); +VAR* var_get(const char *var_name, const char** var_name_end, + my_bool raw, my_bool ignore_not_existing); +void eval_expr(VAR* v, const char *p, const char** p_end, + bool open_end=false, bool do_eval=true); +my_bool match_delimiter(int c, const char *delim, size_t length); +void dump_result_to_reject_file(char *buf, int size); +void dump_warning_messages(); + +void do_eval(DYNAMIC_STRING *query_eval, const char *query, + const char *query_end, my_bool pass_through_escape_chars); +void str_to_file(const char *fname, char *str, size_t size); +void str_to_file2(const char *fname, char *str, size_t size, my_bool append); + +void fix_win_paths(char *val, size_t len); +const char *get_errname_from_code (uint error_code); +int multi_reg_replace(struct st_replace_regex* r,char* val); + +#ifdef _WIN32 +void free_win_path_patterns(); +#endif + + +/* For replace_column */ +static char *replace_column[MAX_COLUMNS]; +static uint max_replace_column= 0; +void do_get_replace_column(struct st_command*); +void free_replace_column(); + +/* For replace */ +void do_get_replace(struct st_command *command); +void free_replace(); + +/* For replace_regex */ +void do_get_replace_regex(struct st_command *command); +void free_replace_regex(); + +/* Used by sleep */ +void check_eol_junk_line(const char *eol); + +void free_all_replace(){ + free_replace(); + free_replace_regex(); + free_replace_column(); +} + +void var_set_int(const char* name, int value); +void enable_optimizer_trace(struct st_connection *con); +void display_optimizer_trace(struct st_connection *con, + DYNAMIC_STRING *ds); + + +class LogFile { + FILE* m_file; + char m_file_name[FN_REFLEN]; + size_t m_bytes_written; +public: + LogFile() : m_file(NULL), m_bytes_written(0) { + bzero(m_file_name, sizeof(m_file_name)); + } + + ~LogFile() { + close(); + } + + const char* file_name() const { return m_file_name; } + size_t bytes_written() const { return m_bytes_written; } + + void open(const char* dir, const char* name, const char* ext) + { + DBUG_ENTER("LogFile::open"); + DBUG_PRINT("enter", ("dir: '%s', name: '%s'", dir, name)); + if (!name) + { + m_file= stdout; + DBUG_VOID_RETURN; + } + + fn_format(m_file_name, name, dir, ext, + *dir ? MY_REPLACE_DIR | MY_REPLACE_EXT : + MY_REPLACE_EXT); + + DBUG_PRINT("info", ("file_name: %s", m_file_name)); + + if ((m_file= fopen(m_file_name, "wb+")) == NULL) + die("Failed to open log file %s, errno: %d", m_file_name, errno); + + DBUG_VOID_RETURN; + } + + void close() + { + if (m_file) { + if (m_file != stdout) + fclose(m_file); + else + fflush(m_file); + } + m_file= NULL; + } + + void flush() + { + if (m_file && m_file != stdout) + { + if (fflush(m_file)) + die("Failed to flush '%s', errno: %d", m_file_name, errno); + } + } + + void write(DYNAMIC_STRING* ds) + { + DBUG_ENTER("LogFile::write"); + DBUG_PRINT("enter", ("length: %u", (uint) ds->length)); + + DBUG_ASSERT(m_file); + + if (ds->length == 0) + DBUG_VOID_RETURN; + DBUG_ASSERT(ds->str); + +#ifdef EXTRA_DEBUG + DBUG_DUMP("extra", (uchar*) ds->str, ds->length); +#endif + + if (fwrite(ds->str, 1, ds->length, m_file) != ds->length) + die("Failed to write %lu bytes to '%s', errno: %d", + (unsigned long)ds->length, m_file_name, errno); + m_bytes_written+= ds->length; + DBUG_VOID_RETURN; + } + + void show_tail(uint lines) { + DBUG_ENTER("LogFile::show_tail"); + + if (!m_file || m_file == stdout) + DBUG_VOID_RETURN; + + if (lines == 0) + DBUG_VOID_RETURN; + lines++; + + int show_offset= 0; + char buf[256+1]; /* + zero termination for DBUG_PRINT */ + size_t bytes; + bool found_bof= false; + + /* Search backward in file until "lines" newline has been found */ + while (lines && !found_bof) + { + show_offset-= sizeof(buf)-1; + while(fseek(m_file, show_offset, SEEK_END) != 0 && show_offset < 0) + { + found_bof= true; + // Seeking before start of file + show_offset++; + } + + if ((bytes= fread(buf, 1, sizeof(buf)-1, m_file)) <= 0) + { + // ferror=0 will happen here if no queries executed yet + if (ferror(m_file)) + fprintf(stderr, + "Failed to read from '%s', errno: %d, feof:%d, ferror:%d\n", + m_file_name, errno, feof(m_file), ferror(m_file)); + DBUG_VOID_RETURN; + } + + DBUG_PRINT("info", ("Read %zu bytes from file, buf: %.*s", + bytes, (int)bytes, buf)); + + char* show_from= buf + bytes; + while(show_from > buf && lines > 0 ) + { + show_from--; + if (*show_from == '\n') + lines--; + } + if (show_from != buf) + { + // The last new line was found in this buf, adjust offset + show_offset+= (int)(show_from - buf) + 1; + DBUG_PRINT("info", ("adjusted offset to %d", show_offset)); + } + DBUG_PRINT("info", ("show_offset: %d", show_offset)); + } + + fprintf(stderr, "\nThe result from queries just before the failure was:\n"); + + DBUG_PRINT("info", ("show_offset: %d", show_offset)); + if (!lines) + { + fprintf(stderr, "< snip >\n"); + + if (fseek(m_file, show_offset, SEEK_END) != 0) + { + fprintf(stderr, "Failed to seek to position %d in '%s', errno: %d", + show_offset, m_file_name, errno); + DBUG_VOID_RETURN; + } + + } + else { + DBUG_PRINT("info", ("Showing the whole file")); + if (fseek(m_file, 0L, SEEK_SET) != 0) + { + fprintf(stderr, "Failed to seek to pos 0 in '%s', errno: %d", + m_file_name, errno); + DBUG_VOID_RETURN; + } + } + + while ((bytes= fread(buf, 1, sizeof(buf)-1, m_file)) > 0) + if (bytes != fwrite(buf, 1, bytes, stderr)) + die("Failed to write to '%s', errno: %d", + m_file_name, errno); + + if (!lines) + { + fprintf(stderr, + "\nMore results from queries before failure can be found in %s\n", + m_file_name); + } + fflush(stderr); + + DBUG_VOID_RETURN; + } +}; + +LogFile log_file; +LogFile progress_file; + +void replace_dynstr_append_mem(DYNAMIC_STRING *ds, const char *val, size_t len); +void replace_dynstr_append(DYNAMIC_STRING *ds, const char *val); +void replace_dynstr_append_uint(DYNAMIC_STRING *ds, uint val); +void dynstr_append_sorted(DYNAMIC_STRING* ds, DYNAMIC_STRING* ds_input, + bool keep_header); + +static int match_expected_error(struct st_command *command, + unsigned int err_errno, + const char *err_sqlstate); +void handle_error(struct st_command*, + unsigned int err_errno, const char *err_error, + const char *err_sqlstate, DYNAMIC_STRING *ds); +void handle_no_error(struct st_command*); +void revert_properties(); + +static void handle_no_active_connection(struct st_command* command, + struct st_connection *cn, DYNAMIC_STRING *ds); + + +/* Wrapper for fgets.Strips \r off newlines on Windows. + Should be used with together with my_popen(). +*/ +static char *my_fgets(char * s, int n, FILE * stream, int *len) +{ + char *buf = fgets(s, n, stream); + if (!buf) + { + *len= 0; + return buf; + } + + *len = (int)strlen(buf); +#ifdef _WIN32 + /* Strip '\r' off newlines. */ + if (*len > 1 && buf[*len - 2] == '\r' && buf[*len - 1] == '\n') + { + buf[*len - 2]= '\n'; + buf[*len - 1]= 0; + (*len)--; + } +#endif + return buf; +} + +#ifdef EMBEDDED_LIBRARY + +#define EMB_SEND_QUERY 1 +#define EMB_READ_QUERY_RESULT 2 +#define EMB_END_CONNECTION 3 +#define EMB_PREPARE_STMT 4 +#define EMB_EXECUTE_STMT 5 +#define EMB_CLOSE_STMT 6 + +/* workaround for MySQL BUG#57491 */ +#undef MY_WME +#define MY_WME 0 + +/* attributes of the query thread */ +pthread_attr_t cn_thd_attrib; + + +/* + This procedure represents the connection and actually + runs queries when in the EMBEDDED-SERVER mode. + The run_query_normal() just sends request for running + mysql_send_query and mysql_read_query_result() here. +*/ + +pthread_handler_t connection_thread(void *arg) +{ + struct st_connection *cn= (struct st_connection*)arg; + + mysql_thread_init(); + while (cn->command != EMB_END_CONNECTION) + { + if (!cn->command) + { + pthread_mutex_lock(&cn->query_mutex); + while (!cn->command) + pthread_cond_wait(&cn->query_cond, &cn->query_mutex); + pthread_mutex_unlock(&cn->query_mutex); + } + switch (cn->command) + { + case EMB_END_CONNECTION: + goto end_thread; + case EMB_SEND_QUERY: + cn->result= mysql_send_query(cn->mysql, + cn->cur_query, cn->cur_query_len); + break; + case EMB_READ_QUERY_RESULT: + cn->result= mysql_read_query_result(cn->mysql); + break; + case EMB_PREPARE_STMT: + cn->result= mysql_stmt_prepare(cn->stmt, + cn->cur_query, cn->cur_query_len); + break; + case EMB_EXECUTE_STMT: + cn->result= mysql_stmt_execute(cn->stmt); + break; + case EMB_CLOSE_STMT: + cn->result= mysql_stmt_close(cn->stmt); + break; + default: + DBUG_ASSERT(0); + } + cn->command= 0; + pthread_mutex_lock(&cn->result_mutex); + cn->query_done= 1; + pthread_cond_signal(&cn->result_cond); + pthread_mutex_unlock(&cn->result_mutex); + } + +end_thread: + cn->query_done= 1; + mysql_close(cn->mysql); + cn->mysql= 0; + mysql_thread_end(); + pthread_exit(0); + return 0; +} + +static void wait_query_thread_done(struct st_connection *con) +{ + DBUG_ASSERT(con->has_thread); + if (!con->query_done) + { + pthread_mutex_lock(&con->result_mutex); + while (!con->query_done) + pthread_cond_wait(&con->result_cond, &con->result_mutex); + pthread_mutex_unlock(&con->result_mutex); + } +} + + +static void signal_connection_thd(struct st_connection *cn, int command) +{ + DBUG_ASSERT(cn->has_thread); + cn->query_done= 0; + cn->command= command; + pthread_mutex_lock(&cn->query_mutex); + pthread_cond_signal(&cn->query_cond); + pthread_mutex_unlock(&cn->query_mutex); +} + + +/* + Sometimes we try to execute queries when the connection is closed. + It's done to make sure it was closed completely. + So that if our connection is closed (cn->has_thread == 0), we just return + the mysql_send_query() result which is an error in this case. +*/ + +static int do_send_query(struct st_connection *cn, const char *q, int q_len) +{ + if (!cn->has_thread) + return mysql_send_query(cn->mysql, q, q_len); + cn->cur_query= q; + cn->cur_query_len= q_len; + signal_connection_thd(cn, EMB_SEND_QUERY); + return 0; +} + +static int do_read_query_result(struct st_connection *cn) +{ + DBUG_ASSERT(cn->has_thread); + wait_query_thread_done(cn); + if (cn->result) + goto exit_func; + + signal_connection_thd(cn, EMB_READ_QUERY_RESULT); + wait_query_thread_done(cn); + +exit_func: + return cn->result; +} + + +static int do_stmt_prepare(struct st_connection *cn, const char *q, int q_len) +{ + /* The cn->stmt is already set. */ + DBUG_ENTER("do_stmt_prepare"); + if (!cn->has_thread) + DBUG_RETURN(mysql_stmt_prepare(cn->stmt, q, q_len)); + cn->cur_query= q; + cn->cur_query_len= q_len; + signal_connection_thd(cn, EMB_PREPARE_STMT); + wait_query_thread_done(cn); + DBUG_RETURN(cn->result); +} + + +static int do_stmt_execute(struct st_connection *cn) +{ + DBUG_ENTER("do_stmt_execute"); + /* The cn->stmt is already set. */ + if (!cn->has_thread) + DBUG_RETURN(mysql_stmt_execute(cn->stmt)); + signal_connection_thd(cn, EMB_EXECUTE_STMT); + wait_query_thread_done(cn); + DBUG_RETURN(cn->result); +} + + +static int do_stmt_close(struct st_connection *cn) +{ + DBUG_ENTER("do_stmt_close"); + /* The cn->stmt is already set. */ + if (!cn->has_thread) + DBUG_RETURN(mysql_stmt_close(cn->stmt)); + signal_connection_thd(cn, EMB_CLOSE_STMT); + wait_query_thread_done(cn); + DBUG_RETURN(cn->result); +} + + +static void emb_close_connection(struct st_connection *cn) +{ + if (!cn->has_thread) + return; + wait_query_thread_done(cn); + signal_connection_thd(cn, EMB_END_CONNECTION); + pthread_join(cn->tid, NULL); + cn->has_thread= FALSE; + pthread_mutex_destroy(&cn->query_mutex); + pthread_cond_destroy(&cn->query_cond); + pthread_mutex_destroy(&cn->result_mutex); + pthread_cond_destroy(&cn->result_cond); +} + + +static void init_connection_thd(struct st_connection *cn) +{ + cn->query_done= 1; + cn->command= 0; + if (pthread_mutex_init(&cn->query_mutex, NULL) || + pthread_cond_init(&cn->query_cond, NULL) || + pthread_mutex_init(&cn->result_mutex, NULL) || + pthread_cond_init(&cn->result_cond, NULL) || + pthread_create(&cn->tid, &cn_thd_attrib, connection_thread, (void*)cn)) + die("Error in the thread library"); + cn->has_thread=TRUE; +} + +#else /* ! EMBEDDED_LIBRARY*/ + +#define init_connection_thd(X) do { } while(0) +#define do_send_query(cn,q,q_len) mysql_send_query(cn->mysql, q, (ulong)q_len) +#define do_read_query_result(cn) mysql_read_query_result(cn->mysql) +#define do_stmt_prepare(cn, q, q_len) mysql_stmt_prepare(cn->stmt, q, (ulong)q_len) +#define do_stmt_execute(cn) mysql_stmt_execute(cn->stmt) +#define do_stmt_close(cn) mysql_stmt_close(cn->stmt) + +#endif /*EMBEDDED_LIBRARY*/ + +void do_eval(DYNAMIC_STRING *query_eval, const char *query, + const char *query_end, my_bool pass_through_escape_chars) +{ + const char *p; + char c, next_c; + int escaped = 0; + VAR *v; + DBUG_ENTER("do_eval"); + + for (p= query; (c= *p) && p < query_end; ++p) + { + switch(c) { + case '$': + if (escaped) + { + escaped= 0; + dynstr_append_mem(query_eval, p, 1); + } + else + { + if (!(v= var_get(p, &p, 0, 0))) + { + report_or_die( "Bad variable in eval"); + DBUG_VOID_RETURN; + } + dynstr_append_mem(query_eval, v->str_val, v->str_val_len); + } + break; + case '\\': + next_c= *(p+1); + if (escaped) + { + escaped= 0; + dynstr_append_mem(query_eval, p, 1); + } + else if (next_c == '\\' || next_c == '$' || next_c == '"') + { + /* Set escaped only if next char is \, " or $ */ + escaped= 1; + + if (pass_through_escape_chars) + { + /* The escape char should be added to the output string. */ + dynstr_append_mem(query_eval, p, 1); + } + } + else + dynstr_append_mem(query_eval, p, 1); + break; + default: + escaped= 0; + dynstr_append_mem(query_eval, p, 1); + break; + } + } + fix_win_paths(query_eval->str, query_eval->length); + DBUG_VOID_RETURN; +} + + +/* + Show any warnings just before the error. Since the last error + is added to the warning stack, only print @@warning_count-1 warnings. + + NOTE! This function should be safe to call when an error + has occurred and this any further errors will be ignored(although logged) + + SYNOPSIS + show_warnings_before_error + mysql - connection to use + +*/ + +static void show_warnings_before_error(MYSQL* mysql) +{ + MYSQL_RES* res; + const char* query= "SHOW WARNINGS"; + DBUG_ENTER("show_warnings_before_error"); + + if (!mysql) + DBUG_VOID_RETURN; + + if (mysql_query(mysql, query)) + { + log_msg("Error running query '%s': %d %s", + query, mysql_errno(mysql), mysql_error(mysql)); + DBUG_VOID_RETURN; + } + + if ((res= mysql_store_result(mysql)) == NULL) + { + /* No result set returned */ + DBUG_VOID_RETURN; + } + + if (mysql_num_rows(res) <= 1) + { + /* Don't display the last row, it's "last error" */ + } + else + { + MYSQL_ROW row; + unsigned int row_num= 0; + unsigned int num_fields= mysql_num_fields(res); + + fprintf(stderr, "\nWarnings from just before the error:\n"); + while ((row= mysql_fetch_row(res))) + { + unsigned int i; + unsigned long *lengths= mysql_fetch_lengths(res); + + if (++row_num >= mysql_num_rows(res)) + { + /* Don't display the last row, it's "last error" */ + break; + } + + for(i= 0; i < num_fields; i++) + { + fprintf(stderr, "%.*s ", (int)lengths[i], + row[i] ? row[i] : "NULL"); + } + fprintf(stderr, "\n"); + } + } + mysql_free_result(res); + + DBUG_VOID_RETURN; +} + + +enum arg_type +{ + ARG_STRING, + ARG_REST +}; + +struct command_arg { + const char *argname; /* Name of argument */ + enum arg_type type; /* Type of argument */ + my_bool required; /* Argument required */ + DYNAMIC_STRING *ds; /* Storage for argument */ + const char *description; /* Description of the argument */ +}; + + +void check_command_args(struct st_command *command, + const char *arguments, + const struct command_arg *args, + int num_args, const char delimiter_arg) +{ + int i; + const char *ptr= arguments; + const char *start; + DBUG_ENTER("check_command_args"); + DBUG_PRINT("enter", ("num_args: %d", num_args)); + + for (i= 0; i < num_args; i++) + { + const struct command_arg *arg= &args[i]; + char delimiter; + + switch (arg->type) { + /* A string */ + case ARG_STRING: + /* Skip leading spaces */ + while (*ptr && *ptr == ' ') + ptr++; + start= ptr; + delimiter = delimiter_arg; + /* If start of arg is ' ` or " search to matching quote end instead */ + if (*ptr && strchr ("'`\"", *ptr)) + { + delimiter= *ptr; + start= ++ptr; + } + /* Find end of arg, terminated by "delimiter" */ + while (*ptr && *ptr != delimiter) + ptr++; + if (ptr > start) + { + init_dynamic_string(arg->ds, 0, ptr-start, 32); + do_eval(arg->ds, start, ptr, FALSE); + } + else + { + /* Empty string */ + init_dynamic_string(arg->ds, "", 0, 0); + } + /* Find real end of arg, terminated by "delimiter_arg" */ + /* This will do nothing if arg was not closed by quotes */ + while (*ptr && *ptr != delimiter_arg) + ptr++; + + command->last_argument= (char*)ptr; + + /* Step past the delimiter */ + if (*ptr && *ptr == delimiter_arg) + ptr++; + DBUG_PRINT("info", ("val: %s", arg->ds->str)); + break; + + /* Rest of line */ + case ARG_REST: + start= ptr; + init_dynamic_string(arg->ds, 0, command->query_len, 256); + do_eval(arg->ds, start, command->end, FALSE); + command->last_argument= command->end; + DBUG_PRINT("info", ("val: %s", arg->ds->str)); + break; + + default: + DBUG_ASSERT("Unknown argument type"); + break; + } + + /* Check required arg */ + if (arg->ds->length == 0 && arg->required) + die("Missing required argument '%s' to command '%.*b'", arg->argname, + command->first_word_len, command->query); + + } + /* Check for too many arguments passed */ + ptr= command->last_argument; + while(ptr <= command->end && *ptr != '#') + { + if (*ptr && *ptr != ' ') + die("Extra argument '%s' passed to '%.*b'", + ptr, command->first_word_len, command->query); + ptr++; + } + DBUG_VOID_RETURN; +} + +void handle_command_error(struct st_command *command, uint error, + int sys_errno) +{ + DBUG_ENTER("handle_command_error"); + DBUG_PRINT("enter", ("error: %d", error)); + var_set_int("$sys_errno",sys_errno); + var_set_int("$errno",error); + if (error != 0) + { + int i; + + if (command->abort_on_error) + { + report_or_die("command \"%.*b\" failed with error: %u my_errno: %d " + "errno: %d", + command->first_word_len, command->query, error, my_errno, + sys_errno); + DBUG_VOID_RETURN; + } + + i= match_expected_error(command, error, NULL); + + if (i >= 0) + { + DBUG_PRINT("info", ("command \"%.*s\" failed with expected error: %u, errno: %d", + command->first_word_len, command->query, error, + sys_errno)); + revert_properties(); + DBUG_VOID_RETURN; + } + if (command->expected_errors.count > 0) + report_or_die("command \"%.*b\" failed with wrong error: %u " + "my_errno: %d errno: %d", + command->first_word_len, command->query, error, my_errno, + sys_errno); + } + else if (command->expected_errors.err[0].type == ERR_ERRNO && + command->expected_errors.err[0].code.errnum != 0) + { + /* Error code we wanted was != 0, i.e. not an expected success */ + report_or_die("command \"%.*b\" succeeded - should have failed with " + "errno %d...", + command->first_word_len, command->query, + command->expected_errors.err[0].code.errnum); + } + revert_properties(); + DBUG_VOID_RETURN; +} + + +void close_connections() +{ + DBUG_ENTER("close_connections"); + for (--next_con; next_con >= connections; --next_con) + { + if (next_con->stmt) + do_stmt_close(next_con); +#ifdef EMBEDDED_LIBRARY + emb_close_connection(next_con); +#endif + next_con->stmt= 0; + mysql_close(next_con->mysql); + next_con->mysql= 0; + if (next_con->util_mysql) + mysql_close(next_con->util_mysql); + my_free(next_con->name); + } + my_free(connections); + DBUG_VOID_RETURN; +} + +void close_util_connections() +{ + DBUG_ENTER("close_util_connections"); + if (cur_con->util_mysql) + { + mysql_close(cur_con->util_mysql); + cur_con->util_mysql = 0; + } + DBUG_VOID_RETURN; +} + +void close_statements() +{ + struct st_connection *con; + DBUG_ENTER("close_statements"); + for (con= connections; con < next_con; con++) + { + if (con->stmt) + do_stmt_close(con); + con->stmt= 0; + } + DBUG_VOID_RETURN; +} + + +void close_files() +{ + DBUG_ENTER("close_files"); + for (; cur_file >= file_stack; cur_file--) + { + if (cur_file->file && cur_file->file != stdin) + { + DBUG_PRINT("info", ("closing file: %s", cur_file->file_name)); + fclose(cur_file->file); + } + my_free(cur_file->file_name); + cur_file->file_name= 0; + } + DBUG_VOID_RETURN; +} + + +void free_used_memory() +{ + uint i; + DBUG_ENTER("free_used_memory"); + + if (connections) + close_connections(); + close_files(); + my_hash_free(&var_hash); + + for (i= 0 ; i < q_lines.elements ; i++) + { + struct st_command **q= dynamic_element(&q_lines, i, struct st_command**); + my_free((*q)->query_buf); + if ((*q)->eval_query.str) + dynstr_free(&(*q)->eval_query); + if ((*q)->content.str) + dynstr_free(&(*q)->content); + my_free((*q)); + } + for (i= 0; i < 10; i++) + { + if (var_reg[i].alloced_len) + my_free(var_reg[i].str_val); + } + while (embedded_server_arg_count > 1) + my_free(embedded_server_args[--embedded_server_arg_count]); + delete_dynamic(&q_lines); + dynstr_free(&ds_res); + if (ds_warn) + dynstr_free(ds_warn); + free_all_replace(); + my_free(opt_pass); + free_defaults(default_argv); + free_root(&require_file_root, MYF(0)); + free_re(); + my_free(read_command_buf); +#ifdef _WIN32 + free_win_path_patterns(); +#endif + DBUG_VOID_RETURN; +} + + +#ifdef EMBEDDED_LIBRARY +void ha_pre_shutdown(); +#endif + + +ATTRIBUTE_NORETURN static void cleanup_and_exit(int exit_code) +{ +#ifdef EMBEDDED_LIBRARY + if (server_initialized) + ha_pre_shutdown(); +#endif + + free_used_memory(); + + /* Only call mysql_server_end if mysql_server_init has been called */ + if (server_initialized) + mysql_server_end(); + + /* + mysqltest is fundamentally written in a way that makes impossible + to free all memory before exit (consider memory allocated + for frame local DYNAMIC_STRING's and die() invoked down the stack. + + We close stderr here to stop unavoidable safemalloc reports + from polluting the output. + */ + fclose(stderr); + + my_end(my_end_arg); + + if (!silent) { + switch (exit_code) { + case 1: + printf("not ok\n"); + break; + case 0: + printf("ok\n"); + break; + case 62: + printf("skipped\n"); + break; + default: + printf("unknown exit code: %d\n", exit_code); + DBUG_ASSERT(0); + } + } + + exit(exit_code); +} + +size_t print_file_stack(char *s, const char *end) +{ + char *start= s; + struct st_test_file* err_file= cur_file; + if (err_file == file_stack) + return 0; + + for (;;) + { + err_file--; + s+= my_snprintf(s, end - s, "included from %s at line %d:\n", + err_file->file_name, err_file->lineno); + if (err_file == file_stack) + break; + } + return s - start; +} + + +static void make_error_message(char *buf, size_t len, const char *fmt, va_list args) +{ + char *s= buf, *end= buf + len; + s+= my_snprintf(s, end - s, "mysqltest: "); + if (cur_file && cur_file != file_stack) + { + s+= my_snprintf(s, end - s, "In included file \"%s\": \n", + cur_file->file_name); + s+= print_file_stack(s, end); + } + + if (start_lineno > 0) + s+= my_snprintf(s, end -s, "At line %u: ", start_lineno); + if (!fmt) + fmt= "unknown error"; + + s+= my_vsnprintf(s, end - s, fmt, args); + s+= my_snprintf(s, end -s, "\n"); +} + +static void die(const char *fmt, ...) +{ + char buff[DIE_BUFF_SIZE]; + va_list args; + DBUG_ENTER("die"); + + va_start(args, fmt); + make_error_message(buff, sizeof(buff), fmt, args); + really_die(buff); +} + +static void really_die(const char *msg) +{ + static int dying= 0; + fflush(stdout); + fprintf(stderr, "%s", msg); + fflush(stderr); + + /* + Protect against dying twice + first time 'die' is called, try to write log files + second time, just exit + */ + if (dying) + cleanup_and_exit(1); + dying= 1; + + log_file.show_tail(opt_tail_lines); + + /* + Help debugging by displaying any warnings that might have + been produced prior to the error + */ + if (cur_con && !cur_con->pending) + show_warnings_before_error(cur_con->mysql); + + cleanup_and_exit(1); +} + +void report_or_die(const char *fmt, ...) +{ + va_list args; + DBUG_ENTER("report_or_die"); + + char buff[DIE_BUFF_SIZE]; + + va_start(args, fmt); + make_error_message(buff, sizeof(buff), fmt, args); + va_end(args); + + if (opt_continue_on_error) + { + /* Just log the error and continue */ + replace_dynstr_append(&ds_res, buff); + error_count++; + DBUG_VOID_RETURN; + } + + really_die(buff); +} + + +void abort_not_supported_test(const char *fmt, ...) +{ + va_list args; + DBUG_ENTER("abort_not_supported_test"); + + /* Print include filestack */ + fflush(stdout); + fprintf(stderr, "The test '%s' is not supported by this installation\n", + file_stack->file_name); + fprintf(stderr, "Detected in file %s at line %d\n", + cur_file->file_name, cur_file->lineno); + + char buff[DIE_BUFF_SIZE]; + buff[0] = '\0'; + print_file_stack(buff, buff + sizeof(buff)); + fprintf(stderr, "%s", buff); + + /* Print error message */ + va_start(args, fmt); + if (fmt) + { + fprintf(stderr, "reason: "); + vfprintf(stderr, fmt, args); + fprintf(stderr, "\n"); + fflush(stderr); + } + va_end(args); + + cleanup_and_exit(62); +} + + +void abort_not_in_this_version() +{ + die("Not available in this version of mysqltest"); +} + + +void verbose_msg(const char *fmt, ...) +{ + va_list args; + DBUG_ENTER("verbose_msg"); + DBUG_PRINT("enter", ("format: %s", fmt)); + + if (!verbose) + DBUG_VOID_RETURN; + + fflush(stdout); + va_start(args, fmt); + fprintf(stderr, "mysqltest: "); + if (cur_file && cur_file != file_stack) + fprintf(stderr, "In included file \"%s\": ", + cur_file->file_name); + if (start_lineno != 0) + fprintf(stderr, "At line %u: ", start_lineno); + vfprintf(stderr, fmt, args); + fprintf(stderr, "\n"); + va_end(args); + fflush(stderr); + + DBUG_VOID_RETURN; +} + + +void log_msg(const char *fmt, ...) +{ + va_list args; + char buff[1024]; + size_t len; + DBUG_ENTER("log_msg"); + + va_start(args, fmt); + len= my_vsnprintf(buff, sizeof(buff)-1, fmt, args); + va_end(args); + + dynstr_append_mem(&ds_res, buff, len); + dynstr_append(&ds_res, "\n"); + + DBUG_VOID_RETURN; +} + + +/* + Read a file and append it to ds + + SYNOPSIS + cat_file + ds - pointer to dynamic string where to add the files content + filename - name of the file to read + +*/ + +int cat_file(DYNAMIC_STRING* ds, const char* filename) +{ + int fd; + size_t len; + char *buff; + + if ((fd= my_open(filename, O_RDONLY, MYF(0))) < 0) + return 1; + + len= (size_t) my_seek(fd, 0, SEEK_END, MYF(0)); + my_seek(fd, 0, SEEK_SET, MYF(0)); + if (len == (size_t)MY_FILEPOS_ERROR || + !(buff= (char*)my_malloc(PSI_NOT_INSTRUMENTED, len + 1, + MYF(MY_WME|MY_FAE)))) + { + my_close(fd, MYF(0)); + return 1; + } + len= my_read(fd, (uchar*)buff, len, MYF(0)); + my_close(fd, MYF(0)); + + { + char *p= buff, *start= buff,*end=buff+len; + while (p < end) + { + /* Convert cr/lf to lf */ + if (*p == '\r' && p+1 < end && *(p+1)== '\n') + { + /* Add fake newline instead of cr and output the line */ + *p= '\n'; + p++; /* Step past the "fake" newline */ + *p= 0; + replace_dynstr_append_mem(ds, start, p-start); + p++; /* Step past the "fake" newline */ + start= p; + } + else + p++; + } + /* Output any chars that migh be left */ + *p= 0; + replace_dynstr_append_mem(ds, start, p-start); + } + my_free(buff); + return 0; +} + + +/* + Run the specified command with popen + + SYNOPSIS + run_command + cmd - command to execute(should be properly quoted + ds_res- pointer to dynamic string where to store the result + +*/ + +static int run_command(char* cmd, + DYNAMIC_STRING *ds_res) +{ + char buf[512]= {0}; + FILE *res_file; + int error; + DBUG_ENTER("run_command"); + DBUG_PRINT("enter", ("cmd: %s", cmd)); + + if (!(res_file= my_popen(cmd, "r"))) + { + report_or_die("popen(\"%s\", \"r\") failed", cmd); + DBUG_RETURN(-1); + } + + int len; + while (my_fgets(buf, sizeof(buf), res_file, &len)) + { + DBUG_PRINT("info", ("buf: %s", buf)); + if(ds_res) + { + /* Save the output of this command in the supplied string */ + dynstr_append_mem(ds_res, buf,len); + } + else + { + /* Print it directly on screen */ + fprintf(stdout, "%s", buf); + } + } + + error= my_pclose(res_file); + DBUG_RETURN(WEXITSTATUS(error)); +} + + +/* + Run the specified tool with variable number of arguments + + SYNOPSIS + run_tool + tool_path - the name of the tool to run + ds_res - pointer to dynamic string where to store the result + ... - variable number of arguments that will be properly + quoted and appended after the tool's name + +*/ + +static int run_tool(const char *tool_path, DYNAMIC_STRING *ds_res, ...) +{ + int ret; + const char* arg; + va_list args; + DYNAMIC_STRING ds_cmdline; + + DBUG_ENTER("run_tool"); + DBUG_PRINT("enter", ("tool_path: %s", tool_path)); + + if (init_dynamic_string(&ds_cmdline, IF_WIN("\"", ""), FN_REFLEN, FN_REFLEN)) + die("Out of memory"); + + dynstr_append_os_quoted(&ds_cmdline, tool_path, NullS); + dynstr_append(&ds_cmdline, " "); + + va_start(args, ds_res); + + while ((arg= va_arg(args, char *))) + { + /* Options should be os quoted */ + if (strncmp(arg, "--", 2) == 0) + dynstr_append_os_quoted(&ds_cmdline, arg, NullS); + else + dynstr_append(&ds_cmdline, arg); + dynstr_append(&ds_cmdline, " "); + } + + va_end(args); + +#ifdef _WIN32 + dynstr_append(&ds_cmdline, "\""); +#endif + + DBUG_PRINT("info", ("Running: %s", ds_cmdline.str)); + ret= run_command(ds_cmdline.str, ds_res); + DBUG_PRINT("exit", ("ret: %d", ret)); + dynstr_free(&ds_cmdline); + DBUG_RETURN(ret); +} + + +/* + Test if diff is present. This is needed on Windows systems + as the OS returns 1 whether diff is successful or if it is + not present. + + We run diff -v and look for output in stdout. + We don't redirect stderr to stdout to make for a simplified check + Windows will output '"diff"' is not recognized... to stderr if it is + not present. +*/ + +#ifdef _WIN32 + +static int diff_check(const char *diff_name) +{ + FILE *res_file; + char buf[128]; + int have_diff= 0; + + my_snprintf(buf, sizeof(buf), "%s -v", diff_name); + + if (!(res_file= my_popen(buf, "r"))) + die("popen(\"%s\", \"r\") failed", buf); + + /* + if diff is not present, nothing will be in stdout to increment + have_diff + */ + int len; + if (my_fgets(buf, sizeof(buf), res_file, &len)) + have_diff= 1; + + pclose(res_file); + + return have_diff; +} + +#endif + + +/* + Show the diff of two files using the systems builtin diff + command. If no such diff command exist, just dump the content + of the two files and inform about how to get "diff" + + SYNOPSIS + show_diff + ds - pointer to dynamic string where to add the diff(may be NULL) + filename1 - name of first file + filename2 - name of second file + +*/ + +void show_diff(DYNAMIC_STRING* ds, + const char* filename1, const char* filename2) +{ + DYNAMIC_STRING ds_tmp; + const char *diff_name = 0; + + if (init_dynamic_string(&ds_tmp, "", 256, 256)) + die("Out of memory"); + + /* determine if we have diff on Windows + needs special processing due to return values + on that OS + This test is only done on Windows since it's only needed there + in order to correctly detect non-availability of 'diff', and + the way it's implemented does not work with default 'diff' on Solaris. + */ +#ifdef _WIN32 + if (diff_check("diff")) + diff_name = "diff"; + else if (diff_check("mtrdiff")) + diff_name = "mtrdiff"; + else + diff_name = 0; +#else + diff_name = "diff"; /* Otherwise always assume it's called diff */ +#endif + + if (diff_name) + { + /* First try with unified diff */ + if (run_tool(diff_name, + &ds_tmp, /* Get output from diff in ds_tmp */ + "-u", + filename1, + filename2, + "2>&1", + NULL) > 1) /* Most "diff" tools return >1 if error */ + { + dynstr_set(&ds_tmp, ""); + + /* Fallback to context diff with "diff -c" */ + if (run_tool(diff_name, + &ds_tmp, /* Get output from diff in ds_tmp */ + "-c", + filename1, + filename2, + "2>&1", + NULL) > 1) /* Most "diff" tools return >1 if error */ + { + dynstr_set(&ds_tmp, ""); + + /* Fallback to simple diff with "diff" */ + if (run_tool(diff_name, + &ds_tmp, /* Get output from diff in ds_tmp */ + filename1, + filename2, + "2>&1", + NULL) > 1) /* Most "diff" tools return >1 if error */ + { + diff_name= 0; + } + } + } + } + + if (! diff_name) + { + /* + Fallback to dump both files to result file and inform + about installing "diff" + */ + dynstr_append(&ds_tmp, "\n"); + dynstr_append(&ds_tmp, +"\n" +"The two files differ but it was not possible to execute 'diff' in\n" +"order to show only the difference. Instead the whole content of the\n" +"two files was shown for you to diff manually.\n\n" +"To get a better report you should install 'diff' on your system, which you\n" +"for example can get from http://www.gnu.org/software/diffutils/diffutils.html\n" +#ifdef _WIN32 +"or http://gnuwin32.sourceforge.net/packages/diffutils.htm\n" +#endif +"\n"); + + dynstr_append(&ds_tmp, " --- "); + dynstr_append(&ds_tmp, filename1); + dynstr_append(&ds_tmp, " >>>\n"); + cat_file(&ds_tmp, filename1); + dynstr_append(&ds_tmp, "<<<\n --- "); + dynstr_append(&ds_tmp, filename1); + dynstr_append(&ds_tmp, " >>>\n"); + cat_file(&ds_tmp, filename2); + dynstr_append(&ds_tmp, "<<<<\n"); + } + + if (ds) + { + /* Add the diff to output */ + dynstr_append_mem(ds, ds_tmp.str, ds_tmp.length); + } + else + { + /* Print diff directly to stdout */ + fprintf(stderr, "%s\n", ds_tmp.str); + } + + dynstr_free(&ds_tmp); + +} + + +enum compare_files_result_enum { + RESULT_OK= 0, + RESULT_CONTENT_MISMATCH= 1, + RESULT_LENGTH_MISMATCH= 2 +}; + +/* + Compare two files, given a fd to the first file and + name of the second file + + SYNOPSIS + compare_files2 + fd - Open file descriptor of the first file + filename2 - Name of second file + + RETURN VALUES + According to the values in "compare_files_result_enum" + +*/ + +int compare_files2(File fd1, const char* filename2) +{ + int error= RESULT_OK; + File fd2; + size_t fd1_length, fd2_length; + DYNAMIC_STRING fd1_result, fd2_result; + + if ((fd2= my_open(filename2, O_RDONLY, MYF(0))) < 0) + { + my_close(fd1, MYF(0)); + die("Failed to open second file: '%s'", filename2); + } + + fd1_length= (size_t) my_seek(fd1, 0, SEEK_END, MYF(0)); + fd2_length= (size_t) my_seek(fd2, 0, SEEK_END, MYF(0)); + + if (init_dynamic_string(&fd1_result, 0, fd1_length, 0) || + init_dynamic_string(&fd2_result, 0, fd2_length, 0)) + die("Out of memory when allocating data for result"); + + fd1_result.length= fd1_length; + fd2_result.length= fd2_length; + + (void) my_seek(fd1, 0, SEEK_SET, MYF(0)); + (void) my_seek(fd2, 0, SEEK_SET, MYF(0)); + if (my_read(fd1, (uchar*) fd1_result.str, fd1_length, MYF(MY_WME | MY_NABP))) + die("Error when reading data from result file"); + if (my_read(fd2, (uchar*) fd2_result.str, fd2_length, MYF(MY_WME | MY_NABP))) + die("Error when reading data from result file"); + + if (global_subst && + (fd1_length != fd2_length || + memcmp(fd1_result.str, fd2_result.str, fd1_length))) + { + /** + @todo MARIA_HACK + This serves for when a test is run with --default-storage-engine=X + where X is not MyISAM: tests using SHOW CREATE TABLE will always fail + because SHOW CREATE TABLE prints X instead of MyISAM. With + --global-subst=X,MyISAM , such trivial differences are eliminated and + test may be reported as passing. + --global-subst is only a quick way to run a lot of existing tests + with Maria and find bugs; it is not good enough for reaching the main + trees when Maria is merged into them. + --global-subst should be removed. + */ + size_t global_subst_from_len= strlen(global_subst_from); + size_t global_subst_to_len= strlen(global_subst_to); + while (replace(&fd1_result, + global_subst_from, global_subst_from_len, + global_subst_to, global_subst_to_len) == 0) + /* do nothing */ ; + /* let's compare again to see if it is ok now */ + } + + if (fd1_result.length != fd2_result.length) + error= RESULT_LENGTH_MISMATCH; + else if ((memcmp(fd1_result.str, fd2_result.str, fd1_result.length))) + error= RESULT_CONTENT_MISMATCH; + + my_close(fd2, MYF(0)); + dynstr_free(&fd1_result); + dynstr_free(&fd2_result); + + return error; +} + + +/* + Compare two files, given their filenames + + SYNOPSIS + compare_files + filename1 - Name of first file + filename2 - Name of second file + + RETURN VALUES + See 'compare_files2' + +*/ + +int compare_files(const char* filename1, const char* filename2) +{ + File fd; + int error; + + if ((fd= my_open(filename1, O_RDONLY, MYF(0))) < 0) + die("Failed to open first file: '%s'", filename1); + + error= compare_files2(fd, filename2); + + my_close(fd, MYF(0)); + + return error; +} + + +/* + Compare content of the string in ds to content of file fname + + SYNOPSIS + dyn_string_cmp + ds - Dynamic string containing the string o be compared + fname - Name of file to compare with + + RETURN VALUES + See 'compare_files2' +*/ + +int dyn_string_cmp(DYNAMIC_STRING* ds, const char *fname) +{ + int error; + File fd; + char temp_file_path[FN_REFLEN]; + + DBUG_ENTER("dyn_string_cmp"); + DBUG_PRINT("enter", ("fname: %s", fname)); + + if ((fd= create_temp_file(temp_file_path, TMPDIR, "tmp", O_SHARE, + MYF(MY_WME))) < 0) + die("Failed to create temporary file for ds"); + + /* Write ds to temporary file and set file pos to beginning*/ + if (my_write(fd, (uchar *) ds->str, ds->length, + MYF(MY_FNABP | MY_WME)) || + my_seek(fd, 0, SEEK_SET, MYF(0)) == MY_FILEPOS_ERROR) + { + my_close(fd, MYF(0)); + /* Remove the temporary file */ + my_delete(temp_file_path, MYF(MY_WME)); + die("Failed to write file '%s'", temp_file_path); + } + + error= compare_files2(fd, fname); + + my_close(fd, MYF(0)); + /* Remove the temporary file */ + my_delete(temp_file_path, MYF(MY_WME)); + + DBUG_RETURN(error); +} + + +/* + Check the content of log against result file + + SYNOPSIS + check_result + + RETURN VALUES + error - the function will not return + +*/ + +void check_result() +{ + const char *mess= 0; + + DBUG_ENTER("check_result"); + DBUG_ASSERT(result_file_name); + DBUG_PRINT("enter", ("result_file_name: %s", result_file_name)); + + switch (compare_files(log_file.file_name(), result_file_name)) { + case RESULT_OK: + if (!error_count) + break; /* ok */ + mess= "Got errors while running test"; + /* Fallthrough */ + case RESULT_LENGTH_MISMATCH: + if (!mess) + mess= "Result length mismatch\n"; + /* Fallthrough */ + case RESULT_CONTENT_MISMATCH: + { + /* + Result mismatched, dump results to .reject file + and then show the diff + */ + char reject_file[FN_REFLEN]; + size_t reject_length; + + if (!mess) + mess= "Result content mismatch\n"; + + dirname_part(reject_file, result_file_name, &reject_length); + + if (access(reject_file, W_OK) == 0) + { + /* Result file directory is writable, save reject file there */ + fn_format(reject_file, result_file_name, "", + ".reject", MY_REPLACE_EXT); + } + else + { + /* Put reject file in opt_logdir */ + fn_format(reject_file, result_file_name, opt_logdir, + ".reject", MY_REPLACE_DIR | MY_REPLACE_EXT); + } + + if (my_copy(log_file.file_name(), reject_file, MYF(0)) != 0) + die("Failed to copy '%s' to '%s', errno: %d", + log_file.file_name(), reject_file, errno); + + show_diff(NULL, result_file_name, reject_file); + die("%s", mess); + break; + } + default: /* impossible */ + die("Unknown error code from dyn_string_cmp()"); + } + + DBUG_VOID_RETURN; +} + + +/* + Check the content of ds against a require file + If match fails, abort the test with special error code + indicating that test is not supported + + SYNOPSIS + check_require + ds - content to be checked + fname - name of file to check against + + RETURN VALUES + error - the function will not return + +*/ + +void check_require(DYNAMIC_STRING* ds, const char *fname) +{ + DBUG_ENTER("check_require"); + + if (dyn_string_cmp(ds, fname)) + { + char reason[FN_REFLEN]; + fn_format(reason, fname, "", "", MY_REPLACE_EXT | MY_REPLACE_DIR); + abort_not_supported_test("Test requires: '%s'", reason); + } + DBUG_VOID_RETURN; +} + + +/* + Remove surrounding chars from string + + Return 1 if first character is found but not last +*/ +static int strip_surrounding(char* str, char c1, char c2) +{ + char* ptr= str; + + /* Check if the first non space character is c1 */ + while(*ptr && my_isspace(charset_info, *ptr)) + ptr++; + if (*ptr == c1) + { + /* Replace it with a space */ + *ptr= ' '; + + /* Last non space character should be c2 */ + ptr= strend(str)-1; + while(*ptr && my_isspace(charset_info, *ptr)) + ptr--; + if (*ptr == c2) + { + /* Replace it with \0 */ + *ptr= 0; + } + else + { + /* Mismatch detected */ + return 1; + } + } + return 0; +} + + +static void strip_parentheses(struct st_command *command) +{ + if (strip_surrounding(command->first_argument, '(', ')')) + die("%.*b - argument list started with '%c' must be ended with '%c'", + command->first_word_len, command->query, '(', ')'); +} + + +C_MODE_START + +static uchar *get_var_key(const uchar* var, size_t *len, + my_bool __attribute__((unused)) t) +{ + char* key; + key = ((VAR*)var)->name; + *len = ((VAR*)var)->name_len; + return (uchar*)key; +} + + +static void var_free(void *v) +{ + VAR *var= (VAR*) v; + my_free(var->str_val); + if (var->alloced) + my_free(var); +} + +C_MODE_END + +void var_check_int(VAR *v) +{ + char *endptr; + char *str= v->str_val; + + /* Initially assume not a number */ + v->int_val= 0; + v->is_int= false; + v->int_dirty= false; + if (!str) return; + + v->int_val = (int) strtol(str, &endptr, 10); + /* It is an int if strtol consumed something up to end/space/tab */ + if (endptr > str && (!*endptr || *endptr == ' ' || *endptr == '\t')) + v->is_int= true; +} + + +VAR *var_init(VAR *v, const char *name, size_t name_len, const char *val, size_t val_len) +{ + size_t val_alloc_len; + VAR *tmp_var; + if (!name_len && name) + name_len = strlen(name); + if (!val_len && val) + val_len = strlen(val) ; + if (!val) + val_len= 0; + val_alloc_len = val_len + 16; /* room to grow */ + if (!(tmp_var=v) && !(tmp_var = (VAR*)my_malloc(PSI_NOT_INSTRUMENTED, sizeof(*tmp_var) + + name_len+2, MYF(MY_WME|MY_FAE)))) + die("Out of memory"); + + if (name != NULL) + { + tmp_var->name= reinterpret_cast<char*>(tmp_var) + sizeof(*tmp_var); + memcpy(tmp_var->name, name, name_len); + tmp_var->name[name_len]= 0; + } + else + tmp_var->name= NULL; + + tmp_var->alloced = (v == 0); + + if (!(tmp_var->str_val = (char*)my_malloc(PSI_NOT_INSTRUMENTED, + val_alloc_len+1, MYF(MY_WME|MY_FAE)))) + die("Out of memory"); + + if (val) + memcpy(tmp_var->str_val, val, val_len); + tmp_var->str_val[val_len]= 0; + + var_check_int(tmp_var); + tmp_var->name_len = name_len; + tmp_var->str_val_len = val_len; + tmp_var->alloced_len = val_alloc_len; + return tmp_var; +} + + +VAR* var_from_env(const char *name, const char *def_val) +{ + const char *tmp; + VAR *v; + if (!(tmp = getenv(name))) + tmp = def_val; + + v = var_init(0, name, strlen(name), tmp, strlen(tmp)); + my_hash_insert(&var_hash, (uchar*)v); + return v; +} + + +VAR* var_get(const char *var_name, const char **var_name_end, my_bool raw, + my_bool ignore_not_existing) +{ + int digit; + VAR *v; + DBUG_ENTER("var_get"); + DBUG_PRINT("enter", ("var_name: %s",var_name)); + + if (*var_name != '$') + goto err; + digit = *++var_name - '0'; + if (digit < 0 || digit >= 10) + { + const char *save_var_name = var_name, *end; + uint length; + end = (var_name_end) ? *var_name_end : 0; + while (my_isvar(charset_info,*var_name) && var_name != end) + var_name++; + if (var_name == save_var_name) + { + if (ignore_not_existing) + DBUG_RETURN(0); + die("Empty variable"); + } + length= (uint) (var_name - save_var_name); + if (length >= MAX_VAR_NAME_LENGTH) + die("Too long variable name: %s", save_var_name); + + if (!(v = (VAR*) my_hash_search(&var_hash, (const uchar*) save_var_name, + length))) + { + char buff[MAX_VAR_NAME_LENGTH+1]; + strmake(buff, save_var_name, length); + v= var_from_env(buff, ""); + } + var_name--; /* Point at last character */ + } + else + v = var_reg + digit; + + if (!raw && v->int_dirty) + { + sprintf(v->str_val, "%d", v->int_val); + v->int_dirty= false; + v->str_val_len = strlen(v->str_val); + } + if (var_name_end) + *var_name_end = var_name ; + DBUG_RETURN(v); +err: + if (var_name_end) + *var_name_end = 0; + die("Unsupported variable name: %s", var_name); + DBUG_RETURN(0); +} + + +VAR *var_obtain(const char *name, int len) +{ + VAR* v; + if ((v = (VAR*)my_hash_search(&var_hash, (const uchar *) name, len))) + return v; + v = var_init(0, name, len, "", 0); + my_hash_insert(&var_hash, (uchar*)v); + return v; +} + + +/* + - if variable starts with a $ it is regarded as a local test variable + - if not it is treated as a environment variable, and the corresponding + environment variable will be updated +*/ + +void var_set(const char *var_name, const char *var_name_end, + const char *var_val, const char *var_val_end) +{ + int digit, env_var= 0; + VAR *v; + DBUG_ENTER("var_set"); + DBUG_PRINT("enter", ("var_name: '%.*s' = '%.*s' (length: %d)", + (int) (var_name_end - var_name), var_name, + (int) (var_val_end - var_val), var_val, + (int) (var_val_end - var_val))); + + if (*var_name != '$') + env_var= 1; + else + var_name++; + + digit= *var_name - '0'; + if (!(digit < 10 && digit >= 0)) + { + v= var_obtain(var_name, (uint) (var_name_end - var_name)); + } + else + v= var_reg + digit; + + eval_expr(v, var_val, (const char**) &var_val_end); + + if (env_var) + { + if (v->int_dirty) + { + sprintf(v->str_val, "%d", v->int_val); + v->int_dirty=false; + v->str_val_len= strlen(v->str_val); + } + /* setenv() expects \0-terminated strings */ + DBUG_ASSERT(v->name[v->name_len] == 0); + setenv(v->name, v->str_val, 1); + } + DBUG_VOID_RETURN; +} + + +void var_set_string(const char* name, const char* value) +{ + var_set(name, name + strlen(name), value, value + strlen(value)); +} + + +void var_set_int(const char* name, int value) +{ + char buf[21]; + my_snprintf(buf, sizeof(buf), "%d", value); + var_set_string(name, buf); +} + + +/* + Store an integer (typically the returncode of the last SQL) + statement in the mysqltest builtin variable $mysql_errno +*/ + +void var_set_errno(int sql_errno) +{ + var_set_int("$mysql_errno", sql_errno); + var_set_string("$mysql_errname", get_errname_from_code(sql_errno)); +} + +/* Functions to handle --disable and --enable properties */ + +void set_once_property(enum_prop prop, my_bool val) +{ + property &pr= prop_list[prop]; + pr.set= 1; + pr.old= *pr.var; + *pr.var= val; + var_set_int(pr.env_name, (val != pr.reverse)); + once_property= TRUE; +} + +void set_property(st_command *command, enum_prop prop, my_bool val) +{ + char* p= command->first_argument; + if (p && !strcmp (p, "ONCE")) + { + command->last_argument= p + 4; + set_once_property(prop, val); + return; + } + property &pr= prop_list[prop]; + *pr.var= val; + pr.set= 0; + var_set_int(pr.env_name, (val != pr.reverse)); +} + +void revert_properties() +{ + if (! once_property) + return; + for (int i= 0; i < (int) P_MAX; i++) + { + property &pr= prop_list[i]; + if (pr.set) + { + *pr.var= pr.old; + pr.set= 0; + var_set_int(pr.env_name, (pr.old != pr.reverse)); + } + } + once_property=FALSE; +} + + +/* + Set variable from the result of a query + + SYNOPSIS + var_query_set() + var variable to set from query + query start of query string to execute + query_end end of the query string to execute + + + DESCRIPTION + let @<var_name> = `<query>` + + Execute the query and assign the first row of result to var as + a tab separated strings + + Also assign each column of the result set to + variable "$<var_name>_<column_name>" + Thus the tab separated output can be read from $<var_name> and + and each individual column can be read as $<var_name>_<col_name> + +*/ + +void var_query_set(VAR *var, const char *query, const char** query_end) +{ + char *end = (char*)((query_end && *query_end) ? + *query_end : query + strlen(query)); + MYSQL_RES *UNINIT_VAR(res); + MYSQL_ROW row; + MYSQL* mysql = cur_con->mysql; + DYNAMIC_STRING ds_query; + DBUG_ENTER("var_query_set"); + + if (!mysql) + { + struct st_command command; + DBUG_ASSERT(query_end); + memset(&command, 0, sizeof(command)); + command.query= (char*)query; + command.first_word_len= (int)(*query_end - query); + command.first_argument= command.query + command.first_word_len; + command.end= (char*)*query_end; + command.abort_on_error= 1; /* avoid uninitialized variables */ + handle_no_active_connection(&command, cur_con, &ds_res); + DBUG_VOID_RETURN; + } + + /* Only white space or ) allowed past ending ` */ + while (end > query && *end != '`') + { + if (*end && (*end != ' ' && *end != '\t' && *end != '\n' && *end != ')')) + die("Spurious text after `query` expression"); + --end; + } + + if (query == end) + die("Syntax error in query, missing '`'"); + ++query; + + /* Eval the query, thus replacing all environment variables */ + init_dynamic_string(&ds_query, 0, (end - query) + 32, 256); + do_eval(&ds_query, query, end, FALSE); + + if (mysql_real_query(mysql, ds_query.str, (ulong)ds_query.length) || + !(res= mysql_store_result(mysql))) + { + handle_error(curr_command, mysql_errno(mysql), mysql_error(mysql), + mysql_sqlstate(mysql), &ds_res); + /* If error was acceptable, return empty string */ + dynstr_free(&ds_query); + eval_expr(var, "", 0); + DBUG_VOID_RETURN; + } + + dynstr_free(&ds_query); + + if ((row= mysql_fetch_row(res)) && row[0]) + { + /* + Concatenate all fields in the first row with tab in between + and assign that string to the $variable + */ + DYNAMIC_STRING result; + uint i; + ulong *lengths; + + init_dynamic_string(&result, "", 512, 512); + lengths= mysql_fetch_lengths(res); + for (i= 0; i < mysql_num_fields(res); i++) + { + if (row[i]) + { + /* Add column to tab separated string */ + char *val= row[i]; + size_t len= lengths[i]; + + if (glob_replace_regex) + { + /* Regex replace */ + if (!multi_reg_replace(glob_replace_regex, (char*)val)) + { + val= glob_replace_regex->buf; + len= strlen(val); + } + } + + if (glob_replace) + replace_strings_append(glob_replace, &result, val); + else + dynstr_append_mem(&result, val, len); + } + dynstr_append_mem(&result, "\t", 1); + } + end= result.str + result.length-1; + /* Evaluation should not recurse via backtick */ + eval_expr(var, result.str, (const char**) &end, false, false); + dynstr_free(&result); + } + else + eval_expr(var, "", 0); + + mysql_free_result(res); + DBUG_VOID_RETURN; +} + + +static void +set_result_format_version(ulong new_version) +{ + switch (new_version){ + case 1: + /* The first format */ + break; + case 2: + /* New format that also writes comments and empty lines + from test file to result */ + break; + default: + die("Version format %lu has not yet been implemented", new_version); + break; + } + opt_result_format_version= new_version; +} + + +/* + Set the result format version to use when generating + the .result file +*/ + +static void +do_result_format_version(struct st_command *command) +{ + long version; + static DYNAMIC_STRING ds_version; + const struct command_arg result_format_args[] = { + {"version", ARG_STRING, TRUE, &ds_version, "Version to use"} + }; + + DBUG_ENTER("do_result_format_version"); + + check_command_args(command, command->first_argument, + result_format_args, + sizeof(result_format_args)/sizeof(struct command_arg), + ','); + + /* Convert version number to int */ + if (!str2int(ds_version.str, 10, (long) 0, (long) INT_MAX, &version)) + die("Invalid version number: '%s'", ds_version.str); + + set_result_format_version(version); + + dynstr_append(&ds_res, "result_format: "); + dynstr_append_mem(&ds_res, ds_version.str, ds_version.length); + dynstr_append(&ds_res, "\n"); + dynstr_free(&ds_version); +} + + +/* + Set variable from the result of a field in a query + + This function is useful when checking for a certain value + in the output from a query that can't be restricted to only + return some values. A very good example of that is most SHOW + commands. + + SYNOPSIS + var_set_query_get_value() + + DESCRIPTION + let $variable= query_get_value(<query to run>,<column name>,<row no>); + + <query to run> - The query that should be sent to the server + <column name> - Name of the column that holds the field be compared + against the expected value + <row no> - Number of the row that holds the field to be + compared against the expected value + +*/ + +void var_set_query_get_value(struct st_command *command, VAR *var) +{ + long row_no; + int col_no= -1; + MYSQL_RES* UNINIT_VAR(res); + MYSQL* mysql= cur_con->mysql; + + static DYNAMIC_STRING ds_query; + static DYNAMIC_STRING ds_col; + static DYNAMIC_STRING ds_row; + const struct command_arg query_get_value_args[] = { + {"query", ARG_STRING, TRUE, &ds_query, "Query to run"}, + {"column name", ARG_STRING, TRUE, &ds_col, "Name of column"}, + {"row number", ARG_STRING, TRUE, &ds_row, "Number for row"} + }; + + DBUG_ENTER("var_set_query_get_value"); + + if (!mysql) + { + handle_no_active_connection(command, cur_con, &ds_res); + DBUG_VOID_RETURN; + } + + strip_parentheses(command); + DBUG_PRINT("info", ("query: %s", command->query)); + check_command_args(command, command->first_argument, query_get_value_args, + sizeof(query_get_value_args)/sizeof(struct command_arg), + ','); + + DBUG_PRINT("info", ("query: %s", ds_query.str)); + DBUG_PRINT("info", ("col: %s", ds_col.str)); + + /* Convert row number to int */ + if (!str2int(ds_row.str, 10, (long) 0, (long) INT_MAX, &row_no)) + die("Invalid row number: '%s'", ds_row.str); + DBUG_PRINT("info", ("row: %s, row_no: %ld", ds_row.str, row_no)); + dynstr_free(&ds_row); + + /* Remove any surrounding "'s from the query - if there is any */ + if (strip_surrounding(ds_query.str, '"', '"')) + die("Mismatched \"'s around query '%s'", ds_query.str); + + /* Run the query */ + if (mysql_real_query(mysql, ds_query.str, (ulong)ds_query.length)) + { + handle_error(curr_command, mysql_errno(mysql), mysql_error(mysql), + mysql_sqlstate(mysql), &ds_res); + /* If error was acceptable, return empty string */ + dynstr_free(&ds_query); + dynstr_free(&ds_col); + eval_expr(var, "", 0); + DBUG_VOID_RETURN; + } + + if (!(res= mysql_store_result(mysql))) + { + report_or_die("Query '%s' didn't return a result set", ds_query.str); + dynstr_free(&ds_query); + dynstr_free(&ds_col); + eval_expr(var, "", 0); + DBUG_VOID_RETURN; + } + + { + /* Find column number from the given column name */ + uint i; + uint num_fields= mysql_num_fields(res); + MYSQL_FIELD *fields= mysql_fetch_fields(res); + + for (i= 0; i < num_fields; i++) + { + if (strcmp(fields[i].name, ds_col.str) == 0 && + strlen(fields[i].name) == ds_col.length) + { + col_no= i; + break; + } + } + if (col_no == -1) + { + mysql_free_result(res); + report_or_die("Could not find column '%s' in the result of '%s'", + ds_col.str, ds_query.str); + dynstr_free(&ds_query); + dynstr_free(&ds_col); + DBUG_VOID_RETURN; + } + DBUG_PRINT("info", ("Found column %d with name '%s'", + i, fields[i].name)); + } + dynstr_free(&ds_col); + + { + /* Get the value */ + MYSQL_ROW row; + long rows= 0; + const char* value= "No such row"; + + while ((row= mysql_fetch_row(res))) + { + if (++rows == row_no) + { + + DBUG_PRINT("info", ("At row %ld, column %d is '%s'", + row_no, col_no, row[col_no])); + /* Found the row to get */ + if (row[col_no]) + value= row[col_no]; + else + value= "NULL"; + + break; + } + } + eval_expr(var, value, 0, false, false); + } + dynstr_free(&ds_query); + mysql_free_result(res); + + DBUG_VOID_RETURN; +} + + +void var_copy(VAR *dest, VAR *src) +{ + dest->int_val= src->int_val; + dest->is_int= src->is_int; + dest->int_dirty= src->int_dirty; + + /* Alloc/realloc data for str_val in dest */ + if (dest->alloced_len < src->alloced_len && + !(dest->str_val= dest->str_val + ? (char*)my_realloc(PSI_NOT_INSTRUMENTED, dest->str_val, src->alloced_len, + MYF(MY_WME|MY_FAE)) + : (char*)my_malloc(PSI_NOT_INSTRUMENTED, src->alloced_len, + MYF(MY_WME|MY_FAE)))) + die("Out of memory"); + else + dest->alloced_len= src->alloced_len; + + /* Copy str_val data to dest */ + dest->str_val_len= src->str_val_len; + if (src->str_val_len) + memcpy(dest->str_val, src->str_val, src->str_val_len); +} + + +void eval_expr(VAR *v, const char *p, const char **p_end, + bool open_end, bool do_eval) +{ + + DBUG_ENTER("eval_expr"); + DBUG_PRINT("enter", ("p: '%s'", p)); + + /* Skip to treat as pure string if no evaluation */ + if (! do_eval) + goto NO_EVAL; + + if (*p == '$') + { + VAR *vp; + const char* expected_end= *p_end; // Remember var end + if ((vp= var_get(p, p_end, 0, 0))) + var_copy(v, vp); + + /* Apparently it is not safe to assume null-terminated string */ + v->str_val[v->str_val_len]= 0; + + /* Make sure there was just a $variable and nothing else */ + const char* end= *p_end + 1; + if (end < expected_end && !open_end) + die("Found junk '%.*b' after $variable in expression", + (int)(expected_end - end - 1), end); + + DBUG_VOID_RETURN; + } + + if (*p == '`') + { + var_query_set(v, p, p_end); + DBUG_VOID_RETURN; + } + + { + /* Check if this is a "let $var= query_get_value()" */ + const char* get_value_str= "query_get_value"; + const size_t len= strlen(get_value_str); + if (strncmp(p, get_value_str, len)==0) + { + struct st_command command; + memset(&command, 0, sizeof(command)); + command.query= (char*)p; + command.first_word_len= (int)len; + command.first_argument= command.query + len; + command.end= (char*)*p_end; + command.abort_on_error= 1; /* avoid uninitialized variables */ + var_set_query_get_value(&command, v); + DBUG_VOID_RETURN; + } + } + + NO_EVAL: + { + size_t new_val_len = (p_end && *p_end) ? + (size_t)(*p_end - p) : strlen(p); + if (new_val_len + 1 >= v->alloced_len) + { + static size_t MIN_VAR_ALLOC= 32; + v->alloced_len = (new_val_len < MIN_VAR_ALLOC - 1) ? + MIN_VAR_ALLOC : new_val_len + 1; + if (!(v->str_val = + v->str_val ? + (char*)my_realloc(PSI_NOT_INSTRUMENTED, v->str_val, v->alloced_len+1, + MYF(MY_WME|MY_FAE)) : + (char*)my_malloc(PSI_NOT_INSTRUMENTED, v->alloced_len+1, + MYF(MY_WME|MY_FAE)))) + die("Out of memory"); + } + v->str_val_len = new_val_len; + memcpy(v->str_val, p, new_val_len); + v->str_val[new_val_len] = 0; + var_check_int(v); + } + DBUG_VOID_RETURN; +} + + +bool open_and_set_current(const char *name) +{ + FILE *opened= fopen(name, "rb"); + + if (!opened) + return false; + + cur_file++; + cur_file->file= opened; + cur_file->file_name= my_strdup(PSI_NOT_INSTRUMENTED, name, MYF(MY_FAE)); + cur_file->lineno=1; + return true; +} + + +void open_file(const char *name) +{ + char buff[FN_REFLEN]; + size_t length; + char *curname= cur_file->file_name; + DBUG_ENTER("open_file"); + DBUG_PRINT("enter", ("name: %s", name)); + + if (cur_file == file_stack_end) + die("Source directives are nesting too deep"); + + if (test_if_hard_path(name)) + { + if (open_and_set_current(name)) + DBUG_VOID_RETURN; + } + else + { + /* + if overlay-dir is specified, and the file is located somewhere + under overlay-dir or under suite-dir, the search works as follows: + + 0.let suffix be current file dirname relative to suite-dir or overlay-dir + 1.try in overlay-dir/suffix + 2.try in suite-dir/suffix + 3.try in overlay-dir + 4.try in suite-dir + 5.try in basedir + + consider an example: 'rty' overlay of the 'qwe' suite, + file qwe/include/some.inc contains the line + --source thing.inc + we look for it in this order: + 0.suffix is "include/" + 1.try in rty/include/thing.inc + 2.try in qwe/include/thing.inc + 3.try in try/thing.inc | this is useful when t/a.test has + 4.try in qwe/thing.inc | source include/b.inc; + 5.try in mysql-test/include/thing.inc + + otherwise the search is as follows + 1.try in current file dirname + 3.try in overlay-dir (if any) + 4.try in suite-dir + 5.try in basedir + */ + + fix_win_paths(curname, sizeof(curname)); + + bool in_overlay= opt_overlay_dir && + !strncmp(curname, opt_overlay_dir, overlay_dir_len); + bool in_suiteir= opt_overlay_dir && !in_overlay && + !strncmp(curname, opt_suite_dir, suite_dir_len); + if (in_overlay || in_suiteir) + { + size_t prefix_len = in_overlay ? overlay_dir_len : suite_dir_len; + char buf2[FN_REFLEN], *suffix= buf2 + prefix_len; + dirname_part(buf2, curname, &length); + + /* 1. first we look in the overlay dir */ + strxnmov(buff, sizeof(buff), opt_overlay_dir, suffix, name, NullS); + + /* + Overlaid rty/include/thing.inc can contain the line + --source thing.inc + which would mean to include qwe/include/thing.inc. + But it looks like including "itself", so don't try to open the file, + if buff contains the same file name as curname. + */ + if (strcmp(buff, curname) && open_and_set_current(buff)) + DBUG_VOID_RETURN; + + /* 2. if that failed, we look in the suite dir */ + strxnmov(buff, sizeof(buff), opt_suite_dir, suffix, name, NullS); + + /* buff can not be equal to curname, as a file can never include itself */ + if (open_and_set_current(buff)) + DBUG_VOID_RETURN; + } + else + { + /* 1. try in current file dirname */ + dirname_part(buff, curname, &length); + strxnmov(buff, sizeof(buff), buff, name, NullS); + if (open_and_set_current(buff)) + DBUG_VOID_RETURN; + } + + /* 3. now, look in the overlay dir */ + if (opt_overlay_dir) + { + strxmov(buff, opt_overlay_dir, name, NullS); + if (open_and_set_current(buff)) + DBUG_VOID_RETURN; + } + + /* 4. if that failed - look in the suite dir */ + strxmov(buff, opt_suite_dir, name, NullS); + if (open_and_set_current(buff)) + DBUG_VOID_RETURN; + + /* 5. the last resort - look in the base dir */ + strxnmov(buff, sizeof(buff), opt_basedir, name, NullS); + if (open_and_set_current(buff)) + DBUG_VOID_RETURN; + } + + die("Could not open '%s' for reading, errno: %d", name, errno); + DBUG_VOID_RETURN; +} + + +/* + Source and execute the given file + + SYNOPSIS + do_source() + query called command + + DESCRIPTION + source <file_name> + + Open the file <file_name> and execute it + +*/ + +void do_source(struct st_command *command) +{ + static DYNAMIC_STRING ds_filename; + const struct command_arg source_args[] = { + { "filename", ARG_STRING, TRUE, &ds_filename, "File to source" } + }; + DBUG_ENTER("do_source"); + + check_command_args(command, command->first_argument, source_args, + sizeof(source_args)/sizeof(struct command_arg), + ' '); + + /* + If this file has already been sourced, don't source it again. + It's already available in the q_lines cache. + */ + if (parser.current_line < (parser.read_lines - 1)) + ; /* Do nothing */ + else + { + DBUG_PRINT("info", ("sourcing file: %s", ds_filename.str)); + open_file(ds_filename.str); + } + + dynstr_free(&ds_filename); + DBUG_VOID_RETURN; +} + + +static void init_builtin_echo(void) +{ +#ifdef _WIN32 + size_t echo_length; + + /* Look for "echo.exe" in same dir as mysqltest was started from */ + dirname_part(builtin_echo, my_progname, &echo_length); + fn_format(builtin_echo, ".\\echo.exe", + builtin_echo, "", MYF(MY_REPLACE_DIR)); + + /* Make sure echo.exe exists */ + if (access(builtin_echo, F_OK) != 0) + builtin_echo[0]= 0; + return; + +#else + + builtin_echo[0]= 0; + return; + +#endif +} + + +/* + Replace a substring + + SYNOPSIS + replace + ds_str The string to search and perform the replace in + search_str The string to search for + search_len Length of the string to search for + replace_str The string to replace with + replace_len Length of the string to replace with + + RETURN + 0 String replaced + 1 Could not find search_str in str +*/ + +static int replace(DYNAMIC_STRING *ds_str, + const char *search_str, size_t search_len, + const char *replace_str, size_t replace_len) +{ + DYNAMIC_STRING ds_tmp; + const char *start= strstr(ds_str->str, search_str); + if (!start) + return 1; + init_dynamic_string(&ds_tmp, "", + ds_str->length + replace_len, 256); + dynstr_append_mem(&ds_tmp, ds_str->str, start - ds_str->str); + dynstr_append_mem(&ds_tmp, replace_str, replace_len); + dynstr_append(&ds_tmp, start + search_len); + dynstr_set(ds_str, ds_tmp.str); + dynstr_free(&ds_tmp); + return 0; +} + +#ifdef _WIN32 +/** + Check if background execution of command was requested. + Like in Unix shell, we assume background execution of the last + character in command is a ampersand (we do not tokenize though) +*/ +static bool is_background_command(const DYNAMIC_STRING *ds) +{ + for (size_t i= ds->length - 1; i > 1; i--) + { + char c= ds->str[i]; + if (!isspace(c)) + return (c == '&'); + } + return false; +} + +/** + Execute OS command in background. We assume that the last character + is ampersand, i.e is_background_command() returned +*/ +#include <string> +static int execute_in_background(char *cmd) +{ + STARTUPINFO s{}; + PROCESS_INFORMATION pi{}; + char *end= strrchr(cmd, '&'); + DBUG_ASSERT(end); + *end =0; + std::string scmd("cmd /c "); + scmd.append(cmd); + BOOL ok= + CreateProcess(0, (char *)scmd.c_str(), 0, 0, 0, CREATE_NO_WINDOW, 0, 0, &s, &pi); + *end= '&'; + if (!ok) + return (int) GetLastError(); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return 0; +} +#endif + +/* + Execute given command. + + SYNOPSIS + do_exec() + query called command + + DESCRIPTION + exec <command> + + Execute the text between exec and end of line in a subprocess. + The error code returned from the subprocess is checked against the + expected error array, previously set with the --error command. + It can thus be used to execute a command that shall fail. + + NOTE + Although mysqltest is executed from cygwin shell, the command will be + executed in "cmd.exe". Thus commands like "rm" etc can NOT be used, use + mysqltest command(s) like "remove_file" for that +*/ + +void do_exec(struct st_command *command) +{ + int error; + char buf[512]; + FILE *res_file; + char *cmd= command->first_argument; + DYNAMIC_STRING ds_cmd; + DYNAMIC_STRING ds_sorted, *ds_result; + DBUG_ENTER("do_exec"); + DBUG_PRINT("enter", ("cmd: '%s'", cmd)); + + var_set_int("$sys_errno",0); + + /* Skip leading space */ + while (*cmd && my_isspace(charset_info, *cmd)) + cmd++; + if (!*cmd) + { + report_or_die("Missing argument in exec"); + DBUG_VOID_RETURN; + } + command->last_argument= command->end; + + init_dynamic_string(&ds_cmd, 0, command->query_len+256, 256); + /* Eval the command, thus replacing all environment variables */ + do_eval(&ds_cmd, cmd, command->end, !is_windows); + + /* Check if echo should be replaced with "builtin" echo */ + if (builtin_echo[0] && strncmp(cmd, "echo", 4) == 0) + { + /* Replace echo with our "builtin" echo */ + replace(&ds_cmd, "echo", 4, builtin_echo, strlen(builtin_echo)); + } + +#ifdef _WIN32 + /* Replace /dev/null with NUL */ + while(replace(&ds_cmd, "/dev/null", 9, "NUL", 3) == 0) + ; + /* Replace "closed stdout" with non existing output fd */ + while(replace(&ds_cmd, ">&-", 3, ">&4", 3) == 0) + ; +#endif + + if (disable_result_log) + { + /* Collect stderr output as well, for the case app. crashes or returns error.*/ + dynstr_append(&ds_cmd, " 2>&1"); + } + + DBUG_PRINT("info", ("Executing '%s' as '%s'", + command->first_argument, ds_cmd.str)); + +#ifdef _WIN32 + if (is_background_command(&ds_cmd)) + { + error= execute_in_background(ds_cmd.str); + goto end; + } +#endif + + if (!(res_file= my_popen(ds_cmd.str, "r"))) + { + dynstr_free(&ds_cmd); + if (command->abort_on_error) + report_or_die("popen(\"%s\", \"r\") failed", command->first_argument); + DBUG_VOID_RETURN; + } + + ds_result= &ds_res; + if (display_result_sorted) + { + init_dynamic_string(&ds_sorted, "", 1024, 1024); + ds_result= &ds_sorted; + } + int len; + while (my_fgets(buf, sizeof(buf), res_file,&len)) + { + replace_dynstr_append_mem(ds_result, buf, len); + } + error= my_pclose(res_file); + + if (display_result_sorted) + { + dynstr_append_sorted(&ds_res, &ds_sorted, 0); + dynstr_free(&ds_sorted); + } +#ifdef _WIN32 +end: +#endif + if (error) + { + uint status= WEXITSTATUS(error); + int i; + + if (command->abort_on_error) + { + report_or_die("exec of '%s' failed, error: %d, status: %d, errno: %d\n" + "Output from before failure:\n%s\n", + ds_cmd.str, error, status, errno, + ds_res.str); + dynstr_free(&ds_cmd); + DBUG_VOID_RETURN; + } + + DBUG_PRINT("info", + ("error: %d, status: %d", error, status)); + + i= match_expected_error(command, status, NULL); + + if (i >= 0) + DBUG_PRINT("info", ("command \"%s\" failed with expected error: %d", + command->first_argument, status)); + else + { + dynstr_free(&ds_cmd); + if (command->expected_errors.count > 0) + report_or_die("command \"%s\" failed with wrong error: %d", + command->first_argument, status); + } + var_set_int("$sys_errno",status); + } + else if (command->expected_errors.err[0].type == ERR_ERRNO && + command->expected_errors.err[0].code.errnum != 0) + { + /* Error code we wanted was != 0, i.e. not an expected success */ + log_msg("exec of '%s failed, error: %d, errno: %d", + ds_cmd.str, error, errno); + dynstr_free(&ds_cmd); + report_or_die("command \"%s\" succeeded - should have failed with " + "errno %d...", + command->first_argument, + command->expected_errors.err[0].code.errnum); + } + + dynstr_free(&ds_cmd); + + if (disable_result_log) + { + /* Disable output in case of successful exit.*/ + dynstr_set(&ds_res,""); + } + DBUG_VOID_RETURN; +} + +enum enum_operator +{ + DO_DEC, + DO_INC +}; + + +/* + Decrease or increase the value of a variable + + SYNOPSIS + do_modify_var() + query called command + op operation to perform on the var + + DESCRIPTION + dec $var_name + inc $var_name + +*/ + +int do_modify_var(struct st_command *command, + enum enum_operator op) +{ + const char *p= command->first_argument; + VAR* v; + if (!*p) + die("Missing argument to %.*b", command->first_word_len, + command->query); + if (*p != '$') + die("The argument to %.*b must be a variable (start with $)", + command->first_word_len, command->query); + v= var_get(p, &p, 1, 0); + if (! v->is_int) + die("Cannot perform inc/dec on a non-numeric value"); + switch (op) { + case DO_DEC: + v->int_val--; + break; + case DO_INC: + v->int_val++; + break; + default: + die("Invalid operator to do_modify_var"); + break; + } + v->int_dirty= true; + command->last_argument= (char*)++p; + return 0; +} + + +/* + Wrapper for 'system' function + + NOTE + If mysqltest is executed from cygwin shell, the command will be + executed in the "windows command interpreter" cmd.exe and we prepend "sh" + to make it be executed by cygwins "bash". Thus commands like "rm", + "mkdir" as well as shellscripts can executed by "system" in Windows. + +*/ + +int my_system(DYNAMIC_STRING* ds_cmd) +{ + return system(ds_cmd->str); +} + + +/* + SYNOPSIS + do_system + command called command + + DESCRIPTION + system <command> + + Eval the query to expand any $variables in the command. + Execute the command with the "system" command. + +*/ + +void do_system(struct st_command *command) +{ + DYNAMIC_STRING ds_cmd; + DBUG_ENTER("do_system"); + + if (strlen(command->first_argument) == 0) + { + report_or_die("Missing arguments to system, nothing to do!"); + DBUG_VOID_RETURN; + } + + init_dynamic_string(&ds_cmd, 0, command->query_len + 64, 256); + + /* Eval the system command, thus replacing all environment variables */ + do_eval(&ds_cmd, command->first_argument, command->end, !is_windows); + +#ifdef _WIN32 + /* Replace /dev/null with NUL */ + while(replace(&ds_cmd, "/dev/null", 9, "NUL", 3) == 0) + ; +#endif + + + DBUG_PRINT("info", ("running system command '%s' as '%s'", + command->first_argument, ds_cmd.str)); + if (my_system(&ds_cmd)) + { + if (command->abort_on_error) + report_or_die("system command '%s' failed", command->first_argument); + else + { + /* If ! abort_on_error, log message and continue */ + dynstr_append(&ds_res, "system command '"); + replace_dynstr_append(&ds_res, command->first_argument); + dynstr_append(&ds_res, "' failed\n"); + } + } + + command->last_argument= command->end; + dynstr_free(&ds_cmd); + DBUG_VOID_RETURN; +} + + +/* returns TRUE if path is inside a sandbox */ +bool is_sub_path(const char *path, size_t plen, const char *sandbox) +{ + size_t len; + if (!sandbox) + return false; + len= strlen(sandbox); + if (plen <= len || memcmp(path, sandbox, len-1) || path[len] != '/') + return false; + return true; +} + + +/* returns TRUE if path cannot be modified */ +bool bad_path(const char *path) +{ + size_t plen= strlen(path); + + const char *vardir= getenv("MYSQLTEST_VARDIR"); + if (is_sub_path(path, plen, vardir)) + return false; + + const char *tmpdir= getenv("MYSQL_TMP_DIR"); + if (is_sub_path(path, plen, tmpdir)) + return false; + + report_or_die("Path '%s' is not a subdirectory of MYSQLTEST_VARDIR '%s'" + "or MYSQL_TMP_DIR '%s'", + path, vardir, tmpdir); + return true; +} + + +/* + SYNOPSIS + set_wild_chars + set true to set * etc. as wild char, false to reset + + DESCRIPTION + Auxiliary function to set "our" wild chars before calling wild_compare + This is needed because the default values are changed to SQL syntax + in mysqltest_embedded. +*/ + +void set_wild_chars (my_bool set) +{ + static char old_many= 0, old_one, old_prefix; + + if (set) + { + if (wild_many == '*') return; // No need + old_many= wild_many; + old_one= wild_one; + old_prefix= wild_prefix; + wild_many= '*'; + wild_one= '?'; + wild_prefix= 0; + } + else + { + if (! old_many) return; // Was not set + wild_many= old_many; + wild_one= old_one; + wild_prefix= old_prefix; + } +} + + +/* + SYNOPSIS + do_remove_file + command called command + + DESCRIPTION + remove_file <file_name> + Remove the file <file_name> +*/ + +void do_remove_file(struct st_command *command) +{ + int error; + static DYNAMIC_STRING ds_filename; + const struct command_arg rm_args[] = { + { "filename", ARG_STRING, TRUE, &ds_filename, "File to delete" } + }; + DBUG_ENTER("do_remove_file"); + + check_command_args(command, command->first_argument, + rm_args, sizeof(rm_args)/sizeof(struct command_arg), + ' '); + + if (bad_path(ds_filename.str)) + DBUG_VOID_RETURN; + + DBUG_PRINT("info", ("removing file: %s", ds_filename.str)); + error= my_delete(ds_filename.str, MYF(disable_warnings ? 0 : MY_WME)) != 0; + handle_command_error(command, error, my_errno); + dynstr_free(&ds_filename); + DBUG_VOID_RETURN; +} + + +/* + SYNOPSIS + do_remove_files_wildcard + command called command + + DESCRIPTION + remove_files_wildcard <directory> [<file_name_pattern>] + Remove the files in <directory> optionally matching <file_name_pattern> +*/ + +void do_remove_files_wildcard(struct st_command *command) +{ + int error= 0, sys_errno= 0; + size_t i, directory_length; + MY_DIR *dir_info; + FILEINFO *file; + char dir_separator[2]; + static DYNAMIC_STRING ds_directory; + static DYNAMIC_STRING ds_wild; + static DYNAMIC_STRING ds_file_to_remove; + char dirname[FN_REFLEN]; + + const struct command_arg rm_args[] = { + { "directory", ARG_STRING, TRUE, &ds_directory, + "Directory containing files to delete" }, + { "filename", ARG_STRING, FALSE, &ds_wild, "File pattern to delete" } + }; + DBUG_ENTER("do_remove_files_wildcard"); + + check_command_args(command, command->first_argument, + rm_args, sizeof(rm_args)/sizeof(struct command_arg), + ' '); + fn_format(dirname, ds_directory.str, "", "", MY_UNPACK_FILENAME); + + if (bad_path(ds_directory.str)) + DBUG_VOID_RETURN; + + DBUG_PRINT("info", ("listing directory: %s", dirname)); + if (!(dir_info= my_dir(dirname, MYF(MY_DONT_SORT | MY_WANT_STAT | MY_WME)))) + { + error= 1; + sys_errno= my_errno; + goto end; + } + init_dynamic_string(&ds_file_to_remove, dirname, 1024, 1024); + dir_separator[0]= FN_LIBCHAR; + dynstr_append_mem(&ds_file_to_remove, dir_separator, 1); + directory_length= ds_file_to_remove.length; + + /* Set default wild chars for wild_compare, is changed in embedded mode */ + set_wild_chars(1); + + for (i= 0; i < dir_info->number_of_files; i++) + { + file= dir_info->dir_entry + i; + /* Remove only regular files, i.e. no directories etc. */ + /* if (!MY_S_ISREG(file->mystat->st_mode)) */ + /* MY_S_ISREG does not work here on Windows, just skip directories */ + if (MY_S_ISDIR(file->mystat->st_mode)) + continue; + if (ds_wild.length && + wild_compare(file->name, ds_wild.str, 0)) + continue; + ds_file_to_remove.length= directory_length; + dynstr_append(&ds_file_to_remove, file->name); + DBUG_PRINT("info", ("removing file: %s", ds_file_to_remove.str)); + if ((error= (my_delete(ds_file_to_remove.str, MYF(MY_WME)) != 0))) + sys_errno= my_errno; + if (error) + break; + } + set_wild_chars(0); + my_dirend(dir_info); + +end: + handle_command_error(command, error, sys_errno); + dynstr_free(&ds_directory); + dynstr_free(&ds_wild); + dynstr_free(&ds_file_to_remove); + DBUG_VOID_RETURN; +} + + +/* + SYNOPSIS + do_copy_file + command command handle + + DESCRIPTION + copy_file <from_file> <to_file> + Copy <from_file> to <to_file> + + NOTE! Will fail if <to_file> exists +*/ + +void do_copy_file(struct st_command *command) +{ + int error; + static DYNAMIC_STRING ds_from_file; + static DYNAMIC_STRING ds_to_file; + const struct command_arg copy_file_args[] = { + { "from_file", ARG_STRING, TRUE, &ds_from_file, "Filename to copy from" }, + { "to_file", ARG_STRING, TRUE, &ds_to_file, "Filename to copy to" } + }; + DBUG_ENTER("do_copy_file"); + + check_command_args(command, command->first_argument, + copy_file_args, + sizeof(copy_file_args)/sizeof(struct command_arg), + ' '); + + if (bad_path(ds_to_file.str)) + DBUG_VOID_RETURN; + + DBUG_PRINT("info", ("Copy %s to %s", ds_from_file.str, ds_to_file.str)); + /* MY_HOLD_ORIGINAL_MODES prevents attempts to chown the file */ + error= (my_copy(ds_from_file.str, ds_to_file.str, + MYF(MY_DONT_OVERWRITE_FILE | MY_WME | MY_HOLD_ORIGINAL_MODES)) != 0); + handle_command_error(command, error, my_errno); + dynstr_free(&ds_from_file); + dynstr_free(&ds_to_file); + DBUG_VOID_RETURN; +} + + +/* + SYNOPSIS + do_move_file + command command handle + + DESCRIPTION + move_file <from_file> <to_file> + Move <from_file> to <to_file> +*/ + +void do_move_file(struct st_command *command) +{ + int error; + static DYNAMIC_STRING ds_from_file; + static DYNAMIC_STRING ds_to_file; + const struct command_arg move_file_args[] = { + { "from_file", ARG_STRING, TRUE, &ds_from_file, "Filename to move from" }, + { "to_file", ARG_STRING, TRUE, &ds_to_file, "Filename to move to" } + }; + DBUG_ENTER("do_move_file"); + + check_command_args(command, command->first_argument, + move_file_args, + sizeof(move_file_args)/sizeof(struct command_arg), + ' '); + + size_t from_plen = strlen(ds_from_file.str); + size_t to_plen = strlen(ds_to_file.str); + const char *vardir= getenv("MYSQLTEST_VARDIR"); + const char *tmpdir= getenv("MYSQL_TMP_DIR"); + + if (!((is_sub_path(ds_from_file.str, from_plen, vardir) && + is_sub_path(ds_to_file.str, to_plen, vardir)) || + (is_sub_path(ds_from_file.str, from_plen, tmpdir) && + is_sub_path(ds_to_file.str, to_plen, tmpdir)))) { + report_or_die("Paths '%s' and '%s' are not both under MYSQLTEST_VARDIR '%s'" + "or both under MYSQL_TMP_DIR '%s'", + ds_from_file, ds_to_file, vardir, tmpdir); + DBUG_VOID_RETURN; + } + + DBUG_PRINT("info", ("Move %s to %s", ds_from_file.str, ds_to_file.str)); + error= (my_rename(ds_from_file.str, ds_to_file.str, + MYF(disable_warnings ? 0 : MY_WME)) != 0); + handle_command_error(command, error, my_errno); + dynstr_free(&ds_from_file); + dynstr_free(&ds_to_file); + DBUG_VOID_RETURN; +} + + +/* + SYNOPSIS + do_chmod_file + command command handle + + DESCRIPTION + chmod <octal> <file_name> + Change file permission of <file_name> + +*/ + +void do_chmod_file(struct st_command *command) +{ + long mode= 0; + int err_code; + static DYNAMIC_STRING ds_mode; + static DYNAMIC_STRING ds_file; + const struct command_arg chmod_file_args[] = { + { "mode", ARG_STRING, TRUE, &ds_mode, "Mode of file(octal) ex. 0660"}, + { "filename", ARG_STRING, TRUE, &ds_file, "Filename of file to modify" } + }; + DBUG_ENTER("do_chmod_file"); + + check_command_args(command, command->first_argument, + chmod_file_args, + sizeof(chmod_file_args)/sizeof(struct command_arg), + ' '); + + if (bad_path(ds_file.str)) + DBUG_VOID_RETURN; + + /* Parse what mode to set */ + if (ds_mode.length != 4 || + str2int(ds_mode.str, 8, 0, INT_MAX, &mode) == NullS) + die("You must write a 4 digit octal number for mode"); + + DBUG_PRINT("info", ("chmod %o %s", (uint)mode, ds_file.str)); + err_code= chmod(ds_file.str, mode); + if (err_code < 0) + err_code= 1; + handle_command_error(command, err_code, errno); + dynstr_free(&ds_mode); + dynstr_free(&ds_file); + DBUG_VOID_RETURN; +} + + +/* + SYNOPSIS + do_file_exists + command called command + + DESCRIPTION + fiile_exist <file_name> + Check if file <file_name> exists +*/ + +void do_file_exist(struct st_command *command) +{ + int error; + static DYNAMIC_STRING ds_filename; + const struct command_arg file_exist_args[] = { + { "filename", ARG_STRING, TRUE, &ds_filename, "File to check if it exist" } + }; + DBUG_ENTER("do_file_exist"); + + check_command_args(command, command->first_argument, + file_exist_args, + sizeof(file_exist_args)/sizeof(struct command_arg), + ' '); + + DBUG_PRINT("info", ("Checking for existence of file: %s", ds_filename.str)); + error= (access(ds_filename.str, F_OK) != 0); + handle_command_error(command, error, errno); + dynstr_free(&ds_filename); + DBUG_VOID_RETURN; +} + + +/* + SYNOPSIS + do_mkdir + command called command + + DESCRIPTION + mkdir <dir_name> + Create the directory <dir_name> +*/ + +void do_mkdir(struct st_command *command) +{ + int error; + static DYNAMIC_STRING ds_dirname; + const struct command_arg mkdir_args[] = { + {"dirname", ARG_STRING, TRUE, &ds_dirname, "Directory to create"} + }; + DBUG_ENTER("do_mkdir"); + + check_command_args(command, command->first_argument, + mkdir_args, sizeof(mkdir_args)/sizeof(struct command_arg), + ' '); + + if (bad_path(ds_dirname.str)) + DBUG_VOID_RETURN; + + DBUG_PRINT("info", ("creating directory: %s", ds_dirname.str)); + error= my_mkdir(ds_dirname.str, 0777, MYF(MY_WME)) != 0; + handle_command_error(command, error, my_errno); + dynstr_free(&ds_dirname); + DBUG_VOID_RETURN; +} + + + +/* + SYNOPSIS + do_rmdir + command called command + + DESCRIPTION + rmdir <dir_name> + Remove the directory tree +*/ + +void do_rmdir(struct st_command *command) +{ + static DYNAMIC_STRING ds_dirname; + const struct command_arg rmdir_args[] = { + { "dirname", ARG_STRING, TRUE, &ds_dirname, "Directory to remove" } + }; + DBUG_ENTER("do_rmdir"); + + check_command_args(command, command->first_argument, + rmdir_args, sizeof(rmdir_args)/sizeof(struct command_arg), + ' '); + + if (bad_path(ds_dirname.str)) + DBUG_VOID_RETURN; + + DBUG_PRINT("info", ("removing directory: %s", ds_dirname.str)); + if (my_rmtree(ds_dirname.str, MYF(0))) + handle_command_error(command, 1, errno); + + dynstr_free(&ds_dirname); + DBUG_VOID_RETURN; +} + + +/* + SYNOPSIS + get_list_files + ds output + ds_dirname dir to list + ds_wild wild-card file pattern (can be empty) + + DESCRIPTION + list all entries in directory (matching ds_wild if given) +*/ + +static int get_list_files(DYNAMIC_STRING *ds, const DYNAMIC_STRING *ds_dirname, + const DYNAMIC_STRING *ds_wild) +{ + size_t i; + MY_DIR *dir_info; + FILEINFO *file; + DBUG_ENTER("get_list_files"); + + DBUG_PRINT("info", ("listing directory: %s", ds_dirname->str)); + if (!(dir_info= my_dir(ds_dirname->str, MYF(MY_WANT_SORT)))) + DBUG_RETURN(1); + set_wild_chars(1); + for (i= 0; i < dir_info->number_of_files; i++) + { + file= dir_info->dir_entry + i; + if (ds_wild && ds_wild->length && + wild_compare(file->name, ds_wild->str, 0)) + continue; + replace_dynstr_append(ds, file->name); + dynstr_append(ds, "\n"); + } + set_wild_chars(0); + my_dirend(dir_info); + DBUG_RETURN(0); +} + + +/* + SYNOPSIS + do_list_files + command called command + + DESCRIPTION + list_files <dir_name> [<file_name>] + List files and directories in directory <dir_name> (like `ls`) + [Matching <file_name>, where wild-cards are allowed] +*/ + +static void do_list_files(struct st_command *command) +{ + int error; + static DYNAMIC_STRING ds_dirname; + static DYNAMIC_STRING ds_wild; + const struct command_arg list_files_args[] = { + {"dirname", ARG_STRING, TRUE, &ds_dirname, "Directory to list"}, + {"file", ARG_STRING, FALSE, &ds_wild, "Filename (incl. wildcard)"} + }; + DBUG_ENTER("do_list_files"); + command->used_replace= 1; + + check_command_args(command, command->first_argument, + list_files_args, + sizeof(list_files_args)/sizeof(struct command_arg), ' '); + + error= get_list_files(&ds_res, &ds_dirname, &ds_wild); + handle_command_error(command, error, my_errno); + dynstr_free(&ds_dirname); + dynstr_free(&ds_wild); + DBUG_VOID_RETURN; +} + + +/* + SYNOPSIS + do_list_files_write_file_command + command called command + append append file, or create new + + DESCRIPTION + list_files_{write|append}_file <filename> <dir_name> [<match_file>] + List files and directories in directory <dir_name> (like `ls`) + [Matching <match_file>, where wild-cards are allowed] + + Note: File will be truncated if exists and append is not true. +*/ + +static void do_list_files_write_file_command(struct st_command *command, + my_bool append) +{ + int error; + static DYNAMIC_STRING ds_content; + static DYNAMIC_STRING ds_filename; + static DYNAMIC_STRING ds_dirname; + static DYNAMIC_STRING ds_wild; + const struct command_arg list_files_args[] = { + {"filename", ARG_STRING, TRUE, &ds_filename, "Filename for write"}, + {"dirname", ARG_STRING, TRUE, &ds_dirname, "Directory to list"}, + {"file", ARG_STRING, FALSE, &ds_wild, "Filename (incl. wildcard)"} + }; + DBUG_ENTER("do_list_files_write_file"); + command->used_replace= 1; + + check_command_args(command, command->first_argument, + list_files_args, + sizeof(list_files_args)/sizeof(struct command_arg), ' '); + + if (bad_path(ds_filename.str)) + DBUG_VOID_RETURN; + + init_dynamic_string(&ds_content, "", 1024, 1024); + error= get_list_files(&ds_content, &ds_dirname, &ds_wild); + handle_command_error(command, error, my_errno); + str_to_file2(ds_filename.str, ds_content.str, ds_content.length, append); + dynstr_free(&ds_content); + dynstr_free(&ds_filename); + dynstr_free(&ds_dirname); + dynstr_free(&ds_wild); + DBUG_VOID_RETURN; +} + + +/* + Read characters from line buffer or file. This is needed to allow + my_ungetc() to buffer MAX_DELIMITER_LENGTH characters for a file + + NOTE: + This works as long as one doesn't change files (with 'source file_name') + when there is things pushed into the buffer. This should however not + happen for any tests in the test suite. +*/ + +int my_getc(FILE *file) +{ + if (line_buffer_pos == line_buffer) + return fgetc(file); + return *--line_buffer_pos; +} + + +void my_ungetc(int c) +{ + *line_buffer_pos++= (char) c; +} + + +void read_until_delimiter(DYNAMIC_STRING *ds, + DYNAMIC_STRING *ds_delimiter) +{ + char c; + DBUG_ENTER("read_until_delimiter"); + DBUG_PRINT("enter", ("delimiter: %s, length: %u", + ds_delimiter->str, (uint) ds_delimiter->length)); + + if (ds_delimiter->length > MAX_DELIMITER_LENGTH) + die("Max delimiter length(%d) exceeded", MAX_DELIMITER_LENGTH); + + /* Read from file until delimiter is found */ + while (1) + { + c= my_getc(cur_file->file); + if (c == '\r') + c= my_getc(cur_file->file); + if (c == '\n') + { + cur_file->lineno++; + + /* Skip newline from the same line as the command */ + if (start_lineno == (cur_file->lineno - 1)) + continue; + } + else if (start_lineno == cur_file->lineno) + { + /* + No characters except \n are allowed on + the same line as the command + */ + report_or_die("Trailing characters found after command"); + } + + if (feof(cur_file->file)) + report_or_die("End of file encountered before '%s' delimiter was found", + ds_delimiter->str); + + if (match_delimiter(c, ds_delimiter->str, ds_delimiter->length)) + { + DBUG_PRINT("exit", ("Found delimiter '%s'", ds_delimiter->str)); + break; + } + dynstr_append_mem(ds, (const char*)&c, 1); + } + DBUG_PRINT("exit", ("ds: %s", ds->str)); + DBUG_VOID_RETURN; +} + + +void do_write_file_command(struct st_command *command, my_bool append) +{ + static DYNAMIC_STRING ds_content; + static DYNAMIC_STRING ds_filename; + static DYNAMIC_STRING ds_delimiter; + const struct command_arg write_file_args[] = { + { "filename", ARG_STRING, TRUE, &ds_filename, "File to write to" }, + { "delimiter", ARG_STRING, FALSE, &ds_delimiter, "Delimiter to read until" } + }; + DBUG_ENTER("do_write_file"); + + check_command_args(command, + command->first_argument, + write_file_args, + sizeof(write_file_args)/sizeof(struct command_arg), + ' '); + + if (bad_path(ds_filename.str)) + DBUG_VOID_RETURN; + + if (!append && access(ds_filename.str, F_OK) == 0) + { + /* The file should not be overwritten */ + die("File already exist: '%s'", ds_filename.str); + } + + ds_content= command->content; + /* If it hasn't been done already by a loop iteration, fill it in */ + if (! ds_content.str) + { + /* If no delimiter was provided, use EOF */ + if (ds_delimiter.length == 0) + dynstr_set(&ds_delimiter, "EOF"); + + init_dynamic_string(&ds_content, "", 1024, 1024); + read_until_delimiter(&ds_content, &ds_delimiter); + command->content= ds_content; + } + /* This function could be called even if "false", so check before printing */ + if (cur_block->ok) + { + DBUG_PRINT("info", ("Writing to file: %s", ds_filename.str)); + str_to_file2(ds_filename.str, ds_content.str, ds_content.length, append); + } + dynstr_free(&ds_filename); + dynstr_free(&ds_delimiter); + DBUG_VOID_RETURN; +} + + +/* + SYNOPSIS + do_write_file + command called command + + DESCRIPTION + write_file <file_name> [<delimiter>]; + <what to write line 1> + <...> + < what to write line n> + EOF + + --write_file <file_name>; + <what to write line 1> + <...> + < what to write line n> + EOF + + Write everything between the "write_file" command and 'delimiter' + to "file_name" + + NOTE! Will fail if <file_name> exists + + Default <delimiter> is EOF + +*/ + +void do_write_file(struct st_command *command) +{ + do_write_file_command(command, FALSE); +} + + +/* + SYNOPSIS + do_append_file + command called command + + DESCRIPTION + append_file <file_name> [<delimiter>]; + <what to write line 1> + <...> + < what to write line n> + EOF + + --append_file <file_name>; + <what to write line 1> + <...> + < what to write line n> + EOF + + Append everything between the "append_file" command + and 'delimiter' to "file_name" + + Default <delimiter> is EOF + +*/ + +void do_append_file(struct st_command *command) +{ + do_write_file_command(command, TRUE); +} + + +/* + SYNOPSIS + do_cat_file + command called command + + DESCRIPTION + cat_file <file_name>; + + Print the given file to result log + +*/ + +void do_cat_file(struct st_command *command) +{ + int error; + static DYNAMIC_STRING ds_filename; + const struct command_arg cat_file_args[] = { + { "filename", ARG_STRING, TRUE, &ds_filename, "File to read from" } + }; + DBUG_ENTER("do_cat_file"); + + check_command_args(command, + command->first_argument, + cat_file_args, + sizeof(cat_file_args)/sizeof(struct command_arg), + ' '); + + DBUG_PRINT("info", ("Reading from, file: %s", ds_filename.str)); + + error= cat_file(&ds_res, ds_filename.str); + handle_command_error(command, error, my_errno); + dynstr_free(&ds_filename); + DBUG_VOID_RETURN; +} + + +/* + SYNOPSIS + do_diff_files + command called command + + DESCRIPTION + diff_files <file1> <file2>; + + Fails if the two files differ. + +*/ + +void do_diff_files(struct st_command *command) +{ + int error= 0; + static DYNAMIC_STRING ds_filename; + static DYNAMIC_STRING ds_filename2; + const struct command_arg diff_file_args[] = { + { "file1", ARG_STRING, TRUE, &ds_filename, "First file to diff" }, + { "file2", ARG_STRING, TRUE, &ds_filename2, "Second file to diff" } + }; + DBUG_ENTER("do_diff_files"); + + check_command_args(command, + command->first_argument, + diff_file_args, + sizeof(diff_file_args)/sizeof(struct command_arg), + ' '); + + if (access(ds_filename.str, F_OK) != 0) + die("command \"diff_files\" failed, file '%s' does not exist", + ds_filename.str); + + if (access(ds_filename2.str, F_OK) != 0) + die("command \"diff_files\" failed, file '%s' does not exist", + ds_filename2.str); + + if ((error= compare_files(ds_filename.str, ds_filename2.str)) && + match_expected_error(command, error, NULL) < 0) + { + /* + Compare of the two files failed, append them to output + so the failure can be analyzed, but only if it was not + expected to fail. + */ + show_diff(&ds_res, ds_filename.str, ds_filename2.str); + log_file.write(&ds_res); + log_file.flush(); + dynstr_set(&ds_res, 0); + } + + dynstr_free(&ds_filename); + dynstr_free(&ds_filename2); + handle_command_error(command, error, -1); + DBUG_VOID_RETURN; +} + + +struct st_connection * find_connection_by_name(const char *name) +{ + struct st_connection *con; + for (con= connections; con < next_con; con++) + { + if (!strcmp(con->name, name)) + { + return con; + } + } + return 0; /* Connection not found */ +} + + +/* + SYNOPSIS + do_send_quit + command called command + + DESCRIPTION + Sends a simple quit command to the server for the named connection. + +*/ + +void do_send_quit(struct st_command *command) +{ + char *p= command->first_argument, *name; + struct st_connection *con; + + DBUG_ENTER("do_send_quit"); + DBUG_PRINT("enter",("name: '%s'",p)); + + if (!*p) + die("Missing connection name in send_quit"); + name= p; + while (*p && !my_isspace(charset_info,*p)) + p++; + + if (*p) + *p++= 0; + command->last_argument= p; + + if (!(con= find_connection_by_name(name))) + die("connection '%s' not found in connection pool", name); + + simple_command(con->mysql,COM_QUIT,0,0,1); + + DBUG_VOID_RETURN; +} + + +/* + SYNOPSIS + do_change_user + command called command + + DESCRIPTION + change_user [<user>], [<passwd>], [<db>] + <user> - user to change to + <passwd> - user password + <db> - default database + + Changes the user and causes the database specified by db to become + the default (current) database for the the current connection. + +*/ + +void do_change_user(struct st_command *command) +{ + MYSQL *mysql = cur_con->mysql; + /* static keyword to make the NetWare compiler happy. */ + static DYNAMIC_STRING ds_user, ds_passwd, ds_db; + const struct command_arg change_user_args[] = { + { "user", ARG_STRING, FALSE, &ds_user, "User to connect as" }, + { "password", ARG_STRING, FALSE, &ds_passwd, "Password used when connecting" }, + { "database", ARG_STRING, FALSE, &ds_db, "Database to select after connect" }, + }; + + DBUG_ENTER("do_change_user"); + + check_command_args(command, command->first_argument, + change_user_args, + sizeof(change_user_args)/sizeof(struct command_arg), + ','); + + if (cur_con->stmt) + { + mysql_stmt_close(cur_con->stmt); + cur_con->stmt= NULL; + } + + if (!ds_user.length) + { + dynstr_set(&ds_user, mysql->user); + + if (!ds_passwd.length) + dynstr_set(&ds_passwd, mysql->passwd); + + if (!ds_db.length) + dynstr_set(&ds_db, mysql->db); + } + + DBUG_PRINT("info",("connection: '%s' user: '%s' password: '%s' database: '%s'", + cur_con->name, ds_user.str, ds_passwd.str, ds_db.str)); + + if (mysql_change_user(mysql, ds_user.str, ds_passwd.str, ds_db.str)) + handle_error(command, mysql_errno(mysql), mysql_error(mysql), + mysql_sqlstate(mysql), &ds_res); + else + handle_no_error(command); + + dynstr_free(&ds_user); + dynstr_free(&ds_passwd); + dynstr_free(&ds_db); + + DBUG_VOID_RETURN; +} + + +/* + SYNOPSIS + do_perl + command command handle + + DESCRIPTION + perl [<delimiter>]; + <perlscript line 1> + <...> + <perlscript line n> + EOF + + Execute everything after "perl" until <delimiter> as perl. + Useful for doing more advanced things + but still being able to execute it on all platforms. + + Default <delimiter> is EOF +*/ + +void do_perl(struct st_command *command) +{ + int error; + File fd; + FILE *res_file; + char buf[FN_REFLEN]; + char temp_file_path[FN_REFLEN]; + static DYNAMIC_STRING ds_script; + static DYNAMIC_STRING ds_delimiter; + const struct command_arg perl_args[] = { + { "delimiter", ARG_STRING, FALSE, &ds_delimiter, "Delimiter to read until" } + }; + DBUG_ENTER("do_perl"); + + check_command_args(command, + command->first_argument, + perl_args, + sizeof(perl_args)/sizeof(struct command_arg), + ' '); + + ds_script= command->content; + /* If it hasn't been done already by a loop iteration, fill it in */ + if (! ds_script.str) + { + /* If no delimiter was provided, use EOF */ + if (ds_delimiter.length == 0) + dynstr_set(&ds_delimiter, "EOF"); + + init_dynamic_string(&ds_script, "", 1024, 1024); + read_until_delimiter(&ds_script, &ds_delimiter); + command->content= ds_script; + } + + /* This function could be called even if "false", so check before doing */ + if (cur_block->ok) + { + DBUG_PRINT("info", ("Executing perl: %s", ds_script.str)); + + /* Create temporary file name */ + if ((fd= create_temp_file(temp_file_path, getenv("MYSQLTEST_VARDIR"), + "tmp", O_SHARE, MYF(MY_WME))) < 0) + die("Failed to create temporary file for perl command"); + my_close(fd, MYF(0)); + + str_to_file(temp_file_path, ds_script.str, ds_script.length); + + /* Use the same perl executable as the one that runs mysql-test-run.pl */ + const char *mtr_perl=getenv("MTR_PERL"); + if (!mtr_perl) + mtr_perl="perl"; + + /* Format the "perl <filename>" command */ + if (strchr(mtr_perl, ' ')) + my_snprintf(buf, sizeof(buf), "\"%s\" %s", mtr_perl, temp_file_path); + else + my_snprintf(buf, sizeof(buf), "%s %s", mtr_perl, temp_file_path); + + if (!(res_file= my_popen(buf, "r"))) + { + if (command->abort_on_error) + die("popen(\"%s\", \"r\") failed", buf); + dynstr_free(&ds_delimiter); + DBUG_VOID_RETURN; + } + + int len; + while (my_fgets(buf, sizeof(buf), res_file,&len)) + { + if (disable_result_log) + { + buf[len - 1] = 0; + DBUG_PRINT("exec_result", ("%s", buf)); + } + else + { + replace_dynstr_append_mem(&ds_res, buf, len); + } + } + error= my_pclose(res_file); + + /* Remove the temporary file, but keep it if perl failed */ + if (!error) + my_delete(temp_file_path, MYF(MY_WME)); + + /* Check for error code that indicates perl could not be started */ + int exstat= WEXITSTATUS(error); +#ifdef _WIN32 + if (exstat == 1) + /* Text must begin 'perl not found' as mtr looks for it */ + abort_not_supported_test("perl not found in path or did not start"); +#else + if (exstat == 127) + abort_not_supported_test("perl not found in path"); +#endif + else + handle_command_error(command, exstat, my_errno); + } + dynstr_free(&ds_delimiter); + DBUG_VOID_RETURN; +} + + +/* + Print the content between echo and <delimiter> to result file. + Evaluate all variables in the string before printing, allow + for variable names to be escaped using \ + + SYNOPSIS + do_echo() + command called command + + DESCRIPTION + echo text + Print the text after echo until end of command to result file + + echo $<var_name> + Print the content of the variable <var_name> to result file + + echo Some text $<var_name> + Print "Some text" plus the content of the variable <var_name> to + result file + + echo Some text \$<var_name> + Print "Some text" plus $<var_name> to result file +*/ + +int do_echo(struct st_command *command) +{ + DYNAMIC_STRING ds_echo; + DBUG_ENTER("do_echo"); + + init_dynamic_string(&ds_echo, "", command->query_len, 256); + do_eval(&ds_echo, command->first_argument, command->end, FALSE); + dynstr_append_mem(&ds_res, ds_echo.str, ds_echo.length); + dynstr_append_mem(&ds_res, "\n", 1); + dynstr_free(&ds_echo); + command->last_argument= command->end; + DBUG_RETURN(0); +} + + +void do_wait_for_slave_to_stop(struct st_command *c __attribute__((unused))) +{ + static int SLAVE_POLL_INTERVAL= 300000; + MYSQL* mysql = cur_con->mysql; + for (;;) + { + MYSQL_RES *UNINIT_VAR(res); + MYSQL_ROW row; + int done; + + if (mysql_query(mysql,"show status like 'Slave_running'") || + !(res=mysql_store_result(mysql))) + die("Query failed while probing slave for stop: %s", + mysql_error(mysql)); + if (!(row=mysql_fetch_row(res)) || !row[1]) + { + mysql_free_result(res); + die("Strange result from query while probing slave for stop"); + } + done = !strcmp(row[1],"OFF"); + mysql_free_result(res); + if (done) + break; + my_sleep(SLAVE_POLL_INTERVAL); + } + return; +} + + +void do_sync_with_master2(struct st_command *command, long offset, + const char *connection_name) +{ + MYSQL_RES *res; + MYSQL_ROW row; + MYSQL *mysql= cur_con->mysql; + char query_buf[FN_REFLEN+128]; + int timeout= opt_wait_for_pos_timeout; + + if (!master_pos.file[0]) + die("Calling 'sync_with_master' without calling 'save_master_pos'"); + + sprintf(query_buf, "select master_pos_wait('%s', %ld, %d, '%s')", + master_pos.file, master_pos.pos + offset, timeout, + connection_name); + + if (mysql_query(mysql, query_buf)) + die("failed in '%s': %d: %s", query_buf, mysql_errno(mysql), + mysql_error(mysql)); + + if (!(res= mysql_store_result(mysql))) + die("mysql_store_result() returned NULL for '%s'", query_buf); + if (!(row= mysql_fetch_row(res))) + { + mysql_free_result(res); + die("empty result in %s", query_buf); + } + + int result= -99; + const char* result_str= row[0]; + if (result_str) + result= atoi(result_str); + + mysql_free_result(res); + + if (!result_str || result < 0) + { + /* master_pos_wait returned NULL or < 0 */ + fprintf(stderr, "analyze: sync_with_master\n"); + + if (!result_str) + { + /* + master_pos_wait returned NULL. This indicates that + slave SQL thread is not started, the slave's master + information is not initialized, the arguments are + incorrect, or an error has occurred + */ + die("%.*b failed: '%s' returned NULL " \ + "indicating slave SQL thread failure", + command->first_word_len, command->query, query_buf); + + } + + if (result == -1) + die("%.*b failed: '%s' returned -1 " \ + "indicating timeout after %d seconds", + command->first_word_len, command->query, query_buf, timeout); + else + die("%.*b failed: '%s' returned unknown result :%d", + command->first_word_len, command->query, query_buf, result); + } + + return; +} + +void do_sync_with_master(struct st_command *command) +{ + long offset= 0; + char *p= command->first_argument; + const char *offset_start= p; + char *start, *buff= 0; + start= const_cast<char*>(""); + + if (*offset_start) + { + for (; my_isdigit(charset_info, *p); p++) + offset = offset * 10 + *p - '0'; + + if (*p && !my_isspace(charset_info, *p) && *p != ',') + die("Invalid integer argument \"%s\"", offset_start); + + while (*p && my_isspace(charset_info, *p)) + p++; + if (*p == ',') + { + p++; + while (*p && my_isspace(charset_info, *p)) + p++; + start= buff= (char*)my_malloc(PSI_NOT_INSTRUMENTED, strlen(p)+1, + MYF(MY_WME|MY_FAE)); + get_string(&buff, &p, command); + } + command->last_argument= p; + } + do_sync_with_master2(command, offset, start); + if (buff) + my_free(start); + return; +} + + +int do_save_master_pos() +{ + MYSQL_RES *res; + MYSQL_ROW row; + MYSQL *mysql = cur_con->mysql; + const char *query; + DBUG_ENTER("do_save_master_pos"); + + if (mysql_query(mysql, query= "show master status")) + die("failed in 'show master status': %d %s", + mysql_errno(mysql), mysql_error(mysql)); + + if (!(res = mysql_store_result(mysql))) + die("mysql_store_result() returned NULL for '%s'", query); + if (!(row = mysql_fetch_row(res))) + die("empty result in show master status"); + strnmov(master_pos.file, row[0], sizeof(master_pos.file)-1); + master_pos.pos = strtoul(row[1], (char**) 0, 10); + mysql_free_result(res); + DBUG_RETURN(0); +} + + +/* + Assign the variable <var_name> with <var_val> + + SYNOPSIS + do_let() + query called command + + DESCRIPTION + let $<var_name>=<var_val><delimiter> + + <var_name> - is the string string found between the $ and = + <var_val> - is the content between the = and <delimiter>, it may span + multiple line and contain any characters except <delimiter> + <delimiter> - is a string containing of one or more chars, default is ; + + RETURN VALUES + Program will die if error detected +*/ + +void do_let(struct st_command *command) +{ + char *p= command->first_argument; + char *var_name, *var_name_end; + DYNAMIC_STRING let_rhs_expr; + DBUG_ENTER("do_let"); + + init_dynamic_string(&let_rhs_expr, "", 512, 2048); + + /* Find <var_name> */ + if (!*p) + die("Missing arguments to let"); + var_name= p; + while (*p && (*p != '=') && !my_isspace(charset_info,*p)) + p++; + var_name_end= p; + if (var_name == var_name_end || + (var_name+1 == var_name_end && *var_name == '$')) + die("Missing variable name in let"); + while (my_isspace(charset_info,*p)) + p++; + if (*p++ != '=') + die("Missing assignment operator in let"); + + /* Find start of <var_val> */ + while (*p && my_isspace(charset_info,*p)) + p++; + + do_eval(&let_rhs_expr, p, command->end, FALSE); + + command->last_argument= command->end; + /* Assign var_val to var_name */ + var_set(var_name, var_name_end, let_rhs_expr.str, + (let_rhs_expr.str + let_rhs_expr.length)); + dynstr_free(&let_rhs_expr); + revert_properties(); + DBUG_VOID_RETURN; +} + + +/* + Sleep the number of specified seconds + + SYNOPSIS + do_sleep() + q called command + real_sleep use the value from opt_sleep as number of seconds to sleep + if real_sleep is false + + DESCRIPTION + sleep <seconds> + real_sleep <seconds> + + The difference between the sleep and real_sleep commands is that sleep + uses the delay from the --sleep command-line option if there is one. + (If the --sleep option is not given, the sleep command uses the delay + specified by its argument.) The real_sleep command always uses the + delay specified by its argument. The logic is that sometimes delays are + cpu-dependent, and --sleep can be used to set this delay. real_sleep is + used for cpu-independent delays. +*/ + +int do_sleep(struct st_command *command, my_bool real_sleep) +{ + int error= 0; + char *sleep_start, *sleep_end; + double sleep_val; + char *p; + static DYNAMIC_STRING ds_sleep; + const struct command_arg sleep_args[] = { + { "sleep_delay", ARG_STRING, TRUE, &ds_sleep, "Number of seconds to sleep." } + }; + check_command_args(command, command->first_argument, sleep_args, + sizeof(sleep_args)/sizeof(struct command_arg), + ' '); + + p= ds_sleep.str; + sleep_end= ds_sleep.str + ds_sleep.length; + while (my_isspace(charset_info, *p)) + p++; + if (!*p) + die("Missing argument to %.*b", command->first_word_len, + command->query); + sleep_start= p; + /* Check that arg starts with a digit, not handled by my_strtod */ + if (!my_isdigit(charset_info, *sleep_start)) + die("Invalid argument to %.*b \"%s\"", command->first_word_len, + command->query, sleep_start); + sleep_val= my_strtod(sleep_start, &sleep_end, &error); + check_eol_junk_line(sleep_end); + if (error) + die("Invalid argument to %.*b \"%s\"", command->first_word_len, + command->query, command->first_argument); + dynstr_free(&ds_sleep); + + /* Fixed sleep time selected by --sleep option */ + if (opt_sleep >= 0 && !real_sleep) + sleep_val= opt_sleep; + + DBUG_PRINT("info", ("sleep_val: %f", sleep_val)); + if (sleep_val) + my_sleep((ulong) (sleep_val * 1000000L)); + return 0; +} + + +void do_get_file_name(struct st_command *command, + char* dest, uint dest_max_len) +{ + char *p= command->first_argument, *name; + if (!*p) + die("Missing file name argument"); + name= p; + while (*p && !my_isspace(charset_info,*p)) + p++; + if (*p) + *p++= 0; + command->last_argument= p; + strmake(dest, name, dest_max_len - 1); +} + + +void do_set_charset(struct st_command *command) +{ + char *charset_name= command->first_argument; + char *p; + + if (!charset_name || !*charset_name) + die("Missing charset name in 'character_set'"); + /* Remove end space */ + p= charset_name; + while (*p && !my_isspace(charset_info,*p)) + p++; + if(*p) + *p++= 0; + command->last_argument= p; + charset_info= get_charset_by_csname(charset_name,MY_CS_PRIMARY, + MYF(MY_WME | MY_UTF8_IS_UTF8MB3)); + if (!charset_info) + abort_not_supported_test("Test requires charset '%s'", charset_name); +} + + +/* + Run query and return one field in the result set from the + first row and <column> +*/ + +int query_get_string(MYSQL* mysql, const char* query, + int column, DYNAMIC_STRING* ds) +{ + MYSQL_RES *res= NULL; + MYSQL_ROW row; + + if (mysql_query(mysql, query)) + { + report_or_die("'%s' failed: %d %s", query, + mysql_errno(mysql), mysql_error(mysql)); + return 1; + } + if ((res= mysql_store_result(mysql)) == NULL) + { + report_or_die("Failed to store result: %d %s", + mysql_errno(mysql), mysql_error(mysql)); + return 1; + } + + if ((row= mysql_fetch_row(res)) == NULL) + { + mysql_free_result(res); + return 1; + } + init_dynamic_string(ds, (row[column] ? row[column] : "NULL"), ~0, 32); + mysql_free_result(res); + return 0; +} + + +#ifdef _WIN32 +#define SIGKILL 9 +#include <my_minidump.h> +static int my_kill(int pid, int sig) +{ + HANDLE proc; + if (sig == SIGABRT) + { + /* + Create a minidump. If process is being debugged, debug break + Otherwise, terminate. + */ + verbose_msg("Aborting %d",pid); + my_create_minidump(pid,TRUE); + proc= OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); + if(!proc) + return -1; + BOOL debugger_present; + if (CheckRemoteDebuggerPresent(proc,&debugger_present) && debugger_present) + { + if (DebugBreakProcess(proc)) + { + CloseHandle(proc); + return 0; + } + } + } + else if ((proc= OpenProcess(SYNCHRONIZE|PROCESS_TERMINATE, FALSE, pid)) == NULL) + return -1; + if (sig == 0) + { + DWORD wait_result= WaitForSingleObject(proc, 0); + CloseHandle(proc); + return wait_result == WAIT_OBJECT_0?-1:0; + } + (void)TerminateProcess(proc, 201); + CloseHandle(proc); + return 1; +} + + +/* Wait until process is gone, with timeout */ +static int wait_until_dead(int pid, int timeout) +{ + HANDLE proc= OpenProcess(SYNCHRONIZE, FALSE, pid); + if (!proc) + return 0; /* already dead */ + DBUG_ASSERT(timeout >= 0); + DBUG_ASSERT(timeout <= UINT_MAX/1000); + DWORD wait_result= WaitForSingleObject(proc, (DWORD)timeout*1000); + CloseHandle(proc); + return (int)wait_result; +} + +#else /* !_WIN32 */ + + +static int my_kill(int pid, int sig) +{ + DBUG_PRINT("info", ("Killing server, pid: %d", pid)); + return kill(pid, sig); +} + +/* + Shutdown the server of current connection and + make sure it goes away within <timeout> seconds + + NOTE! Currently only works with local server + + SYNOPSIS + do_shutdown_server() + command called command + + DESCRIPTION + shutdown_server [<timeout>] + +*/ + + +static int wait_until_dead(int pid, int timeout) +{ + DBUG_ENTER("wait_until_dead"); + /* Check that server dies */ + while (timeout--) + { + if (my_kill(pid, 0) < 0) + { + DBUG_PRINT("info", ("Process %d does not exist anymore", pid)); + DBUG_RETURN(0); + } + DBUG_PRINT("info", ("Sleeping, timeout: %d", timeout)); + /* Sleep one second */ + my_sleep(1000000L); + } + DBUG_RETURN(1); // Did not die +} +#endif /* _WIN32 */ + + +void do_shutdown_server(struct st_command *command) +{ + long timeout= opt_wait_for_pos_timeout ? opt_wait_for_pos_timeout / 5 : 300; + int pid; + DYNAMIC_STRING ds_pidfile_name; + MYSQL* mysql = cur_con->mysql; + static DYNAMIC_STRING ds_timeout; + const struct command_arg shutdown_args[] = { + {"timeout", ARG_STRING, FALSE, &ds_timeout, "Timeout before killing server"} + }; + DBUG_ENTER("do_shutdown_server"); + + /* the wait-for-pos' default based value of 'timeout' must fit to MDEV-23511 */ + compile_time_assert(default_wait_for_pos_timeout / 5 >= 60); + check_command_args(command, command->first_argument, shutdown_args, + sizeof(shutdown_args)/sizeof(struct command_arg), + ' '); + + if (ds_timeout.length) + { + char* endptr; + timeout= strtol(ds_timeout.str, &endptr, 10); + if (*endptr != '\0') + die("Illegal argument for timeout: '%s'", ds_timeout.str); + } + dynstr_free(&ds_timeout); + + /* Get the servers pid_file name and use it to read pid */ + if (query_get_string(mysql, "SHOW VARIABLES LIKE 'pid_file'", 1, + &ds_pidfile_name)) + die("Failed to get pid_file from server"); + + /* Read the pid from the file */ + { + int fd; + char buff[32]; + + if ((fd= my_open(ds_pidfile_name.str, O_RDONLY, MYF(0))) < 0) + die("Failed to open file '%s'", ds_pidfile_name.str); + dynstr_free(&ds_pidfile_name); + + if (my_read(fd, (uchar*)&buff, sizeof(buff), MYF(0)) <= 0){ + my_close(fd, MYF(0)); + die("pid file was empty"); + } + my_close(fd, MYF(0)); + + pid= atoi(buff); + if (pid == 0) + die("Pidfile didn't contain a valid number"); + } + DBUG_PRINT("info", ("Got pid %d", pid)); + + /* + If timeout == 0, it means we should kill the server hard, without + any shutdown or core (SIGKILL) + + If timeout is given, then we do things in the following order: + - mysql_shutdown() + - If server is not dead within timeout + - kill SIGABRT (to get a core) + - If server is not dead within new timeout + - kill SIGKILL + */ + + if (timeout && mysql_shutdown(mysql, SHUTDOWN_DEFAULT)) + die("mysql_shutdown failed"); + + if (!timeout || wait_until_dead(pid, timeout)) + { + if (timeout) + (void) my_kill(pid, SIGABRT); + /* Give server a few seconds to die in all cases */ + if (!timeout || wait_until_dead(pid, timeout < 5 ? 5 : timeout)) + { + (void) my_kill(pid, SIGKILL); + wait_until_dead(pid, 5); + } + } + DBUG_VOID_RETURN; +} + + +/* List of error names to error codes */ +typedef struct +{ + const char *name; + uint code; + const char *text; +} st_error; + +static st_error global_error_names[] = +{ + { "<No error>", ~0U, "" }, +#include <mysqld_ername.h> + { 0, 0, 0 } +}; + +#include <my_base.h> +static st_error handler_error_names[] = +{ + { "<No error>", UINT_MAX, "" }, +#include <handler_ername.h> + { 0, 0, 0 } +}; + +uint get_errcode_from_name(const char *error_name, const char *error_end, + st_error *e) +{ + DBUG_ENTER("get_errcode_from_name"); + DBUG_PRINT("enter", ("error_name: %s", error_name)); + + /* Loop through the array of known error names */ + for (; e->name; e++) + { + /* + If we get a match, we need to check the length of the name we + matched against in case it was longer than what we are checking + (as in ER_WRONG_VALUE vs. ER_WRONG_VALUE_COUNT). + */ + if (!strncmp(error_name, e->name, (int) (error_end - error_name)) && + (uint) strlen(e->name) == (uint) (error_end - error_name)) + { + DBUG_RETURN(e->code); + } + } + DBUG_RETURN(0); +} + + +uint get_errcode_from_name(const char *error_name, const char *error_end) +{ + uint tmp; + if ((tmp= get_errcode_from_name(error_name, error_end, + global_error_names))) + return tmp; + if ((tmp= get_errcode_from_name(error_name, error_end, + handler_error_names))) + return tmp; + die("Unknown SQL error name '%s'", error_name); + return 0; // Keep compiler happy +} + +const char *unknown_error= "<Unknown>"; + +const char *get_errname_from_code (uint error_code, st_error *e) +{ + DBUG_ENTER("get_errname_from_code"); + DBUG_PRINT("enter", ("error_code: %d", error_code)); + + if (! error_code) + { + DBUG_RETURN(""); + } + for (; e->name; e++) + { + if (e->code == error_code) + { + DBUG_RETURN(e->name); + } + } + /* Apparently, errors without known names may occur */ + DBUG_RETURN(unknown_error); +} + +const char *get_errname_from_code(uint error_code) +{ + const char *name; + if ((name= get_errname_from_code(error_code, global_error_names)) != + unknown_error) + return name; + return get_errname_from_code(error_code, handler_error_names); +} + +void do_get_errcodes(struct st_command *command) +{ + struct st_match_err *to= saved_expected_errors.err; + DBUG_ENTER("do_get_errcodes"); + + if (!*command->first_argument) + die("Missing argument(s) to 'error'"); + + /* TODO: Potentially, there is a possibility of variables + being expanded twice, e.g. + + let $errcodes = 1,\$a; + let $a = 1051; + error $errcodes; + DROP TABLE unknown_table; + ... + Got one of the listed errors + + But since it requires manual escaping, it does not seem + particularly dangerous or error-prone. + */ + DYNAMIC_STRING ds; + init_dynamic_string(&ds, 0, command->query_len + 64, 256); + do_eval(&ds, command->first_argument, command->end, !is_windows); + char *p= ds.str; + + uint count= 0; + char *next; + + do + { + char *end; + + /* Skip leading spaces */ + while (*p && *p == ' ') + p++; + + /* Find end */ + end= p; + while (*end && *end != ',' && *end != ' ') + end++; + + next=end; + + /* code to handle variables passed to mysqltest */ + if( *p == '$') + { + const char* fin; + VAR *var = var_get(p,&fin,0,0); + p=var->str_val; + end=p+var->str_val_len; + } + + if (*p == 'S') + { + char *to_ptr= to->code.sqlstate; + + /* + SQLSTATE string + - Must be SQLSTATE_LENGTH long + - May contain only digits[0-9] and _uppercase_ letters + */ + p++; /* Step past the S */ + if ((end - p) != SQLSTATE_LENGTH) + die("The sqlstate must be exactly %d chars long", SQLSTATE_LENGTH); + + /* Check sqlstate string validity */ + while (*p && p < end) + { + if (my_isdigit(charset_info, *p) || my_isupper(charset_info, *p)) + *to_ptr++= *p++; + else + die("The sqlstate may only consist of digits[0-9] " \ + "and _uppercase_ letters"); + } + + *to_ptr= 0; + to->type= ERR_SQLSTATE; + DBUG_PRINT("info", ("ERR_SQLSTATE: %s", to->code.sqlstate)); + } + else if (*p == 's') + { + die("The sqlstate definition must start with an uppercase S"); + } + else if (*p == 'E' || *p == 'W' || *p == 'H') + { + /* Error name string */ + + DBUG_PRINT("info", ("Error name: %s", p)); + to->code.errnum= get_errcode_from_name(p, end); + to->type= ERR_ERRNO; + DBUG_PRINT("info", ("ERR_ERRNO: %d", to->code.errnum)); + } + else if (*p == 'e' || *p == 'w' || *p == 'h') + { + die("The error name definition must start with an uppercase E or W or H"); + } + else + { + long val; + char *start= p; + /* Check that the string passed to str2int only contain digits */ + while (*p && p != end) + { + if (!my_isdigit(charset_info, *p)) + die("Invalid argument to error: '%s' - " \ + "the errno may only consist of digits[0-9]", + command->first_argument); + p++; + } + + /* Convert the string to int */ + if (!str2int(start, 10, (long) INT_MIN, (long) INT_MAX, &val)) + die("Invalid argument to error: '%s'", command->first_argument); + + to->code.errnum= (uint) val; + to->type= ERR_ERRNO; + DBUG_PRINT("info", ("ERR_ERRNO: %d", to->code.errnum)); + } + to++; + count++; + + if (count >= (sizeof(saved_expected_errors.err) / + sizeof(struct st_match_err))) + die("Too many errorcodes specified"); + + /* Set pointer to the end of the last error code */ + p= next; + + /* Find next ',' */ + while (*p && *p != ',') + p++; + + if (*p) + p++; /* Step past ',' */ + + } while (*p); + + command->last_argument= command->first_argument; + while (*command->last_argument) + command->last_argument++; + + to->type= ERR_EMPTY; /* End of data */ + + DBUG_PRINT("info", ("Expected errors: %d", count)); + saved_expected_errors.count= count; + dynstr_free(&ds); + DBUG_VOID_RETURN; +} + + +/* + Get a string; Return ptr to end of string + Strings may be surrounded by " or ' + + If string is a '$variable', return the value of the variable. +*/ + +static char *get_string(char **to_ptr, char **from_ptr, + struct st_command *command) +{ + char c, sep; + char *to= *to_ptr, *from= *from_ptr, *start=to; + DBUG_ENTER("get_string"); + + /* Find separator */ + if (*from == '"' || *from == '\'') + sep= *from++; + else + sep=' '; /* Separated with space */ + + for ( ; (c=*from) ; from++) + { + if (c == '\\' && from[1]) + { /* Escaped character */ + /* We can't translate \0 -> ASCII 0 as replace can't handle ASCII 0 */ + switch (*++from) { + case 'n': + *to++= '\n'; + break; + case 't': + *to++= '\t'; + break; + case 'r': + *to++ = '\r'; + break; + case 'b': + *to++ = '\b'; + break; + case 'Z': /* ^Z must be escaped on Win32 */ + *to++='\032'; + break; + default: + *to++ = *from; + break; + } + } + else if (c == sep) + { + if (c == ' ' || c != *++from) + break; /* Found end of string */ + *to++=c; /* Copy duplicated separator */ + } + else + *to++=c; + } + if (*from != ' ' && *from) + die("Wrong string argument in %s", command->query); + + while (my_isspace(charset_info,*from)) /* Point to next string */ + from++; + + *to =0; /* End of string marker */ + *to_ptr= to+1; /* Store pointer to end */ + *from_ptr= from; + + /* Check if this was a variable */ + if (*start == '$') + { + const char *end= to; + VAR *var=var_get(start, &end, 0, 1); + if (var && to == (char*) end+1) + { + DBUG_PRINT("info",("var: '%s' -> '%s'", start, var->str_val)); + DBUG_RETURN(var->str_val); /* return found variable value */ + } + } + DBUG_RETURN(start); +} + + + + +/** + Change the current connection to the given st_connection, and update + $mysql_get_server_version and $CURRENT_CONNECTION accordingly. +*/ +void set_current_connection(struct st_connection *con) +{ + cur_con= con; + /* Update $mysql_get_server_version to that of current connection */ + var_set_int("$mysql_get_server_version", + mysql_get_server_version(con->mysql)); + /* Update $CURRENT_CONNECTION to the name of the current connection */ + var_set_string("$CURRENT_CONNECTION", con->name); +} + + +void select_connection_name(const char *name) +{ + DBUG_ENTER("select_connection_name"); + DBUG_PRINT("enter",("name: '%s'", name)); + st_connection *con= find_connection_by_name(name); + + if (!con) + die("connection '%s' not found in connection pool", name); + + set_current_connection(con); + + /* Connection logging if enabled */ + if (!disable_connect_log && !disable_query_log) + { + DYNAMIC_STRING *ds= &ds_res; + + dynstr_append_mem(ds, "connection ", 11); + replace_dynstr_append(ds, name); + dynstr_append_mem(ds, ";\n", 2); + } + + DBUG_VOID_RETURN; +} + + +void select_connection(struct st_command *command) +{ + DBUG_ENTER("select_connection"); + static DYNAMIC_STRING ds_connection; + const struct command_arg connection_args[] = { + { "connection_name", ARG_STRING, TRUE, &ds_connection, "Name of the connection that we switch to." } + }; + check_command_args(command, command->first_argument, connection_args, + sizeof(connection_args)/sizeof(struct command_arg), + ' '); + + DBUG_PRINT("info", ("changing connection: %s", ds_connection.str)); + select_connection_name(ds_connection.str); + dynstr_free(&ds_connection); + DBUG_VOID_RETURN; +} + + +void do_close_connection(struct st_command *command) +{ + DBUG_ENTER("do_close_connection"); + + struct st_connection *con; + static DYNAMIC_STRING ds_connection; + const struct command_arg close_connection_args[] = { + { "connection_name", ARG_STRING, TRUE, &ds_connection, + "Name of the connection to close." } + }; + check_command_args(command, command->first_argument, + close_connection_args, + sizeof(close_connection_args)/sizeof(struct command_arg), + ' '); + + DBUG_PRINT("enter",("connection name: '%s'", ds_connection.str)); + + if (!(con= find_connection_by_name(ds_connection.str))) + die("connection '%s' not found in connection pool", ds_connection.str); + + DBUG_PRINT("info", ("Closing connection %s", con->name)); +#ifndef EMBEDDED_LIBRARY + if (command->type == Q_DIRTY_CLOSE) + { + mariadb_cancel(con->mysql); + } +#endif /*!EMBEDDED_LIBRARY*/ + if (con->stmt) + do_stmt_close(con); + con->stmt= 0; +#ifdef EMBEDDED_LIBRARY + /* + As query could be still executed in a separate thread + we need to check if the query's thread was finished and probably wait + (embedded-server specific) + */ + emb_close_connection(con); +#endif /*EMBEDDED_LIBRARY*/ + + mysql_close(con->mysql); + con->mysql= 0; + + if (con->util_mysql) + mysql_close(con->util_mysql); + con->util_mysql= 0; + con->pending= FALSE; + + my_free(con->name); + + /* + When the connection is closed set name to CLOSED_CONNECTION + to make it possible to reuse the connection name. + */ + if (!(con->name = my_strdup(PSI_NOT_INSTRUMENTED, CLOSED_CONNECTION, + MYF(MY_WME)))) + die("Out of memory"); + con->name_len= sizeof(CLOSED_CONNECTION)-1; + + if (con == cur_con) + { + /* Current connection was closed */ + var_set_int("$mysql_get_server_version", 0xFFFFFFFF); + var_set_string("$CURRENT_CONNECTION", con->name); + } + + /* Connection logging if enabled */ + if (!disable_connect_log && !disable_query_log) + { + DYNAMIC_STRING *ds= &ds_res; + + dynstr_append_mem(ds, "disconnect ", 11); + replace_dynstr_append(ds, ds_connection.str); + dynstr_append_mem(ds, ";\n", 2); + } + + dynstr_free(&ds_connection); + DBUG_VOID_RETURN; +} + + +/* + Connect to a server doing several retries if needed. + + SYNOPSIS + safe_connect() + con - connection structure to be used + host, user, pass, - connection parameters + db, port, sock + + NOTE + + Sometimes in a test the client starts before + the server - to solve the problem, we try again + after some sleep if connection fails the first + time + + This function will try to connect to the given server + "opt_max_connect_retries" times and sleep "connection_retry_sleep" + seconds between attempts before finally giving up. + This helps in situation when the client starts + before the server (which happens sometimes). + It will only ignore connection errors during these retries. + +*/ + +void safe_connect(MYSQL* mysql, const char *name, const char *host, + const char *user, const char *pass, const char *db, + int port, const char *sock) +{ + int failed_attempts= 0; + + DBUG_ENTER("safe_connect"); + + verbose_msg("Connecting to server %s:%d (socket %s) as '%s'" + ", connection '%s', attempt %d ...", + host, port, sock, user, name, failed_attempts); + + mysql_options(mysql, MYSQL_OPT_CONNECT_ATTR_RESET, 0); + mysql_options4(mysql, MYSQL_OPT_CONNECT_ATTR_ADD, + "program_name", "mysqltest"); + while(!mysql_real_connect(mysql, host,user, pass, db, port, sock, + CLIENT_MULTI_STATEMENTS | CLIENT_REMEMBER_OPTIONS)) + { + /* + Connect failed + + Only allow retry if this was an error indicating the server + could not be contacted. Error code differs depending + on protocol/connection type + */ + + if ((mysql_errno(mysql) == CR_CONN_HOST_ERROR || + mysql_errno(mysql) == CR_CONNECTION_ERROR) && + failed_attempts < opt_max_connect_retries) + { + verbose_msg("Connect attempt %d/%d failed: %d: %s", failed_attempts, + opt_max_connect_retries, mysql_errno(mysql), + mysql_error(mysql)); + my_sleep(connection_retry_sleep); + } + else + { + if (failed_attempts > 0) + die("Could not open connection '%s' after %d attempts: %d %s", name, + failed_attempts, mysql_errno(mysql), mysql_error(mysql)); + else + die("Could not open connection '%s': %d %s", name, + mysql_errno(mysql), mysql_error(mysql)); + } + failed_attempts++; + } + verbose_msg("... Connected."); + DBUG_VOID_RETURN; +} + + +/* + Connect to a server and handle connection errors in case they occur. + + SYNOPSIS + connect_n_handle_errors() + q - context of connect "query" (command) + con - connection structure to be used + host, user, pass, - connection parameters + db, port, sock + default_db - 0 if db was explicitly passed + + DESCRIPTION + This function will try to establish a connection to server and handle + possible errors in the same manner as if "connect" was usual SQL-statement + (If error is expected it will ignore it once it occurs and log the + "statement" to the query log). + Unlike safe_connect() it won't do several attempts. + + RETURN VALUES + 1 - Connected + 0 - Not connected + +*/ + +int connect_n_handle_errors(struct st_command *command, + MYSQL* con, const char* host, + const char* user, const char* pass, + const char* db, int port, const char* sock, + my_bool default_db) +{ + DYNAMIC_STRING *ds; + int failed_attempts= 0; + + ds= &ds_res; + + /* Only log if an error is expected */ + if (command->expected_errors.count > 0 && + !disable_query_log) + { + /* + Log the connect to result log + */ + dynstr_append_mem(ds, "connect(", 8); + replace_dynstr_append(ds, host); + dynstr_append_mem(ds, ",", 1); + replace_dynstr_append(ds, user); + dynstr_append_mem(ds, ",", 1); + replace_dynstr_append(ds, pass); + dynstr_append_mem(ds, ",", 1); + if (db) + replace_dynstr_append(ds, db); + dynstr_append_mem(ds, ",", 1); + replace_dynstr_append_uint(ds, port); + dynstr_append_mem(ds, ",", 1); + if (sock) + replace_dynstr_append(ds, sock); + dynstr_append_mem(ds, ")", 1); + dynstr_append_mem(ds, delimiter, delimiter_length); + dynstr_append_mem(ds, "\n", 1); + } + /* Simplified logging if enabled */ + if (!disable_connect_log && !disable_query_log) + { + replace_dynstr_append(ds, command->query); + dynstr_append_mem(ds, ";\n", 2); + } + + mysql_options(con, MYSQL_OPT_CONNECT_ATTR_RESET, 0); + mysql_options4(con, MYSQL_OPT_CONNECT_ATTR_ADD, "program_name", "mysqltest"); + while (!mysql_real_connect(con, host, user, pass, + (default_db ? "" : db), + port, (sock ? sock : 0), + CLIENT_MULTI_STATEMENTS)) + { + /* + If we have used up all our connections check whether this + is expected (by --error). If so, handle the error right away. + Otherwise, give it some extra time to rule out race-conditions. + If extra-time doesn't help, we have an unexpected error and + must abort -- just proceeding to handle_error() when second + and third chances are used up will handle that for us. + + There are various user-limits of which only max_user_connections + and max_connections_per_hour apply at connect time. For the + the second to create a race in our logic, we'd need a limits + test that runs without a FLUSH for longer than an hour, so we'll + stay clear of trying to work out which exact user-limit was + exceeded. + */ + + if (((mysql_errno(con) == ER_TOO_MANY_USER_CONNECTIONS) || + (mysql_errno(con) == ER_USER_LIMIT_REACHED)) && + (failed_attempts++ < opt_max_connect_retries)) + { + int i; + + i= match_expected_error(command, mysql_errno(con), mysql_sqlstate(con)); + + if (i >= 0) + goto do_handle_error; /* expected error, handle */ + + my_sleep(connection_retry_sleep); /* unexpected error, wait */ + continue; /* and give it 1 more chance */ + } + +do_handle_error: + var_set_errno(mysql_errno(con)); + handle_error(command, mysql_errno(con), mysql_error(con), + mysql_sqlstate(con), ds); + return 0; /* Not connected */ + } + + if (default_db && db && db[0] != '\0') + { + mysql_select_db(con, db); + // Ignore errors intentionally + } + + + var_set_errno(0); + handle_no_error(command); + revert_properties(); + return 1; /* Connected */ +} + + +/* + Open a new connection to MySQL Server with the parameters + specified. Make the new connection the current connection. + + SYNOPSIS + do_connect() + q called command + + DESCRIPTION + connect(<name>,<host>,<user>,[<pass>,[<db>,[<port>,<sock>[<opts>]]]]); + connect <name>,<host>,<user>,[<pass>,[<db>,[<port>,<sock>[<opts>]]]]; + + <name> - name of the new connection + <host> - hostname of server + <user> - user to connect as + <pass> - password used when connecting + <db> - initial db when connected + <port> - server port + <sock> - server socket + <opts> - options to use for the connection + * SSL - use SSL if available + * COMPRESS - use compression if available + * PIPE - use named pipe if available + +*/ + +enum use_ssl +{ + USE_SSL_FORBIDDEN = -1, + USE_SSL_IF_POSSIBLE, + USE_SSL_REQUIRED +}; + +void do_connect(struct st_command *command) +{ + uint protocol= opt_protocol; + int con_port= opt_port; + char *con_options; + char *ssl_cipher __attribute__((unused))= 0; + enum use_ssl con_ssl __attribute__((unused))= USE_SSL_IF_POSSIBLE; + my_bool con_compress= 0; + int read_timeout= 0; + int write_timeout= 0; + int connect_timeout= 0; + char *csname=0; + struct st_connection* con_slot; + my_bool default_db; + + static DYNAMIC_STRING ds_connection_name; + static DYNAMIC_STRING ds_host; + static DYNAMIC_STRING ds_user; + static DYNAMIC_STRING ds_password; + static DYNAMIC_STRING ds_database; + static DYNAMIC_STRING ds_port; + static DYNAMIC_STRING ds_sock; + static DYNAMIC_STRING ds_options; + static DYNAMIC_STRING ds_default_auth; + const struct command_arg connect_args[] = { + { "connection name", ARG_STRING, TRUE, &ds_connection_name, "Name of the connection" }, + { "host", ARG_STRING, TRUE, &ds_host, "Host to connect to" }, + { "user", ARG_STRING, FALSE, &ds_user, "User to connect as" }, + { "password", ARG_STRING, FALSE, &ds_password, "Password used when connecting" }, + { "database", ARG_STRING, FALSE, &ds_database, "Database to select after connect" }, + { "port", ARG_STRING, FALSE, &ds_port, "Port to connect to" }, + { "socket", ARG_STRING, FALSE, &ds_sock, "Socket to connect with" }, + { "options", ARG_STRING, FALSE, &ds_options, "Options to use while connecting" }, + { "default_auth", ARG_STRING, FALSE, &ds_default_auth, "Default authentication to use" } + }; + + DBUG_ENTER("do_connect"); + DBUG_PRINT("enter",("connect: %s", command->first_argument)); + + strip_parentheses(command); + check_command_args(command, command->first_argument, connect_args, + sizeof(connect_args)/sizeof(struct command_arg), + ','); + + /* Port */ + if (ds_port.length) + { + con_port= atoi(ds_port.str); + if (con_port == 0) + die("Illegal argument for port: '%s'", ds_port.str); + } + + /* Sock */ + if (ds_sock.length) + { + /* + If the socket is specified just as a name without path + or an abstract socket indicator ('@'), then + append tmpdir in front + */ + if (*ds_sock.str != FN_LIBCHAR && *ds_sock.str != '@') + { + char buff[FN_REFLEN]; + fn_format(buff, ds_sock.str, TMPDIR, "", 0); + dynstr_set(&ds_sock, buff); + } + } + else + { + /* No socket specified, use default */ + dynstr_set(&ds_sock, unix_sock); + } + DBUG_PRINT("info", ("socket: %s", ds_sock.str)); + + + /* Options */ + con_options= ds_options.str; + while (*con_options) + { + size_t length; + char *end; + /* Step past any spaces in beginning of option*/ + while (*con_options && my_isspace(charset_info, *con_options)) + con_options++; + /* Find end of this option */ + end= con_options; + while (*end && !my_isspace(charset_info, *end)) + end++; + length= (size_t) (end - con_options); + if (length == 3 && !strncmp(con_options, "SSL", 3)) + con_ssl= USE_SSL_REQUIRED; + else if (length == 5 && !strncmp(con_options, "NOSSL", 5)) + con_ssl= USE_SSL_FORBIDDEN; + else if (!strncmp(con_options, "SSL-CIPHER=", 11)) + { + con_ssl= USE_SSL_REQUIRED; + ssl_cipher=con_options + 11; + } + else if (length == 8 && !strncmp(con_options, "COMPRESS", 8)) + con_compress= 1; + else if (length == 3 && !strncmp(con_options, "TCP", 3)) + protocol= MYSQL_PROTOCOL_TCP; + else if (length == 7 && !strncmp(con_options, "DEFAULT", 7)) + protocol= MYSQL_PROTOCOL_DEFAULT; + else if (length == 4 && !strncmp(con_options, "PIPE", 4)) + { +#ifdef _WIN32 + protocol= MYSQL_PROTOCOL_PIPE; +#endif + } + else if (length == 6 && !strncmp(con_options, "SOCKET", 6)) + { +#ifndef _WIN32 + protocol= MYSQL_PROTOCOL_SOCKET; +#endif + } + else if (length == 6 && !strncmp(con_options, "MEMORY", 6)) + { +#ifdef _WIN32 + protocol= MYSQL_PROTOCOL_MEMORY; +#endif + } + else if (strncasecmp(con_options, "read_timeout=", + sizeof("read_timeout=")-1) == 0) + { + read_timeout= atoi(con_options + sizeof("read_timeout=")-1); + } + else if (strncasecmp(con_options, "write_timeout=", + sizeof("write_timeout=")-1) == 0) + { + write_timeout= atoi(con_options + sizeof("write_timeout=")-1); + } + else if (strncasecmp(con_options, "connect_timeout=", + sizeof("connect_timeout=")-1) == 0) + { + connect_timeout= atoi(con_options + sizeof("connect_timeout=")-1); + } + else if (strncasecmp(con_options, "CHARSET=", + sizeof("CHARSET=") - 1) == 0) + { + csname= strdup(con_options + sizeof("CHARSET=") - 1); + } + else + die("Illegal option to connect: %.*b", + (int) (end - con_options), con_options); + /* Process next option */ + con_options= end; + } + + if (find_connection_by_name(ds_connection_name.str)) + die("Connection %s already exists", ds_connection_name.str); + + if (next_con != connections_end) + con_slot= next_con; + else + { + if (!(con_slot= find_connection_by_name(CLOSED_CONNECTION))) + die("Connection limit exhausted, you can have max %d connections", + opt_max_connections); + my_free(con_slot->name); + con_slot->name= 0; + } + + init_connection_thd(con_slot); + + if (!(con_slot->mysql= mysql_init(0))) + die("Failed on mysql_init()"); + + if (opt_connect_timeout) + mysql_options(con_slot->mysql, MYSQL_OPT_CONNECT_TIMEOUT, + (void *) &opt_connect_timeout); + if (mysql_options(con_slot->mysql, MYSQL_OPT_NONBLOCK, 0)) + die("Failed to initialise non-blocking API"); + if (opt_compress || con_compress) + mysql_options(con_slot->mysql, MYSQL_OPT_COMPRESS, NullS); + mysql_options(con_slot->mysql, MYSQL_SET_CHARSET_NAME, + csname ? csname : charset_info->cs_name.str); + if (opt_charsets_dir) + mysql_options(con_slot->mysql, MYSQL_SET_CHARSET_DIR, + opt_charsets_dir); + +#if defined(HAVE_OPENSSL) && !defined(EMBEDDED_LIBRARY) + if (con_ssl == USE_SSL_IF_POSSIBLE && opt_use_ssl) + con_ssl= USE_SSL_REQUIRED; + + if (con_ssl == USE_SSL_REQUIRED) + { + mysql_ssl_set(con_slot->mysql, opt_ssl_key, opt_ssl_cert, opt_ssl_ca, + opt_ssl_capath, ssl_cipher ? ssl_cipher : opt_ssl_cipher); + mysql_options(con_slot->mysql, MYSQL_OPT_SSL_CRL, opt_ssl_crl); + mysql_options(con_slot->mysql, MYSQL_OPT_SSL_CRLPATH, opt_ssl_crlpath); + mysql_options(con_slot->mysql, MARIADB_OPT_TLS_VERSION, opt_tls_version); + mysql_options(con_slot->mysql, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, + &opt_ssl_verify_server_cert); + } +#endif + + if (protocol) + mysql_options(con_slot->mysql, MYSQL_OPT_PROTOCOL, (char*) &protocol); + + if (read_timeout) + { + mysql_options(con_slot->mysql, MYSQL_OPT_READ_TIMEOUT, + (char*)&read_timeout); + } + + if (write_timeout) + { + mysql_options(con_slot->mysql, MYSQL_OPT_WRITE_TIMEOUT, + (char*)&write_timeout); + } + + if (connect_timeout) + { + mysql_options(con_slot->mysql, MYSQL_OPT_CONNECT_TIMEOUT, + (char*)&connect_timeout); + } + + /* Use default db name */ + if (ds_database.length == 0) + { + dynstr_set(&ds_database, opt_db); + default_db= 1; + } + else + default_db= 0; + + if (opt_plugin_dir && *opt_plugin_dir) + mysql_options(con_slot->mysql, MYSQL_PLUGIN_DIR, opt_plugin_dir); + + if (ds_default_auth.length) + mysql_options(con_slot->mysql, MYSQL_DEFAULT_AUTH, ds_default_auth.str); + + /* Special database to allow one to connect without a database name */ + if (ds_database.length && !strcmp(ds_database.str,"*NO-ONE*")) + dynstr_set(&ds_database, ""); + + if (connect_n_handle_errors(command, con_slot->mysql, + ds_host.str,ds_user.str, + ds_password.str, ds_database.str, + con_port, ds_sock.str, default_db)) + { + DBUG_PRINT("info", ("Inserting connection %s in connection pool", + ds_connection_name.str)); + if (!(con_slot->name= my_strdup(PSI_NOT_INSTRUMENTED, ds_connection_name.str, MYF(MY_WME)))) + die("Out of memory"); + con_slot->name_len= strlen(con_slot->name); + set_current_connection(con_slot); + + if (con_slot == next_con) + next_con++; /* if we used the next_con slot, advance the pointer */ + } + else // Failed to connect. Free the memory. + { + mysql_close(con_slot->mysql); + con_slot->mysql= NULL; + } + + dynstr_free(&ds_connection_name); + dynstr_free(&ds_host); + dynstr_free(&ds_user); + dynstr_free(&ds_password); + dynstr_free(&ds_database); + dynstr_free(&ds_port); + dynstr_free(&ds_sock); + dynstr_free(&ds_options); + dynstr_free(&ds_default_auth); + free(csname); + DBUG_VOID_RETURN; +} + + +int do_done(struct st_command *command) +{ + /* Check if empty block stack */ + if (cur_block == block_stack) + { + if (*command->query != '}') + die("Stray 'end' command - end of block before beginning"); + die("Stray '}' - end of block before beginning"); + } + + /* Test if inner block has been executed */ + if (cur_block->ok && cur_block->cmd == cmd_while) + { + /* Pop block from stack, re-execute outer block */ + cur_block--; + parser.current_line = cur_block->line; + } + else + { + if (*cur_block->delim) + { + /* Restore "old" delimiter after false if block */ + if (safe_strcpy(delimiter, sizeof(delimiter), cur_block->delim)) + die("Delimiter too long, truncated"); + + delimiter_length= strlen(delimiter); + } + /* Pop block from stack, goto next line */ + cur_block--; + parser.current_line++; + } + return 0; +} + +/* Operands available in if or while conditions */ + +enum block_op { + EQ_OP, + NE_OP, + GT_OP, + GE_OP, + LT_OP, + LE_OP, + ILLEG_OP +}; + + +enum block_op find_operand(const char *start) +{ + char first= *start; + char next= *(start+1); + + if (first == '=' && next == '=') + return EQ_OP; + if (first == '!' && next == '=') + return NE_OP; + if (first == '>' && next == '=') + return GE_OP; + if (first == '>') + return GT_OP; + if (first == '<' && next == '=') + return LE_OP; + if (first == '<') + return LT_OP; + + return ILLEG_OP; +} + + +/* + Process start of a "if" or "while" statement + + SYNOPSIS + do_block() + cmd Type of block + q called command + + DESCRIPTION + if ([!]<expr>) + { + <block statements> + } + + while ([!]<expr>) + { + <block statements> + } + + Evaluates the <expr> and if it evaluates to + greater than zero executes the following code block. + A '!' can be used before the <expr> to indicate it should + be executed if it evaluates to zero. + + <expr> can also be a simple comparison condition: + + <variable> <op> <expr> + + The left hand side must be a variable, the right hand side can be a + variable, number, string or `query`. Operands are ==, !=, <, <=, >, >=. + == and != can be used for strings, all can be used for numerical values. +*/ + +void do_block(enum block_cmd cmd, struct st_command* command) +{ + char *p= command->first_argument; + const char *expr_start, *expr_end; + VAR v; + const char *cmd_name= (cmd == cmd_while ? "while" : "if"); + my_bool not_expr= FALSE; + DBUG_ENTER("do_block"); + DBUG_PRINT("enter", ("%s", cmd_name)); + + /* Check stack overflow */ + if (cur_block == block_stack_end) + die("Nesting too deeply"); + + /* Set way to find outer block again, increase line counter */ + cur_block->line= parser.current_line++; + + /* If this block is ignored */ + if (!cur_block->ok) + { + /* Inner block should be ignored too */ + cur_block++; + cur_block->cmd= cmd; + cur_block->ok= FALSE; + cur_block->delim[0]= '\0'; + DBUG_VOID_RETURN; + } + + /* Parse and evaluate test expression */ + expr_start= strchr(p, '('); + if (!expr_start++) + die("missing '(' in %s", cmd_name); + + while (my_isspace(charset_info, *expr_start)) + expr_start++; + + /* Check for !<expr> */ + if (*expr_start == '!') + { + not_expr= TRUE; + expr_start++; /* Step past the '!', then any whitespace */ + while (*expr_start && my_isspace(charset_info, *expr_start)) + expr_start++; + } + /* Find ending ')' */ + expr_end= strrchr(expr_start, ')'); + if (!expr_end) + die("missing ')' in %s", cmd_name); + p= (char*)expr_end+1; + + while (*p && my_isspace(charset_info, *p)) + p++; + if (*p && *p != '{') + die("Missing '{' after %s. Found \"%s\"", cmd_name, p); + + var_init(&v,0,0,0,0); + + /* If expression starts with a variable, it may be a compare condition */ + + if (*expr_start == '$') + { + const char *curr_ptr= expr_end; + eval_expr(&v, expr_start, &curr_ptr, true); + while (my_isspace(charset_info, *++curr_ptr)) + {} + /* If there was nothing past the variable, skip condition part */ + if (curr_ptr == expr_end) + goto NO_COMPARE; + + enum block_op operand= find_operand(curr_ptr); + if (operand == ILLEG_OP) + die("Found junk '%.*b' after $variable in condition", + (int)(expr_end - curr_ptr), curr_ptr); + + /* We could silently allow this, but may be confusing */ + if (not_expr) + die("Negation and comparison should not be combined, please rewrite"); + + /* Skip the 1 or 2 chars of the operand, then white space */ + if (operand == LT_OP || operand == GT_OP) + { + curr_ptr++; + } + else + { + curr_ptr+= 2; + } + while (my_isspace(charset_info, *curr_ptr)) + curr_ptr++; + if (curr_ptr == expr_end) + die("Missing right operand in comparison"); + + /* Strip off trailing white space */ + while (my_isspace(charset_info, expr_end[-1])) + expr_end--; + /* strip off ' or " around the string */ + if (*curr_ptr == '\'' || *curr_ptr == '"') + { + if (expr_end[-1] != *curr_ptr) + die("Unterminated string value"); + curr_ptr++; + expr_end--; + } + VAR v2; + var_init(&v2,0,0,0,0); + eval_expr(&v2, curr_ptr, &expr_end); + + if ((operand!=EQ_OP && operand!=NE_OP) && ! (v.is_int && v2.is_int)) + die("Only == and != are supported for string values"); + + /* Now we overwrite the first variable with 0 or 1 (for false or true) */ + + switch (operand) + { + case EQ_OP: + if (v.is_int) + v.int_val= (v2.is_int && v2.int_val == v.int_val); + else + v.int_val= !strcmp (v.str_val, v2.str_val); + break; + + case NE_OP: + if (v.is_int) + v.int_val= ! (v2.is_int && v2.int_val == v.int_val); + else + v.int_val= (strcmp (v.str_val, v2.str_val) != 0); + break; + + case LT_OP: + v.int_val= (v.int_val < v2.int_val); + break; + case LE_OP: + v.int_val= (v.int_val <= v2.int_val); + break; + case GT_OP: + v.int_val= (v.int_val > v2.int_val); + break; + case GE_OP: + v.int_val= (v.int_val >= v2.int_val); + break; + case ILLEG_OP: + die("Impossible operator, this cannot happen"); + } + + v.is_int= TRUE; + var_free(&v2); + } else + { + if (*expr_start != '`' && ! my_isdigit(charset_info, *expr_start)) + die("Expression in if/while must begin with $, ` or a number"); + eval_expr(&v, expr_start, &expr_end); + } + + NO_COMPARE: + /* Define inner block */ + cur_block++; + cur_block->cmd= cmd; + if (v.is_int) + { + cur_block->ok= (v.int_val != 0); + } else + /* Any non-empty string which does not begin with 0 is also TRUE */ + { + p= v.str_val; + /* First skip any leading white space or unary -+ */ + while (*p && ((my_isspace(charset_info, *p) || *p == '-' || *p == '+'))) + p++; + + cur_block->ok= (*p && *p != '0') ? TRUE : FALSE; + } + + if (not_expr) + cur_block->ok = !cur_block->ok; + + if (cur_block->ok) + { + cur_block->delim[0]= '\0'; + } + else + { + /* Remember "old" delimiter if entering a false if block */ + if (safe_strcpy(cur_block->delim, sizeof(cur_block->delim), delimiter)) + die("Delimiter too long, truncated"); + } + + DBUG_PRINT("info", ("OK: %d", cur_block->ok)); + + var_free(&v); + DBUG_VOID_RETURN; +} + + +void do_delimiter(struct st_command* command) +{ + char* p= command->first_argument; + DBUG_ENTER("do_delimiter"); + DBUG_PRINT("enter", ("first_argument: %s", command->first_argument)); + + while (*p && my_isspace(charset_info, *p)) + p++; + + if (!(*p)) + die("Can't set empty delimiter"); + + delimiter_length= (uint)(strmake_buf(delimiter, p) - delimiter); + + DBUG_PRINT("exit", ("delimiter: %s", delimiter)); + command->last_argument= p + delimiter_length; + DBUG_VOID_RETURN; +} + + +/* + do_reset_connection + + DESCRIPTION + Reset the current session. +*/ + +static void do_reset_connection() +{ +#ifndef EMBEDDED_LIBRARY + MYSQL *mysql = cur_con->mysql; + + DBUG_ENTER("do_reset_connection"); + if (mysql_reset_connection(mysql)) + die("reset connection failed: %s", mysql_error(mysql)); + if (cur_con->stmt) + { + mysql_stmt_close(cur_con->stmt); + cur_con->stmt= NULL; + } + DBUG_VOID_RETURN; +#else + die("reset connection failed: unsupported by embedded server client library"); + return; +#endif +} + + +my_bool match_delimiter(int c, const char *delim, size_t length) +{ + uint i; + char tmp[MAX_DELIMITER_LENGTH]; + + if (c != *delim) + return 0; + + for (i= 1; i < length && + (c= my_getc(cur_file->file)) == *(delim + i); + i++) + tmp[i]= c; + + if (i == length) + return 1; /* Found delimiter */ + + /* didn't find delimiter, push back things that we read */ + my_ungetc(c); + while (i > 1) + my_ungetc(tmp[--i]); + return 0; +} + + +my_bool end_of_query(int c) +{ + return match_delimiter(c, delimiter, delimiter_length); +} + + +static inline bool is_escape_char(char c, char in_string) +{ + if (c != '\\' || in_string == '`') return false; + if (!cur_con) return true; + uint server_status= cur_con->mysql->server_status; + if (server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES) return false; + return !(server_status & SERVER_STATUS_ANSI_QUOTES && in_string == '"'); +} + + +/* + Read one "line" from the file + + SYNOPSIS + read_line + + DESCRIPTION + This function actually reads several lines and adds them to the + buffer buf. It continues to read until it finds what it believes + is a complete query. + + Normally that means it will read lines until it reaches the + "delimiter" that marks end of query. Default delimiter is ';' + The function should be smart enough not to detect delimiter's + found inside strings surrounded with '"' and '\'' escaped strings. + + If the first line in a query starts with '#' or '-' this line is treated + as a comment. A comment is always terminated when end of line '\n' is + reached. + +*/ + +static size_t read_command_buflen= 0; +static const size_t max_multibyte_length= 6; + +int read_line() +{ + char c, last_quote=0, last_char= 0; + char *p= read_command_buf; + char *buf_end= read_command_buf + read_command_buflen - max_multibyte_length; + int skip_char= 0; + my_bool have_slash= FALSE; + + enum {R_NORMAL, R_Q, R_SLASH_IN_Q, + R_COMMENT, R_LINE_START} state= R_LINE_START; + DBUG_ENTER("read_line"); + + *p= 0; + start_lineno= cur_file->lineno; + DBUG_PRINT("info", ("Starting to read at lineno: %d", start_lineno)); + while (1) + { + if (p >= buf_end) + { + my_ptrdiff_t off= p - read_command_buf; + read_command_buf= (char*)my_realloc(PSI_NOT_INSTRUMENTED, read_command_buf, + read_command_buflen*2, MYF(MY_FAE)); + p= read_command_buf + off; + read_command_buflen*= 2; + buf_end= read_command_buf + read_command_buflen - max_multibyte_length; + } + + skip_char= 0; + c= my_getc(cur_file->file); + if (feof(cur_file->file)) + { + found_eof: + if (cur_file->file != stdin) + { + fclose(cur_file->file); + cur_file->file= 0; + } + my_free(cur_file->file_name); + cur_file->file_name= 0; + if (cur_file == file_stack) + { + /* We're back at the first file, check if + all { have matching } + */ + if (cur_block != block_stack) + die("Missing end of block"); + + *p= 0; + DBUG_PRINT("info", ("end of file at line %d", cur_file->lineno)); + DBUG_RETURN(1); + } + cur_file--; + start_lineno= cur_file->lineno; + continue; + } + + if (c == '\n') + { + /* Line counting is independent of state */ + cur_file->lineno++; + + /* Convert cr/lf to lf */ + if (p != read_command_buf && *(p-1) == '\r') + p--; + } + + switch(state) { + case R_NORMAL: + if (end_of_query(c)) + { + *p= 0; + DBUG_PRINT("exit", ("Found delimiter '%s' at line %d", + delimiter, cur_file->lineno)); + DBUG_RETURN(0); + } + else if ((c == '{' && + (!my_strnncoll_simple(charset_info, (const uchar*) "while", 5, + (uchar*) read_command_buf, MY_MIN(5, p - read_command_buf), 0) || + !my_strnncoll_simple(charset_info, (const uchar*) "if", 2, + (uchar*) read_command_buf, MY_MIN(2, p - read_command_buf), 0)))) + { + /* Only if and while commands can be terminated by { */ + *p++= c; + *p= 0; + DBUG_PRINT("exit", ("Found '{' indicating start of block at line %d", + cur_file->lineno)); + DBUG_RETURN(0); + } + else if (c == '\'' || c == '"' || c == '`') + { + if (! have_slash) + { + last_quote= c; + state= R_Q; + } + } + have_slash= is_escape_char(c, last_quote); + break; + + case R_COMMENT: + if (c == '\n') + { + /* Comments are terminated by newline */ + *p= 0; + DBUG_PRINT("exit", ("Found newline in comment at line: %d", + cur_file->lineno)); + DBUG_RETURN(0); + } + break; + + case R_LINE_START: + if (c == '#' || c == '-') + { + /* A # or - in the first position of the line - this is a comment */ + state = R_COMMENT; + } + else if (my_isspace(charset_info, c)) + { + if (c == '\n') + { + if (last_char == '\n') + { + /* Two new lines in a row, return empty line */ + DBUG_PRINT("info", ("Found two new lines in a row")); + *p++= c; + *p= 0; + DBUG_RETURN(0); + } + + /* Query hasn't started yet */ + start_lineno= cur_file->lineno; + DBUG_PRINT("info", ("Query hasn't started yet, start_lineno: %d", + start_lineno)); + } + + /* Skip all space at beginning of line */ + skip_char= 1; + } + else if (end_of_query(c)) + { + *p= 0; + DBUG_PRINT("exit", ("Found delimiter '%s' at line: %d", + delimiter, cur_file->lineno)); + DBUG_RETURN(0); + } + else if (c == '}') + { + /* A "}" need to be by itself in the beginning of a line to terminate */ + *p++= c; + *p= 0; + DBUG_PRINT("exit", ("Found '}' in beginning of a line at line: %d", + cur_file->lineno)); + DBUG_RETURN(0); + } + else if (c == '\'' || c == '"' || c == '`') + { + last_quote= c; + state= R_Q; + } + else + state= R_NORMAL; + break; + + case R_Q: + if (c == last_quote) + state= R_NORMAL; + else if (is_escape_char(c, last_quote)) + state= R_SLASH_IN_Q; + break; + + case R_SLASH_IN_Q: + state= R_Q; + break; + + } + + last_char= c; + + if (!skip_char) + { + *p++= c; + if (charset_info->use_mb()) + { + const char *mb_start= p - 1; + /* Could be a multibyte character */ + /* See a similar code in "sql_load.cc" */ + for ( ; p < buf_end; ) + { + int charlen= charset_info->charlen(mb_start, p); + if (charlen > 0) + break; /* Full character */ + if (MY_CS_IS_TOOSMALL(charlen)) + { + /* We give up if multibyte character is started but not */ + /* completed before we pass buf_end */ + c= my_getc(cur_file->file); + if (feof(cur_file->file)) + goto found_eof; + *p++ = c; + continue; + } + DBUG_ASSERT(charlen == MY_CS_ILSEQ); + /* It was not a multiline char, push back the characters */ + /* We leave first 'c', i.e. pretend it was a normal char */ + while (p - 1 > mb_start) + my_ungetc(*--p); + break; + } + } + } + } + DBUG_RETURN(0); +} + + +/* + Convert the read query to result format version 1 + + That is: After newline, all spaces need to be skipped + unless the previous char was a quote + + This is due to an old bug that has now been fixed, but the + version 1 output format is preserved by using this function + +*/ + +void convert_to_format_v1(char* query) +{ + int last_c_was_quote= 0; + char *p= query, *to= query; + char *end= strend(query); + char last_c; + + while (p <= end) + { + if (*p == '\n' && !last_c_was_quote) + { + *to++ = *p++; /* Save the newline */ + + /* Skip any spaces on next line */ + while (*p && my_isspace(charset_info, *p)) + p++; + + last_c_was_quote= 0; + } + else if (*p == '\'' || *p == '"' || *p == '`') + { + last_c= *p; + *to++ = *p++; + + /* Copy anything until the next quote of same type */ + while (*p && *p != last_c) + *to++ = *p++; + + *to++ = *p++; + + last_c_was_quote= 1; + } + else + { + *to++ = *p++; + last_c_was_quote= 0; + } + } +} + + +/* + Check for unexpected "junk" after the end of query + This is normally caused by missing delimiters or when + switching between different delimiters +*/ + +void check_eol_junk_line(const char *line) +{ + const char *p= line; + DBUG_ENTER("check_eol_junk_line"); + DBUG_PRINT("enter", ("line: %s", line)); + + /* Check for extra delimiter */ + if (*p && !strncmp(p, delimiter, delimiter_length)) + die("Extra delimiter \"%s\" found", delimiter); + + /* Allow trailing # comment */ + if (*p && *p != '#') + { + if (*p == '\n') + die("Missing delimiter"); + die("End of line junk detected: \"%s\"", p); + } + DBUG_VOID_RETURN; +} + +void check_eol_junk(const char *eol) +{ + const char *p= eol; + DBUG_ENTER("check_eol_junk"); + DBUG_PRINT("enter", ("eol: %s", eol)); + + /* Skip past all spacing chars and comments */ + while (*p && (my_isspace(charset_info, *p) || *p == '#' || *p == '\n')) + { + /* Skip past comments started with # and ended with newline */ + if (*p && *p == '#') + { + p++; + while (*p && *p != '\n') + p++; + } + + /* Check this line */ + if (*p && *p == '\n') + check_eol_junk_line(p); + + if (*p) + p++; + } + + check_eol_junk_line(p); + + DBUG_VOID_RETURN; +} + + +bool is_delimiter(const char* p) +{ + uint match= 0; + char* delim= delimiter; + while (*p && *p == *delim++) + { + match++; + p++; + } + + return (match == delimiter_length); +} + + +/* + Create a command from a set of lines + + SYNOPSIS + read_command() + command_ptr pointer where to return the new query + + DESCRIPTION + Converts lines returned by read_line into a command, this involves + parsing the first word in the read line to find the command type. + + A -- comment may contain a valid query as the first word after the + comment start. Thus it's always checked to see if that is the case. + The advantage with this approach is to be able to execute commands + terminated by new line '\n' regardless how many "delimiter" it contain. +*/ + +int read_command(struct st_command** command_ptr) +{ + struct st_command* command; + DBUG_ENTER("read_command"); + + if (parser.current_line < parser.read_lines) + { + get_dynamic(&q_lines, command_ptr, parser.current_line) ; + DBUG_PRINT("info", ("query: %s", (*command_ptr)->query)); + DBUG_RETURN(0); + } + if (!(*command_ptr= command= + (struct st_command*) my_malloc(PSI_NOT_INSTRUMENTED, sizeof(*command), + MYF(MY_WME|MY_FAE|MY_ZEROFILL))) || + insert_dynamic(&q_lines, &command)) + die("Out of memory"); + command->type= Q_UNKNOWN; + + if (read_line()) + { + check_eol_junk(read_command_buf); + DBUG_RETURN(1); + } + + if (opt_result_format_version == 1) + convert_to_format_v1(read_command_buf); + + char *p= read_command_buf; + DBUG_PRINT("info", ("query: '%s'", read_command_buf)); + if (*p == '#') + { + command->type= Q_COMMENT; + } + else if (p[0] == '-' && p[1] == '-') + { + command->type= Q_COMMENT_WITH_COMMAND; + p+= 2; /* Skip past -- */ + } + else if (*p == '\n') + { + command->type= Q_EMPTY_LINE; + } + + /* Skip leading spaces */ + while (*p && my_isspace(charset_info, *p)) + p++; + + if (!(command->query_buf= command->query= my_strdup(PSI_NOT_INSTRUMENTED, p, MYF(MY_WME)))) + die("Out of memory"); + + /* + Calculate first word length(the command), terminated + by 'space' , '(' or 'delimiter' */ + p= command->query; + while (*p && !my_isspace(charset_info, *p) && *p != '(' && !is_delimiter(p)) + p++; + command->first_word_len= (uint) (p - command->query); + DBUG_PRINT("info", ("first_word: %.*s", + command->first_word_len, command->query)); + + /* Skip spaces between command and first argument */ + while (*p && my_isspace(charset_info, *p)) + p++; + command->first_argument= p; + + command->end= strend(command->query); + command->query_len= (int)(command->end - command->query); + parser.read_lines++; + DBUG_RETURN(0); +} + + +static struct my_option my_long_options[] = +{ + {"help", '?', "Display this help and exit.", 0, 0, 0, GET_NO_ARG, NO_ARG, + 0, 0, 0, 0, 0, 0}, + {"basedir", 'b', "Basedir for tests.", &opt_basedir, + &opt_basedir, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"character-sets-dir", 0, + "Directory for character set files.", &opt_charsets_dir, + &opt_charsets_dir, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"compress", 'C', "Use the compressed server/client protocol.", + &opt_compress, &opt_compress, 0, GET_BOOL, NO_ARG, 0, 0, 0, + 0, 0, 0}, + {"continue-on-error", 0, + "Continue test even if we got an error. " + "This is mostly useful when testing a storage engine to see what from a test file it can execute, " + "or to find all syntax errors in a newly created big test file", + &opt_continue_on_error, &opt_continue_on_error, 0, + GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"cursor-protocol", 0, "Use cursors for prepared statements.", + &cursor_protocol, &cursor_protocol, 0, + GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"database", 'D', "Database to use.", &opt_db, &opt_db, 0, + GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, +#ifdef DBUG_OFF + {"debug", '#', "This is a non-debug version. Catch this and exit", + 0,0, 0, GET_DISABLED, OPT_ARG, 0, 0, 0, 0, 0, 0}, +#else + {"debug", '#', "Output debug log. Often this is 'd:t:o,filename'.", + 0, 0, 0, GET_STR, OPT_ARG, 0, 0, 0, 0, 0, 0}, +#endif + {"debug-check", 0, "Check memory and open file usage at exit.", + &debug_check_flag, &debug_check_flag, 0, + GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"debug-info", 0, "Print some debug info at exit.", + &debug_info_flag, &debug_info_flag, + 0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"host", 'h', "Connect to host.", &opt_host, &opt_host, 0, + GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"prologue", 0, "Include SQL before each test case.", &opt_prologue, + &opt_prologue, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"logdir", OPT_LOG_DIR, "Directory for log files", &opt_logdir, + &opt_logdir, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"mark-progress", 0, + "Write line number and elapsed time to <testname>.progress.", + &opt_mark_progress, &opt_mark_progress, 0, + GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"max-connect-retries", 0, + "Maximum number of attempts to connect to server.", + &opt_max_connect_retries, &opt_max_connect_retries, 0, + GET_INT, REQUIRED_ARG, 500, 1, 10000, 0, 0, 0}, + {"max-connections", 0, + "Max number of open connections to server", + &opt_max_connections, &opt_max_connections, 0, + GET_INT, REQUIRED_ARG, DEFAULT_MAX_CONN, 8, 5120, 0, 0, 0}, + {"password", 'p', "Password to use when connecting to server.", + 0, 0, 0, GET_STR, OPT_ARG, 0, 0, 0, 0, 0, 0}, + {"protocol", OPT_MYSQL_PROTOCOL, "The protocol of connection (tcp,socket,pipe).", + 0, 0, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"port", 'P', "Port number to use for connection or 0 for default to, in " + "order of preference, my.cnf, $MYSQL_TCP_PORT, " +#if MYSQL_PORT_DEFAULT == 0 + "/etc/services, " +#endif + "built-in default (" STRINGIFY_ARG(MYSQL_PORT) ").", + &opt_port, &opt_port, 0, GET_INT, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"ps-protocol", 0, + "Use prepared-statement protocol for communication.", + &ps_protocol, &ps_protocol, 0, + GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"non-blocking-api", 0, + "Use the non-blocking client API for communication.", + &non_blocking_api_enabled, &non_blocking_api_enabled, 0, + GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"quiet", 's', "Suppress all normal output.", &silent, + &silent, 0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"record", 'r', "Record output of test_file into result file.", + 0, 0, 0, GET_NO_ARG, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"result-file", 'R', "Read/store result from/in this file.", + &result_file_name, &result_file_name, 0, + GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"result-format-version", OPT_RESULT_FORMAT_VERSION, + "Version of the result file format to use", + &opt_result_format_version, + &opt_result_format_version, 0, + GET_INT, REQUIRED_ARG, 1, 1, 2, 0, 0, 0}, + {"server-arg", 'A', "Send option value to embedded server as a parameter.", + 0, 0, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"server-file", 'F', "Read embedded server arguments from file.", + 0, 0, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"silent", 's', "Suppress all normal output. Synonym for --quiet.", + &silent, &silent, 0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"sleep", 'T', "Always sleep this many seconds on sleep commands.", + &opt_sleep, &opt_sleep, 0, GET_INT, REQUIRED_ARG, -1, -1, 0, + 0, 0, 0}, + {"socket", 'S', "The socket file to use for connection.", + &unix_sock, &unix_sock, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, + 0, 0, 0}, + {"sp-protocol", 0, "Use stored procedures for select.", + &sp_protocol, &sp_protocol, 0, + GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, +#include "sslopt-longopts.h" + {"tail-lines", 0, + "Number of lines of the result to include in a failure report.", + &opt_tail_lines, &opt_tail_lines, 0, + GET_INT, REQUIRED_ARG, 0, 0, 10000, 0, 0, 0}, + {"test-file", 'x', "Read test from/in this file (default stdin).", + 0, 0, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"timer-file", 'm', "File where the timing in microseconds is stored.", + 0, 0, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"tmpdir", 't', "Temporary directory where sockets are put.", + 0, 0, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"user", 'u', "User for login.", &opt_user, &opt_user, 0, + GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"verbose", 'v', "Write more.", &verbose, &verbose, 0, + GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"version", 'V', "Output version information and exit.", + 0, 0, 0, GET_NO_ARG, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"view-protocol", 0, "Use views for select.", + &view_protocol, &view_protocol, 0, + GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"connect_timeout", 0, + "Number of seconds before connection timeout.", + &opt_connect_timeout, &opt_connect_timeout, 0, GET_UINT, REQUIRED_ARG, + 120, 0, 3600 * 12, 0, 0, 0}, + {"wait_for_pos_timeout", 0, + "Number of seconds to wait for master_pos_wait", + &opt_wait_for_pos_timeout, &opt_wait_for_pos_timeout, 0, GET_UINT, + REQUIRED_ARG, default_wait_for_pos_timeout, 0, 3600 * 12, 0, 0, 0}, + {"plugin_dir", 0, "Directory for client-side plugins.", + &opt_plugin_dir, &opt_plugin_dir, 0, + GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"overlay-dir", 0, "Overlay directory.", &opt_overlay_dir, + &opt_overlay_dir, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"suite-dir", 0, "Suite directory.", &opt_suite_dir, + &opt_suite_dir, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + { 0, 0, 0, 0, 0, 0, GET_NO_ARG, NO_ARG, 0, 0, 0, 0, 0, 0} +}; + + +void print_version(void) +{ + printf("%s Ver %s Distrib %s, for %s (%s)\n",my_progname,MTEST_VERSION, + MYSQL_SERVER_VERSION,SYSTEM_TYPE,MACHINE_TYPE); +} + +void usage() +{ + print_version(); + puts(ORACLE_WELCOME_COPYRIGHT_NOTICE("2000")); + printf("Runs a test against the MariaDB server and compares output with a results file.\n\n"); + printf("Usage: %s [OPTIONS] [database] < test_file\n", my_progname); + print_defaults("my",load_default_groups); + puts(""); + my_print_help(my_long_options); + my_print_variables(my_long_options); +} + + +/* + Read arguments for embedded server and put them into + embedded_server_args[] +*/ + +void read_embedded_server_arguments(const char *name) +{ + char argument[1024],buff[FN_REFLEN], *str=0; + FILE *file; + + if (!test_if_hard_path(name)) + { + strxmov(buff, opt_basedir, name, NullS); + name=buff; + } + fn_format(buff, name, "", "", MY_UNPACK_FILENAME); + + if (!embedded_server_arg_count) + { + embedded_server_arg_count=1; + embedded_server_args[0]= const_cast<char*>(""); /* Progname */ + } + if (!(file=my_fopen(buff, O_RDONLY | FILE_BINARY, MYF(MY_WME)))) + die("Failed to open file '%s'", buff); + + while (embedded_server_arg_count < MAX_EMBEDDED_SERVER_ARGS && + (str=fgets(argument,sizeof(argument), file))) + { + *(strend(str)-1)=0; /* Remove end newline */ + if (!(embedded_server_args[embedded_server_arg_count]= + my_strdup(PSI_NOT_INSTRUMENTED, str, MYF(MY_WME)))) + { + my_fclose(file,MYF(0)); + die("Out of memory"); + + } + embedded_server_arg_count++; + } + my_fclose(file,MYF(0)); + if (str) + die("Too many arguments in option file: %s",name); + + return; +} + + +static my_bool +get_one_option(const struct my_option *opt, const char *argument, const char *) +{ + switch(opt->id) { + case '#': +#ifndef DBUG_OFF + DBUG_PUSH(argument ? argument : "d:t:S:i:O,/tmp/mysqltest.trace"); + debug_check_flag= 1; + debug_info_flag= 1; +#endif + break; + case 'r': + record = 1; + break; + case 'x': + { + char buff[FN_REFLEN]; + if (!test_if_hard_path(argument)) + { + strxmov(buff, opt_basedir, argument, NullS); + argument= buff; + } + fn_format(buff, argument, "", "", MY_UNPACK_FILENAME); + DBUG_ASSERT(cur_file == file_stack && cur_file->file == 0); + if (!(cur_file->file= + fopen(buff, "rb"))) + die("Could not open '%s' for reading, errno: %d", buff, errno); + cur_file->file_name= my_strdup(PSI_NOT_INSTRUMENTED, buff, MYF(MY_FAE)); + cur_file->lineno= 1; + break; + } + case 'm': + { + static char buff[FN_REFLEN]; + if (!test_if_hard_path(argument)) + { + strxmov(buff, opt_basedir, argument, NullS); + argument= buff; + } + fn_format(buff, argument, "", "", MY_UNPACK_FILENAME); + timer_file= buff; + unlink(timer_file); /* Ignore error, may not exist */ + break; + } + case 'p': + if (argument == disabled_my_option) + argument= const_cast<char*>(""); // Don't require password + if (argument) + { + /* + One should not really change the argument, but we make an + exception for passwords + */ + my_free(opt_pass); + opt_pass= my_strdup(PSI_NOT_INSTRUMENTED, argument, MYF(MY_FAE)); + while (*argument) + *(char*)argument++= 'x'; /* Destroy argument */ + tty_password= 0; + } + else + tty_password= 1; + break; +#include <sslopt-case.h> + case 't': + strnmov(TMPDIR, argument, sizeof(TMPDIR)); + break; + case 'A': + if (!embedded_server_arg_count) + { + embedded_server_arg_count=1; + embedded_server_args[0]= const_cast<char*>(""); + } + if (embedded_server_arg_count == MAX_EMBEDDED_SERVER_ARGS-1 || + !(embedded_server_args[embedded_server_arg_count++]= + my_strdup(PSI_NOT_INSTRUMENTED, argument, MYF(MY_FAE)))) + { + die("Can't use server argument"); + } + break; + case OPT_LOG_DIR: + /* Check that the file exists */ + if (access(opt_logdir, F_OK) != 0) + die("The specified log directory does not exist: '%s'", opt_logdir); + break; + case 'F': + read_embedded_server_arguments(argument); + break; + case OPT_RESULT_FORMAT_VERSION: + set_result_format_version(opt_result_format_version); + break; + case 'V': + print_version(); + exit(0); + case OPT_MYSQL_PROTOCOL: +#ifndef EMBEDDED_LIBRARY + if ((opt_protocol= find_type_with_warning(argument, &sql_protocol_typelib, + opt->name)) <= 0) + exit(1); +#endif + break; + case '?': + usage(); + exit(0); + } + return 0; +} + + +int parse_args(int argc, char **argv) +{ + load_defaults_or_exit("my", load_default_groups, &argc, &argv); + default_argv= argv; + + if ((handle_options(&argc, &argv, my_long_options, get_one_option))) + exit(1); + + if (argc > 1) + { + usage(); + exit(1); + } + if (argc == 1) + opt_db= *argv; + if (tty_password) + opt_pass= my_get_tty_password(NullS); /* purify tested */ + if (debug_info_flag) + my_end_arg= MY_CHECK_ERROR | MY_GIVE_INFO; + if (debug_check_flag) + my_end_arg|= MY_CHECK_ERROR; + + if (global_subst != NULL) + { + char *comma= strstr(global_subst, ","); + if (comma == NULL) + die("wrong --global-subst, must be X,Y"); + memcpy(global_subst_from, global_subst, (comma-global_subst)); + global_subst_from[comma-global_subst]= 0; + memcpy(global_subst_to, comma+1, strlen(comma)); + } + + if (!opt_suite_dir) + opt_suite_dir= "./"; + suite_dir_len= strlen(opt_suite_dir); + overlay_dir_len= opt_overlay_dir ? strlen(opt_overlay_dir) : 0; + + if (!record) + { + /* Check that the result file exists */ + if (result_file_name && access(result_file_name, F_OK) != 0) + die("The specified result file '%s' does not exist", + result_file_name); + } + + return 0; +} + +/* + Write the content of str into file + + SYNOPSIS + str_to_file2 + fname - name of file to truncate/create and write to + str - content to write to file + size - size of content witten to file + append - append to file instead of overwriting old file +*/ + +void str_to_file2(const char *fname, char *str, size_t size, my_bool append) +{ + int fd; + char buff[FN_REFLEN]; + int flags= O_WRONLY | O_CREAT; + if (!test_if_hard_path(fname)) + { + strxmov(buff, opt_basedir, fname, NullS); + fname= buff; + } + fn_format(buff, fname, "", "", MY_UNPACK_FILENAME); + + if (!append) + flags|= O_TRUNC; + if ((fd= my_open(buff, flags, + MYF(MY_WME | MY_FFNF))) < 0) + die("Could not open '%s' for writing, errno: %d", buff, errno); + if (append && my_seek(fd, 0, SEEK_END, MYF(0)) == MY_FILEPOS_ERROR) + die("Could not find end of file '%s', errno: %d", buff, errno); + if (my_write(fd, (uchar*)str, size, MYF(MY_WME|MY_FNABP))) + die("write failed, errno: %d", errno); + my_close(fd, MYF(0)); +} + +/* + Write the content of str into file + + SYNOPSIS + str_to_file + fname - name of file to truncate/create and write to + str - content to write to file + size - size of content witten to file +*/ + +void str_to_file(const char *fname, char *str, size_t size) +{ + str_to_file2(fname, str, size, FALSE); +} + + +void check_regerr(regex_t* r, int err) +{ + char err_buf[1024]; + + if (err) + { + regerror(err,r,err_buf,sizeof(err_buf)); + die("Regex error: %s\n", err_buf); + } +} + + +#ifdef _WIN32 + +DYNAMIC_ARRAY patterns; + +/* + init_win_path_patterns + + DESCRIPTION + Setup string patterns that will be used to detect filenames that + needs to be converted from Win to Unix format + +*/ + +void init_win_path_patterns() +{ + /* List of string patterns to match in order to find paths */ + const char* paths[] = { "$MYSQL_TEST_DIR", + "$MYSQL_TMP_DIR", + "$MYSQLTEST_VARDIR", + "$MASTER_MYSOCK", + "$MYSQL_SHAREDIR", + "$MYSQL_LIBDIR", + "./test/" }; + int num_paths= sizeof(paths)/sizeof(char*); + int i; + char* p; + + DBUG_ENTER("init_win_path_patterns"); + + my_init_dynamic_array(PSI_NOT_INSTRUMENTED, &patterns, sizeof(const char*), 16, 16, MYF(0)); + + /* Loop through all paths in the array */ + for (i= 0; i < num_paths; i++) + { + VAR* v; + if (*(paths[i]) == '$') + { + v= var_get(paths[i], 0, 0, 0); + p= my_strdup(PSI_NOT_INSTRUMENTED, v->str_val, MYF(MY_FAE)); + } + else + p= my_strdup(PSI_NOT_INSTRUMENTED, paths[i], MYF(MY_FAE)); + + /* Don't insert zero length strings in patterns array */ + if (strlen(p) == 0) + { + my_free(p); + continue; + } + + if (insert_dynamic(&patterns, &p)) + die("Out of memory"); + + DBUG_PRINT("info", ("p: %s", p)); + while (*p) + { + if (*p == '/') + *p='\\'; + p++; + } + } + DBUG_VOID_RETURN; +} + +void free_win_path_patterns() +{ + uint i= 0; + for (i=0 ; i < patterns.elements ; i++) + { + const char** pattern= dynamic_element(&patterns, i, const char**); + my_free((void *) *pattern); + } + delete_dynamic(&patterns); +} +#endif + +/* + fix_win_paths + + DESCRIPTION + Search the string 'val' for the patterns that are known to be + strings that contain filenames. Convert all \ to / in the + filenames that are found. + + Ex: + val = 'Error "c:\mysql\mysql-test\var\test\t1.frm" didn't exist' + => $MYSQL_TEST_DIR is found by strstr + => all \ from c:\mysql\m... until next space is converted into / +*/ + +void fix_win_paths(char *val, size_t len) +{ +#ifdef _WIN32 + uint i; + char *p; + + DBUG_ENTER("fix_win_paths"); + for (i= 0; i < patterns.elements; i++) + { + const char** pattern= dynamic_element(&patterns, i, const char**); + DBUG_PRINT("info", ("pattern: %s", *pattern)); + + /* Search for the path in string */ + while ((p= strstr(val, *pattern))) + { + DBUG_PRINT("info", ("Found %s in val p: %s", *pattern, p)); + + while (*p && !my_isspace(charset_info, *p)) + { + if (*p == '\\') + *p= '/'; + p++; + } + DBUG_PRINT("info", ("Converted \\ to /, p: %s", p)); + } + } + DBUG_PRINT("exit", (" val: %s, len: %zu", val, len)); + DBUG_VOID_RETURN; +#endif +} + + + +/* + Append the result for one field to the dynamic string ds +*/ + +void append_field(DYNAMIC_STRING *ds, uint col_idx, MYSQL_FIELD* field, + char* val, size_t len, my_bool is_null) +{ + char null[]= "NULL"; + + if (col_idx < max_replace_column && replace_column[col_idx]) + { + val= replace_column[col_idx]; + len= strlen(val); + } + else if (is_null) + { + val= null; + len= 4; + } +#ifdef _WIN32 + else if ((field->type == MYSQL_TYPE_DOUBLE || + field->type == MYSQL_TYPE_FLOAT ) && + field->decimals >= 31) + { + /* Convert 1.2e+018 to 1.2e+18 and 1.2e-018 to 1.2e-18 */ + char *start= strchr(val, 'e'); + if (start && strlen(start) >= 5 && + (start[1] == '-' || start[1] == '+') && start[2] == '0') + { + start+=2; /* Now points at first '0' */ + if (field->flags & ZEROFILL_FLAG) + { + /* Move all chars before the first '0' one step right */ + memmove(val + 1, val, start - val); + *val= '0'; + } + else + { + /* Move all chars after the first '0' one step left */ + memmove(start, start + 1, strlen(start)); + len--; + } + } + } +#endif + + if (!display_result_vertically) + { + if (col_idx) + dynstr_append_mem(ds, "\t", 1); + replace_dynstr_append_mem(ds, val, len); + } + else + { + dynstr_append(ds, field->name); + dynstr_append_mem(ds, "\t", 1); + replace_dynstr_append_mem(ds, val, len); + dynstr_append_mem(ds, "\n", 1); + } +} + + +/* + Append all results to the dynamic string separated with '\t' + Values may be converted with 'replace_column' +*/ + +void append_result(DYNAMIC_STRING *ds, MYSQL_RES *res) +{ + MYSQL_ROW row; + uint num_fields= mysql_num_fields(res); + MYSQL_FIELD *fields= mysql_fetch_fields(res); + ulong *lengths; + + while ((row = mysql_fetch_row(res))) + { + uint i; + lengths = mysql_fetch_lengths(res); + for (i = 0; i < num_fields; i++) + append_field(ds, i, &fields[i], + row[i], lengths[i], !row[i]); + if (!display_result_vertically) + dynstr_append_mem(ds, "\n", 1); + } +} + + +/* + Append all results from ps execution to the dynamic string separated + with '\t'. Values may be converted with 'replace_column' +*/ + +void append_stmt_result(DYNAMIC_STRING *ds, MYSQL_STMT *stmt, + MYSQL_FIELD *fields, uint num_fields) +{ + MYSQL_BIND *my_bind; + my_bool *is_null; + ulong *length; + uint i; + int error; + + /* Allocate array with bind structs, lengths and NULL flags */ + my_bind= (MYSQL_BIND*) my_malloc(PSI_NOT_INSTRUMENTED, num_fields * sizeof(MYSQL_BIND), + MYF(MY_WME|MY_FAE|MY_ZEROFILL)); + length= (ulong*) my_malloc(PSI_NOT_INSTRUMENTED, num_fields * sizeof(ulong), + MYF(MY_WME|MY_FAE)); + is_null= (my_bool*) my_malloc(PSI_NOT_INSTRUMENTED, num_fields * sizeof(my_bool), + MYF(MY_WME|MY_FAE)); + + /* Allocate data for the result of each field */ + for (i= 0; i < num_fields; i++) + { + uint max_length= fields[i].max_length + 1; + my_bind[i].buffer_type= MYSQL_TYPE_STRING; + my_bind[i].buffer= my_malloc(PSI_NOT_INSTRUMENTED, max_length, + MYF(MY_WME|MY_FAE)); + my_bind[i].buffer_length= max_length; + my_bind[i].is_null= &is_null[i]; + my_bind[i].length= &length[i]; + + DBUG_PRINT("bind", ("col[%d]: buffer_type: %d, buffer_length: %lu", + i, my_bind[i].buffer_type, my_bind[i].buffer_length)); + } + + if (mysql_stmt_bind_result(stmt, my_bind)) + die("mysql_stmt_bind_result failed: %d: %s", + mysql_stmt_errno(stmt), mysql_stmt_error(stmt)); + + while ((error=mysql_stmt_fetch(stmt)) == 0) + { + for (i= 0; i < num_fields; i++) + append_field(ds, i, &fields[i], (char*)my_bind[i].buffer, + *my_bind[i].length, *my_bind[i].is_null); + if (!display_result_vertically) + dynstr_append_mem(ds, "\n", 1); + } + + if (error != MYSQL_NO_DATA) + die("mysql_fetch didn't end with MYSQL_NO_DATA from statement: " + "error: %d", error); + if (mysql_stmt_fetch(stmt) != MYSQL_NO_DATA) + die("mysql_fetch didn't end with MYSQL_NO_DATA from statement: %d %s", + mysql_stmt_errno(stmt), mysql_stmt_error(stmt)); + + for (i= 0; i < num_fields; i++) + { + /* Free data for output */ + my_free(my_bind[i].buffer); + } + /* Free array with bind structs, lengths and NULL flags */ + my_free(my_bind); + my_free(length); + my_free(is_null); +} + + +/* + Append metadata for fields to output +*/ + +void append_metadata(DYNAMIC_STRING *ds, + MYSQL_FIELD *field, + uint num_fields) +{ + MYSQL_FIELD *field_end; + dynstr_append(ds,"Catalog\tDatabase\tTable\tTable_alias\tColumn\t" + "Column_alias\tType\tLength\tMax length\tIs_null\t" + "Flags\tDecimals\tCharsetnr\n"); + + for (field_end= field+num_fields ; + field < field_end ; + field++) + { + dynstr_append_mem(ds, field->catalog, + field->catalog_length); + dynstr_append_mem(ds, "\t", 1); + dynstr_append_mem(ds, field->db, field->db_length); + dynstr_append_mem(ds, "\t", 1); + dynstr_append_mem(ds, field->org_table, + field->org_table_length); + dynstr_append_mem(ds, "\t", 1); + dynstr_append_mem(ds, field->table, + field->table_length); + dynstr_append_mem(ds, "\t", 1); + dynstr_append_mem(ds, field->org_name, + field->org_name_length); + dynstr_append_mem(ds, "\t", 1); + dynstr_append_mem(ds, field->name, field->name_length); + dynstr_append_mem(ds, "\t", 1); + replace_dynstr_append_uint(ds, field->type); + + Client_field_metadata metadata(field); + BinaryStringBuffer<128> data_type_metadata_str; + metadata.print_data_type_related_attributes(&data_type_metadata_str); + if (data_type_metadata_str.length()) + { + dynstr_append_mem(ds, " (", 2); + dynstr_append_mem(ds, data_type_metadata_str.ptr(), + data_type_metadata_str.length()); + dynstr_append_mem(ds, ")", 1); + } + dynstr_append_mem(ds, "\t", 1); + replace_dynstr_append_uint(ds, field->length); + dynstr_append_mem(ds, "\t", 1); + replace_dynstr_append_uint(ds, field->max_length); + dynstr_append_mem(ds, "\t", 1); + dynstr_append_mem(ds, (IS_NOT_NULL(field->flags) ? "N" : "Y"), 1); + dynstr_append_mem(ds, "\t", 1); + replace_dynstr_append_uint(ds, field->flags); + dynstr_append_mem(ds, "\t", 1); + replace_dynstr_append_uint(ds, field->decimals); + dynstr_append_mem(ds, "\t", 1); + replace_dynstr_append_uint(ds, field->charsetnr); + dynstr_append_mem(ds, "\n", 1); + } +} + + +/* + Append affected row count and other info to output +*/ + +void append_info(DYNAMIC_STRING *ds, ulonglong affected_rows, + const char *info) +{ + char buf[40], buff2[21]; + sprintf(buf,"affected rows: %s\n", llstr(affected_rows, buff2)); + dynstr_append(ds, buf); + if (info) + { + dynstr_append(ds, "info: "); + dynstr_append(ds, info); + dynstr_append_mem(ds, "\n", 1); + } +} + + +#ifndef EMBEDDED_LIBRARY +static const char *trking_info_desc[SESSION_TRACK_END + 1]= +{ + "Tracker : SESSION_TRACK_SYSTEM_VARIABLES\n", + "Tracker : SESSION_TRACK_SCHEMA\n", + "Tracker : SESSION_TRACK_STATE_CHANGE\n", + "Tracker : SESSION_TRACK_GTIDS\n", + "Tracker : SESSION_TRACK_TRANSACTION_CHARACTERISTICS\n", + "Tracker : SESSION_TRACK_TRANSACTION_TYPE\n" +#ifdef USER_VAR_TRACKING + , + "Tracker : SESSION_TRACK_MYSQL_RESERVED1\n", + "Tracker : SESSION_TRACK_MYSQL_RESERVED2\n", + "Tracker : SESSION_TRACK_MYSQL_RESERVED3\n", + "Tracker : SESSION_TRACK_MYSQL_RESERVED4\n", + "Tracker : SESSION_TRACK_MYSQL_RESERVED5\n", + "Tracker : SESSION_TRACK_MYSQL_RESERVED6\n", + "Tracker : SESSION_TRACK_USER_VARIABLES\n" +#endif // USER_VAR_TRACKING +}; +#endif // EMBEDDED_LIBRARY + +/** + @brief Append state change information (received through Ok packet) to the output. + + @param [in,out] ds Dynamic string to hold the content to be printed. + @param [in] mysql Connection handle. +*/ + +static void append_session_track_info(DYNAMIC_STRING *ds, MYSQL *mysql) +{ +#ifndef EMBEDDED_LIBRARY + for (unsigned int type= SESSION_TRACK_BEGIN; type <= SESSION_TRACK_END; type++) + { + const char *data; + size_t data_length; + + if (!mysql_session_track_get_first(mysql, + (enum_session_state_type) type, + &data, &data_length)) + { + dynstr_append(ds, "-- "); + if (type <= SESSION_TRACK_END) + { + dynstr_append(ds, trking_info_desc[type]); + } + else + { + DBUG_ASSERT(0); + dynstr_append(ds, "Tracker???\n"); + } + + dynstr_append(ds, "-- "); + dynstr_append_mem(ds, data, data_length); + } + else + continue; + while (!mysql_session_track_get_next(mysql, + (enum_session_state_type) type, + &data, &data_length)) + { + dynstr_append(ds, "\n-- "); + if (data == NULL) + { + DBUG_ASSERT(data_length == 0); + dynstr_append_mem(ds, "<NULL>", sizeof("<NULL>") - 1); + } + else + dynstr_append_mem(ds, data, data_length); + } + dynstr_append(ds, "\n\n"); + } +#endif /* EMBEDDED_LIBRARY */ +} + + +/* + Display the table headings with the names tab separated +*/ + +void append_table_headings(DYNAMIC_STRING *ds, + MYSQL_FIELD *field, + uint num_fields) +{ + uint col_idx; + if (disable_column_names) + return; + for (col_idx= 0; col_idx < num_fields; col_idx++) + { + if (col_idx) + dynstr_append_mem(ds, "\t", 1); + replace_dynstr_append(ds, field[col_idx].name); + } + dynstr_append_mem(ds, "\n", 1); +} + +/* + Fetch warnings from server and append to ds + + RETURN VALUE + Number of warnings appended to ds +*/ + +int append_warnings(DYNAMIC_STRING *ds, MYSQL* mysql) +{ + uint count; + MYSQL_RES *warn_res; + DYNAMIC_STRING res; + DBUG_ENTER("append_warnings"); + + if (!(count= mysql_warning_count(mysql))) + DBUG_RETURN(0); + DBUG_PRINT("info", ("Warnings: %ud", count)); + + /* + If one day we will support execution of multi-statements + through PS API we should not issue SHOW WARNINGS until + we have not read all results... + */ + DBUG_ASSERT(!mysql_more_results(mysql)); + + if (mysql_real_query(mysql, "SHOW WARNINGS", 13)) + die("Error running query \"SHOW WARNINGS\": %s", mysql_error(mysql)); + + if (!(warn_res= mysql_store_result(mysql))) + die("Warning count is %u but didn't get any warnings", + count); + + init_dynamic_string(&res, "", 1024, 1024); + + append_result(&res, warn_res); + mysql_free_result(warn_res); + + DBUG_PRINT("warnings", ("%s", res.str)); + + if (display_result_sorted) + dynstr_append_sorted(ds, &res, 0); + else + dynstr_append_mem(ds, res.str, res.length); + dynstr_free(&res); + DBUG_RETURN(count); +} + + +/* + Handle situation where query is sent but there is no active connection + (e.g directly after disconnect). + + We emulate MySQL-compatible behaviour of sending something on a closed + connection. +*/ +static void handle_no_active_connection(struct st_command *command, + struct st_connection *cn, DYNAMIC_STRING *ds) +{ + handle_error(command, 2006, "MariaDB server has gone away", "000000", ds); + cn->pending= FALSE; + var_set_errno(2006); +} + +/* handler functions to execute prepared statement calls in client C API */ +void run_prepare_stmt(struct st_connection *cn, struct st_command *command, const char *query, + size_t query_len, DYNAMIC_STRING *ds, DYNAMIC_STRING *ds_warnings); +void run_bind_stmt(struct st_connection *cn, struct st_command *command, const char *query, + size_t query_len, DYNAMIC_STRING *ds, DYNAMIC_STRING *ds_warnings); +void run_execute_stmt(struct st_connection *cn, struct st_command *command, const char *query, + size_t query_len, DYNAMIC_STRING *ds, DYNAMIC_STRING *ds_warnings); +void run_close_stmt(struct st_connection *cn, struct st_command *command, const char *query, + size_t query_len, DYNAMIC_STRING *ds, DYNAMIC_STRING *ds_warnings); + +/* + Run query using MySQL C API + + SYNOPSIS + run_query_normal() + mysql mysql handle + command current command pointer + flags flags indicating if we should SEND and/or REAP + query query string to execute + query_len length query string to execute + ds output buffer where to store result form query +*/ + +void run_query_normal(struct st_connection *cn, struct st_command *command, + int flags, const char *query, size_t query_len, + DYNAMIC_STRING *ds, DYNAMIC_STRING *ds_warnings) +{ + MYSQL_RES *res= 0; + MYSQL *mysql= cn->mysql; + int err= 0, counter= 0; + DBUG_ENTER("run_query_normal"); + DBUG_PRINT("enter",("flags: %d", flags)); + DBUG_PRINT("enter", ("query: '%-.60s'", query)); + + if (!mysql) + { + handle_no_active_connection(command, cn, ds); + DBUG_VOID_RETURN; + } + + /* handle prepared statement commands */ + switch (command->type) { + case Q_PS_PREPARE: + run_prepare_stmt(cn, command, query, query_len, ds, ds_warnings); + flags &= ~QUERY_SEND_FLAG; + goto end; + break; + case Q_PS_BIND: + run_bind_stmt(cn, command, query, query_len, ds, ds_warnings); + flags &= ~QUERY_SEND_FLAG; + goto end; + break; + case Q_PS_EXECUTE: + run_execute_stmt(cn, command, query, query_len, ds, ds_warnings); + flags &= ~QUERY_SEND_FLAG; + goto end; + break; + case Q_PS_CLOSE: + run_close_stmt(cn, command, query, query_len, ds, ds_warnings); + flags &= ~QUERY_SEND_FLAG; + goto end; + break; + default: /* not a prepared statement command */ + break; + } + + if (flags & QUERY_SEND_FLAG) + { + /* + Send the query + */ + if (do_send_query(cn, query, query_len)) + { + handle_error(command, mysql_errno(mysql), mysql_error(mysql), + mysql_sqlstate(mysql), ds); + goto end; + } + } + if (!(flags & QUERY_REAP_FLAG)) + { + cn->pending= TRUE; + DBUG_VOID_RETURN; + } + + do + { + /* + When on first result set, call mysql_read_query_result to retrieve + answer to the query sent earlier + */ + if ((counter==0) && do_read_query_result(cn)) + { + /* we've failed to collect the result set */ + cn->pending= TRUE; + handle_error(command, mysql_errno(mysql), mysql_error(mysql), + mysql_sqlstate(mysql), ds); + goto end; + + } + + /* + Store the result of the query if it will return any fields + */ + if (mysql_field_count(mysql) && ((res= mysql_store_result(mysql)) == 0)) + { + handle_error(command, mysql_errno(mysql), mysql_error(mysql), + mysql_sqlstate(mysql), ds); + goto end; + } + + if (!disable_result_log) + { + if (res) + { + MYSQL_FIELD *fields= mysql_fetch_fields(res); + uint num_fields= mysql_num_fields(res); + + if (display_metadata) + append_metadata(ds, fields, num_fields); + + if (!display_result_vertically) + append_table_headings(ds, fields, num_fields); + + append_result(ds, res); + } + + /* + Need to call mysql_affected_rows() before the "new" + query to find the warnings. + */ + if (!disable_info) + append_info(ds, mysql_affected_rows(mysql), mysql_info(mysql)); + + if (display_session_track_info) + append_session_track_info(ds, mysql); + + /* + Add all warnings to the result. We can't do this if we are in + the middle of processing results from multi-statement, because + this will break protocol. + */ + if (!disable_warnings && !mysql_more_results(mysql)) + { + if (append_warnings(ds_warnings, mysql) || ds_warnings->length) + { + dynstr_append_mem(ds, "Warnings:\n", 10); + dynstr_append_mem(ds, ds_warnings->str, ds_warnings->length); + } + } + } + + if (res) + { + mysql_free_result(res); + res= 0; + } + counter++; + } while (!(err= mysql_next_result(mysql))); + if (err > 0) + { + /* We got an error from mysql_next_result, maybe expected */ + handle_error(command, mysql_errno(mysql), mysql_error(mysql), + mysql_sqlstate(mysql), ds); + goto end; + } + DBUG_ASSERT(err == -1); /* Successful and there are no more results */ + + /* If we come here the query is both executed and read successfully */ + handle_no_error(command); + display_optimizer_trace(cn, ds); + revert_properties(); + +end: + + cn->pending= FALSE; + /* + We save the return code (mysql_errno(mysql)) from the last call sent + to the server into the mysqltest builtin variable $mysql_errno. This + variable then can be used from the test case itself. + */ + var_set_errno(mysql_errno(mysql)); + DBUG_VOID_RETURN; +} + + +/* + Check whether given error is in list of expected errors + + SYNOPSIS + match_expected_error() + + PARAMETERS + command the current command (and its expect-list) + err_errno error number of the error that actually occurred + err_sqlstate SQL-state that was thrown, or NULL for impossible + (file-ops, diff, etc.) + + RETURNS + -1 for not in list, index in list of expected errors otherwise + + NOTE + If caller needs to know whether the list was empty, they should + check command->expected_errors.count. +*/ + +static int match_expected_error(struct st_command *command, + unsigned int err_errno, + const char *err_sqlstate) +{ + uint i; + + for (i= 0 ; (uint) i < command->expected_errors.count ; i++) + { + if ((command->expected_errors.err[i].type == ERR_ERRNO) && + (command->expected_errors.err[i].code.errnum == err_errno)) + return i; + + if (command->expected_errors.err[i].type == ERR_SQLSTATE) + { + /* + NULL is quite likely, but not in conjunction with a SQL-state expect! + */ + if (unlikely(err_sqlstate == NULL)) + die("expecting a SQL-state (%s) from query '%s' which cannot " + "produce one...", + command->expected_errors.err[i].code.sqlstate, command->query); + + if (strncmp(command->expected_errors.err[i].code.sqlstate, + err_sqlstate, SQLSTATE_LENGTH) == 0) + return i; + } + } + return -1; +} + + +/* + Handle errors which occurred during execution + + SYNOPSIS + handle_error() + q - query context + err_errno - error number + err_error - error message + err_sqlstate - sql state + ds - dynamic string which is used for output buffer + + NOTE + If there is an unexpected error this function will abort mysqltest + immediately. +*/ + +void handle_error(struct st_command *command, + unsigned int err_errno, const char *err_error, + const char *err_sqlstate, DYNAMIC_STRING *ds) +{ + int i; + DBUG_ENTER("handle_error"); + + var_set_int("$errno", err_errno); + + command->used_replace= 1; + if (command->require_file) + { + /* + The query after a "--require" failed. This is fine as long the server + returned a valid response. Don't allow 2013 or 2006 to trigger an + abort_not_supported_test + */ + if (err_errno == CR_SERVER_LOST || + err_errno == CR_SERVER_GONE_ERROR) + die("require query '%s' failed: %s (%d): %s", command->query, + get_errname_from_code(err_errno), + err_errno, err_error); + + /* Abort the run of this test, pass the failed query as reason */ + abort_not_supported_test("Query '%s' failed, required functionality " \ + "not supported", command->query); + } + + if (command->abort_on_error) + { + report_or_die("query '%s' failed: %s (%d): %s", command->query, + get_errname_from_code(err_errno), + err_errno, + err_error); + DBUG_VOID_RETURN; + } + + DBUG_PRINT("info", ("expected_errors.count: %d", + command->expected_errors.count)); + + i= match_expected_error(command, err_errno, err_sqlstate); + + if (i >= 0) + { + if (!disable_result_log) + { + if (command->expected_errors.count == 1) + { + /* Only log error if there is one possible error */ + dynstr_append_mem(ds, "ERROR ", 6); + replace_dynstr_append(ds, err_sqlstate); + dynstr_append_mem(ds, ": ", 2); + replace_dynstr_append(ds, err_error); + dynstr_append_mem(ds,"\n",1); + } + /* Don't log error if we may not get an error */ + else if (command->expected_errors.err[0].type == ERR_SQLSTATE || + (command->expected_errors.err[0].type == ERR_ERRNO && + command->expected_errors.err[0].code.errnum != 0)) + dynstr_append(ds,"Got one of the listed errors\n"); + } + /* OK */ + revert_properties(); + DBUG_VOID_RETURN; + } + + DBUG_PRINT("info",("i: %d expected_errors: %d", i, + command->expected_errors.count)); + + if (!disable_result_log) + { + dynstr_append_mem(ds, "ERROR ",6); + replace_dynstr_append(ds, err_sqlstate); + dynstr_append_mem(ds, ": ", 2); + replace_dynstr_append(ds, err_error); + dynstr_append_mem(ds, "\n", 1); + } + + if (command->expected_errors.count > 0) + { + if (command->expected_errors.err[0].type == ERR_ERRNO) + report_or_die("query '%s' failed with wrong errno %s (%d): '%s', " + "instead of %s (%d)...", + command->query, + get_errname_from_code(err_errno), + err_errno, err_error, + get_errname_from_code(command->expected_errors.err[0].code.errnum), + command->expected_errors.err[0].code.errnum); + else + report_or_die("query '%s' failed with wrong sqlstate %s: '%s', " + "instead of %s...", + command->query, err_sqlstate, err_error, + command->expected_errors.err[0].code.sqlstate); + } + + revert_properties(); + DBUG_VOID_RETURN; +} + + +/* + Handle absence of errors after execution + + SYNOPSIS + handle_no_error() + q - context of query + + RETURN VALUE + error - function will not return +*/ + +void handle_no_error(struct st_command *command) +{ + DBUG_ENTER("handle_no_error"); + + if (command->expected_errors.err[0].type == ERR_ERRNO && + command->expected_errors.err[0].code.errnum != 0) + { + /* Error code we wanted was != 0, i.e. not an expected success */ + report_or_die("query '%s' succeeded - should have failed with error " + "%s (%d)...", + command->query, + get_errname_from_code(command->expected_errors.err[0].code.errnum), + command->expected_errors.err[0].code.errnum); + } + else if (command->expected_errors.err[0].type == ERR_SQLSTATE && + strcmp(command->expected_errors.err[0].code.sqlstate,"00000") != 0) + { + /* SQLSTATE we wanted was != "00000", i.e. not an expected success */ + report_or_die("query '%s' succeeded - should have failed with " + "sqlstate %s...", + command->query, + command->expected_errors.err[0].code.sqlstate); + } + DBUG_VOID_RETURN; +} + + +/* + Run query using prepared statement C API + + SYNOPSIS + run_query_stmt + mysql - mysql handle + command - current command pointer + query - query string to execute + query_len - length query string to execute + ds - output buffer where to store result form query + + RETURN VALUE + error - function will not return +*/ + +void run_query_stmt(struct st_connection *cn, struct st_command *command, + char *query, size_t query_len, DYNAMIC_STRING *ds, + DYNAMIC_STRING *ds_warnings) +{ + my_bool ignore_second_execution= 0; + MYSQL_RES *res= NULL; /* Note that here 'res' is meta data result set */ + MYSQL *mysql= cn->mysql; + MYSQL_STMT *stmt; + DYNAMIC_STRING ds_prepare_warnings; + DYNAMIC_STRING ds_execute_warnings; + DBUG_ENTER("run_query_stmt"); + DBUG_PRINT("query", ("'%-.60s'", query)); + DBUG_PRINT("info", + ("disable_warnings: %d prepare_warnings_enabled: %d", + (int) disable_warnings, (int) prepare_warnings_enabled)); + + if (!mysql) + { + handle_no_active_connection(command, cn, ds); + DBUG_VOID_RETURN; + } + + /* + Init a new stmt if it's not already one created for this connection + */ + if(!(stmt= cn->stmt)) + { + if (!(stmt= mysql_stmt_init(mysql))) + die("unable to init stmt structure"); + cn->stmt= stmt; + } + + /* Init dynamic strings for warnings */ + if (!disable_warnings) + { + init_dynamic_string(&ds_prepare_warnings, NULL, 0, 256); + init_dynamic_string(&ds_execute_warnings, NULL, 0, 256); + } + + /* + Prepare the query + */ + if (do_stmt_prepare(cn, query, query_len)) + { + handle_error(command, mysql_stmt_errno(stmt), + mysql_stmt_error(stmt), mysql_stmt_sqlstate(stmt), ds); + goto end; + } + + /* + Get the warnings from mysql_stmt_prepare and keep them in a + separate string + */ + if (!disable_warnings && prepare_warnings_enabled) + append_warnings(&ds_prepare_warnings, mysql); + + /* + No need to call mysql_stmt_bind_param() because we have no + parameter markers. + */ + +#if MYSQL_VERSION_ID >= 50000 + if (cursor_protocol_enabled) + { + /* + Use cursor when retrieving result + */ + ulong type= CURSOR_TYPE_READ_ONLY; + if (mysql_stmt_attr_set(stmt, STMT_ATTR_CURSOR_TYPE, (void*) &type)) + die("mysql_stmt_attr_set(STMT_ATTR_CURSOR_TYPE) failed': %d %s", + mysql_stmt_errno(stmt), mysql_stmt_error(stmt)); + } +#endif + + /* + Execute the query first time if second execution enable + */ + if(ps2_protocol_enabled && match_re(&ps2_re, query)) + { + if (do_stmt_execute(cn)) + { + handle_error(command, mysql_stmt_errno(stmt), + mysql_stmt_error(stmt), mysql_stmt_sqlstate(stmt), ds); + goto end; + } + /* + We cannot run query twice if we get prepare warnings as these will otherwise be + disabled + */ + ignore_second_execution= (prepare_warnings_enabled && + mysql_warning_count(mysql) != 0); + } + + /* + Execute the query + */ + if (!ignore_second_execution && do_stmt_execute(cn)) + { + handle_error(command, mysql_stmt_errno(stmt), + mysql_stmt_error(stmt), mysql_stmt_sqlstate(stmt), ds); + goto end; + } + + int err; + do + { + /* + When running in cursor_protocol get the warnings from execute here + and keep them in a separate string for later. + */ + if (cursor_protocol_enabled && !disable_warnings) + append_warnings(&ds_execute_warnings, mysql); + + /* + We instruct that we want to update the "max_length" field in + mysql_stmt_store_result(), this is our only way to know how much + buffer to allocate for result data + */ + { + my_bool one= 1; + if (mysql_stmt_attr_set(stmt, STMT_ATTR_UPDATE_MAX_LENGTH, (void*) &one)) + die("mysql_stmt_attr_set(STMT_ATTR_UPDATE_MAX_LENGTH) failed': %d %s", + mysql_stmt_errno(stmt), mysql_stmt_error(stmt)); + } + + /* + If we got here the statement succeeded and was expected to do so, + get data. Note that this can still give errors found during execution! + Store the result of the query if if will return any fields + */ + if (mysql_stmt_field_count(stmt) && mysql_stmt_store_result(stmt)) + { + handle_error(command, mysql_stmt_errno(stmt), + mysql_stmt_error(stmt), mysql_stmt_sqlstate(stmt), ds); + goto end; + } + + if (!disable_result_log) + { + /* + Not all statements creates a result set. If there is one we can + now create another normal result set that contains the meta + data. This set can be handled almost like any other non prepared + statement result set. + */ + if ((res= mysql_stmt_result_metadata(stmt)) != NULL) + { + /* Take the column count from meta info */ + MYSQL_FIELD *fields= mysql_fetch_fields(res); + uint num_fields= mysql_num_fields(res); + + if (display_metadata) + append_metadata(ds, fields, num_fields); + + if (!display_result_vertically) + append_table_headings(ds, fields, num_fields); + + append_stmt_result(ds, stmt, fields, num_fields); + + mysql_free_result(res); /* Free normal result set with meta data */ + + /* + Normally, if there is a result set, we do not show warnings from the + prepare phase. This is because some warnings are generated both during + prepare and execute; this would generate different warning output + between normal and ps-protocol test runs. + + The --enable_prepare_warnings command can be used to change this so + that warnings from both the prepare and execute phase are shown. + */ + if (!disable_warnings && !prepare_warnings_enabled) + { + DBUG_PRINT("info", ("warnings disabled")); + dynstr_set(&ds_prepare_warnings, NULL); + } + } + else + { + /* + This is a query without resultset + */ + } + + /* + Fetch info before fetching warnings, since it will be reset + otherwise. + */ + if (!disable_info) + append_info(ds, mysql_stmt_affected_rows(stmt), mysql_info(mysql)); + + if (display_session_track_info) + append_session_track_info(ds, mysql); + + + if (!disable_warnings && !mysql_more_results(stmt->mysql)) + { + /* Get the warnings from execute */ + + /* Append warnings to ds - if there are any */ + if (append_warnings(&ds_execute_warnings, mysql) || + ds_execute_warnings.length || + ds_prepare_warnings.length || + ds_warnings->length) + { + dynstr_append_mem(ds, "Warnings:\n", 10); + if (ds_warnings->length) + dynstr_append_mem(ds, ds_warnings->str, + ds_warnings->length); + if (ds_prepare_warnings.length) + dynstr_append_mem(ds, ds_prepare_warnings.str, + ds_prepare_warnings.length); + if (ds_execute_warnings.length) + dynstr_append_mem(ds, ds_execute_warnings.str, + ds_execute_warnings.length); + } + } + } + } while ( !(err= mysql_stmt_next_result(stmt))); + + if (err > 0) + /* We got an error from mysql_next_result, maybe expected */ + handle_error(command, mysql_errno(mysql), mysql_error(mysql), + mysql_sqlstate(mysql), ds); + else + handle_no_error(command); +end: + if (!disable_warnings) + { + dynstr_free(&ds_prepare_warnings); + dynstr_free(&ds_execute_warnings); + } + + /* + We save the return code (mysql_stmt_errno(stmt)) from the last call sent + to the server into the mysqltest builtin variable $mysql_errno. This + variable then can be used from the test case itself. + */ + + var_set_errno(mysql_stmt_errno(stmt)); + + revert_properties(); + + /* Close the statement if reconnect, need new prepare */ + { +#ifndef EMBEDDED_LIBRARY + my_bool reconnect; + mysql_get_option(mysql, MYSQL_OPT_RECONNECT, &reconnect); + if (reconnect) +#else + if (mysql->reconnect) +#endif + { + mysql_stmt_close(stmt); + cn->stmt= NULL; + } + } + + + DBUG_VOID_RETURN; +} + +/* + prepare query using prepared statement C API + + SYNPOSIS + run_prepare_stmt + mysql - mysql handle + command - current command pointer + query - query string to execute + query_len - length query string to execute + ds - output buffer where to store result form query + + RETURN VALUE + error - function will not return +*/ + +void run_prepare_stmt(struct st_connection *cn, struct st_command *command, + const char *query, size_t query_len, DYNAMIC_STRING *ds, + DYNAMIC_STRING *ds_warnings) +{ + + MYSQL *mysql= cn->mysql; + MYSQL_STMT *stmt; + DYNAMIC_STRING ds_prepare_warnings; + DBUG_ENTER("run_prepare_stmt"); + DBUG_PRINT("query", ("'%-.60s'", query)); + + /* + Init a new stmt if it's not already one created for this connection + */ + if(!(stmt= cn->stmt)) + { + if (!(stmt= mysql_stmt_init(mysql))) + die("unable to init stmt structure"); + cn->stmt= stmt; + } + + /* Init dynamic strings for warnings */ + if (!disable_warnings) + { + init_dynamic_string(&ds_prepare_warnings, NULL, 0, 256); + } + + /* + Prepare the query + */ + char* PS_query= command->first_argument; + size_t PS_query_len= command->end - command->first_argument; + if (do_stmt_prepare(cn, PS_query, PS_query_len)) + { + handle_error(command, mysql_stmt_errno(stmt), + mysql_stmt_error(stmt), mysql_stmt_sqlstate(stmt), ds); + goto end; + } + + /* + Get the warnings from mysql_stmt_prepare and keep them in a + separate string + */ + if (!disable_warnings) + { + append_warnings(&ds_prepare_warnings, mysql); + dynstr_free(&ds_prepare_warnings); + } + end: + DBUG_VOID_RETURN; +} + +/* + bind parameters for a prepared statement C API + + SYNPOSIS + run_bind_stmt + mysql - mysql handle + command - current command pointer + query - query string to execute + query_len - length query string to execute + ds - output buffer where to store result form query + + RETURN VALUE + error - function will not return +*/ + +void run_bind_stmt(struct st_connection *cn, struct st_command *command, + const char *query, size_t query_len, DYNAMIC_STRING *ds, + DYNAMIC_STRING *ds_warnings + ) +{ + MYSQL_STMT *stmt= cn->stmt; + DBUG_ENTER("run_bind_stmt"); + DBUG_PRINT("query", ("'%-.60s'", query)); + MYSQL_BIND *ps_params= cn->ps_params; + if (ps_params) + { + for (size_t i=0; i<stmt->param_count; i++) + { + my_free(ps_params[i].buffer); + ps_params[i].buffer= NULL; + } + my_free(ps_params); + ps_params= NULL; + } + + /* Init PS-parameters. */ + cn->ps_params= ps_params = (MYSQL_BIND*)my_malloc(PSI_NOT_INSTRUMENTED, + sizeof(MYSQL_BIND) * + stmt->param_count, + MYF(MY_WME|MY_FAE)); + bzero((char *) ps_params, sizeof(MYSQL_BIND) * stmt->param_count); + + int i=0; + char *c; + long *l; + double *d; + + char *p= strtok((char*)command->first_argument, " "); + while (p != nullptr) + { + (void)strtol(p, &c, 10); + if (!*c) + { + ps_params[i].buffer_type= MYSQL_TYPE_LONG; + l= (long*)my_malloc(PSI_NOT_INSTRUMENTED, sizeof(long), + MYF(MY_WME|MY_FAE)); + *l= strtol(p, &c, 10); + ps_params[i].buffer= (void*)l; + ps_params[i].buffer_length= 8; + } + else + { + (void)strtod(p, &c); + if (!*c) + { + ps_params[i].buffer_type= MYSQL_TYPE_DECIMAL; + d= (double*)my_malloc(PSI_NOT_INSTRUMENTED, sizeof(double), + MYF(MY_WME|MY_FAE)); + *d= strtod(p, &c); + ps_params[i].buffer= (void*)d; + ps_params[i].buffer_length= 8; + } + else + { + ps_params[i].buffer_type= MYSQL_TYPE_STRING; + ps_params[i].buffer= my_strdup(PSI_NOT_INSTRUMENTED, p, + MYF(MY_WME|MY_FAE)); + ps_params[i].buffer_length= (unsigned long)strlen(p); + } + } + + p= strtok(nullptr, " "); + i++; + } + + int rc= mysql_stmt_bind_param(stmt, ps_params); + if (rc) + { + die("mysql_stmt_bind_param() failed': %d %s", + mysql_stmt_errno(stmt), mysql_stmt_error(stmt)); + } + + DBUG_VOID_RETURN; +} + +/* + execute query using prepared statement C API + + SYNPOSIS + run_axecute_stmt + mysql - mysql handle + command - current command pointer + query - query string to execute + query_len - length query string to execute + ds - output buffer where to store result form query + + RETURN VALUE + error - function will not return +*/ + +void run_execute_stmt(struct st_connection *cn, struct st_command *command, + const char *query, size_t query_len, DYNAMIC_STRING *ds, + DYNAMIC_STRING *ds_warnings) +{ + MYSQL_RES *res= NULL; /* Note that here 'res' is meta data result set */ + MYSQL *mysql= cn->mysql; + MYSQL_STMT *stmt= cn->stmt; + DYNAMIC_STRING ds_execute_warnings; + DBUG_ENTER("run_execute_stmt"); + DBUG_PRINT("query", ("'%-.60s'", query)); + + /* Init dynamic strings for warnings */ + if (!disable_warnings) + { + init_dynamic_string(&ds_execute_warnings, NULL, 0, 256); + } + +#if MYSQL_VERSION_ID >= 50000 + if (cursor_protocol_enabled) + { + /* + Use cursor when retrieving result + */ + ulong type= CURSOR_TYPE_READ_ONLY; + if (mysql_stmt_attr_set(stmt, STMT_ATTR_CURSOR_TYPE, (void*) &type)) + die("mysql_stmt_attr_set(STMT_ATTR_CURSOR_TYPE) failed': %d %s", + mysql_stmt_errno(stmt), mysql_stmt_error(stmt)); + } +#endif + + /* + Execute the query + */ + if (do_stmt_execute(cn)) + { + handle_error(command, mysql_stmt_errno(stmt), + mysql_stmt_error(stmt), mysql_stmt_sqlstate(stmt), ds); + goto end; + } + + /* + When running in cursor_protocol get the warnings from execute here + and keep them in a separate string for later. + */ + if (cursor_protocol_enabled && !disable_warnings) + append_warnings(&ds_execute_warnings, mysql); + + /* + We instruct that we want to update the "max_length" field in + mysql_stmt_store_result(), this is our only way to know how much + buffer to allocate for result data + */ + { + my_bool one= 1; + if (mysql_stmt_attr_set(stmt, STMT_ATTR_UPDATE_MAX_LENGTH, (void*) &one)) + die("mysql_stmt_attr_set(STMT_ATTR_UPDATE_MAX_LENGTH) failed': %d %s", + mysql_stmt_errno(stmt), mysql_stmt_error(stmt)); + } + + /* + If we got here the statement succeeded and was expected to do so, + get data. Note that this can still give errors found during execution! + Store the result of the query if if will return any fields + */ + if (mysql_stmt_field_count(stmt) && mysql_stmt_store_result(stmt)) + { + handle_error(command, mysql_stmt_errno(stmt), + mysql_stmt_error(stmt), mysql_stmt_sqlstate(stmt), ds); + goto end; + } + + /* If we got here the statement was both executed and read successfully */ + handle_no_error(command); + if (!disable_result_log) + { + /* + Not all statements creates a result set. If there is one we can + now create another normal result set that contains the meta + data. This set can be handled almost like any other non prepared + statement result set. + */ + if ((res= mysql_stmt_result_metadata(stmt)) != NULL) + { + /* Take the column count from meta info */ + MYSQL_FIELD *fields= mysql_fetch_fields(res); + uint num_fields= mysql_num_fields(res); + + if (display_metadata) + append_metadata(ds, fields, num_fields); + + if (!display_result_vertically) + append_table_headings(ds, fields, num_fields); + + append_stmt_result(ds, stmt, fields, num_fields); + + mysql_free_result(res); /* Free normal result set with meta data */ + + /* + Normally, if there is a result set, we do not show warnings from the + prepare phase. This is because some warnings are generated both during + prepare and execute; this would generate different warning output + between normal and ps-protocol test runs. + + The --enable_prepare_warnings command can be used to change this so + that warnings from both the prepare and execute phase are shown. + */ + } + else + { + /* + This is a query without resultset + */ + } + + /* + Fetch info before fetching warnings, since it will be reset + otherwise. + */ + if (!disable_info) + append_info(ds, mysql_stmt_affected_rows(stmt), mysql_info(mysql)); + + if (display_session_track_info) + append_session_track_info(ds, mysql); + + + if (!disable_warnings) + { + /* Get the warnings from execute */ + + /* Append warnings to ds - if there are any */ + if (append_warnings(&ds_execute_warnings, mysql) || + ds_execute_warnings.length || + ds_warnings->length) + { + dynstr_append_mem(ds, "Warnings:\n", 10); + if (ds_warnings->length) + dynstr_append_mem(ds, ds_warnings->str, + ds_warnings->length); + if (ds_execute_warnings.length) + dynstr_append_mem(ds, ds_execute_warnings.str, + ds_execute_warnings.length); + } + } + } + +end: + if (!disable_warnings) + { + dynstr_free(&ds_execute_warnings); + } + + /* + We save the return code (mysql_stmt_errno(stmt)) from the last call sent + to the server into the mysqltest builtin variable $mysql_errno. This + variable then can be used from the test case itself. + */ + + var_set_errno(mysql_stmt_errno(stmt)); + + revert_properties(); + + /* Close the statement if reconnect, need new prepare */ + { +#ifndef EMBEDDED_LIBRARY + my_bool reconnect; + mysql_get_option(mysql, MYSQL_OPT_RECONNECT, &reconnect); + if (reconnect) +#else + if (mysql->reconnect) +#endif + { + if (cn->ps_params) + { + for (size_t i=0; i<stmt->param_count; i++) + { + my_free(cn->ps_params[i].buffer); + cn->ps_params[i].buffer= NULL; + } + my_free(cn->ps_params); + } + mysql_stmt_close(stmt); + cn->stmt= NULL; + cn->ps_params= NULL; + } + } + DBUG_VOID_RETURN; +} + +/* + close a prepared statement C API + + SYNPOSIS + run_close_stmt + mysql - mysql handle + command - current command pointer + query - query string to execute + query_len - length query string to execute + ds - output buffer where to store result form query + + RETURN VALUE + error - function will not return +*/ + +void run_close_stmt(struct st_connection *cn, struct st_command *command, + const char *query, size_t query_len, DYNAMIC_STRING *ds, + DYNAMIC_STRING *ds_warnings + ) +{ + MYSQL_STMT *stmt= cn->stmt; + DBUG_ENTER("run_close_stmt"); + DBUG_PRINT("query", ("'%-.60s'", query)); + + if (cn->ps_params) + { + + for (size_t i=0; i<stmt->param_count; i++) + { + my_free(cn->ps_params[i].buffer); + cn->ps_params[i].buffer= NULL; + } + my_free(cn->ps_params); + } + + /* Close the statement */ + if (stmt) + { + mysql_stmt_close(stmt); + cn->stmt= NULL; + } + cn->ps_params= NULL; + + DBUG_VOID_RETURN; +} + + + +/* + Create a util connection if one does not already exists + and use that to run the query + This is done to avoid implicit commit when creating/dropping objects such + as view, sp etc. +*/ + +int util_query(MYSQL* org_mysql, const char* query){ + + MYSQL* mysql; + DBUG_ENTER("util_query"); + + if (service_connection_enabled) + { + if(!(mysql= cur_con->util_mysql)) + { + DBUG_PRINT("info", ("Creating util_mysql")); + if (!(mysql= mysql_init(mysql))) + die("Failed in mysql_init()"); + + if (opt_connect_timeout) + mysql_options(mysql, MYSQL_OPT_CONNECT_TIMEOUT, + (void *) &opt_connect_timeout); + + /* enable local infile, in non-binary builds often disabled by default */ + mysql_options(mysql, MYSQL_OPT_LOCAL_INFILE, 0); + mysql_options(mysql, MYSQL_OPT_NONBLOCK, 0); + safe_connect(mysql, "util", org_mysql->host, org_mysql->user, + org_mysql->passwd, org_mysql->db, org_mysql->port, + org_mysql->unix_socket); + + cur_con->util_mysql= mysql; + if (mysql->charset != org_mysql->charset) + mysql_set_character_set(mysql, org_mysql->charset-> + IF_EMBEDDED(cs_name.str, csname)); + } + } + else + mysql= org_mysql; + + int ret= mysql_query(mysql, query); + DBUG_RETURN(ret); +} + + + +/* + Run query + + SYNOPSIS + run_query() + mysql mysql handle + command current command pointer + + flags control the phased/stages of query execution to be performed + if QUERY_SEND_FLAG bit is on, the query will be sent. If QUERY_REAP_FLAG + is on the result will be read - for regular query, both bits must be on +*/ + +void run_query(struct st_connection *cn, struct st_command *command, int flags) +{ + MYSQL *mysql= cn->mysql; + DYNAMIC_STRING *ds; + DYNAMIC_STRING *save_ds= NULL; + DYNAMIC_STRING ds_result; + DYNAMIC_STRING ds_sorted; + DYNAMIC_STRING ds_warnings; + char *query; + size_t query_len; + my_bool view_created= 0, sp_created= 0; + my_bool complete_query= ((flags & QUERY_SEND_FLAG) && + (flags & QUERY_REAP_FLAG)); + DBUG_ENTER("run_query"); + + if (cn->pending && (flags & QUERY_SEND_FLAG)) + die("Cannot run query on connection between send and reap"); + + if (!(flags & QUERY_SEND_FLAG) && !cn->pending) + die("Cannot reap on a connection without pending send"); + + init_dynamic_string(&ds_warnings, NULL, 0, 256); + ds_warn= &ds_warnings; + + /* + Evaluate query if this is an eval command + */ + if (command->type == Q_EVAL || command->type == Q_SEND_EVAL || + command->type == Q_EVALP) + { + if (!command->eval_query.str) + init_dynamic_string(&command->eval_query, "", command->query_len + 256, + 1024); + else + dynstr_set(&command->eval_query, 0); + do_eval(&command->eval_query, command->query, command->end, FALSE); + query= command->eval_query.str; + query_len= command->eval_query.length; + } + else + { + query = command->query; + query_len = strlen(query); + } + + /* + When command->require_file is set the output of _this_ query + should be compared with an already existing file + Create a temporary dynamic string to contain the output from + this query. + */ + if (command->require_file) + { + init_dynamic_string(&ds_result, "", 1024, 1024); + ds= &ds_result; + } + else + ds= &ds_res; + + /* + Log the query into the output buffer + */ + if (!disable_query_log && (flags & QUERY_SEND_FLAG)) + { + char *print_query= query; + size_t print_len= query_len; + if (flags & QUERY_PRINT_ORIGINAL_FLAG) + { + print_query= command->query; + print_len= (int)(command->end - command->query); + } + replace_dynstr_append_mem(ds, print_query, print_len); + dynstr_append_mem(ds, delimiter, delimiter_length); + dynstr_append_mem(ds, "\n", 1); + } + + /* We're done with this flag */ + flags &= ~QUERY_PRINT_ORIGINAL_FLAG; + + /* + Write the command to the result file before we execute the query + This is needed to be able to analyse the log if something goes + wrong + */ + log_file.write(&ds_res); + log_file.flush(); + dynstr_set(&ds_res, 0); + + if (view_protocol_enabled && mysql && + complete_query && + match_re(&view_re, query)) + { + /* + Create the query as a view. + Use replace since view can exist from a failed mysqltest run + */ + DYNAMIC_STRING query_str; + init_dynamic_string(&query_str, + "CREATE OR REPLACE VIEW mysqltest_tmp_v AS ", + query_len+64, 256); + dynstr_append_mem(&query_str, query, query_len); + if (util_query(mysql, query_str.str)) + { + /* + Failed to create the view, this is not fatal + just run the query the normal way + */ + DBUG_PRINT("view_create_error", + ("Failed to create view '%s': %d: %s", query_str.str, + mysql_errno(mysql), mysql_error(mysql))); + + /* Log error to create view */ + verbose_msg("Failed to create view '%s' %d: %s", query_str.str, + mysql_errno(mysql), mysql_error(mysql)); + } + else + { + /* + Yes, it was possible to create this query as a view + */ + view_created= 1; + query= const_cast<char*>("SELECT * FROM mysqltest_tmp_v"); + query_len = strlen(query); + + /* + Collect warnings from create of the view that should otherwise + have been produced when the SELECT was executed + */ + append_warnings(&ds_warnings, + service_connection_enabled ? + cur_con->util_mysql : + mysql); + } + + dynstr_free(&query_str); + } + + if (sp_protocol_enabled && mysql && + complete_query && + match_re(&sp_re, query)) + { + /* + Create the query as a stored procedure + Drop first since sp can exist from a failed mysqltest run + */ + DYNAMIC_STRING query_str; + init_dynamic_string(&query_str, + "DROP PROCEDURE IF EXISTS mysqltest_tmp_sp;", + query_len+64, 256); + util_query(mysql, query_str.str); + dynstr_set(&query_str, "CREATE PROCEDURE mysqltest_tmp_sp()\n"); + dynstr_append_mem(&query_str, query, query_len); + if (util_query(mysql, query_str.str)) + { + /* + Failed to create the stored procedure for this query, + this is not fatal just run the query the normal way + */ + DBUG_PRINT("sp_create_error", + ("Failed to create sp '%s': %d: %s", query_str.str, + mysql_errno(mysql), mysql_error(mysql))); + + /* Log error to create sp */ + verbose_msg("Failed to create sp '%s' %d: %s", query_str.str, + mysql_errno(mysql), mysql_error(mysql)); + + } + else + { + sp_created= 1; + + query= const_cast<char*>("CALL mysqltest_tmp_sp()"); + query_len = strlen(query); + } + dynstr_free(&query_str); + } + + if (display_result_sorted) + { + /* + Collect the query output in a separate string + that can be sorted before it's added to the + global result string + */ + init_dynamic_string(&ds_sorted, "", 1024, 1024); + save_ds= ds; /* Remember original ds */ + ds= &ds_sorted; + } + + /* + Find out how to run this query + + Always run with normal C API if it's not a complete + SEND + REAP + + If it is a '?' in the query it may be a SQL level prepared + statement already and we can't do it twice + */ + if (ps_protocol_enabled && + complete_query && + /* + Check that a statement is not one of PREPARE FROM, EXECUTE, + DEALLOCATE PREPARE (possibly prefixed with the 'SET STATEMENT ... FOR' + clause. These statement shouldn't be run using prepared statement C API. + All other statements can be run using prepared statement C API. + */ + !match_re(&ps_re, query)) + run_query_stmt(cn, command, query, query_len, ds, &ds_warnings); + else + run_query_normal(cn, command, flags, query, query_len, + ds, &ds_warnings); + + dynstr_free(&ds_warnings); + ds_warn= 0; + + if (display_result_sorted) + { + /* Sort the result set and append it to result */ + dynstr_append_sorted(save_ds, &ds_sorted, 1); + ds= save_ds; + dynstr_free(&ds_sorted); + } + + if (sp_created) + { + if (util_query(mysql, "DROP PROCEDURE mysqltest_tmp_sp ")) + report_or_die("Failed to drop sp: %d: %s", mysql_errno(mysql), + mysql_error(mysql)); + } + + if (view_created) + { + if (util_query(mysql, "DROP VIEW mysqltest_tmp_v ")) + report_or_die("Failed to drop view: %d: %s", + mysql_errno(mysql), mysql_error(mysql)); + } + + if (command->require_file) + { + /* A result file was specified for _this_ query + and the output should be checked against an already + existing file which has been specified using --require or --result + */ + check_require(ds, command->require_file); + } + + if (ds == &ds_result) + dynstr_free(&ds_result); + DBUG_VOID_RETURN; +} + +/****************************************************************************/ +/* + Functions to detect different SQL statements +*/ + +char *re_eprint(int err) +{ + static char epbuf[100]; + size_t len __attribute__((unused))= + regerror(err, (regex_t *)NULL, epbuf, sizeof(epbuf)); + assert(len <= sizeof(epbuf)); + return(epbuf); +} + +void init_re_comp(regex_t *re, const char* str) +{ + int err= regcomp(re, str, (REG_EXTENDED | REG_ICASE | REG_NOSUB | REG_DOTALL)); + if (err) + { + char erbuf[100]; + size_t len= regerror(err, re, erbuf, sizeof(erbuf)); + die("error %s, %d/%d `%s'\n", + re_eprint(err), (int)len, (int)sizeof(erbuf), erbuf); + } +} + +void init_re(void) +{ + /* + * Prior to the task MDEV-16708 a value of the string ps_re_str contained + * a regular expression to match statements that SHOULD BE run in PS mode. + * The task MDEV-16708 modifies interpretation of this regular expression + * and now it is used for matching statements that SHOULDN'T be run in + * PS mode. These statement are PREPARE FROM, EXECUTE, DEALLOCATE PREPARE + * possibly prefixed with the clause SET STATEMENT ... FOR + */ + const char *ps_re_str = + "^(" + "[[:space:]]*PREPARE[[:space:]]|" + "[[:space:]]*EXECUTE[[:space:]]|" + "[[:space:]]*DEALLOCATE[[:space:]]+PREPARE[[:space:]]|" + "[[:space:]]*DROP[[:space:]]+PREPARE[[:space:]]|" + "(SET[[:space:]]+STATEMENT[[:space:]]+.+[[:space:]]+FOR[[:space:]]+)?" + "EXECUTE[[:space:]]+|" + "(SET[[:space:]]+STATEMENT[[:space:]]+.+[[:space:]]+FOR[[:space:]]+)?" + "PREPARE[[:space:]]+" + ")"; + + /* + Filter for queries that can be run using the + Stored procedures + */ + const char *sp_re_str = + "^(" + "[[:space:]]*ALTER[[:space:]]+SEQUENCE[[:space:]]|" + "[[:space:]]*ALTER[[:space:]]+TABLE[[:space:]]|" + "[[:space:]]*ALTER[[:space:]]+USER[[:space:]]|" + "[[:space:]]*ANALYZE[[:space:]]|" + "[[:space:]]*ASSIGN[[:space:]]|" + //"[[:space:]]*CALL[[:space:]]|" // XXX run_query_stmt doesn't read multiple result sets + "[[:space:]]*CHANGE[[:space:]]|" + "[[:space:]]*CHECKSUM[[:space:]]|" + "[[:space:]]*COMMIT[[:space:]]|" + "[[:space:]]*COMPOUND[[:space:]]|" + "[[:space:]]*CREATE[[:space:]]+DATABASE[[:space:]]|" + "[[:space:]]*CREATE[[:space:]]+INDEX[[:space:]]|" + "[[:space:]]*CREATE[[:space:]]+ROLE[[:space:]]|" + "[[:space:]]*CREATE[[:space:]]+SEQUENCE[[:space:]]|" + "[[:space:]]*CREATE[[:space:]]+TABLE[[:space:]]|" + "[[:space:]]*CREATE[[:space:]]+USER[[:space:]]|" + "[[:space:]]*CREATE[[:space:]]+VIEW[[:space:]]|" + "[[:space:]]*DELETE[[:space:]]|" + "[[:space:]]*DO[[:space:]]|" + "[[:space:]]*DROP[[:space:]]+DATABASE[[:space:]]|" + "[[:space:]]*DROP[[:space:]]+INDEX[[:space:]]|" + "[[:space:]]*DROP[[:space:]]+ROLE[[:space:]]|" + "[[:space:]]*DROP[[:space:]]+SEQUENCE[[:space:]]|" + "[[:space:]]*DROP[[:space:]]+TABLE[[:space:]]|" + "[[:space:]]*DROP[[:space:]]+USER[[:space:]]|" + "[[:space:]]*DROP[[:space:]]+VIEW[[:space:]]|" + "[[:space:]]*FLUSH[[:space:]]|" + "[[:space:]]*GRANT[[:space:]]|" + "[[:space:]]*HANDLER[[:space:]]+.*[[:space:]]+READ[[:space:]]|" + "[[:space:]]*INSERT[[:space:]]|" + "[[:space:]]*INSTALL[[:space:]]+|" + "[[:space:]]*KILL[[:space:]]|" + "[[:space:]]*OPTIMIZE[[:space:]]|" + "[[:space:]]*PRELOAD[[:space:]]|" + "[[:space:]]*RENAME[[:space:]]+TABLE[[:space:]]|" + "[[:space:]]*RENAME[[:space:]]+USER[[:space:]]|" + "[[:space:]]*REPAIR[[:space:]]|" + "[[:space:]]*REPLACE[[:space:]]|" + "[[:space:]]*RESET[[:space:]]|" + "[[:space:]]*REVOKE[[:space:]]|" + "[[:space:]]*ROLLBACK[[:space:]]|" + "[[:space:]]*SELECT[[:space:]]|" + "[[:space:]]*SET[[:space:]]+OPTION[[:space:]]|" + "[[:space:]]*SHOW[[:space:]]|" + "[[:space:]]*SHUTDOWN[[:space:]]|" + "[[:space:]]*SLAVE[[:space:]]|" + "[[:space:]]*TRUNCATE[[:space:]]|" + "[[:space:]]*UNINSTALL[[:space:]]+|" + "[[:space:]]*UPDATE[[:space:]]" + ")"; + /* + Filter for queries that can be run for second + execution of prepare statement + */ + const char *ps2_re_str = + "^(" + "[[:space:]]*SELECT[[:space:]])"; + + /* + Filter for queries that can be run as views + */ + const char *view_re_str = + "^(" + "[[:space:]]*SELECT[[:space:]])"; + + init_re_comp(&ps_re, ps_re_str); + init_re_comp(&ps2_re, ps2_re_str); + init_re_comp(&sp_re, sp_re_str); + init_re_comp(&view_re, view_re_str); +} + + +int match_re(regex_t *re, char *str) +{ + while (my_isspace(charset_info, *str)) + str++; + if (str[0] == '/' && str[1] == '*') + { + char *comm_end= strstr (str, "*/"); + if (! comm_end) + die("Statement is unterminated comment"); + str= comm_end + 2; + } + + int err= regexec(re, str, (size_t)0, NULL, 0); + + if (err == 0) + return 1; + else if (err == REG_NOMATCH) + return 0; + + { + char erbuf[100]; + size_t len= regerror(err, re, erbuf, sizeof(erbuf)); + die("error %s, %d/%d `%s'\n", + re_eprint(err), (int)len, (int)sizeof(erbuf), erbuf); + } + return 0; +} + +void free_re(void) +{ + regfree(&ps_re); + regfree(&ps2_re); + regfree(&sp_re); + regfree(&view_re); +} + +/****************************************************************************/ + +void get_command_type(struct st_command* command) +{ + char save; + uint type; + DBUG_ENTER("get_command_type"); + + if (*command->query == '}') + { + command->type = Q_END_BLOCK; + DBUG_VOID_RETURN; + } + + save= command->query[command->first_word_len]; + command->query[command->first_word_len]= 0; + type= find_type(command->query, &command_typelib, FIND_TYPE_NO_PREFIX); + command->query[command->first_word_len]= save; + if (type > 0) + { + command->type=(enum enum_commands) type; /* Found command */ + + /* + Look for case where "query" was explicitly specified to + force command being sent to server + */ + if (type == Q_QUERY) + { + /* Skip the "query" part */ + command->query= command->first_argument; + } + } + else + { + /* No mysqltest command matched */ + + if (command->type != Q_COMMENT_WITH_COMMAND) + { + /* A query that will sent to mysqld */ + command->type= Q_QUERY; + } + else + { + /* -- "comment" that didn't contain a mysqltest command */ + report_or_die("Found line beginning with -- that didn't contain " \ + "a valid mysqltest command, check your syntax or " \ + "use # if you intended to write a comment"); + command->type= Q_COMMENT; + } + } + + /* Set expected error on command */ + memcpy(&command->expected_errors, &saved_expected_errors, + sizeof(saved_expected_errors)); + DBUG_PRINT("info", ("There are %d expected errors", + command->expected_errors.count)); + DBUG_VOID_RETURN; +} + + + +/* + Record how many milliseconds it took to execute the test file + up until the current line and write it to .progress file + +*/ + +void mark_progress(struct st_command* command __attribute__((unused)), + int line) +{ + static ulonglong progress_start= 0; // < Beware + DYNAMIC_STRING ds_progress; + + char buf[32], *end; + ulonglong timer= timer_now(); + if (!progress_start) + progress_start= timer; + timer-= progress_start; + + if (init_dynamic_string(&ds_progress, "", 256, 256)) + die("Out of memory"); + + /* Milliseconds since start */ + end= longlong10_to_str(timer, buf, 10); + dynstr_append_mem(&ds_progress, buf, (int)(end-buf)); + dynstr_append_mem(&ds_progress, "\t", 1); + + /* Parser line number */ + end= int10_to_str(line, buf, 10); + dynstr_append_mem(&ds_progress, buf, (int)(end-buf)); + dynstr_append_mem(&ds_progress, "\t", 1); + + /* Filename */ + dynstr_append(&ds_progress, cur_file->file_name); + dynstr_append_mem(&ds_progress, ":", 1); + + /* Line in file */ + end= int10_to_str(cur_file->lineno, buf, 10); + dynstr_append_mem(&ds_progress, buf, (int)(end-buf)); + + + dynstr_append_mem(&ds_progress, "\n", 1); + + progress_file.write(&ds_progress); + + dynstr_free(&ds_progress); + +} + +#ifdef HAVE_STACKTRACE + +static void dump_backtrace(void) +{ + struct st_connection *conn= cur_con; + + fprintf(stderr, "read_command_buf (%p): ", read_command_buf); + fprintf(stderr, "%.*s\n", (int)read_command_buflen, read_command_buf); + fputc('\n', stderr); + + if (conn) + { + fprintf(stderr, "conn->name (%p): ", conn->name); + my_safe_print_str(conn->name, conn->name_len); + fputc('\n', stderr); +#ifdef EMBEDDED_LIBRARY + fprintf(stderr, "conn->cur_query (%p): ", conn->cur_query); + my_safe_print_str(conn->cur_query, conn->cur_query_len); + fputc('\n', stderr); +#endif + } + fputs("Attempting backtrace...\n", stderr); + my_print_stacktrace(NULL, (ulong)my_thread_stack_size, 0); +} + +#else + +static void dump_backtrace(void) +{ + fputs("Backtrace not available.\n", stderr); +} + +#endif + +static sig_handler signal_handler(int sig) +{ + fprintf(stderr, "mysqltest got " SIGNAL_FMT "\n", sig); + dump_backtrace(); + + fprintf(stderr, "Writing a core file...\n"); + fflush(stderr); + my_write_core(sig); +#ifndef _WIN32 + exit(1); // Shouldn't get here but just in case +#endif +} + +#ifdef _WIN32 + +LONG WINAPI exception_filter(EXCEPTION_POINTERS *exp) +{ + __try + { + my_set_exception_pointers(exp); + signal_handler(exp->ExceptionRecord->ExceptionCode); + } + __except(EXCEPTION_EXECUTE_HANDLER) + { + fputs("Got exception in exception handler!\n", stderr); + } + + return EXCEPTION_CONTINUE_SEARCH; +} + + +static void init_signal_handling(void) +{ + UINT mode; + + /* Set output destination of messages to the standard error stream. */ + _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE); + _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR); + _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE); + _CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR); + _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE); + _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR); + + /* Do not not display the a error message box. */ + mode= SetErrorMode(0) | SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX; + SetErrorMode(mode); + + SetUnhandledExceptionFilter(exception_filter); +} + +#else /* _WIN32 */ + +static void init_signal_handling(void) +{ + struct sigaction sa; + DBUG_ENTER("init_signal_handling"); + + sa.sa_flags = SA_RESETHAND | SA_NODEFER; + sigemptyset(&sa.sa_mask); + sigprocmask(SIG_SETMASK, &sa.sa_mask, NULL); + + sa.sa_handler= signal_handler; + + sigaction(SIGSEGV, &sa, NULL); + sigaction(SIGABRT, &sa, NULL); +#ifdef SIGBUS + sigaction(SIGBUS, &sa, NULL); +#endif + sigaction(SIGILL, &sa, NULL); + sigaction(SIGFPE, &sa, NULL); + DBUG_VOID_RETURN; +} + +#endif /* !_WIN32 */ + +int main(int argc, char **argv) +{ + struct st_command *command; + my_bool q_send_flag= 0, abort_flag= 0; + uint command_executed= 0, last_command_executed= 0; + char save_file[FN_REFLEN]; + bool empty_result= FALSE; + MY_INIT(argv[0]); + DBUG_ENTER("main"); + + /* mysqltest has no way to free all its memory correctly */ + sf_leaking_memory= 1; + + save_file[0]= 0; + TMPDIR[0]= 0; + + init_signal_handling(); + + /* Init expected errors */ + memset(&saved_expected_errors, 0, sizeof(saved_expected_errors)); + +#ifdef EMBEDDED_LIBRARY + /* set appropriate stack for the 'query' threads */ + (void) pthread_attr_init(&cn_thd_attrib); + pthread_attr_setstacksize(&cn_thd_attrib, DEFAULT_THREAD_STACK); +#endif /*EMBEDDED_LIBRARY*/ + + /* Init file stack */ + memset(file_stack, 0, sizeof(file_stack)); + file_stack_end= + file_stack + (sizeof(file_stack)/sizeof(struct st_test_file)) - 1; + cur_file= file_stack; + + /* Init block stack */ + memset(block_stack, 0, sizeof(block_stack)); + block_stack_end= + block_stack + (sizeof(block_stack)/sizeof(struct st_block)) - 1; + cur_block= block_stack; + cur_block->ok= TRUE; /* Outer block should always be executed */ + cur_block->cmd= cmd_none; + + my_init_dynamic_array(PSI_NOT_INSTRUMENTED, &q_lines, sizeof(struct st_command*), 1024, 1024, MYF(0)); + + if (my_hash_init2(PSI_NOT_INSTRUMENTED, &var_hash, 64, charset_info, 128, 0, + 0, get_var_key, 0, var_free, MYF(0))) + die("Variable hash initialization failed"); + + { + char path_separator[]= { FN_LIBCHAR, 0 }; + var_set_string("SYSTEM_PATH_SEPARATOR", path_separator); + } + var_set_string("MYSQL_SERVER_VERSION", MYSQL_SERVER_VERSION); + var_set_string("MYSQL_SYSTEM_TYPE", SYSTEM_TYPE); + var_set_string("MYSQL_MACHINE_TYPE", MACHINE_TYPE); + if (sizeof(void *) == 8) { + var_set_string("MYSQL_SYSTEM_ARCHITECTURE", "64"); + } else { + var_set_string("MYSQL_SYSTEM_ARCHITECTURE", "32"); + } + + memset(&master_pos, 0, sizeof(master_pos)); + + parser.current_line= parser.read_lines= 0; + memset(&var_reg, 0, sizeof(var_reg)); + + init_builtin_echo(); +#ifdef _WIN32 + is_windows= 1; + init_win_path_patterns(); +#endif + + read_command_buf= (char*)my_malloc(PSI_NOT_INSTRUMENTED, read_command_buflen= 65536, MYF(MY_FAE)); + + init_dynamic_string(&ds_res, "", 2048, 2048); + init_alloc_root(PSI_NOT_INSTRUMENTED, &require_file_root, 1024, 1024, MYF(0)); + + parse_args(argc, argv); + + log_file.open(opt_logdir, result_file_name, ".log"); + verbose_msg("Logging to '%s'.", log_file.file_name()); + if (opt_mark_progress) + { + progress_file.open(opt_logdir, result_file_name, ".progress"); + verbose_msg("Tracing progress in '%s'.", progress_file.file_name()); + } + + /* Init connections, allocate 1 extra as buffer + 1 for default */ + connections= (struct st_connection*) + my_malloc(PSI_NOT_INSTRUMENTED, (opt_max_connections+2) * sizeof(struct st_connection), + MYF(MY_WME|MY_FAE|MY_ZEROFILL)); + connections_end= connections + opt_max_connections +1; + next_con= connections + 1; + + var_set_int("$PS_PROTOCOL", ps_protocol); + var_set_int("$NON_BLOCKING_API", non_blocking_api_enabled); + var_set_int("$SP_PROTOCOL", sp_protocol); + var_set_int("$VIEW_PROTOCOL", view_protocol); + var_set_int("$CURSOR_PROTOCOL", cursor_protocol); + + var_set_int("$ENABLED_QUERY_LOG", 1); + var_set_int("$ENABLED_ABORT_ON_ERROR", 1); + var_set_int("$ENABLED_RESULT_LOG", 1); + var_set_int("$ENABLED_CONNECT_LOG", 0); + var_set_int("$ENABLED_WARNINGS", 1); + var_set_int("$ENABLED_INFO", 0); + var_set_int("$ENABLED_METADATA", 0); + + DBUG_PRINT("info",("result_file: '%s'", + result_file_name ? result_file_name : "")); + verbose_msg("Results saved in '%s'.", + result_file_name ? result_file_name : ""); + if (mysql_server_init(embedded_server_arg_count, + embedded_server_args, + (char**) embedded_server_groups)) + die("Can't initialize MariaDB server"); + server_initialized= 1; + if (cur_file == file_stack && cur_file->file == 0) + { + cur_file->file= stdin; + cur_file->file_name= my_strdup(PSI_NOT_INSTRUMENTED, "<stdin>", MYF(MY_WME)); + cur_file->lineno= 1; + } + var_set_string("MYSQLTEST_FILE", cur_file->file_name); + init_re(); + + /* Cursor protocol implies ps protocol */ + if (cursor_protocol) + ps_protocol= 1; + + /* Enable second execution of SELECT for ps-protocol + if ps-protocol is used */ + ps2_protocol_enabled= ps_protocol; + ps_protocol_enabled= ps_protocol; + sp_protocol_enabled= sp_protocol; + view_protocol_enabled= view_protocol; + cursor_protocol_enabled= cursor_protocol; + + st_connection *con= connections; + init_connection_thd(con); + if (! (con->mysql= mysql_init(0))) + die("Failed in mysql_init()"); + if (opt_connect_timeout) + mysql_options(con->mysql, MYSQL_OPT_CONNECT_TIMEOUT, + (void *) &opt_connect_timeout); + if (opt_compress) + mysql_options(con->mysql,MYSQL_OPT_COMPRESS,NullS); + mysql_options(con->mysql, MYSQL_SET_CHARSET_NAME, + charset_info->cs_name.str); + if (opt_charsets_dir) + mysql_options(con->mysql, MYSQL_SET_CHARSET_DIR, + opt_charsets_dir); + + if (opt_protocol) + mysql_options(con->mysql,MYSQL_OPT_PROTOCOL,(char*)&opt_protocol); + + if (opt_plugin_dir && *opt_plugin_dir) + mysql_options(con->mysql, MYSQL_PLUGIN_DIR, opt_plugin_dir); + +#if defined(HAVE_OPENSSL) && !defined(EMBEDDED_LIBRARY) + + if (opt_use_ssl) + { + mysql_ssl_set(con->mysql, opt_ssl_key, opt_ssl_cert, opt_ssl_ca, + opt_ssl_capath, opt_ssl_cipher); + mysql_options(con->mysql, MYSQL_OPT_SSL_CRL, opt_ssl_crl); + mysql_options(con->mysql, MYSQL_OPT_SSL_CRLPATH, opt_ssl_crlpath); + mysql_options(con->mysql, MARIADB_OPT_TLS_VERSION, opt_tls_version); + mysql_options(con->mysql, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, + &opt_ssl_verify_server_cert); + } +#endif + + if (!(con->name = my_strdup(PSI_NOT_INSTRUMENTED, "default", MYF(MY_WME)))) + die("Out of memory"); + mysql_options(con->mysql, MYSQL_OPT_NONBLOCK, 0); + + safe_connect(con->mysql, con->name, opt_host, opt_user, opt_pass, + opt_db, opt_port, unix_sock); + + /* Use all time until exit if no explicit 'start_timer' */ + timer_start= timer_now(); + + /* + Initialize $mysql_errno with -1, so we can + - distinguish it from valid values ( >= 0 ) and + - detect if there was never a command sent to the server + */ + var_set_errno(-1); + + set_current_connection(con); + + if (opt_prologue) + { + open_file(opt_prologue); + } + + verbose_msg("Start processing test commands from '%s' ...", cur_file->file_name); + while (!abort_flag && !read_command(&command)) + { + my_bool ok_to_do; + int current_line_inc = 1, processed = 0; + if (command->type == Q_UNKNOWN || command->type == Q_COMMENT_WITH_COMMAND) + get_command_type(command); + + if (parsing_disabled && + command->type != Q_ENABLE_PARSING && + command->type != Q_DISABLE_PARSING) + { + /* Parsing is disabled, silently convert this line to a comment */ + command->type= Q_COMMENT; + } + + /* (Re-)set abort_on_error for this command */ + command->abort_on_error= (command->expected_errors.count == 0 && + abort_on_error); + + /* + some commands need to be executed or at least parsed unconditionally, + because they change the grammar. + */ + ok_to_do= cur_block->ok || command->type == Q_DELIMITER + || command->type == Q_PERL; + /* + Some commands need to be "done" the first time if they may get + re-iterated over in a true context. This can only happen if there's + a while loop at some level above the current block. + */ + if (!ok_to_do) + { + if (command->type == Q_SOURCE || + command->type == Q_ERROR || + command->type == Q_WRITE_FILE || + command->type == Q_APPEND_FILE) + { + for (struct st_block *stb= cur_block-1; stb >= block_stack; stb--) + { + if (stb->cmd == cmd_while) + { + ok_to_do= 1; + break; + } + } + } + } + + if (ok_to_do) + { + command->last_argument= command->first_argument; + processed = 1; + /* Need to remember this for handle_error() */ + curr_command= command; + switch (command->type) { + case Q_CONNECT: + do_connect(command); + break; + case Q_CONNECTION: select_connection(command); break; + case Q_DISCONNECT: + case Q_DIRTY_CLOSE: + do_close_connection(command); break; + case Q_ENABLE_PREPARE_WARNINGS: prepare_warnings_enabled=1; break; + case Q_DISABLE_PREPARE_WARNINGS: prepare_warnings_enabled=0; break; + case Q_ENABLE_QUERY_LOG: + set_property(command, P_QUERY, 0); + break; + case Q_DISABLE_QUERY_LOG: + set_property(command, P_QUERY, 1); + break; + case Q_ENABLE_ABORT_ON_ERROR: + set_property(command, P_ABORT, 1); + break; + case Q_DISABLE_ABORT_ON_ERROR: + set_property(command, P_ABORT, 0); + break; + case Q_ENABLE_RESULT_LOG: + set_property(command, P_RESULT, 0); + break; + case Q_DISABLE_RESULT_LOG: + set_property(command, P_RESULT, 1); + break; + case Q_ENABLE_CONNECT_LOG: + set_property(command, P_CONNECT, 0); + break; + case Q_DISABLE_CONNECT_LOG: + set_property(command, P_CONNECT, 1); + break; + case Q_ENABLE_WARNINGS: + set_property(command, P_WARN, 0); + break; + case Q_DISABLE_WARNINGS: + set_property(command, P_WARN, 1); + break; + case Q_ENABLE_INFO: + set_property(command, P_INFO, 0); + break; + case Q_DISABLE_INFO: + set_property(command, P_INFO, 1); + break; + case Q_ENABLE_SESSION_TRACK_INFO: + set_property(command, P_SESSION_TRACK, 1); + break; + case Q_DISABLE_SESSION_TRACK_INFO: + set_property(command, P_SESSION_TRACK, 0); + break; + case Q_ENABLE_METADATA: + set_property(command, P_META, 1); + break; + case Q_DISABLE_METADATA: + set_property(command, P_META, 0); + break; + case Q_ENABLE_COLUMN_NAMES: + disable_column_names= 0; + var_set_int("$ENABLED_COLUMN_NAMES", 0); + break; + case Q_DISABLE_COLUMN_NAMES: + disable_column_names= 1; + var_set_int("$ENABLED_COLUMN_NAMES", 1); + break; + case Q_SOURCE: do_source(command); break; + case Q_SLEEP: do_sleep(command, 0); break; + case Q_REAL_SLEEP: do_sleep(command, 1); break; + case Q_WAIT_FOR_SLAVE_TO_STOP: do_wait_for_slave_to_stop(command); break; + case Q_INC: do_modify_var(command, DO_INC); break; + case Q_DEC: do_modify_var(command, DO_DEC); break; + case Q_ECHO: do_echo(command); command_executed++; break; + case Q_SYSTEM: do_system(command); break; + case Q_REMOVE_FILE: do_remove_file(command); break; + case Q_REMOVE_FILES_WILDCARD: do_remove_files_wildcard(command); break; + case Q_MKDIR: do_mkdir(command); break; + case Q_RMDIR: do_rmdir(command); break; + case Q_LIST_FILES: do_list_files(command); break; + case Q_LIST_FILES_WRITE_FILE: + do_list_files_write_file_command(command, FALSE); + break; + case Q_LIST_FILES_APPEND_FILE: + do_list_files_write_file_command(command, TRUE); + break; + case Q_FILE_EXIST: do_file_exist(command); break; + case Q_WRITE_FILE: do_write_file(command); break; + case Q_APPEND_FILE: do_append_file(command); break; + case Q_DIFF_FILES: do_diff_files(command); break; + case Q_SEND_QUIT: do_send_quit(command); break; + case Q_CHANGE_USER: do_change_user(command); break; + case Q_CAT_FILE: do_cat_file(command); break; + case Q_COPY_FILE: do_copy_file(command); break; + case Q_MOVE_FILE: do_move_file(command); break; + case Q_CHMOD_FILE: do_chmod_file(command); break; + case Q_PERL: do_perl(command); break; + case Q_RESULT_FORMAT_VERSION: do_result_format_version(command); break; + case Q_DELIMITER: + do_delimiter(command); + break; + case Q_DISPLAY_VERTICAL_RESULTS: + display_result_vertically= TRUE; + break; + case Q_DISPLAY_HORIZONTAL_RESULTS: + display_result_vertically= FALSE; + break; + case Q_SORTED_RESULT: + /* + Turn on sorting of result set, will be reset after next + command + */ + display_result_sorted= TRUE; + break; + case Q_LOWERCASE: + /* + Turn on lowercasing of result, will be reset after next + command + */ + display_result_lower= TRUE; + break; + case Q_LET: do_let(command); break; + case Q_EVAL_RESULT: + die("'eval_result' command is deprecated"); + break; // never called but keep compiler calm + case Q_EVAL: + case Q_EVALP: + case Q_QUERY_VERTICAL: + case Q_QUERY_HORIZONTAL: + if (command->query == command->query_buf) + { + /* Skip the first part of command, i.e query_xxx */ + command->query= command->first_argument; + command->first_word_len= 0; + } + /* fall through */ + case Q_QUERY: + case Q_REAP: + case Q_PS_PREPARE: + case Q_PS_BIND: + case Q_PS_EXECUTE: + case Q_PS_CLOSE: + { + my_bool old_display_result_vertically= display_result_vertically; + /* Default is full query, both reap and send */ + int flags= QUERY_REAP_FLAG | QUERY_SEND_FLAG; + + if (q_send_flag) + { + /* Last command was an empty 'send' */ + flags= QUERY_SEND_FLAG; + q_send_flag= 0; + } + else if (command->type == Q_REAP) + { + flags= QUERY_REAP_FLAG; + } + + if (command->type == Q_EVALP) + flags |= QUERY_PRINT_ORIGINAL_FLAG; + + /* Check for special property for this query */ + display_result_vertically|= (command->type == Q_QUERY_VERTICAL); + + if (save_file[0]) + { + if (!(command->require_file= strdup_root(&require_file_root, + save_file))) + die("out of memory for require_file"); + save_file[0]= 0; + } + run_query(cur_con, command, flags); + command_executed++; + command->last_argument= command->end; + + /* Restore settings */ + display_result_vertically= old_display_result_vertically; + + break; + } + case Q_SEND: + case Q_SEND_EVAL: + if (!*command->first_argument) + { + /* + This is a send without arguments, it indicates that _next_ query + should be send only + */ + q_send_flag= 1; + break; + } + + /* Remove "send" if this is first iteration */ + if (command->query == command->query_buf) + command->query= command->first_argument; + + /* + run_query() can execute a query partially, depending on the flags. + QUERY_SEND_FLAG flag without QUERY_REAP_FLAG tells it to just send + the query and read the result some time later when reap instruction + is given on this connection. + */ + run_query(cur_con, command, QUERY_SEND_FLAG); + command_executed++; + command->last_argument= command->end; + break; + case Q_REQUIRE: + do_get_file_name(command, save_file, sizeof(save_file)); + break; + case Q_ERROR: + do_get_errcodes(command); + break; + case Q_REPLACE: + do_get_replace(command); + break; + case Q_REPLACE_REGEX: + do_get_replace_regex(command); + break; + case Q_REPLACE_COLUMN: + do_get_replace_column(command); + break; + case Q_SAVE_MASTER_POS: do_save_master_pos(); break; + case Q_SYNC_WITH_MASTER: do_sync_with_master(command); break; + case Q_SYNC_SLAVE_WITH_MASTER: + { + do_save_master_pos(); + if (*command->first_argument) + select_connection(command); + else + select_connection_name("slave"); + do_sync_with_master2(command, 0, ""); + break; + } + case Q_COMMENT: + { + command->last_argument= command->end; + + /* Don't output comments in v1 */ + if (opt_result_format_version == 1) + break; + + /* Don't output comments if query logging is off */ + if (disable_query_log) + break; + + /* Write comment's with two starting #'s to result file */ + const char* p= command->query; + if (p && *p == '#' && *(p+1) == '#') + { + dynstr_append_mem(&ds_res, command->query, command->query_len); + dynstr_append(&ds_res, "\n"); + } + break; + } + case Q_EMPTY_LINE: + /* Don't output newline in v1 */ + if (opt_result_format_version == 1) + break; + + /* Don't output newline if query logging is off */ + if (disable_query_log) + break; + + dynstr_append(&ds_res, "\n"); + break; + case Q_PING: + handle_command_error(command, mysql_ping(cur_con->mysql), -1); + break; + case Q_RESET_CONNECTION: + do_reset_connection(); + break; + case Q_OPTIMIZER_TRACE: + enable_optimizer_trace(cur_con); + break; + case Q_SEND_SHUTDOWN: + handle_command_error(command, + mysql_shutdown(cur_con->mysql, + SHUTDOWN_DEFAULT), -1); + break; + case Q_SHUTDOWN_SERVER: + do_shutdown_server(command); + break; + case Q_EXEC: + do_exec(command); + command_executed++; + break; + case Q_START_TIMER: + /* Overwrite possible earlier start of timer */ + timer_start= timer_now(); + break; + case Q_END_TIMER: + /* End timer before ending mysqltest */ + timer_output(); + break; + case Q_CHARACTER_SET: + do_set_charset(command); + break; + case Q_DISABLE_PS_PROTOCOL: + set_property(command, P_PS, 0); + /* Close any open statements */ + close_statements(); + break; + case Q_ENABLE_PS_PROTOCOL: + set_property(command, P_PS, ps_protocol); + break; + case Q_DISABLE_PS2_PROTOCOL: + set_property(command, P_PS2, 0); + break; + case Q_ENABLE_PS2_PROTOCOL: + set_property(command, P_PS2, ps_protocol); + break; + case Q_DISABLE_VIEW_PROTOCOL: + set_property(command, P_VIEW, 0); + /* Close only util connections */ + close_util_connections(); + break; + case Q_ENABLE_VIEW_PROTOCOL: + set_property(command, P_VIEW, view_protocol); + break; + case Q_DISABLE_SERVICE_CONNECTION: + set_property(command, P_CONN, 0); + /* Close only util connections */ + close_util_connections(); + break; + case Q_ENABLE_SERVICE_CONNECTION: + set_property(command, P_CONN, view_protocol); + break; + case Q_DISABLE_NON_BLOCKING_API: + non_blocking_api_enabled= 0; + break; + case Q_ENABLE_NON_BLOCKING_API: + non_blocking_api_enabled= 1; + break; + case Q_DISABLE_RECONNECT: + mysql_options(cur_con->mysql, MYSQL_OPT_RECONNECT, &my_false); + break; + case Q_ENABLE_RECONNECT: + mysql_options(cur_con->mysql, MYSQL_OPT_RECONNECT, &my_true); + /* Close any open statements - no reconnect, need new prepare */ + close_statements(); + break; + case Q_DISABLE_PARSING: + if (parsing_disabled == 0) + parsing_disabled= 1; + else + report_or_die("Parsing is already disabled"); + break; + case Q_ENABLE_PARSING: + /* + Ensure we don't get parsing_disabled < 0 as this would accidentally + disable code we don't want to have disabled + */ + if (parsing_disabled == 1) + parsing_disabled= 0; + else + report_or_die("Parsing is already enabled"); + break; + case Q_DIE: + { + char message[160]; + const char *msg; + DYNAMIC_STRING ds_echo; + + if (command->first_argument[0]) + { + /* Evaluate variables in the message */ + init_dynamic_string(&ds_echo, "", command->query_len, 256); + do_eval(&ds_echo, command->first_argument, command->end, FALSE); + strmake(message, ds_echo.str, MY_MIN(sizeof(message)-1, + ds_echo.length)); + dynstr_free(&ds_echo); + msg= message; + } + else + msg= "Explicit --die command executed"; + + /* Abort test with error code and error message */ + die("%s", msg); + break; + } + case Q_EXIT: + /* Stop processing any more commands */ + abort_flag= 1; + break; + case Q_SKIP: + /* Eval the query, thus replacing all environment variables */ + dynstr_set(&ds_res, 0); + do_eval(&ds_res, command->first_argument, command->end, FALSE); + abort_not_supported_test("%s",ds_res.str); + break; + case Q_RESULT: + die("result, deprecated command"); + break; + default: + processed= 0; + break; + } + } + + if (!processed) + { + current_line_inc= 0; + switch (command->type) { + case Q_WHILE: do_block(cmd_while, command); break; + case Q_IF: do_block(cmd_if, command); break; + case Q_END_BLOCK: do_done(command); break; + default: current_line_inc = 1; break; + } + } + else + check_eol_junk(command->last_argument); + + if (command->type != Q_ERROR && + command->type != Q_COMMENT) + { + /* + As soon as any non "error" command or comment has been executed, + the array with expected errors should be cleared + */ + memset(&saved_expected_errors, 0, sizeof(saved_expected_errors)); + } + + if (command_executed != last_command_executed || command->used_replace) + { + /* + As soon as any command has been executed, + the replace structures should be cleared + */ + free_all_replace(); + + /* Also reset "sorted_result" and "lowercase"*/ + display_result_sorted= FALSE; + display_result_lower= FALSE; + } + last_command_executed= command_executed; + + parser.current_line += current_line_inc; + if ( opt_mark_progress ) + mark_progress(command, parser.current_line); + + /* Write result from command to log file immediately */ + log_file.write(&ds_res); + log_file.flush(); + dynstr_set(&ds_res, 0); + } + + log_file.close(); + + start_lineno= 0; + verbose_msg("... Done processing test commands."); + + if (parsing_disabled) + die("Test ended with parsing disabled"); + + /* + The whole test has been executed _successfully_. + Time to compare result or save it to record file. + The entire output from test is in the log file + */ + if (log_file.bytes_written()) + { + if (result_file_name) + { + /* A result file has been specified */ + + if (record) + { + /* Recording */ + + /* save a copy of the log to result file */ + if (my_copy(log_file.file_name(), result_file_name, MYF(0)) != 0) + die("Failed to copy '%s' to '%s', errno: %d", + log_file.file_name(), result_file_name, errno); + + } + else + { + /* Check that the output from test is equal to result file */ + check_result(); + } + } + } + else + { + /* Empty output is an error *unless* we also have an empty result file */ + if (! result_file_name || record || + compare_files (log_file.file_name(), result_file_name)) + { + die("The test didn't produce any output"); + } + else + { + empty_result= TRUE; /* Meaning empty was expected */ + } + } + + if (!command_executed && result_file_name && !empty_result) + die("No queries executed but non-empty result file found!"); + + verbose_msg("Test has succeeded!"); + timer_output(); + /* Yes, if we got this far the test has succeeded! Sakila smiles */ + cleanup_and_exit(0); + return 0; /* Keep compiler happy too */ +} + + +/* + A primitive timer that give results in milliseconds if the + --timer-file=<filename> is given. The timer result is written + to that file when the result is available. To not confuse + mysql-test-run with an old obsolete result, we remove the file + before executing any commands. The time we measure is + + - If no explicit 'start_timer' or 'end_timer' is given in the + test case, the timer measure how long we execute in mysqltest. + + - If only 'start_timer' is given we measure how long we execute + from that point until we terminate mysqltest. + + - If only 'end_timer' is given we measure how long we execute + from that we enter mysqltest to the 'end_timer' is command is + executed. + + - If both 'start_timer' and 'end_timer' are given we measure + the time between executing the two commands. +*/ + +void timer_output(void) +{ + if (timer_file) + { + char buf[32], *end; + ulonglong timer= timer_now() - timer_start; + end= longlong10_to_str(timer, buf, 10); + str_to_file(timer_file,buf, (int) (end-buf)); + /* Timer has been written to the file, don't use it anymore */ + timer_file= 0; + } +} + + +ulonglong timer_now(void) +{ + return my_interval_timer() / 1000000; +} + + +/* + Get arguments for replace_columns. The syntax is: + replace-column column_number to_string [column_number to_string ...] + Where each argument may be quoted with ' or " + A argument may also be a variable, in which case the value of the + variable is replaced. +*/ + +void do_get_replace_column(struct st_command *command) +{ + char *from= command->first_argument; + char *buff, *start; + DBUG_ENTER("get_replace_columns"); + + free_replace_column(); + if (!*from) + die("Missing argument in %s", command->query); + + /* Allocate a buffer for results */ + start= buff= (char*)my_malloc(PSI_NOT_INSTRUMENTED, strlen(from)+1, + MYF(MY_WME|MY_FAE)); + while (*from) + { + char *to; + uint column_number; + to= get_string(&buff, &from, command); + if (!(column_number= atoi(to)) || column_number > MAX_COLUMNS) + die("Wrong column number to replace_column in '%s'", command->query); + if (!*from) + die("Wrong number of arguments to replace_column in '%s'", + command->query); + to= get_string(&buff, &from, command); + my_free(replace_column[column_number-1]); + replace_column[column_number-1]= my_strdup(PSI_NOT_INSTRUMENTED, to, + MYF(MY_WME|MY_FAE)); + set_if_bigger(max_replace_column, column_number); + } + my_free(start); + command->last_argument= command->end; + + DBUG_VOID_RETURN; +} + + +void free_replace_column() +{ + uint i; + for (i=0 ; i < max_replace_column ; i++) + { + if (replace_column[i]) + { + my_free(replace_column[i]); + replace_column[i]= 0; + } + } + max_replace_column= 0; +} + + +/****************************************************************************/ +/* + Replace functions +*/ + +/* Definitions for replace result */ + +typedef struct st_pointer_array { /* when using array-strings */ + TYPELIB typelib; /* Pointer to strings */ + uchar *str; /* Strings is here */ + uint8 *flag; /* Flag about each var. */ + uint array_allocs,max_count,length,max_length; +} POINTER_ARRAY; + +struct st_replace *init_replace(char * *from, char * *to, uint count, + char * word_end_chars); +int insert_pointer_name(POINTER_ARRAY *pa,char * name); +void free_pointer_array(POINTER_ARRAY *pa); + +/* + Get arguments for replace. The syntax is: + replace from to [from to ...] + Where each argument may be quoted with ' or " + A argument may also be a variable, in which case the value of the + variable is replaced. +*/ + +void do_get_replace(struct st_command *command) +{ + uint i; + char *from= command->first_argument; + char *buff, *start; + char word_end_chars[256], *pos; + POINTER_ARRAY to_array, from_array; + DBUG_ENTER("do_get_replace"); + + free_replace(); + + bzero(&to_array,sizeof(to_array)); + bzero(&from_array,sizeof(from_array)); + if (!*from) + die("Missing argument in %s", command->query); + start= buff= (char*)my_malloc(PSI_NOT_INSTRUMENTED, strlen(from)+1, + MYF(MY_WME|MY_FAE)); + while (*from) + { + char *to= buff; + to= get_string(&buff, &from, command); + if (!*from) + die("Wrong number of arguments to replace_result in '%s'", + command->query); + fix_win_paths(to, from - to); + insert_pointer_name(&from_array,to); + to= get_string(&buff, &from, command); + insert_pointer_name(&to_array,to); + } + for (i= 1,pos= word_end_chars ; i < 256 ; i++) + if (my_isspace(charset_info,i)) + *pos++= i; + *pos=0; /* End pointer */ + if (!(glob_replace= init_replace((char**) from_array.typelib.type_names, + (char**) to_array.typelib.type_names, + (uint) from_array.typelib.count, + word_end_chars))) + die("Can't initialize replace from '%s'", command->query); + free_pointer_array(&from_array); + free_pointer_array(&to_array); + my_free(start); + command->last_argument= command->end; + DBUG_VOID_RETURN; +} + + +void free_replace() +{ + DBUG_ENTER("free_replace"); + my_free(glob_replace); + glob_replace= NULL; + DBUG_VOID_RETURN; +} + + +typedef struct st_replace { + int found; + struct st_replace *next[256]; +} REPLACE; + +typedef struct st_replace_found { + int found; + uint to_offset; + int from_offset; + char *replace_string; +} REPLACE_STRING; + + +void replace_strings_append(REPLACE *rep, DYNAMIC_STRING* ds, + const char *str) +{ + REPLACE *rep_pos; + REPLACE_STRING *rep_str; + const char *start, *from; + DBUG_ENTER("replace_strings_append"); + + start= from= str; + rep_pos=rep+1; + for (;;) + { + /* Loop through states */ + DBUG_PRINT("info", ("Looping through states")); + while (!rep_pos->found) + rep_pos= rep_pos->next[(uchar) *from++]; + + /* Does this state contain a string to be replaced */ + if (!(rep_str = ((REPLACE_STRING*) rep_pos))->replace_string) + { + /* No match found */ + dynstr_append_mem(ds, start, from - start - 1); + DBUG_PRINT("exit", ("Found no more string to replace, appended: %s", start)); + DBUG_VOID_RETURN; + } + + /* Found a string that needs to be replaced */ + DBUG_PRINT("info", ("found: %d, to_offset: %u, from_offset: %d, string: %s", + rep_str->found, rep_str->to_offset, + rep_str->from_offset, rep_str->replace_string)); + + /* Append part of original string before replace string */ + dynstr_append_mem(ds, start, (from - rep_str->to_offset) - start); + + /* Append replace string */ + dynstr_append_mem(ds, rep_str->replace_string, + strlen(rep_str->replace_string)); + + if (!*(from-=rep_str->from_offset) && rep_pos->found != 2) + { + /* End of from string */ + DBUG_PRINT("exit", ("Found end of from string")); + DBUG_VOID_RETURN; + } + start= from; + rep_pos=rep; + } +} + + +/* + Regex replace functions +*/ + + +/* Stores regex substitutions */ + +struct st_regex +{ + char* pattern; /* Pattern to be replaced */ + char* replace; /* String or expression to replace the pattern with */ + int icase; /* true if the match is case insensitive */ +}; + +int reg_replace(char** buf_p, int* buf_len_p, char *pattern, char *replace, + char *string, int icase); + +bool parse_re_part(char *start_re, char *end_re, + char **p, char *end, char **buf) +{ + if (*start_re != *end_re) + { + switch ((*start_re= *(*p)++)) { + case '(': *end_re= ')'; break; + case '[': *end_re= ']'; break; + case '{': *end_re= '}'; break; + case '<': *end_re= '>'; break; + default: *end_re= *start_re; + } + } + + while (*p < end && **p != *end_re) + { + if ((*p)[0] == '\\' && *p + 1 < end && (*p)[1] == *end_re) + (*p)++; + + *(*buf)++= *(*p)++; + } + *(*buf)++= 0; + + (*p)++; + + return *p > end; +} + +/* + Initializes the regular substitution expression to be used in the + result output of test. + + Returns: st_replace_regex struct with pairs of substitutions +*/ +void append_replace_regex(char*, char*, struct st_replace_regex*, char**); + +struct st_replace_regex* init_replace_regex(char* expr) +{ + char *expr_end, *buf_p; + struct st_replace_regex* res; + size_t expr_len= strlen(expr); + + /* my_malloc() will die on fail with MY_FAE */ + res=(struct st_replace_regex*)my_malloc(PSI_NOT_INSTRUMENTED, + sizeof(*res)+8192 ,MYF(MY_FAE+MY_WME)); + my_init_dynamic_array(PSI_NOT_INSTRUMENTED, &res->regex_arr, sizeof(struct st_regex), 128, 128, MYF(0)); + + expr_end= expr + expr_len; + buf_p= (char*)res + sizeof(*res); + append_replace_regex(expr, expr_end, res, &buf_p); + + res->odd_buf_len= res->even_buf_len= 8192; + res->even_buf= (char*)my_malloc(PSI_NOT_INSTRUMENTED, res->even_buf_len,MYF(MY_WME+MY_FAE)); + res->odd_buf= (char*)my_malloc(PSI_NOT_INSTRUMENTED, res->odd_buf_len,MYF(MY_WME+MY_FAE)); + res->buf= res->even_buf; + + return res; +} + + +void append_replace_regex(char* expr, char *expr_end, struct st_replace_regex* res, + char **buf_p) +{ + char* p, start_re, end_re= 1; + struct st_regex reg; + + p= expr; + + /* for each regexp substitution statement */ + while (p < expr_end) + { + bzero(®,sizeof(reg)); + /* find the start of the statement */ + while (my_isspace(charset_info, *p) && p < expr_end) + p++; + + if (p >= expr_end) + { + if (res->regex_arr.elements) + break; + else + goto err; + } + + start_re= 0; + reg.pattern= *buf_p; + + /* Allow variable for the *entire* list of replacements */ + if (*p == '$') + { + const char *v_end= 0; + VAR *val= var_get(p, &v_end, 0, 1); + + if (val) + { + char *expr, *expr_end; + expr= val->str_val; + expr_end= expr + val->str_val_len; + append_replace_regex(expr, expr_end, res, buf_p); + } + + p= (char *) v_end + 1; + continue; + } + else + { + if (parse_re_part(&start_re, &end_re, &p, expr_end, buf_p)) + goto err; + + reg.replace= *buf_p; + if (parse_re_part(&start_re, &end_re, &p, expr_end, buf_p)) + goto err; + } + + /* Check if we should do matching case insensitive */ + if (p < expr_end && *p == 'i') + { + p++; + reg.icase= 1; + } + + /* done parsing the statement, now place it in regex_arr */ + if (insert_dynamic(&res->regex_arr, ®)) + die("Out of memory"); + } + + return; + +err: + my_free(res->regex_arr.buffer); + my_free(res); + die("Error parsing replace_regex \"%s\"", expr); +} + +/* + Execute all substitutions on val. + + Returns: true if substitution was made, false otherwise + Side-effect: Sets r->buf to be the buffer with all substitutions done. + + IN: + struct st_replace_regex* r + char* val + Out: + struct st_replace_regex* r + r->buf points at the resulting buffer + r->even_buf and r->odd_buf might have been reallocated + r->even_buf_len and r->odd_buf_len might have been changed + + TODO: at some point figure out if there is a way to do everything + in one pass +*/ + +int multi_reg_replace(struct st_replace_regex* r,char* val) +{ + uint i; + char* in_buf, *out_buf; + int* buf_len_p; + + in_buf= val; + out_buf= r->even_buf; + buf_len_p= &r->even_buf_len; + r->buf= 0; + + /* For each substitution, do the replace */ + for (i= 0; i < r->regex_arr.elements; i++) + { + struct st_regex re; + char* save_out_buf= out_buf; + + get_dynamic(&r->regex_arr, &re, i); + + if (!reg_replace(&out_buf, buf_len_p, re.pattern, re.replace, + in_buf, re.icase)) + { + /* if the buffer has been reallocated, make adjustments */ + if (save_out_buf != out_buf) + { + if (save_out_buf == r->even_buf) + r->even_buf= out_buf; + else + r->odd_buf= out_buf; + } + + r->buf= out_buf; + if (in_buf == val) + in_buf= r->odd_buf; + + swap_variables(char*,in_buf,out_buf); + + buf_len_p= (out_buf == r->even_buf) ? &r->even_buf_len : + &r->odd_buf_len; + } + } + + return (r->buf == 0); +} + +/* + Parse the regular expression to be used in all result files + from now on. + + The syntax is --replace_regex /from/to/i /from/to/i ... + i means case-insensitive match. If omitted, the match is + case-sensitive + +*/ +void do_get_replace_regex(struct st_command *command) +{ + char *expr= command->first_argument; + free_replace_regex(); + if (expr && *expr && !(glob_replace_regex=init_replace_regex(expr))) + die("Could not init replace_regex"); + command->last_argument= command->end; +} + +void free_replace_regex() +{ + if (glob_replace_regex) + { + delete_dynamic(&glob_replace_regex->regex_arr); + my_free(glob_replace_regex->even_buf); + my_free(glob_replace_regex->odd_buf); + my_free(glob_replace_regex); + glob_replace_regex=0; + } +} + + + +/* + auxiliary macro used by reg_replace + makes sure the result buffer has sufficient length +*/ +#define SECURE_REG_BUF if (buf_len < need_buf_len) \ + { \ + ssize_t off= res_p - buf; \ + buf= (char*)my_realloc(PSI_NOT_INSTRUMENTED, buf,need_buf_len,MYF(MY_WME+MY_FAE)); \ + res_p= buf + off; \ + buf_len= need_buf_len; \ + } \ + \ +/* + Performs a regex substitution + + IN: + + buf_p - result buffer pointer. Will change if reallocated + buf_len_p - result buffer length. Will change if the buffer is reallocated + pattern - regexp pattern to match + replace - replacement expression + string - the string to perform substitutions in + icase - flag, if set to 1 the match is case insensitive +*/ +int reg_replace(char** buf_p, int* buf_len_p, char *pattern, + char *replace, char *string, int icase) +{ + regex_t r; + regmatch_t *subs; + char *replace_end; + char *buf= *buf_p; + size_t len; + size_t buf_len, need_buf_len; + int cflags= REG_EXTENDED | REG_DOTALL; + int err_code; + char *res_p,*str_p,*str_end; + + DBUG_ASSERT(*buf_len_p > 0); + + buf_len= (size_t)*buf_len_p; + len= strlen(string); + str_end= string + len; + + /* start with a buffer of a reasonable size that hopefully will not + need to be reallocated + */ + need_buf_len= len * 2 + 1; + res_p= buf; + + SECURE_REG_BUF + + if (icase) + cflags|= REG_ICASE; + + if ((err_code= regcomp(&r,pattern,cflags))) + { + check_regerr(&r,err_code); + return 1; + } + + subs= (regmatch_t*)my_malloc(PSI_NOT_INSTRUMENTED, sizeof(regmatch_t) * (r.re_nsub+1), + MYF(MY_WME+MY_FAE)); + + *res_p= 0; + str_p= string; + replace_end= replace + strlen(replace); + + /* for each pattern match instance perform a replacement */ + while (!err_code) + { + /* find the match */ + err_code= regexec(&r,str_p, r.re_nsub+1, subs, + (str_p == string) ? 0 : REG_NOTBOL); + + /* if regular expression error (eg. bad syntax, or out of memory) */ + if (err_code && err_code != REG_NOMATCH) + { + check_regerr(&r,err_code); + regfree(&r); + return 1; + } + + /* if match found */ + if (!err_code) + { + char* expr_p= replace; + int c; + + /* + we need at least what we have so far in the buffer + the part + before this match + */ + need_buf_len= (res_p - buf) + (int) subs[0].rm_so; + + /* on this pass, calculate the memory for the result buffer */ + while (expr_p < replace_end) + { + int back_ref_num= -1; + c= *expr_p; + + if (c == '\\' && expr_p + 1 < replace_end) + { + back_ref_num= (int) (expr_p[1] - '0'); + } + + /* found a valid back_ref (eg. \1)*/ + if (back_ref_num >= 0 && back_ref_num <= (int)r.re_nsub) + { + regoff_t start_off, end_off; + if ((start_off=subs[back_ref_num].rm_so) > -1 && + (end_off=subs[back_ref_num].rm_eo) > -1) + { + need_buf_len += (int) (end_off - start_off); + } + expr_p += 2; + } + else + { + expr_p++; + need_buf_len++; + } + } + need_buf_len++; + /* + now that we know the size of the buffer, + make sure it is big enough + */ + SECURE_REG_BUF + + /* copy the pre-match part */ + if (subs[0].rm_so) + { + memcpy(res_p, str_p, (size_t) subs[0].rm_so); + res_p+= subs[0].rm_so; + } + + expr_p= replace; + + /* copy the match and expand back_refs */ + while (expr_p < replace_end) + { + int back_ref_num= -1; + c= *expr_p; + + if (c == '\\' && expr_p + 1 < replace_end) + { + back_ref_num= expr_p[1] - '0'; + } + + if (back_ref_num >= 0 && back_ref_num <= (int)r.re_nsub) + { + regoff_t start_off, end_off; + if ((start_off=subs[back_ref_num].rm_so) > -1 && + (end_off=subs[back_ref_num].rm_eo) > -1) + { + int block_len= (int) (end_off - start_off); + memcpy(res_p,str_p + start_off, block_len); + res_p += block_len; + } + expr_p += 2; + } + else + { + *res_p++ = *expr_p++; + } + } + + /* handle the post-match part */ + if (subs[0].rm_so == subs[0].rm_eo) + { + if (str_p + subs[0].rm_so >= str_end) + break; + str_p += subs[0].rm_eo ; + *res_p++ = *str_p++; + } + else + { + str_p += subs[0].rm_eo; + } + } + else /* no match this time, just copy the string as is */ + { + size_t left_in_str= str_end-str_p; + need_buf_len= (res_p-buf) + left_in_str; + SECURE_REG_BUF + memcpy(res_p,str_p,left_in_str); + res_p += left_in_str; + str_p= str_end; + } + } + my_free(subs); + regfree(&r); + *res_p= 0; + *buf_p= buf; + *buf_len_p= (int)buf_len; + return 0; +} + + +#ifndef WORD_BIT +#define WORD_BIT (8*sizeof(uint)) +#endif + +#define SET_MALLOC_HUNC 64 +#define LAST_CHAR_CODE 259 + +typedef struct st_rep_set { + uint *bits; /* Pointer to used sets */ + short next[LAST_CHAR_CODE]; /* Pointer to next sets */ + uint found_len; /* Best match to date */ + int found_offset; + uint table_offset; + uint size_of_bits; /* For convenience */ +} REP_SET; + +typedef struct st_rep_sets { + uint count; /* Number of sets */ + uint extra; /* Extra sets in buffer */ + uint invisible; /* Sets not chown */ + uint size_of_bits; + REP_SET *set,*set_buffer; + uint *bit_buffer; +} REP_SETS; + +typedef struct st_found_set { + uint table_offset; + int found_offset; +} FOUND_SET; + +typedef struct st_follow { + int chr; + uint table_offset; + uint len; +} FOLLOWS; + + +int init_sets(REP_SETS *sets,uint states); +REP_SET *make_new_set(REP_SETS *sets); +void make_sets_invisible(REP_SETS *sets); +void free_last_set(REP_SETS *sets); +void free_sets(REP_SETS *sets); +void internal_set_bit(REP_SET *set, uint bit); +void internal_clear_bit(REP_SET *set, uint bit); +void or_bits(REP_SET *to,REP_SET *from); +void copy_bits(REP_SET *to,REP_SET *from); +int cmp_bits(REP_SET *set1,REP_SET *set2); +int get_next_bit(REP_SET *set,uint lastpos); +int find_set(REP_SETS *sets,REP_SET *find); +int find_found(FOUND_SET *found_set,uint table_offset, + int found_offset); +uint start_at_word(char * pos); +uint end_of_word(char * pos); + +static uint found_sets=0; + + +uint replace_len(char * str) +{ + uint len=0; + while (*str) + { + str++; + len++; + } + return len; +} + +/* Init a replace structure for further calls */ + +REPLACE *init_replace(char * *from, char * *to,uint count, + char * word_end_chars) +{ + static const int SPACE_CHAR= 256; + static const int END_OF_LINE= 258; + + uint i,j,states,set_nr,len,result_len,max_length,found_end,bits_set,bit_nr; + int used_sets,chr,default_state; + char used_chars[LAST_CHAR_CODE],is_word_end[256]; + char * pos, *to_pos, **to_array; + REP_SETS sets; + REP_SET *set,*start_states,*word_states,*new_set; + FOLLOWS *follow,*follow_ptr; + REPLACE *replace; + FOUND_SET *found_set; + REPLACE_STRING *rep_str; + DBUG_ENTER("init_replace"); + + /* Count number of states */ + for (i=result_len=max_length=0 , states=2 ; i < count ; i++) + { + len=replace_len(from[i]); + if (!len) + { + errno=EINVAL; + DBUG_RETURN(0); + } + states+=len+1; + result_len+=(uint) strlen(to[i])+1; + if (len > max_length) + max_length=len; + } + bzero(is_word_end, sizeof(is_word_end)); + for (i=0 ; word_end_chars[i] ; i++) + is_word_end[(uchar) word_end_chars[i]]=1; + + if (init_sets(&sets,states)) + DBUG_RETURN(0); + found_sets=0; + if (!(found_set= (FOUND_SET*) my_malloc(PSI_NOT_INSTRUMENTED, sizeof(FOUND_SET)*max_length*count, + MYF(MY_WME|MY_FAE)))) + { + free_sets(&sets); + DBUG_RETURN(0); + } + (void) make_new_set(&sets); /* Set starting set */ + make_sets_invisible(&sets); /* Hide previous sets */ + used_sets=-1; + word_states=make_new_set(&sets); /* Start of new word */ + start_states=make_new_set(&sets); /* This is first state */ + if (!(follow=(FOLLOWS*) my_malloc(PSI_NOT_INSTRUMENTED, (states+2)*sizeof(FOLLOWS),MYF(MY_WME|MY_FAE)))) + { + free_sets(&sets); + my_free(found_set); + DBUG_RETURN(0); + } + + /* Init follow_ptr[] */ + for (i=0, states=1, follow_ptr=follow+1 ; i < count ; i++) + { + if (from[i][0] == '\\' && from[i][1] == '^') + { + internal_set_bit(start_states,states+1); + if (!from[i][2]) + { + start_states->table_offset=i; + start_states->found_offset=1; + } + } + else if (from[i][0] == '\\' && from[i][1] == '$') + { + internal_set_bit(start_states,states); + internal_set_bit(word_states,states); + if (!from[i][2] && start_states->table_offset == (uint) ~0) + { + start_states->table_offset=i; + start_states->found_offset=0; + } + } + else + { + internal_set_bit(word_states,states); + if (from[i][0] == '\\' && (from[i][1] == 'b' && from[i][2])) + internal_set_bit(start_states,states+1); + else + internal_set_bit(start_states,states); + } + for (pos=from[i], len=0; *pos ; pos++) + { + follow_ptr->chr= (uchar) *pos; + follow_ptr->table_offset=i; + follow_ptr->len= ++len; + follow_ptr++; + } + follow_ptr->chr=0; + follow_ptr->table_offset=i; + follow_ptr->len=len; + follow_ptr++; + states+=(uint) len+1; + } + + + for (set_nr=0,pos=0 ; set_nr < sets.count ; set_nr++) + { + set=sets.set+set_nr; + default_state= 0; /* Start from beginning */ + + /* If end of found-string not found or start-set with current set */ + + for (i= (uint) ~0; (i=get_next_bit(set,i)) ;) + { + if (!follow[i].chr) + { + if (! default_state) + default_state= find_found(found_set,set->table_offset, + set->found_offset+1); + } + } + copy_bits(sets.set+used_sets,set); /* Save set for changes */ + if (!default_state) + or_bits(sets.set+used_sets,sets.set); /* Can restart from start */ + + /* Find all chars that follows current sets */ + bzero(used_chars, sizeof(used_chars)); + for (i= (uint) ~0; (i=get_next_bit(sets.set+used_sets,i)) ;) + { + used_chars[follow[i].chr]=1; + if ((follow[i].chr == SPACE_CHAR && !follow[i+1].chr && + follow[i].len > 1) || follow[i].chr == END_OF_LINE) + used_chars[0]=1; + } + + /* Mark word_chars used if \b is in state */ + if (used_chars[SPACE_CHAR]) + for (pos= word_end_chars ; *pos ; pos++) + used_chars[(int) (uchar) *pos] = 1; + + /* Handle other used characters */ + for (chr= 0 ; chr < 256 ; chr++) + { + if (! used_chars[chr]) + set->next[chr]= chr ? default_state : -1; + else + { + new_set=make_new_set(&sets); + set=sets.set+set_nr; /* if realloc */ + new_set->table_offset=set->table_offset; + new_set->found_len=set->found_len; + new_set->found_offset=set->found_offset+1; + found_end=0; + + for (i= (uint) ~0 ; (i=get_next_bit(sets.set+used_sets,i)) ; ) + { + if (!follow[i].chr || follow[i].chr == chr || + (follow[i].chr == SPACE_CHAR && + (is_word_end[chr] || + (!chr && follow[i].len > 1 && ! follow[i+1].chr))) || + (follow[i].chr == END_OF_LINE && ! chr)) + { + if ((! chr || (follow[i].chr && !follow[i+1].chr)) && + follow[i].len > found_end) + found_end=follow[i].len; + if (chr && follow[i].chr) + internal_set_bit(new_set,i+1); /* To next set */ + else + internal_set_bit(new_set,i); + } + } + if (found_end) + { + new_set->found_len=0; /* Set for testing if first */ + bits_set=0; + for (i= (uint) ~0; (i=get_next_bit(new_set,i)) ;) + { + if ((follow[i].chr == SPACE_CHAR || + follow[i].chr == END_OF_LINE) && ! chr) + bit_nr=i+1; + else + bit_nr=i; + if (follow[bit_nr-1].len < found_end || + (new_set->found_len && + (chr == 0 || !follow[bit_nr].chr))) + internal_clear_bit(new_set,i); + else + { + if (chr == 0 || !follow[bit_nr].chr) + { /* best match */ + new_set->table_offset=follow[bit_nr].table_offset; + if (chr || (follow[i].chr == SPACE_CHAR || + follow[i].chr == END_OF_LINE)) + new_set->found_offset=found_end; /* New match */ + new_set->found_len=found_end; + } + bits_set++; + } + } + if (bits_set == 1) + { + set->next[chr] = find_found(found_set, + new_set->table_offset, + new_set->found_offset); + free_last_set(&sets); + } + else + set->next[chr] = find_set(&sets,new_set); + } + else + set->next[chr] = find_set(&sets,new_set); + } + } + } + + /* Alloc replace structure for the replace-state-machine */ + + if ((replace=(REPLACE*) my_malloc(PSI_NOT_INSTRUMENTED, sizeof(REPLACE)*(sets.count)+ + sizeof(REPLACE_STRING)*(found_sets+1)+ + sizeof(char *)*count+result_len, + MYF(MY_WME|MY_FAE|MY_ZEROFILL)))) + { + rep_str=(REPLACE_STRING*) (replace+sets.count); + to_array= (char **) (rep_str+found_sets+1); + to_pos=(char *) (to_array+count); + for (i=0 ; i < count ; i++) + { + to_array[i]=to_pos; + to_pos=strmov(to_pos,to[i])+1; + } + rep_str[0].found=1; + rep_str[0].replace_string=0; + for (i=1 ; i <= found_sets ; i++) + { + pos=from[found_set[i-1].table_offset]; + rep_str[i].found= !strncmp(pos, "\\^", 3) ? 2 : 1; + rep_str[i].replace_string=to_array[found_set[i-1].table_offset]; + rep_str[i].to_offset=found_set[i-1].found_offset-start_at_word(pos); + rep_str[i].from_offset=found_set[i-1].found_offset-replace_len(pos)+ + end_of_word(pos); + } + for (i=0 ; i < sets.count ; i++) + { + for (j=0 ; j < 256 ; j++) + if (sets.set[i].next[j] >= 0) + replace[i].next[j]=replace+sets.set[i].next[j]; + else + replace[i].next[j]=(REPLACE*) (rep_str+(-sets.set[i].next[j]-1)); + } + } + my_free(follow); + free_sets(&sets); + my_free(found_set); + DBUG_PRINT("exit",("Replace table has %d states",sets.count)); + DBUG_RETURN(replace); +} + + +int init_sets(REP_SETS *sets,uint states) +{ + bzero(sets, sizeof(*sets)); + sets->size_of_bits=((states+7)/8); + if (!(sets->set_buffer=(REP_SET*) my_malloc(PSI_NOT_INSTRUMENTED, sizeof(REP_SET)*SET_MALLOC_HUNC, + MYF(MY_WME|MY_FAE)))) + return 1; + if (!(sets->bit_buffer=(uint*) my_malloc(PSI_NOT_INSTRUMENTED, sizeof(uint)*sets->size_of_bits* + SET_MALLOC_HUNC,MYF(MY_WME|MY_FAE)))) + { + my_free(sets->set); + return 1; + } + return 0; +} + +/* Make help sets invisible for nicer coding */ + +void make_sets_invisible(REP_SETS *sets) +{ + sets->invisible=sets->count; + sets->set+=sets->count; + sets->count=0; +} + +REP_SET *make_new_set(REP_SETS *sets) +{ + uint i,count,*bit_buffer; + REP_SET *set; + if (sets->extra) + { + sets->extra--; + set=sets->set+ sets->count++; + bzero(set->bits, sizeof(uint) * sets->size_of_bits); + bzero(&set->next[0], sizeof(set->next[0]) * LAST_CHAR_CODE); + set->found_offset=0; + set->found_len=0; + set->table_offset= (uint) ~0; + set->size_of_bits=sets->size_of_bits; + return set; + } + count=sets->count+sets->invisible+SET_MALLOC_HUNC; + if (!(set=(REP_SET*) my_realloc(PSI_NOT_INSTRUMENTED, sets->set_buffer, sizeof(REP_SET)*count, + MYF(MY_WME)))) + return 0; + sets->set_buffer=set; + sets->set=set+sets->invisible; + if (!(bit_buffer=(uint*) my_realloc(PSI_NOT_INSTRUMENTED, sets->bit_buffer, + (sizeof(uint)*sets->size_of_bits)*count, + MYF(MY_WME)))) + return 0; + sets->bit_buffer=bit_buffer; + for (i=0 ; i < count ; i++) + { + sets->set_buffer[i].bits=bit_buffer; + bit_buffer+=sets->size_of_bits; + } + sets->extra=SET_MALLOC_HUNC; + return make_new_set(sets); +} + +void free_last_set(REP_SETS *sets) +{ + sets->count--; + sets->extra++; + return; +} + +void free_sets(REP_SETS *sets) +{ + my_free(sets->set_buffer); + my_free(sets->bit_buffer); + return; +} + +void internal_set_bit(REP_SET *set, uint bit) +{ + set->bits[bit / WORD_BIT] |= 1 << (bit % WORD_BIT); + return; +} + +void internal_clear_bit(REP_SET *set, uint bit) +{ + set->bits[bit / WORD_BIT] &= ~ (1 << (bit % WORD_BIT)); + return; +} + + +void or_bits(REP_SET *to,REP_SET *from) +{ + uint i; + for (i=0 ; i < to->size_of_bits ; i++) + to->bits[i]|=from->bits[i]; + return; +} + +void copy_bits(REP_SET *to,REP_SET *from) +{ + memcpy(to->bits, from->bits, + (size_t) (sizeof(uint) * to->size_of_bits)); +} + +int cmp_bits(REP_SET *set1,REP_SET *set2) +{ + return memcmp(set1->bits, set2->bits, + sizeof(uint) * set1->size_of_bits); +} + + +/* Get next set bit from set. */ + +int get_next_bit(REP_SET *set,uint lastpos) +{ + uint pos,*start,*end,bits; + + start=set->bits+ ((lastpos+1) / WORD_BIT); + end=set->bits + set->size_of_bits; + bits=start[0] & ~((1U << ((lastpos+1) % WORD_BIT)) -1); + + while (! bits && ++start < end) + bits=start[0]; + if (!bits) + return 0; + pos=(uint) (start-set->bits)*WORD_BIT; + while (! (bits & 1)) + { + bits>>=1; + pos++; + } + return pos; +} + +/* find if there is a same set in sets. If there is, use it and + free given set, else put in given set in sets and return its + position */ + +int find_set(REP_SETS *sets,REP_SET *find) +{ + uint i; + for (i=0 ; i < sets->count-1 ; i++) + { + if (!cmp_bits(sets->set+i,find)) + { + free_last_set(sets); + return i; + } + } + return i; /* return new position */ +} + +/* find if there is a found_set with same table_offset & found_offset + If there is return offset to it, else add new offset and return pos. + Pos returned is -offset-2 in found_set_structure because it is + saved in set->next and set->next[] >= 0 points to next set and + set->next[] == -1 is reserved for end without replaces. +*/ + +int find_found(FOUND_SET *found_set,uint table_offset, int found_offset) +{ + int i; + for (i=0 ; (uint) i < found_sets ; i++) + if (found_set[i].table_offset == table_offset && + found_set[i].found_offset == found_offset) + return -i-2; + found_set[i].table_offset=table_offset; + found_set[i].found_offset=found_offset; + found_sets++; + return -i-2; /* return new position */ +} + +/* Return 1 if regexp starts with \b or ends with \b*/ + +uint start_at_word(char * pos) +{ + return (((!memcmp(pos, "\\b",2) && pos[2]) || + !memcmp(pos, "\\^", 2)) ? 1 : 0); +} + +uint end_of_word(char * pos) +{ + char * end=strend(pos); + return ((end > pos+2 && !memcmp(end-2, "\\b", 2)) || + (end >= pos+2 && !memcmp(end-2, "\\$",2))) ? 1 : 0; +} + +/**************************************************************************** + * Handle replacement of strings + ****************************************************************************/ + +#define PC_MALLOC 256 /* Bytes for pointers */ +#define PS_MALLOC 512 /* Bytes for data */ + +int insert_pointer_name(POINTER_ARRAY *pa,char * name) +{ + uint i,length,old_count; + uchar *new_pos; + const char **new_array; + DBUG_ENTER("insert_pointer_name"); + + if (! pa->typelib.count) + { + if (!(pa->typelib.type_names=(const char **) + my_malloc(PSI_NOT_INSTRUMENTED, ((PC_MALLOC-MALLOC_OVERHEAD)/ + (sizeof(char *)+sizeof(*pa->flag))* + (sizeof(char *)+sizeof(*pa->flag))),MYF(MY_WME|MY_FAE)))) + DBUG_RETURN(-1); + if (!(pa->str= (uchar*) my_malloc(PSI_NOT_INSTRUMENTED, PS_MALLOC - MALLOC_OVERHEAD, + MYF(MY_WME|MY_FAE)))) + { + my_free(pa->typelib.type_names); + DBUG_RETURN (-1); + } + pa->max_count=(PC_MALLOC-MALLOC_OVERHEAD)/(sizeof(uchar*)+ + sizeof(*pa->flag)); + pa->flag= (uint8*) (pa->typelib.type_names+pa->max_count); + pa->length=0; + pa->max_length=PS_MALLOC-MALLOC_OVERHEAD; + pa->array_allocs=1; + } + length=(uint) strlen(name)+1; + if (pa->length+length >= pa->max_length) + { + if (!(new_pos= (uchar*) my_realloc(PSI_NOT_INSTRUMENTED, pa->str, pa->length + length + PS_MALLOC, + MYF(MY_WME)))) + DBUG_RETURN(1); + if (new_pos != pa->str) + { + my_ptrdiff_t diff=PTR_BYTE_DIFF(new_pos,pa->str); + for (i=0 ; i < pa->typelib.count ; i++) + pa->typelib.type_names[i]= ADD_TO_PTR(pa->typelib.type_names[i],diff, + char*); + pa->str=new_pos; + } + pa->max_length= pa->length+length+PS_MALLOC; + } + if (pa->typelib.count >= pa->max_count-1) + { + int len; + pa->array_allocs++; + len=(PC_MALLOC*pa->array_allocs - MALLOC_OVERHEAD); + if (!(new_array=(const char **) my_realloc(PSI_NOT_INSTRUMENTED, pa->typelib.type_names, + len/ + (sizeof(uchar*)+sizeof(*pa->flag))* + (sizeof(uchar*)+sizeof(*pa->flag)), + MYF(MY_WME)))) + DBUG_RETURN(1); + pa->typelib.type_names=new_array; + old_count=pa->max_count; + pa->max_count=len/(sizeof(uchar*) + sizeof(*pa->flag)); + pa->flag= (uint8*) (pa->typelib.type_names+pa->max_count); + memcpy(pa->flag, (pa->typelib.type_names +old_count), + old_count*sizeof(*pa->flag)); + } + pa->flag[pa->typelib.count]=0; /* Reset flag */ + pa->typelib.type_names[pa->typelib.count++]= (char*) pa->str+pa->length; + pa->typelib.type_names[pa->typelib.count]= NullS; /* Put end-mark */ + (void) strmov((char*) pa->str + pa->length,name); + pa->length+=length; + DBUG_RETURN(0); +} /* insert_pointer_name */ + + +/* free pointer array */ + +void free_pointer_array(POINTER_ARRAY *pa) +{ + if (pa->typelib.count) + { + pa->typelib.count=0; + my_free(pa->typelib.type_names); + pa->typelib.type_names=0; + my_free(pa->str); + } +} /* free_pointer_array */ + + +/* Functions that uses replace and replace_regex */ + +/* Append the string to ds, with optional replace */ +void replace_dynstr_append_mem(DYNAMIC_STRING *ds, const char *val, size_t len) +{ + char lower[1024]; + + if (len < sizeof(lower) - 1) + { + if (display_result_lower) + { + /* Convert to lower case, and do this first */ + char *c= lower; + for (const char *v= val, *end_v= v + len; v < end_v; v++) + *c++= my_tolower(charset_info, *v); + *c= '\0'; + /* Copy from this buffer instead */ + } + else + { + memcpy(lower, val, len); + lower[len]= 0; + } + fix_win_paths(lower, len); + val= lower; + } + + if (glob_replace_regex) + { + /* Regex replace */ + if (!multi_reg_replace(glob_replace_regex, (char*)val)) + { + val= glob_replace_regex->buf; + len= strlen(val); + } + } + + if (glob_replace) + { + /* Normal replace */ + replace_strings_append(glob_replace, ds, val); + } + else + dynstr_append_mem(ds, val, len); +} + + +/* Append zero-terminated string to ds, with optional replace */ +void replace_dynstr_append(DYNAMIC_STRING *ds, const char *val) +{ + replace_dynstr_append_mem(ds, val, strlen(val)); +} + +/* Append uint to ds, with optional replace */ +void replace_dynstr_append_uint(DYNAMIC_STRING *ds, uint val) +{ + char buff[22]; /* This should be enough for any int */ + char *end= longlong10_to_str(val, buff, 10); + replace_dynstr_append_mem(ds, buff, end - buff); +} + + +/* + Build a list of pointer to each line in ds_input, sort + the list and use the sorted list to append the strings + sorted to the output ds + + SYNOPSIS + dynstr_append_sorted() + ds string where the sorted output will be appended + ds_input string to be sorted + keep_header If header should not be sorted +*/ + +static int comp_lines(const char **a, const char **b) +{ + return (strcmp(*a,*b)); +} + +void dynstr_append_sorted(DYNAMIC_STRING* ds, DYNAMIC_STRING *ds_input, + bool keep_header) +{ + unsigned i; + char *start= ds_input->str; + DYNAMIC_ARRAY lines; + DBUG_ENTER("dynstr_append_sorted"); + + if (!*start) + DBUG_VOID_RETURN; /* No input */ + + my_init_dynamic_array(PSI_NOT_INSTRUMENTED, &lines, sizeof(const char*), 32, 32, MYF(0)); + + if (keep_header) + { + /* First line is result header, skip past it */ + while (*start && *start != '\n') + start++; + start++; /* Skip past \n */ + dynstr_append_mem(ds, ds_input->str, start - ds_input->str); + } + + /* Insert line(s) in array */ + while (*start) + { + char* line_end= (char*)start; + + /* Find end of line */ + while (*line_end && *line_end != '\n') + line_end++; + *line_end= 0; + + /* Insert pointer to the line in array */ + if (insert_dynamic(&lines, &start)) + die("Out of memory inserting lines to sort"); + + start= line_end+1; + } + + /* Sort array */ + qsort(lines.buffer, lines.elements, + sizeof(uchar *), (qsort_cmp)comp_lines); + + /* Create new result */ + for (i= 0; i < lines.elements ; i++) + { + const char **line= dynamic_element(&lines, i, const char**); + dynstr_append(ds, *line); + dynstr_append(ds, "\n"); + } + + delete_dynamic(&lines); + DBUG_VOID_RETURN; +} + +#ifndef HAVE_SETENV +static int setenv(const char *name, const char *value, int overwrite) +{ + size_t buflen= strlen(name) + strlen(value) + 2; + char *envvar= (char *)malloc(buflen); + if(!envvar) + return ENOMEM; + + snprintf(envvar, buflen, "%s=%s", name, value); + putenv(envvar); + return 0; +} +#endif + +/* + for the purpose of testing (see dialog.test) + we replace default mysql_authentication_dialog_ask function with the one, + that always reads from stdin with explicit echo. + +*/ +extern "C" +#ifdef _MSC_VER +__declspec(dllexport) +#endif +char *mysql_authentication_dialog_ask(MYSQL *mysql, int type, + const char *prompt, + char *buf, int buf_len) +{ + char *s=buf; + + fputs(prompt, stdout); + fputs(" ", stdout); + + if (!fgets(buf, buf_len-1, stdin)) + buf[0]= 0; + else if (buf[0] && (s= strend(buf))[-1] == '\n') + s[-1]= 0; + + for (s= buf; *s; s++) + fputc(type == 2 ? '*' : *s, stdout); + + fputc('\n', stdout); + + return buf; +} + +/* + Enable optimizer trace for the next command +*/ + +LEX_CSTRING enable_optimizer_trace_query= +{ + STRING_WITH_LEN("set @mysqltest_save_optimzer_trace=@@optimizer_trace,@@optimizer_trace=\"enabled=on\"") +}; + +LEX_CSTRING restore_optimizer_trace_query= +{ + STRING_WITH_LEN("set @@optimizer_trace=@mysqltest_save_optimzer_trace") +}; + + +LEX_CSTRING display_optimizer_trace_query +{ + STRING_WITH_LEN("SELECT * from information_schema.optimizer_trace") +}; + + +void enable_optimizer_trace(struct st_connection *con) +{ + MYSQL *mysql= con->mysql; + my_bool save_ps_protocol_enabled= ps_protocol_enabled; + my_bool save_view_protocol_enabled= view_protocol_enabled; + DYNAMIC_STRING ds_result; + DYNAMIC_STRING ds_warnings; + struct st_command command; + DBUG_ENTER("enable_optimizer_trace"); + + if (!mysql) + DBUG_VOID_RETURN; + ps_protocol_enabled= view_protocol_enabled= 0; + + init_dynamic_string(&ds_result, NULL, 0, 256); + init_dynamic_string(&ds_warnings, NULL, 0, 256); + bzero(&command, sizeof(command)); + + run_query_normal(con, &command, QUERY_SEND_FLAG | QUERY_REAP_FLAG, + enable_optimizer_trace_query.str, + enable_optimizer_trace_query.length, + &ds_result, &ds_warnings); + dynstr_free(&ds_result); + dynstr_free(&ds_warnings); + ps_protocol_enabled= save_ps_protocol_enabled; + view_protocol_enabled= save_view_protocol_enabled; + optimizer_trace_active= 1; + DBUG_VOID_RETURN; +} + + +void display_optimizer_trace(struct st_connection *con, + DYNAMIC_STRING *ds) +{ + my_bool save_ps_protocol_enabled= ps_protocol_enabled; + my_bool save_view_protocol_enabled= view_protocol_enabled; + DYNAMIC_STRING ds_result; + DYNAMIC_STRING ds_warnings; + struct st_command command; + DBUG_ENTER("display_optimizer_trace"); + + if (!optimizer_trace_active) + DBUG_VOID_RETURN; + + optimizer_trace_active= 0; + ps_protocol_enabled= view_protocol_enabled= 0; + + init_dynamic_string(&ds_result, NULL, 0, 256); + init_dynamic_string(&ds_warnings, NULL, 0, 256); + bzero(&command, sizeof(command)); + + run_query_normal(con, &command, QUERY_SEND_FLAG | QUERY_REAP_FLAG, + display_optimizer_trace_query.str, + display_optimizer_trace_query.length, + ds, &ds_warnings); + run_query_normal(con, &command, QUERY_SEND_FLAG | QUERY_REAP_FLAG, + restore_optimizer_trace_query.str, + restore_optimizer_trace_query.length, + ds, &ds_warnings); + dynstr_free(&ds_result); + dynstr_free(&ds_warnings); + ps_protocol_enabled= save_ps_protocol_enabled; + view_protocol_enabled= save_view_protocol_enabled; + DBUG_VOID_RETURN; +} |