You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
11053 lines
349 KiB
11053 lines
349 KiB
// Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License, version 2.0,
|
|
// as published by the Free Software Foundation.
|
|
//
|
|
// This program is also distributed with certain software (including
|
|
// but not limited to OpenSSL) that is licensed under separate terms,
|
|
// as designated in a particular file or component or in included license
|
|
// documentation. The authors of MySQL hereby grant you an additional
|
|
// permission to link the program and your derivative works with the
|
|
// separately licensed software that they have included with MySQL.
|
|
//
|
|
// 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, version 2.0, 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-1301 USA.
|
|
|
|
/// @file
|
|
///
|
|
/// mysqltest client - Tool used for executing a .test file.
|
|
///
|
|
/// See @ref PAGE_MYSQL_TEST_RUN "The MySQL Test Framework" for more
|
|
/// information.
|
|
|
|
#include "client/mysqltest/error_names.h"
|
|
#include "client/mysqltest/expected_errors.h"
|
|
#include "client/mysqltest/expected_warnings.h"
|
|
#include "client/mysqltest/logfile.h"
|
|
#include "client/mysqltest/regular_expressions.h"
|
|
#include "client/mysqltest/secondary_engine.h"
|
|
#include "client/mysqltest/utils.h"
|
|
#include "compression.h"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cmath> // std::isinf
|
|
#include <limits>
|
|
#include <new>
|
|
#include <sstream>
|
|
#ifdef _WIN32
|
|
#include <thread> // std::thread
|
|
#endif
|
|
|
|
#include <assert.h>
|
|
#if defined MY_MSCRT_DEBUG || defined _WIN32
|
|
#include <crtdbg.h>
|
|
#endif
|
|
#ifdef _WIN32
|
|
#include <direct.h>
|
|
#endif
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <limits.h>
|
|
#include <mysql_async.h>
|
|
#include <mysql_version.h>
|
|
#include <mysqld_error.h>
|
|
#include <signal.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <sys/types.h>
|
|
#ifndef _WIN32
|
|
#include <poll.h>
|
|
#include <sys/time.h>
|
|
#include <sys/wait.h>
|
|
#endif
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
#include "caching_sha2_passwordopt-vars.h"
|
|
#include "client/client_priv.h"
|
|
#include "m_ctype.h"
|
|
#include "map_helpers.h"
|
|
#include "mf_wcomp.h" // wild_compare
|
|
#include "my_compiler.h"
|
|
#include "my_config.h"
|
|
#include "my_dbug.h"
|
|
#include "my_default.h"
|
|
#include "my_dir.h"
|
|
#include "my_inttypes.h"
|
|
#include "my_macros.h"
|
|
#include "my_pointer_arithmetic.h"
|
|
#include "my_stacktrace.h"
|
|
#include "my_systime.h" // my_sleep()
|
|
#include "my_thread_local.h"
|
|
#include "prealloced_array.h"
|
|
#include "print_version.h"
|
|
#include "sql_common.h"
|
|
#include "template_utils.h"
|
|
#include "typelib.h"
|
|
#include "violite.h"
|
|
#include "welcome_copyright_notice.h" // ORACLE_WELCOME_COPYRIGHT_NOTICE
|
|
|
|
#ifdef _WIN32
|
|
#define SIGNAL_FMT "exception 0x%x"
|
|
#else
|
|
#define SIGNAL_FMT "signal %d"
|
|
#endif
|
|
|
|
#ifdef _WIN32
|
|
#define setenv(a, b, c) _putenv_s(a, b)
|
|
#define popen _popen
|
|
#define pclose _pclose
|
|
#endif
|
|
|
|
#define MAX_VAR_NAME_LENGTH 256
|
|
#define MAX_COLUMNS 256
|
|
#define MAX_DELIMITER_LENGTH 16
|
|
#define DEFAULT_MAX_CONN 128
|
|
#define REPLACE_ROUND_MAX 16
|
|
|
|
/* Flags controlling send and reap */
|
|
#define QUERY_SEND_FLAG 1
|
|
#define QUERY_REAP_FLAG 2
|
|
|
|
#define APPEND_TYPE(type) \
|
|
{ \
|
|
dynstr_append(ds, "-- "); \
|
|
switch (type) { \
|
|
case SESSION_TRACK_SYSTEM_VARIABLES: \
|
|
dynstr_append(ds, "Tracker : SESSION_TRACK_SYSTEM_VARIABLES\n"); \
|
|
break; \
|
|
case SESSION_TRACK_SCHEMA: \
|
|
dynstr_append(ds, "Tracker : SESSION_TRACK_SCHEMA\n"); \
|
|
break; \
|
|
case SESSION_TRACK_STATE_CHANGE: \
|
|
dynstr_append(ds, "Tracker : SESSION_TRACK_STATE_CHANGE\n"); \
|
|
break; \
|
|
case SESSION_TRACK_GTIDS: \
|
|
dynstr_append(ds, "Tracker : SESSION_TRACK_GTIDS\n"); \
|
|
break; \
|
|
case SESSION_TRACK_TRANSACTION_CHARACTERISTICS: \
|
|
dynstr_append( \
|
|
ds, "Tracker : SESSION_TRACK_TRANSACTION_CHARACTERISTICS\n"); \
|
|
break; \
|
|
case SESSION_TRACK_TRANSACTION_STATE: \
|
|
dynstr_append(ds, "Tracker : SESSION_TRACK_TRANSACTION_STATE\n"); \
|
|
break; \
|
|
default: \
|
|
dynstr_append(ds, "\n"); \
|
|
} \
|
|
}
|
|
|
|
extern CHARSET_INFO my_charset_utf16le_bin;
|
|
|
|
// List of error codes specified with 'error' command.
|
|
Expected_errors *expected_errors = new Expected_errors();
|
|
|
|
// List of warnings disabled with 'disable_warnings' command.
|
|
Expected_warnings *disabled_warnings = new Expected_warnings();
|
|
|
|
// List of warnings enabled with 'enable_warnings' command.
|
|
Expected_warnings *enabled_warnings = new Expected_warnings();
|
|
|
|
enum {
|
|
OPT_COLORED_DIFF = OPT_MAX_CLIENT_OPTION,
|
|
OPT_CURSOR_PROTOCOL,
|
|
OPT_EXPLAIN_PROTOCOL,
|
|
OPT_JSON_EXPLAIN_PROTOCOL,
|
|
OPT_LOG_DIR,
|
|
OPT_MARK_PROGRESS,
|
|
OPT_MAX_CONNECT_RETRIES,
|
|
OPT_MAX_CONNECTIONS,
|
|
OPT_NO_SKIP,
|
|
OPT_OFFLOAD_COUNT_FILE,
|
|
OPT_PS_PROTOCOL,
|
|
OPT_RESULT_FORMAT_VERSION,
|
|
#ifdef _WIN32
|
|
OPT_SAFEPROCESS_PID,
|
|
#endif
|
|
OPT_SP_PROTOCOL,
|
|
OPT_TAIL_LINES,
|
|
OPT_TRACE_EXEC,
|
|
OPT_TRACE_PROTOCOL,
|
|
OPT_VIEW_PROTOCOL,
|
|
};
|
|
|
|
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 *excluded_string = 0;
|
|
static char *shared_memory_base_name = 0;
|
|
const char *opt_logdir = "";
|
|
const char *opt_include = 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 bool opt_colored_diff = 0;
|
|
static bool opt_compress = 0, silent = 0, verbose = 0, trace_exec = 0;
|
|
static bool debug_info_flag = 0, debug_check_flag = 0;
|
|
static bool tty_password = 0;
|
|
static bool opt_mark_progress = 0;
|
|
static bool ps_protocol = 0, ps_protocol_enabled = 0;
|
|
static bool sp_protocol = 0, sp_protocol_enabled = 0;
|
|
static bool no_skip = 0;
|
|
static bool view_protocol = 0, view_protocol_enabled = 0;
|
|
static bool opt_trace_protocol = 0, opt_trace_protocol_enabled = 0;
|
|
static bool explain_protocol = 0, explain_protocol_enabled = 0;
|
|
static bool json_explain_protocol = 0, json_explain_protocol_enabled = 0;
|
|
static bool cursor_protocol = 0, cursor_protocol_enabled = 0;
|
|
static bool testcase_disabled = 0;
|
|
static bool display_result_vertically = false, display_result_lower = false,
|
|
display_metadata = false, display_result_sorted = false,
|
|
display_session_track_info = false;
|
|
static int start_sort_column = 0;
|
|
static bool disable_query_log = 0, disable_result_log = 0;
|
|
static bool disable_connect_log = 1;
|
|
static bool disable_warnings = 0;
|
|
static bool disable_info = 1;
|
|
static bool abort_on_error = 1;
|
|
static bool server_initialized = 0;
|
|
static bool is_windows = 0;
|
|
static MEM_ROOT argv_alloc{PSI_NOT_INSTRUMENTED, 512};
|
|
static const char *load_default_groups[] = {"mysqltest", "client", 0};
|
|
static char line_buffer[MAX_DELIMITER_LENGTH], *line_buffer_pos = line_buffer;
|
|
static bool can_handle_expired_passwords = true;
|
|
|
|
/*
|
|
These variables control the behavior of the asynchronous operations for
|
|
mysqltest client. If --async-client is specified, use_async_client is true.
|
|
Each command checks enable_async_client (which can be forced off or
|
|
disabled for a single command) to decide the mode it uses to run.
|
|
*/
|
|
static bool use_async_client = false;
|
|
static bool enable_async_client = false;
|
|
|
|
// Secondary engine options
|
|
static const char *opt_offload_count_file;
|
|
|
|
static Secondary_engine *secondary_engine = nullptr;
|
|
|
|
static uint opt_zstd_compress_level = default_zstd_compression_level;
|
|
static char *opt_compress_algorithm = nullptr;
|
|
|
|
#ifdef _WIN32
|
|
static DWORD opt_safe_process_pid;
|
|
static HANDLE mysqltest_thread;
|
|
// Event handle for stacktrace request event
|
|
static HANDLE stacktrace_request_event = NULL;
|
|
static std::thread wait_for_stacktrace_request_event_thread;
|
|
#endif
|
|
|
|
Logfile log_file;
|
|
|
|
/// Info on properties that can be set with '--disable_X' and
|
|
/// '--disable_X' commands.
|
|
struct Property {
|
|
bool *var; // Actual variable
|
|
bool set; // Has been set for ONCE command
|
|
bool old; // If set, thus is the old value
|
|
bool reverse; // Variable is true if disabled
|
|
const char *env_name; // Environment variable name
|
|
};
|
|
|
|
static struct Property prop_list[] = {
|
|
{&abort_on_error, 0, 1, 0, "$ENABLE_ABORT_ON_ERROR"},
|
|
{&disable_connect_log, 0, 1, 1, "$ENABLE_CONNECT_LOG"},
|
|
{&disable_info, 0, 1, 1, "$ENABLE_INFO"},
|
|
{&display_session_track_info, 0, 1, 1, "$ENABLE_STATE_CHANGE_INFO"},
|
|
{&display_metadata, 0, 0, 0, "$ENABLE_METADATA"},
|
|
{&ps_protocol_enabled, 0, 0, 0, "$ENABLE_PS_PROTOCOL"},
|
|
{&disable_query_log, 0, 0, 1, "$ENABLE_QUERY_LOG"},
|
|
{&disable_result_log, 0, 0, 1, "$ENABLE_RESULT_LOG"},
|
|
{&disable_warnings, 0, 0, 1, "$ENABLE_WARNINGS"},
|
|
{&enable_async_client, 0, 0, 0, "$ENABLE_ASYNC_CLIENT"}};
|
|
|
|
static bool once_property = false;
|
|
|
|
enum enum_prop {
|
|
P_ABORT = 0,
|
|
P_CONNECT,
|
|
P_INFO,
|
|
P_SESSION_TRACK,
|
|
P_META,
|
|
P_PS,
|
|
P_QUERY,
|
|
P_RESULT,
|
|
P_WARN,
|
|
P_ASYNC,
|
|
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 char delimiter[MAX_DELIMITER_LENGTH] = ";";
|
|
static size_t delimiter_length = 1;
|
|
|
|
static char TMPDIR[FN_REFLEN];
|
|
|
|
/* Block stack */
|
|
enum block_cmd { cmd_none, cmd_if, cmd_while };
|
|
|
|
struct st_block {
|
|
int line; /* Start line of block */
|
|
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 const char *default_charset = MYSQL_DEFAULT_CHARSET_NAME;
|
|
CHARSET_INFO *charset_info =
|
|
&my_charset_utf8mb4_0900_ai_ci; /* Default charset */
|
|
|
|
/*
|
|
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 char *opt_plugin_dir = 0;
|
|
|
|
/* To retrieve a filename from a filepath */
|
|
const char *get_filename_from_path(const char *path) {
|
|
const char *fname = NULL;
|
|
if (is_windows)
|
|
fname = strrchr(path, '\\');
|
|
else
|
|
fname = strrchr(path, '/');
|
|
if (fname == NULL)
|
|
return path;
|
|
else
|
|
return ++fname;
|
|
}
|
|
|
|
static uint opt_protocol = 0;
|
|
|
|
#if defined(_WIN32)
|
|
static uint opt_protocol_for_default_connection = MYSQL_PROTOCOL_PIPE;
|
|
#endif
|
|
|
|
struct st_command;
|
|
typedef Prealloced_array<st_command *, 1024> Q_lines;
|
|
Q_lines *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];
|
|
|
|
struct var_free {
|
|
void operator()(VAR *var) const;
|
|
};
|
|
|
|
collation_unordered_map<std::string, std::unique_ptr<VAR, var_free>> *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;
|
|
/* Set after send to disallow other queries before reap */
|
|
bool pending;
|
|
};
|
|
|
|
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_EXPR,
|
|
Q_WHILE,
|
|
Q_END_BLOCK,
|
|
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_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_ASYNC_CLIENT,
|
|
Q_DISABLE_ASYNC_CLIENT,
|
|
Q_EXEC,
|
|
Q_EXECW,
|
|
Q_EXEC_BACKGROUND,
|
|
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_PARTIALLY_SORTED_RESULT,
|
|
Q_LOWERCASE,
|
|
Q_START_TIMER,
|
|
Q_END_TIMER,
|
|
Q_CHARACTER_SET,
|
|
Q_DISABLE_PS_PROTOCOL,
|
|
Q_ENABLE_PS_PROTOCOL,
|
|
Q_DISABLE_RECONNECT,
|
|
Q_ENABLE_RECONNECT,
|
|
Q_IF,
|
|
Q_DISABLE_TESTCASE,
|
|
Q_ENABLE_TESTCASE,
|
|
Q_REPLACE_REGEX,
|
|
Q_REPLACE_NUMERIC_ROUND,
|
|
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_FORCE_RMDIR,
|
|
Q_FORCE_CPDIR,
|
|
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_COPY_FILES_WILDCARD,
|
|
Q_SEND_EVAL,
|
|
Q_OUTPUT, /* redirect output to a file */
|
|
Q_RESET_CONNECTION,
|
|
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", "expr", "while", "end",
|
|
"save_master_pos", "sync_with_master", "sync_slave_with_master", "error",
|
|
"send", "reap", "dirty_close", "replace_result", "replace_column", "ping",
|
|
"eval",
|
|
/* 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_async_client",
|
|
"disable_async_client", "exec", "execw", "exec_in_background", "delimiter",
|
|
"disable_abort_on_error", "enable_abort_on_error", "vertical_results",
|
|
"horizontal_results", "query_vertical", "query_horizontal", "sorted_result",
|
|
"partially_sorted_result", "lowercase_result", "start_timer", "end_timer",
|
|
"character_set", "disable_ps_protocol", "enable_ps_protocol",
|
|
"disable_reconnect", "enable_reconnect", "if", "disable_testcase",
|
|
"enable_testcase", "replace_regex", "replace_numeric_round", "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", "force-rmdir", "force-cpdir",
|
|
"list_files", "list_files_write_file", "list_files_append_file",
|
|
"send_shutdown", "shutdown_server", "result_format", "move_file",
|
|
"remove_files_wildcard", "copy_files_wildcard", "send_eval", "output",
|
|
"reset_connection",
|
|
|
|
0};
|
|
|
|
struct st_command {
|
|
char *query, *query_buf, *first_argument, *last_argument, *end;
|
|
DYNAMIC_STRING content;
|
|
size_t first_word_len, query_len;
|
|
bool abort_on_error, used_replace;
|
|
char output_file[FN_REFLEN];
|
|
enum enum_commands type;
|
|
// Line number of the command
|
|
uint lineno;
|
|
};
|
|
|
|
TYPELIB command_typelib = {array_elements(command_names), "", command_names, 0};
|
|
|
|
DYNAMIC_STRING ds_res;
|
|
DYNAMIC_STRING ds_result;
|
|
/* 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];
|
|
|
|
/* Stores regex substitutions */
|
|
|
|
struct st_replace_regex *glob_replace_regex = 0;
|
|
|
|
struct REPLACE;
|
|
REPLACE *glob_replace = 0;
|
|
void replace_strings_append(REPLACE *rep, DYNAMIC_STRING *ds, const char *from,
|
|
size_t len);
|
|
|
|
static void cleanup_and_exit(int exit_code) MY_ATTRIBUTE((noreturn));
|
|
|
|
void die(const char *fmt, ...) MY_ATTRIBUTE((format(printf, 1, 2)))
|
|
MY_ATTRIBUTE((noreturn));
|
|
void abort_not_supported_test(const char *fmt, ...)
|
|
MY_ATTRIBUTE((format(printf, 1, 2))) MY_ATTRIBUTE((noreturn));
|
|
void verbose_msg(const char *fmt, ...) MY_ATTRIBUTE((format(printf, 1, 2)));
|
|
void log_msg(const char *fmt, ...) MY_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, bool raw,
|
|
bool ignore_not_existing);
|
|
void eval_expr(VAR *v, const char *p, const char **p_end, bool open_end = false,
|
|
bool do_eval = true);
|
|
bool match_delimiter(int c, const char *delim, size_t length);
|
|
|
|
void do_eval(DYNAMIC_STRING *query_eval, const char *query,
|
|
const char *query_end, 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, bool append);
|
|
|
|
void fix_win_paths(const char *val, size_t len);
|
|
|
|
#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();
|
|
|
|
/* For replace numeric round */
|
|
static int glob_replace_numeric_round = -1;
|
|
void do_get_replace_numeric_round(struct st_command *command);
|
|
void free_replace_numeric_round();
|
|
void replace_numeric_round_append(int round, DYNAMIC_STRING *ds,
|
|
const char *from, size_t len);
|
|
|
|
/* Used by sleep */
|
|
void check_eol_junk_line(const char *eol);
|
|
|
|
static void var_set(const char *var_name, const char *var_name_end,
|
|
const char *var_val, const char *var_val_end);
|
|
|
|
static void free_all_replace() {
|
|
free_replace();
|
|
free_replace_regex();
|
|
free_replace_column();
|
|
free_replace_numeric_round();
|
|
}
|
|
|
|
/*
|
|
To run tests via the async API, invoke mysqltest with --async-client.
|
|
*/
|
|
class AsyncTimer {
|
|
public:
|
|
explicit AsyncTimer(std::string label)
|
|
: label_(label), time_(std::chrono::system_clock::now()), start_(time_) {}
|
|
|
|
~AsyncTimer() {
|
|
auto now = std::chrono::system_clock::now();
|
|
auto delta = now - start_;
|
|
ulonglong MY_ATTRIBUTE((unused)) micros =
|
|
std::chrono::duration_cast<std::chrono::microseconds>(delta).count();
|
|
DBUG_PRINT("async_timing",
|
|
("%s total micros: %llu", label_.c_str(), micros));
|
|
}
|
|
|
|
void check() {
|
|
auto now = std::chrono::system_clock::now();
|
|
auto delta = now - time_;
|
|
time_ = now;
|
|
ulonglong MY_ATTRIBUTE((unused)) micros =
|
|
std::chrono::duration_cast<std::chrono::microseconds>(delta).count();
|
|
DBUG_PRINT("async_timing", ("%s op micros: %llu", label_.c_str(), micros));
|
|
}
|
|
|
|
private:
|
|
std::string label_;
|
|
std::chrono::system_clock::time_point time_;
|
|
std::chrono::system_clock::time_point start_;
|
|
};
|
|
|
|
#ifdef _WIN32
|
|
/*
|
|
Check if any data is available in the socket to be read or written.
|
|
*/
|
|
static int socket_event_listen(net_async_block_state async_blocking_state,
|
|
my_socket fd) {
|
|
int result;
|
|
fd_set readfds, writefds, exceptfds;
|
|
|
|
FD_ZERO(&readfds);
|
|
FD_ZERO(&writefds);
|
|
FD_ZERO(&exceptfds);
|
|
|
|
FD_SET(fd, &exceptfds);
|
|
|
|
switch (async_blocking_state) {
|
|
case NET_NONBLOCKING_READ:
|
|
FD_SET(fd, &readfds);
|
|
break;
|
|
case NET_NONBLOCKING_WRITE:
|
|
case NET_NONBLOCKING_CONNECT:
|
|
FD_SET(fd, &writefds);
|
|
break;
|
|
default:
|
|
DBUG_ASSERT(false);
|
|
}
|
|
result = select((int)(fd + 1), &readfds, &writefds, &exceptfds, NULL);
|
|
if (result < 0) {
|
|
perror("select");
|
|
}
|
|
return result;
|
|
}
|
|
#else
|
|
static int socket_event_listen(net_async_block_state async_blocking_state,
|
|
my_socket fd) {
|
|
int result;
|
|
pollfd pfd;
|
|
pfd.fd = fd;
|
|
switch (async_blocking_state) {
|
|
case NET_NONBLOCKING_READ:
|
|
pfd.events = POLLIN;
|
|
break;
|
|
case NET_NONBLOCKING_WRITE:
|
|
pfd.events = POLLOUT;
|
|
break;
|
|
case NET_NONBLOCKING_CONNECT:
|
|
pfd.events = POLLIN | POLLOUT;
|
|
break;
|
|
default:
|
|
DBUG_ASSERT(false);
|
|
}
|
|
result = poll(&pfd, 1, -1);
|
|
if (result < 0) {
|
|
perror("poll");
|
|
}
|
|
return result;
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
Below async_mysql_*_wrapper functions are used to measure how much time
|
|
each nonblocking call spends before completing the operations.
|
|
i*/
|
|
static MYSQL_ROW async_mysql_fetch_row_wrapper(MYSQL_RES *res) {
|
|
MYSQL_ROW row;
|
|
MYSQL *mysql = res->handle;
|
|
AsyncTimer t(__func__);
|
|
while (mysql_fetch_row_nonblocking(res, &row) == NET_ASYNC_NOT_READY) {
|
|
t.check();
|
|
NET_ASYNC *net_async = NET_ASYNC_DATA((&(mysql->net)));
|
|
int result = socket_event_listen(net_async->async_blocking_state,
|
|
mysql_get_socket_descriptor(mysql));
|
|
if (result == -1) return NULL;
|
|
}
|
|
return row;
|
|
}
|
|
|
|
static MYSQL_RES *async_mysql_store_result_wrapper(MYSQL *mysql) {
|
|
MYSQL_RES *result;
|
|
AsyncTimer t(__func__);
|
|
while (mysql_store_result_nonblocking(mysql, &result) ==
|
|
NET_ASYNC_NOT_READY) {
|
|
t.check();
|
|
NET_ASYNC *net_async = NET_ASYNC_DATA(&(mysql->net));
|
|
int result = socket_event_listen(net_async->async_blocking_state,
|
|
mysql_get_socket_descriptor(mysql));
|
|
if (result == -1) return NULL;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static int async_mysql_real_query_wrapper(MYSQL *mysql, const char *query,
|
|
ulong length) {
|
|
net_async_status status;
|
|
AsyncTimer t(__func__);
|
|
while ((status = mysql_real_query_nonblocking(mysql, query, length)) ==
|
|
NET_ASYNC_NOT_READY) {
|
|
t.check();
|
|
NET_ASYNC *net_async = NET_ASYNC_DATA(&(mysql->net));
|
|
int result = socket_event_listen(net_async->async_blocking_state,
|
|
mysql_get_socket_descriptor(mysql));
|
|
if (result == -1) return 1;
|
|
}
|
|
if (status == NET_ASYNC_ERROR) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int async_mysql_send_query_wrapper(MYSQL *mysql, const char *query,
|
|
ulong length) {
|
|
net_async_status status;
|
|
ASYNC_DATA(mysql)->async_query_length = length;
|
|
ASYNC_DATA(mysql)->async_query_state = QUERY_SENDING;
|
|
|
|
AsyncTimer t(__func__);
|
|
while ((status = mysql_send_query_nonblocking(mysql, query, length)) ==
|
|
NET_ASYNC_NOT_READY) {
|
|
t.check();
|
|
NET_ASYNC *net_async = NET_ASYNC_DATA(&(mysql->net));
|
|
int result = socket_event_listen(net_async->async_blocking_state,
|
|
mysql_get_socket_descriptor(mysql));
|
|
if (result == -1) return 1;
|
|
}
|
|
if (status == NET_ASYNC_ERROR) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static bool async_mysql_read_query_result_wrapper(MYSQL *mysql) {
|
|
net_async_status status;
|
|
AsyncTimer t(__func__);
|
|
while ((status = (*mysql->methods->read_query_result_nonblocking)(mysql)) ==
|
|
NET_ASYNC_NOT_READY) {
|
|
t.check();
|
|
NET_ASYNC *net_async = NET_ASYNC_DATA(&(mysql->net));
|
|
int result = socket_event_listen(net_async->async_blocking_state,
|
|
mysql_get_socket_descriptor(mysql));
|
|
if (result == -1) return true;
|
|
}
|
|
if (status == NET_ASYNC_ERROR) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static int async_mysql_next_result_wrapper(MYSQL *mysql) {
|
|
net_async_status status;
|
|
AsyncTimer t(__func__);
|
|
while ((status = mysql_next_result_nonblocking(mysql)) ==
|
|
NET_ASYNC_NOT_READY) {
|
|
t.check();
|
|
NET_ASYNC *net_async = NET_ASYNC_DATA(&(mysql->net));
|
|
int result = socket_event_listen(net_async->async_blocking_state,
|
|
mysql_get_socket_descriptor(mysql));
|
|
if (result == -1) return 1;
|
|
}
|
|
if (status == NET_ASYNC_ERROR)
|
|
return 1;
|
|
else if (status == NET_ASYNC_COMPLETE_NO_MORE_RESULTS)
|
|
return -1;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
static MYSQL *async_mysql_real_connect_wrapper(
|
|
MYSQL *mysql, const char *host, const char *user, const char *passwd,
|
|
const char *db, uint port, const char *unix_socket, ulong client_flag) {
|
|
net_async_status status;
|
|
AsyncTimer t(__func__);
|
|
|
|
while ((status = mysql_real_connect_nonblocking(
|
|
mysql, host, user, passwd, db, port, unix_socket, client_flag)) ==
|
|
NET_ASYNC_NOT_READY) {
|
|
t.check();
|
|
NET_ASYNC *net_async = NET_ASYNC_DATA(&(mysql->net));
|
|
socket_event_listen(net_async->async_blocking_state,
|
|
mysql_get_socket_descriptor(mysql));
|
|
}
|
|
if (status == NET_ASYNC_ERROR)
|
|
return nullptr;
|
|
else
|
|
return mysql;
|
|
}
|
|
|
|
static int async_mysql_query_wrapper(MYSQL *mysql, const char *query) {
|
|
net_async_status status;
|
|
AsyncTimer t(__func__);
|
|
while ((status = mysql_real_query_nonblocking(mysql, query, strlen(query))) ==
|
|
NET_ASYNC_NOT_READY) {
|
|
t.check();
|
|
NET_ASYNC *net_async = NET_ASYNC_DATA(&(mysql->net));
|
|
int result = socket_event_listen(net_async->async_blocking_state,
|
|
mysql_get_socket_descriptor(mysql));
|
|
if (result == -1) return 1;
|
|
}
|
|
if (status == NET_ASYNC_ERROR) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void async_mysql_free_result_wrapper(MYSQL_RES *result) {
|
|
AsyncTimer t(__func__);
|
|
while (mysql_free_result_nonblocking(result) == NET_ASYNC_NOT_READY) {
|
|
t.check();
|
|
MYSQL *mysql = result->handle;
|
|
NET_ASYNC *net_async = NET_ASYNC_DATA(&(mysql->net));
|
|
int result = socket_event_listen(net_async->async_blocking_state,
|
|
mysql_get_socket_descriptor(mysql));
|
|
if (result == -1) return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
/*
|
|
Below are the wrapper functions which are defined on top of standard C APIs
|
|
to make a decision on whether to call blocking or non blocking API based on
|
|
--async-client option is set or not.
|
|
*/
|
|
static MYSQL_ROW mysql_fetch_row_wrapper(MYSQL_RES *res) {
|
|
if (enable_async_client)
|
|
return async_mysql_fetch_row_wrapper(res);
|
|
else
|
|
return mysql_fetch_row(res);
|
|
}
|
|
|
|
static MYSQL_RES *mysql_store_result_wrapper(MYSQL *mysql) {
|
|
if (enable_async_client)
|
|
return async_mysql_store_result_wrapper(mysql);
|
|
else
|
|
return mysql_store_result(mysql);
|
|
}
|
|
|
|
static int mysql_real_query_wrapper(MYSQL *mysql, const char *query,
|
|
ulong length) {
|
|
if (enable_async_client)
|
|
return async_mysql_real_query_wrapper(mysql, query, length);
|
|
else
|
|
return mysql_real_query(mysql, query, length);
|
|
}
|
|
|
|
static int mysql_send_query_wrapper(MYSQL *mysql, const char *query,
|
|
ulong length) {
|
|
if (enable_async_client)
|
|
return async_mysql_send_query_wrapper(mysql, query, length);
|
|
else
|
|
return mysql_send_query(mysql, query, length);
|
|
}
|
|
|
|
static bool mysql_read_query_result_wrapper(MYSQL *mysql) {
|
|
bool ret;
|
|
if (enable_async_client)
|
|
ret = async_mysql_read_query_result_wrapper(mysql);
|
|
else
|
|
ret = mysql_read_query_result(mysql);
|
|
return ret;
|
|
}
|
|
|
|
static int mysql_query_wrapper(MYSQL *mysql, const char *query) {
|
|
if (enable_async_client)
|
|
return async_mysql_query_wrapper(mysql, query);
|
|
else
|
|
return mysql_query(mysql, query);
|
|
}
|
|
|
|
static int mysql_next_result_wrapper(MYSQL *mysql) {
|
|
if (enable_async_client)
|
|
return async_mysql_next_result_wrapper(mysql);
|
|
else
|
|
return mysql_next_result(mysql);
|
|
}
|
|
|
|
static MYSQL *mysql_real_connect_wrapper(MYSQL *mysql, const char *host,
|
|
const char *user, const char *passwd,
|
|
const char *db, uint port,
|
|
const char *unix_socket,
|
|
ulong client_flag) {
|
|
if (enable_async_client)
|
|
return async_mysql_real_connect_wrapper(mysql, host, user, passwd, db, port,
|
|
unix_socket, client_flag);
|
|
else
|
|
return mysql_real_connect(mysql, host, user, passwd, db, port, unix_socket,
|
|
client_flag);
|
|
}
|
|
|
|
static void mysql_free_result_wrapper(MYSQL_RES *result) {
|
|
if (enable_async_client)
|
|
return async_mysql_free_result_wrapper(result);
|
|
else
|
|
return mysql_free_result(result);
|
|
}
|
|
|
|
/* async client test code (end) */
|
|
|
|
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,
|
|
int start_sort_column);
|
|
|
|
void revert_properties();
|
|
|
|
void do_eval(DYNAMIC_STRING *query_eval, const char *query,
|
|
const char *query_end, bool pass_through_escape_chars) {
|
|
const char *p;
|
|
char c, next_c;
|
|
int escaped = 0;
|
|
VAR *v;
|
|
DBUG_TRACE;
|
|
|
|
for (p = query; (c = *p) && p < query_end; ++p) {
|
|
next_c = *(p + 1);
|
|
switch (c) {
|
|
case '$':
|
|
if (escaped ||
|
|
// a JSON path expression
|
|
next_c == '.' || next_c == '[' || next_c == '\'' || next_c == '"') {
|
|
escaped = 0;
|
|
dynstr_append_mem(query_eval, p, 1);
|
|
} else {
|
|
if (!(v = var_get(p, &p, 0, 0))) die("Bad variable in eval");
|
|
dynstr_append_mem(query_eval, v->str_val, v->str_val_len);
|
|
}
|
|
break;
|
|
case '\\':
|
|
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;
|
|
}
|
|
}
|
|
#ifdef _WIN32
|
|
fix_win_paths(query_eval->str, query_eval->length);
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
Run query and dump the result to stderr in vertical format
|
|
|
|
NOTE! This function should be safe to call when an error
|
|
has occurred and thus any further errors will be ignored(although logged)
|
|
|
|
SYNOPSIS
|
|
show_query
|
|
mysql - connection to use
|
|
query - query to run
|
|
|
|
*/
|
|
|
|
static void show_query(MYSQL *mysql, const char *query) {
|
|
MYSQL_RES *res;
|
|
DBUG_TRACE;
|
|
|
|
if (!mysql) return;
|
|
|
|
if (mysql_query_wrapper(mysql, query)) {
|
|
log_msg("Error running query '%s': %d %s", query, mysql_errno(mysql),
|
|
mysql_error(mysql));
|
|
return;
|
|
}
|
|
|
|
if ((res = mysql_store_result_wrapper(mysql)) == NULL) {
|
|
/* No result set returned */
|
|
return;
|
|
}
|
|
|
|
{
|
|
MYSQL_ROW row;
|
|
unsigned int i;
|
|
unsigned int row_num = 0;
|
|
unsigned int num_fields = mysql_num_fields(res);
|
|
MYSQL_FIELD *fields = mysql_fetch_fields(res);
|
|
|
|
fprintf(stderr, "=== %s ===\n", query);
|
|
while ((row = mysql_fetch_row_wrapper(res))) {
|
|
unsigned long *lengths = mysql_fetch_lengths(res);
|
|
row_num++;
|
|
|
|
fprintf(stderr, "---- %d. ----\n", row_num);
|
|
for (i = 0; i < num_fields; i++) {
|
|
/* looks ugly , but put here to convince parfait */
|
|
assert(lengths);
|
|
fprintf(stderr, "%s\t%.*s\n", fields[i].name, (int)lengths[i],
|
|
row[i] ? row[i] : "NULL");
|
|
}
|
|
}
|
|
for (i = 0; i < std::strlen(query) + 8; i++) fprintf(stderr, "=");
|
|
fprintf(stderr, "\n\n");
|
|
}
|
|
mysql_free_result_wrapper(res);
|
|
}
|
|
|
|
/*
|
|
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_TRACE;
|
|
|
|
if (!mysql) return;
|
|
|
|
if (mysql_query_wrapper(mysql, query)) {
|
|
log_msg("Error running query '%s': %d %s", query, mysql_errno(mysql),
|
|
mysql_error(mysql));
|
|
return;
|
|
}
|
|
|
|
if ((res = mysql_store_result_wrapper(mysql)) == NULL) {
|
|
/* No result set returned */
|
|
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_wrapper(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++) {
|
|
/* looks ugly , but put here to convince parfait */
|
|
assert(lengths);
|
|
fprintf(stderr, "%.*s ", (int)lengths[i], row[i] ? row[i] : "NULL");
|
|
}
|
|
fprintf(stderr, "\n");
|
|
}
|
|
}
|
|
mysql_free_result_wrapper(res);
|
|
}
|
|
|
|
enum arg_type { ARG_STRING, ARG_REST };
|
|
|
|
struct command_arg {
|
|
const char *argname; /* Name of argument */
|
|
enum arg_type type; /* Type of argument */
|
|
bool required; /* Argument required */
|
|
DYNAMIC_STRING *ds; /* Storage for argument */
|
|
const char *description; /* Description of the argument */
|
|
};
|
|
|
|
static void check_command_args(struct st_command *command, char *arguments,
|
|
const struct command_arg *args, int num_args,
|
|
const char delimiter_arg) {
|
|
int i;
|
|
char *ptr = arguments;
|
|
const char *start;
|
|
DBUG_TRACE;
|
|
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 = 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 '%.*s'", arg->argname,
|
|
static_cast<int>(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 '%.*s'", ptr,
|
|
static_cast<int>(command->first_word_len), command->query);
|
|
ptr++;
|
|
}
|
|
}
|
|
|
|
/// Check whether given error is in list of expected errors.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
/// @param err_errno Error number of the error that actually occurred.
|
|
/// @param err_sqlstate SQLSTATE that was thrown, or NULL for impossible
|
|
/// (file-ops, diff, etc.)
|
|
///
|
|
/// @retval -1 if the given error is not in the list, index in the
|
|
/// list of expected errors otherwise.
|
|
///
|
|
/// @note
|
|
/// If caller needs to know whether the list was empty, they should
|
|
/// check the value of expected_errors.count.
|
|
static int match_expected_error(struct st_command *command,
|
|
std::uint32_t err_errno,
|
|
const char *err_sqlstate) {
|
|
std::uint8_t index = 0;
|
|
|
|
// Iterator for list/vector of expected errors
|
|
std::vector<std::unique_ptr<Error>>::iterator error =
|
|
expected_errors->begin();
|
|
|
|
// Iterate over list of expected errors
|
|
for (; error != expected_errors->end(); error++) {
|
|
if ((*error)->type() == ERR_ERRNO) {
|
|
// Error type is ERR_ERRNO
|
|
if ((*error)->error_code() == err_errno) return index;
|
|
} else if ((*error)->type() == ERR_SQLSTATE) {
|
|
// Error type is ERR_SQLSTATE. NULL is quite likely, but not in
|
|
// conjunction with a SQL-state expect.
|
|
if (unlikely(err_sqlstate == NULL)) {
|
|
die("Expecting a SQLSTATE (%s) from query '%s' which cannot produce "
|
|
"one.",
|
|
(*error)->sqlstate(), command->query);
|
|
}
|
|
|
|
if (!std::strncmp((*error)->sqlstate(), err_sqlstate, SQLSTATE_LENGTH))
|
|
return index;
|
|
}
|
|
|
|
index++;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/// Handle errors which occurred during execution of a query.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
/// @param err_errno Error number
|
|
/// @param err_error Error message
|
|
/// @param err_sqlstate SQLSTATE that was thrown
|
|
/// @param ds Dynamic string to store the result.
|
|
///
|
|
/// @note
|
|
/// If there is an unexpected error, this function will abort mysqltest
|
|
/// immediately.
|
|
void handle_error(struct st_command *command, std::uint32_t err_errno,
|
|
const char *err_error, const char *err_sqlstate,
|
|
DYNAMIC_STRING *ds) {
|
|
DBUG_TRACE;
|
|
|
|
if (command->abort_on_error)
|
|
die("Query '%s' failed.\nERROR %d (%s): %s", command->query, err_errno,
|
|
err_sqlstate, err_error);
|
|
|
|
DBUG_PRINT("info", ("Expected errors count: %zu", expected_errors->count()));
|
|
|
|
int i = match_expected_error(command, err_errno, err_sqlstate);
|
|
|
|
if (i >= 0) {
|
|
if (!disable_result_log) {
|
|
if (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 (expected_errors->type() == ERR_SQLSTATE ||
|
|
(expected_errors->type() == ERR_ERRNO &&
|
|
expected_errors->error_code() != 0))
|
|
dynstr_append(ds, "Got one of the listed errors\n");
|
|
}
|
|
|
|
revert_properties();
|
|
return;
|
|
}
|
|
|
|
DBUG_PRINT("info",
|
|
("i: %d Expected errors count: %zu", i, 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 (expected_errors->count()) {
|
|
if (expected_errors->count() == 1) {
|
|
die("Query '%s' failed with wrong error %d: '%s', should have failed "
|
|
"with error '%s'.",
|
|
command->query, err_errno, err_error,
|
|
expected_errors->error_list().c_str());
|
|
} else {
|
|
die("Query '%s' failed with wrong error %d: '%s', should have failed "
|
|
"with any of '%s' errors.",
|
|
command->query, err_errno, err_error,
|
|
expected_errors->error_list().c_str());
|
|
}
|
|
}
|
|
|
|
revert_properties();
|
|
}
|
|
|
|
/// Handle absence of errors after execution.
|
|
///
|
|
/// Abort test run if the query succeeds and was expected to fail with
|
|
/// an error.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
void handle_no_error(struct st_command *command) {
|
|
DBUG_TRACE;
|
|
|
|
if (expected_errors->count()) {
|
|
int index = match_expected_error(command, 0, "00000");
|
|
if (index == -1) {
|
|
if (expected_errors->count() == 1) {
|
|
die("Query '%s' succeeded, should have failed with error '%s'",
|
|
command->query, expected_errors->error_list().c_str());
|
|
} else {
|
|
die("Query '%s' succeeded, should have failed with any of '%s' errors.",
|
|
command->query, expected_errors->error_list().c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Save error code returned by a mysqltest command in '$__error'
|
|
/// variable.
|
|
///
|
|
/// @param error Error code
|
|
static void save_error_code(std::uint32_t error) {
|
|
char error_value[10];
|
|
size_t error_length = std::snprintf(error_value, 10, "%u", error);
|
|
error_value[error_length > 9 ? 9 : error_length] = '0';
|
|
const char *var_name = "__error";
|
|
var_set(var_name, var_name + 7, error_value, error_value + error_length);
|
|
}
|
|
|
|
/// Handle errors which occurred during execution of mysqltest commands
|
|
/// like 'move_file', 'remove_file' etc which are used to perform file
|
|
/// system operations.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
/// @param error Error number
|
|
///
|
|
/// @note
|
|
/// If there is an unexpected error, this function will abort mysqltest
|
|
/// client immediately.
|
|
static void handle_command_error(struct st_command *command,
|
|
std::uint32_t error) {
|
|
DBUG_TRACE;
|
|
DBUG_PRINT("enter", ("error: %d", error));
|
|
|
|
if (error != 0 && command->abort_on_error) {
|
|
die("Command \"%s\" failed with error %d. my_errno=%d.",
|
|
command_names[command->type - 1], error, my_errno());
|
|
}
|
|
|
|
if (expected_errors->count()) {
|
|
int index = match_expected_error(command, error, NULL);
|
|
if (index == -1) {
|
|
if (error != 0) {
|
|
if (expected_errors->count() == 1) {
|
|
die("Command \"%s\" failed with wrong error: %d, my_errno=%d. should "
|
|
"have failed with error '%s'.",
|
|
command_names[command->type - 1], error, my_errno(),
|
|
expected_errors->error_list().c_str());
|
|
} else {
|
|
die("Command \"%s\" failed with wrong error: %d, my_errno=%d. should "
|
|
"have failed with any of '%s' errors.",
|
|
command_names[command->type - 1], error, my_errno(),
|
|
expected_errors->error_list().c_str());
|
|
}
|
|
} else {
|
|
// Command succeeded, should have failed with an error
|
|
if (expected_errors->count() == 1) {
|
|
die("Command \"%s\" succeeded, should have failed with error '%s'.",
|
|
command_names[command->type - 1],
|
|
expected_errors->error_list().c_str());
|
|
} else {
|
|
die("Command \"%s\" succeeded, should have failed with any of '%s' "
|
|
"errors.",
|
|
command_names[command->type - 1],
|
|
expected_errors->error_list().c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save the error code
|
|
save_error_code(error);
|
|
|
|
revert_properties();
|
|
}
|
|
|
|
static void close_connections() {
|
|
DBUG_TRACE;
|
|
for (--next_con; next_con >= connections; --next_con) {
|
|
if (next_con->stmt) mysql_stmt_close(next_con->stmt);
|
|
next_con->stmt = 0;
|
|
mysql_close(&next_con->mysql);
|
|
if (next_con->util_mysql) mysql_close(next_con->util_mysql);
|
|
my_free(next_con->name);
|
|
}
|
|
my_free(connections);
|
|
}
|
|
|
|
static void close_statements() {
|
|
struct st_connection *con;
|
|
DBUG_TRACE;
|
|
for (con = connections; con < next_con; con++) {
|
|
if (con->stmt) mysql_stmt_close(con->stmt);
|
|
con->stmt = 0;
|
|
}
|
|
}
|
|
|
|
static void close_files() {
|
|
DBUG_TRACE;
|
|
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;
|
|
}
|
|
}
|
|
|
|
static void free_used_memory() {
|
|
// Delete the exptected errors pointer
|
|
delete expected_errors;
|
|
|
|
// Delete disabled and enabled warning list
|
|
delete disabled_warnings;
|
|
delete enabled_warnings;
|
|
|
|
if (connections) close_connections();
|
|
close_files();
|
|
delete var_hash;
|
|
var_hash = nullptr;
|
|
|
|
struct st_command **q;
|
|
for (q = q_lines->begin(); q != q_lines->end(); ++q) {
|
|
my_free((*q)->query_buf);
|
|
if ((*q)->content.str) dynstr_free(&(*q)->content);
|
|
my_free((*q));
|
|
}
|
|
|
|
for (size_t i = 0; i < 10; i++) {
|
|
if (var_reg[i].alloced_len) my_free(var_reg[i].str_val);
|
|
}
|
|
|
|
delete q_lines;
|
|
dynstr_free(&ds_res);
|
|
dynstr_free(&ds_result);
|
|
if (ds_warn) dynstr_free(ds_warn);
|
|
free_all_replace();
|
|
my_free(opt_pass);
|
|
#ifdef _WIN32
|
|
free_win_path_patterns();
|
|
#endif
|
|
|
|
// Only call mysql_server_end if mysql_server_init has been called.
|
|
if (server_initialized) mysql_server_end();
|
|
|
|
// Don't use DBUG after mysql_server_end()
|
|
return;
|
|
}
|
|
|
|
static void cleanup_and_exit(int exit_code) {
|
|
if (opt_offload_count_file) {
|
|
// Check if the current connection is active, if not create one.
|
|
if (cur_con->mysql.net.vio == 0) {
|
|
mysql_real_connect(&cur_con->mysql, opt_host, opt_user, opt_pass, opt_db,
|
|
opt_port, unix_sock,
|
|
CLIENT_MULTI_STATEMENTS | CLIENT_REMEMBER_OPTIONS);
|
|
}
|
|
|
|
// Save the final value of secondary engine execution status.
|
|
if (secondary_engine->offload_count(&cur_con->mysql, "after"))
|
|
exit_code = 1;
|
|
secondary_engine->report_offload_count(opt_offload_count_file);
|
|
}
|
|
|
|
free_used_memory();
|
|
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() appears to be not 100% reliable on Windows under some conditions.
|
|
#ifdef _WIN32
|
|
if (opt_safe_process_pid) {
|
|
// Close the stack trace request event handle
|
|
if (stacktrace_request_event != NULL) CloseHandle(stacktrace_request_event);
|
|
|
|
// Detach or stop the thread waiting for stack trace event to occur.
|
|
if (wait_for_stacktrace_request_event_thread.joinable())
|
|
wait_for_stacktrace_request_event_thread.detach();
|
|
|
|
// Close the thread handle
|
|
if (!CloseHandle(mysqltest_thread))
|
|
die("CloseHandle failed, err = %d.\n", GetLastError());
|
|
}
|
|
|
|
fflush(stdout);
|
|
fflush(stderr);
|
|
_exit(exit_code);
|
|
#else
|
|
exit(exit_code);
|
|
#endif
|
|
}
|
|
|
|
static void print_file_stack() {
|
|
fprintf(stderr, "file %s: %d\n", cur_file->file_name, cur_file->lineno);
|
|
|
|
struct st_test_file *err_file;
|
|
for (err_file = cur_file - 1; err_file >= file_stack; err_file--) {
|
|
fprintf(stderr, "included from %s: %d\n", err_file->file_name,
|
|
err_file->lineno);
|
|
}
|
|
}
|
|
|
|
void die(const char *fmt, ...) {
|
|
static int dying = 0;
|
|
va_list args;
|
|
DBUG_PRINT("enter", ("start_lineno: %d", start_lineno));
|
|
|
|
// 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;
|
|
|
|
// Print the error message
|
|
fprintf(stderr, "mysqltest: ");
|
|
|
|
if (start_lineno > 0) fprintf(stderr, "At line %u: ", start_lineno);
|
|
|
|
if (fmt) {
|
|
va_start(args, fmt);
|
|
vfprintf(stderr, fmt, args);
|
|
fprintf(stderr, "\n");
|
|
va_end(args);
|
|
} else
|
|
fprintf(stderr, "Unknown error");
|
|
|
|
// Print the file stack
|
|
if (cur_file && cur_file != file_stack) {
|
|
fprintf(stderr, "In included ");
|
|
print_file_stack();
|
|
}
|
|
|
|
fflush(stderr);
|
|
|
|
if (result_file_name) 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 abort_not_supported_test(const char *fmt, ...) {
|
|
va_list args;
|
|
DBUG_TRACE;
|
|
|
|
/* Print include filestack */
|
|
fprintf(stderr, "The test '%s' is not supported by this installation\n",
|
|
file_stack->file_name);
|
|
fprintf(stderr, "Detected in ");
|
|
print_file_stack();
|
|
|
|
/* 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 verbose_msg(const char *fmt, ...) {
|
|
va_list args;
|
|
DBUG_TRACE;
|
|
if (!verbose) return;
|
|
|
|
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);
|
|
}
|
|
|
|
void log_msg(const char *fmt, ...) {
|
|
va_list args;
|
|
char buff[1024];
|
|
size_t len;
|
|
DBUG_TRACE;
|
|
|
|
va_start(args, fmt);
|
|
len = vsnprintf(buff, sizeof(buff) - 1, fmt, args);
|
|
va_end(args);
|
|
|
|
dynstr_append_mem(&ds_res, buff, len);
|
|
dynstr_append(&ds_res, "\n");
|
|
}
|
|
|
|
/*
|
|
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
|
|
|
|
*/
|
|
|
|
static int cat_file(DYNAMIC_STRING *ds, const char *filename) {
|
|
int fd;
|
|
size_t len;
|
|
char buff[512];
|
|
bool dangling_cr = 0;
|
|
|
|
if ((fd = my_open(filename, O_RDONLY, MYF(0))) < 0) return 1;
|
|
while ((len = my_read(fd, (uchar *)&buff, sizeof(buff), MYF(0))) > 0) {
|
|
char *p = buff, *start = buff;
|
|
if (dangling_cr) {
|
|
if (*p != '\n') dynstr_append_mem(ds, "\r", 1);
|
|
dangling_cr = 0;
|
|
}
|
|
while (p < buff + len) {
|
|
/* Convert cr/lf to lf */
|
|
if (*p == '\r' && *(p + 1) && *(p + 1) == '\n') {
|
|
/* Add fake newline instead of cr and output the line */
|
|
*p = '\n';
|
|
p++; /* Step past the "fake" newline */
|
|
dynstr_append_mem(ds, start, p - start);
|
|
p++; /* Step past the "fake" newline */
|
|
start = p;
|
|
} else
|
|
p++;
|
|
}
|
|
if (*(p - 1) == '\r' && len == 512) dangling_cr = 1;
|
|
/* Output any chars that migh be left */
|
|
if (dangling_cr)
|
|
dynstr_append_mem(ds, start, p - start - 1);
|
|
else
|
|
dynstr_append_mem(ds, start, p - start);
|
|
}
|
|
my_close(fd, MYF(0));
|
|
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;
|
|
|
|
if (!(res_file = popen(cmd, "r"))) die("popen(\"%s\", \"r\") failed", cmd);
|
|
|
|
while (fgets(buf, sizeof(buf), res_file)) {
|
|
DBUG_PRINT("info", ("buf: %s", buf));
|
|
if (ds_res) {
|
|
/* Save the output of this command in the supplied string */
|
|
dynstr_append(ds_res, buf);
|
|
} else {
|
|
/* Print it directly on screen */
|
|
fprintf(stdout, "%s", buf);
|
|
}
|
|
}
|
|
|
|
error = pclose(res_file);
|
|
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_TRACE;
|
|
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 (std::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);
|
|
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;
|
|
|
|
snprintf(buf, sizeof(buf), "%s -v", diff_name);
|
|
|
|
if (!(res_file = popen(buf, "r"))) die("popen(\"%s\", \"r\") failed", buf);
|
|
|
|
/* if diff is not present, nothing will be in stdout to increment have_diff */
|
|
if (fgets(buf, sizeof(buf), res_file)) 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"
|
|
///
|
|
/// @param ds Pointer to dynamic string where to add the
|
|
/// diff. If NULL, print the diff to stderr.
|
|
/// @param filename1 Name of the first file
|
|
/// @param filename2 Name of the second file
|
|
static void show_diff(DYNAMIC_STRING *ds, const char *filename1,
|
|
const char *filename2) {
|
|
DYNAMIC_STRING ds_diff;
|
|
if (init_dynamic_string(&ds_diff, "", 256, 256)) die("Out of memory");
|
|
|
|
const char *diff_name = 0;
|
|
|
|
// Determine if we have diff on Windows. If yes, then 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-availibility 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
|
|
// Otherwise always assume it's called diff
|
|
diff_name = "diff";
|
|
#endif
|
|
|
|
if (diff_name) {
|
|
int exit_code = 0;
|
|
// Use 'diff --color=always' to print the colored diff if it is enabled
|
|
if (opt_colored_diff) {
|
|
// Most "diff" tools return '> 1' if error
|
|
exit_code = run_tool(diff_name, &ds_diff, "-u --color='always'",
|
|
filename1, filename2, "2>&1", NULL);
|
|
|
|
if (exit_code > 1)
|
|
die("Option '--colored-diff' is not supported on this machine. "
|
|
"To get colored diff output, install GNU diffutils version "
|
|
"3.4 or higher.");
|
|
} else {
|
|
// Colored diff is disabled, clear the diff string and try unified
|
|
// diff with "diff -u".
|
|
dynstr_set(&ds_diff, "");
|
|
exit_code = run_tool(diff_name, &ds_diff, "-u", filename1, filename2,
|
|
"2>&1", NULL);
|
|
|
|
if (exit_code > 1) {
|
|
// Clear the diff string and fallback to context diff with "diff -c"
|
|
dynstr_set(&ds_diff, "");
|
|
exit_code = run_tool(diff_name, &ds_diff, "-c", filename1, filename2,
|
|
"2>&1", NULL);
|
|
|
|
if (exit_code > 1) {
|
|
// Clear the diff string and fallback to simple diff with "diff"
|
|
dynstr_set(&ds_diff, "");
|
|
exit_code =
|
|
run_tool(diff_name, &ds_diff, filename1, filename2, "2>&1", NULL);
|
|
if (exit_code > 1) diff_name = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!diff_name) {
|
|
// Fallback to dump both files to result file and inform
|
|
// about installing "diff".
|
|
dynstr_append(&ds_diff, "\n");
|
|
dynstr_append(
|
|
&ds_diff,
|
|
"\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_diff, " --- ");
|
|
dynstr_append(&ds_diff, filename1);
|
|
dynstr_append(&ds_diff, " >>>\n");
|
|
cat_file(&ds_diff, filename1);
|
|
dynstr_append(&ds_diff, "<<<\n --- ");
|
|
dynstr_append(&ds_diff, filename1);
|
|
dynstr_append(&ds_diff, " >>>\n");
|
|
cat_file(&ds_diff, filename2);
|
|
dynstr_append(&ds_diff, "<<<<\n");
|
|
}
|
|
|
|
if (ds)
|
|
// Add the diff to output
|
|
dynstr_append_mem(ds, ds_diff.str, ds_diff.length);
|
|
else
|
|
// Print diff directly to stderr
|
|
fprintf(stderr, "%s\n", ds_diff.str);
|
|
|
|
dynstr_free(&ds_diff);
|
|
}
|
|
|
|
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"
|
|
|
|
*/
|
|
|
|
static int compare_files2(File fd, const char *filename2) {
|
|
int error = RESULT_OK;
|
|
File fd2;
|
|
size_t len, len2;
|
|
char buff[512], buff2[512];
|
|
|
|
if ((fd2 = my_open(filename2, O_RDONLY, MYF(0))) < 0) {
|
|
my_close(fd, MYF(0));
|
|
die("Failed to open second file: '%s'", filename2);
|
|
}
|
|
while ((len = my_read(fd, (uchar *)&buff, sizeof(buff), MYF(0))) > 0) {
|
|
if ((len2 = my_read(fd2, (uchar *)&buff2, sizeof(buff2), MYF(0))) < len) {
|
|
/* File 2 was smaller */
|
|
error = RESULT_LENGTH_MISMATCH;
|
|
break;
|
|
}
|
|
if (len2 > len) {
|
|
/* File 1 was smaller */
|
|
error = RESULT_LENGTH_MISMATCH;
|
|
break;
|
|
}
|
|
if ((memcmp(buff, buff2, len))) {
|
|
/* Content of this part differed */
|
|
error = RESULT_CONTENT_MISMATCH;
|
|
break;
|
|
}
|
|
}
|
|
if (!error && my_read(fd2, (uchar *)&buff2, sizeof(buff2), MYF(0)) > 0) {
|
|
/* File 1 was smaller */
|
|
error = RESULT_LENGTH_MISMATCH;
|
|
}
|
|
|
|
my_close(fd2, MYF(0));
|
|
|
|
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'
|
|
|
|
*/
|
|
|
|
static 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;
|
|
}
|
|
|
|
/*
|
|
Check the content of log against result file
|
|
|
|
SYNOPSIS
|
|
check_result
|
|
|
|
RETURN VALUES
|
|
error - the function will not return
|
|
|
|
*/
|
|
|
|
static void check_result() {
|
|
const char *mess = "Result content mismatch\n";
|
|
|
|
DBUG_TRACE;
|
|
DBUG_ASSERT(result_file_name);
|
|
DBUG_PRINT("enter", ("result_file_name: %s", result_file_name));
|
|
|
|
/*
|
|
Removing the unnecessary warning messages generated
|
|
on GCOV platform.
|
|
*/
|
|
#ifdef HAVE_GCOV
|
|
char cmd[FN_REFLEN];
|
|
strcpy(cmd, "sed -i '/gcda:Merge mismatch for function/d' ");
|
|
std::strcat(cmd, log_file.file_name());
|
|
system(cmd);
|
|
#endif
|
|
|
|
switch (compare_files(log_file.file_name(), result_file_name)) {
|
|
case RESULT_OK:
|
|
break; /* ok */
|
|
case RESULT_LENGTH_MISMATCH:
|
|
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;
|
|
dirname_part(reject_file, result_file_name, &reject_length);
|
|
|
|
/* 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()");
|
|
}
|
|
}
|
|
|
|
/*
|
|
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 charecter 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("%.*s - argument list started with '%c' must be ended with '%c'",
|
|
static_cast<int>(command->first_word_len), command->query, '(', ')');
|
|
}
|
|
|
|
void var_free::operator()(VAR *var) const {
|
|
my_free(var->str_val);
|
|
if (var->alloced) my_free(var);
|
|
}
|
|
|
|
static 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 = std::strlen(name);
|
|
if (!val_len && val) val_len = std::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))))
|
|
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))))
|
|
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, std::strlen(name), tmp, std::strlen(tmp));
|
|
var_hash->emplace(name, std::unique_ptr<VAR, var_free>(v));
|
|
return v;
|
|
}
|
|
|
|
VAR *var_get(const char *var_name, const char **var_name_end, bool raw,
|
|
bool ignore_not_existing) {
|
|
int digit;
|
|
VAR *v;
|
|
DBUG_TRACE;
|
|
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) 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 = find_or_nullptr(*var_hash, std::string(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 = std::strlen(v->str_val);
|
|
}
|
|
if (var_name_end) *var_name_end = var_name;
|
|
return v;
|
|
err:
|
|
if (var_name_end) *var_name_end = 0;
|
|
die("Unsupported variable name: %s", var_name);
|
|
return 0;
|
|
}
|
|
|
|
static VAR *var_obtain(const char *name, int len) {
|
|
VAR *v = find_or_nullptr(*var_hash, std::string(name, len));
|
|
if (v == nullptr) {
|
|
v = var_init(0, name, len, "", 0);
|
|
var_hash->emplace(std::string(name, len),
|
|
std::unique_ptr<VAR, var_free>(v));
|
|
}
|
|
return v;
|
|
}
|
|
|
|
/*
|
|
- if variable starts with a $ it is regarded as a local test varable
|
|
- 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_TRACE;
|
|
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 = std::strlen(v->str_val);
|
|
}
|
|
/* setenv() expects \0-terminated strings */
|
|
DBUG_ASSERT(v->name[v->name_len] == 0);
|
|
setenv(v->name, v->str_val, 1);
|
|
}
|
|
}
|
|
|
|
static void var_set_string(const char *name, const char *value) {
|
|
var_set(name, name + std::strlen(name), value, value + std::strlen(value));
|
|
}
|
|
|
|
static void var_set_int(const char *name, int value) {
|
|
char buf[21];
|
|
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
|
|
*/
|
|
|
|
static 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));
|
|
}
|
|
|
|
/// Variable '$DISABLED_WARNINGS_LIST' contains comma separated list
|
|
/// of disabled warnings and variable '$ENABLED_WARNINGS_LIST' contains
|
|
/// comma separated list of enabled warnings.
|
|
///
|
|
/// Update the value of these two variables with the latest list of
|
|
/// disabled and enabled warnings. The value of these variables will be
|
|
/// empty if there are no disabled or enabled warnings.
|
|
///
|
|
/// These variables will always contain the latest list of disabled
|
|
/// and enabled warnings, and can be referenced inside a test or inside
|
|
/// a test utility file to access the current list of disabled or
|
|
/// enabled warnings.
|
|
static void update_disabled_enabled_warnings_list_var() {
|
|
// Update '$DISABLED_WARNINGS_LIST' variable
|
|
std::string disabled_warning_list = disabled_warnings->warnings_list();
|
|
var_set_string("DISABLED_WARNINGS_LIST", disabled_warning_list.c_str());
|
|
|
|
// Update '$ENABLED_WARNINGS_LIST' variable
|
|
std::string enabled_warning_list = enabled_warnings->warnings_list();
|
|
var_set_string("ENABLED_WARNINGS_LIST", enabled_warning_list.c_str());
|
|
}
|
|
|
|
/// Set a property value to either 0 or 1 for a disable_X or a enable_X
|
|
/// command, and the new value set will be applicable for next statement
|
|
/// only. After that, property value will be reset back to the old value.
|
|
///
|
|
/// @param property Enum value representing a Property
|
|
/// @param value Value for the property, either 0 or 1
|
|
static void set_once_property(enum_prop property, bool value) {
|
|
Property &prop = prop_list[property];
|
|
prop.set = 1;
|
|
prop.old = *prop.var;
|
|
*prop.var = value;
|
|
var_set_int(prop.env_name, (value != prop.reverse));
|
|
once_property = true;
|
|
}
|
|
|
|
/// Set a property value to either 0 or 1 for a disable_X or a enable_X
|
|
/// command.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
/// @param property Enum value representing a Property
|
|
/// @param value Value for the property, either 0 or 1
|
|
static void set_property(st_command *command, enum_prop property, bool value) {
|
|
char *arg = command->first_argument;
|
|
|
|
// If "ONCE" argument is specified, the new value for the property is
|
|
// set for next statement only. After that, property value will be
|
|
// reset back to the old value.
|
|
if (arg) {
|
|
// "ONCE" is the second argument to 'disable_warnings/enable_warnings'
|
|
// command.
|
|
if (((command->type == Q_DISABLE_WARNINGS ||
|
|
command->type == Q_ENABLE_WARNINGS) &&
|
|
std::strstr(arg, "ONCE") != NULL) ||
|
|
!std::strcmp(arg, "ONCE")) {
|
|
command->last_argument = arg + std::strlen(arg);
|
|
set_once_property(property, value);
|
|
return;
|
|
}
|
|
}
|
|
|
|
Property &prop = prop_list[property];
|
|
prop.set = 0;
|
|
*prop.var = value;
|
|
var_set_int(prop.env_name, (value != prop.reverse));
|
|
}
|
|
|
|
/// Reset property value to the old value for all properties which are
|
|
/// set for the next statement only, i.e properties specified using
|
|
/// keyword "ONCE" argument.
|
|
void revert_properties() {
|
|
if (!once_property) return;
|
|
|
|
for (std::size_t i = 0; i < P_MAX; i++) {
|
|
Property &prop = prop_list[i];
|
|
if (prop.set) {
|
|
*prop.var = prop.old;
|
|
prop.set = 0;
|
|
var_set_int(prop.env_name, (prop.old != prop.reverse));
|
|
}
|
|
}
|
|
|
|
// Remove warnings which are disabled or enabled for the next
|
|
// statement only.
|
|
disabled_warnings->update_list();
|
|
enabled_warnings->update_list();
|
|
|
|
// Update $DISABLED_WARNINGS_LIST and $ENABLED_WARNINGS_LIST
|
|
// variable value.
|
|
update_disabled_enabled_warnings_list_var();
|
|
|
|
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>
|
|
|
|
*/
|
|
|
|
static void var_query_set(VAR *var, const char *query, const char **query_end) {
|
|
const char *end =
|
|
(query_end && *query_end) ? *query_end : query + std::strlen(query);
|
|
MYSQL_RES *res = NULL;
|
|
MYSQL_ROW row;
|
|
MYSQL *mysql = &cur_con->mysql;
|
|
DYNAMIC_STRING ds_query;
|
|
DBUG_TRACE;
|
|
|
|
/* 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_wrapper(mysql, ds_query.str,
|
|
static_cast<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);
|
|
eval_expr(var, "", 0);
|
|
return;
|
|
}
|
|
|
|
if (!(res = mysql_store_result_wrapper(mysql)))
|
|
die("Query '%s' didn't return a result set", ds_query.str);
|
|
dynstr_free(&ds_query);
|
|
|
|
if ((row = mysql_fetch_row_wrapper(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) {
|
|
size_t orig_len = len;
|
|
// Regex replace
|
|
if (!multi_reg_replace(glob_replace_regex, (char *)val, &len)) {
|
|
val = glob_replace_regex->buf;
|
|
} else {
|
|
len = orig_len;
|
|
}
|
|
}
|
|
DYNAMIC_STRING ds_temp;
|
|
init_dynamic_string(&ds_temp, "", 512, 512);
|
|
|
|
/* Store result from replace_result in ds_temp */
|
|
if (glob_replace)
|
|
replace_strings_append(glob_replace, &ds_temp, val, len);
|
|
|
|
/*
|
|
Call the replace_numeric_round function with the specified
|
|
precision. It may be used along with replace_result, so use the
|
|
output from replace_result as the input for replace_numeric_round.
|
|
*/
|
|
if (glob_replace_numeric_round >= 0) {
|
|
/* Copy the result from replace_result if it was used, into buffer */
|
|
if (ds_temp.length > 0) {
|
|
char buffer[512];
|
|
strcpy(buffer, ds_temp.str);
|
|
dynstr_free(&ds_temp);
|
|
init_dynamic_string(&ds_temp, "", 512, 512);
|
|
replace_numeric_round_append(glob_replace_numeric_round, &ds_temp,
|
|
buffer, std::strlen(buffer));
|
|
} else
|
|
replace_numeric_round_append(glob_replace_numeric_round, &ds_temp,
|
|
val, len);
|
|
}
|
|
|
|
if (!glob_replace && glob_replace_numeric_round < 0)
|
|
dynstr_append_mem(&result, val, len);
|
|
else
|
|
dynstr_append_mem(&result, ds_temp.str, std::strlen(ds_temp.str));
|
|
dynstr_free(&ds_temp);
|
|
}
|
|
dynstr_append_mem(&result, "\t", 1);
|
|
}
|
|
end = result.str + result.length - 1;
|
|
/* Evaluation should not recurse via backtick */
|
|
eval_expr(var, result.str, &end, false, false);
|
|
dynstr_free(&result);
|
|
} else
|
|
eval_expr(var, "", 0);
|
|
|
|
mysql_free_result_wrapper(res);
|
|
}
|
|
|
|
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_TRACE;
|
|
|
|
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);
|
|
}
|
|
|
|
/// Convert between error numbers and error names/strings.
|
|
///
|
|
/// @code
|
|
/// let $var = convert_error(ER_UNKNOWN_ERROR);
|
|
/// let $var = convert_error(1234);
|
|
/// @endcode
|
|
///
|
|
/// The variable '$var' will be populated with error number if the
|
|
/// argument is string. The variable var will be populated with error
|
|
/// string if the argument is number.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
/// @param var Pointer to VAR object containing a variable
|
|
/// information.
|
|
static void var_set_convert_error(struct st_command *command, VAR *var) {
|
|
// The command->query contains the statement convert_error(1234)
|
|
char *first = std::strchr(command->query, '(') + 1;
|
|
char *last = std::strchr(command->query, ')');
|
|
|
|
// Denoting an empty string
|
|
if (last == first) {
|
|
eval_expr(var, "0", 0);
|
|
return;
|
|
}
|
|
|
|
// If the string is an error string , it starts with 'E' as is the norm
|
|
if (*first == 'E') {
|
|
std::string error_name(first, int(last - first));
|
|
int error = get_errcode_from_name(error_name);
|
|
if (error == -1) die("Unknown SQL error name '%s'.", error_name.c_str());
|
|
char str[100];
|
|
std::sprintf(str, "%d", error);
|
|
eval_expr(var, str, 0);
|
|
} else if (my_isdigit(charset_info, *first)) {
|
|
// Error number argument
|
|
long int err = std::strtol(first, &last, 0);
|
|
const char *err_name = get_errname_from_code(err);
|
|
eval_expr(var, err_name, 0);
|
|
} else {
|
|
die("Invalid error in input");
|
|
}
|
|
}
|
|
|
|
/*
|
|
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
|
|
|
|
*/
|
|
|
|
static void var_set_query_get_value(struct st_command *command, VAR *var) {
|
|
long row_no;
|
|
int col_no = -1;
|
|
MYSQL_RES *res = NULL;
|
|
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_TRACE;
|
|
|
|
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_wrapper(mysql, ds_query.str,
|
|
static_cast<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);
|
|
return;
|
|
}
|
|
|
|
if (!(res = mysql_store_result_wrapper(mysql)))
|
|
die("Query '%s' didn't return a result set", ds_query.str);
|
|
|
|
{
|
|
/* 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 (std::strcmp(fields[i].name, ds_col.str) == 0 &&
|
|
std::strlen(fields[i].name) == ds_col.length) {
|
|
col_no = i;
|
|
break;
|
|
}
|
|
}
|
|
if (col_no == -1) {
|
|
mysql_free_result_wrapper(res);
|
|
die("Could not find column '%s' in the result of '%s'", ds_col.str,
|
|
ds_query.str);
|
|
}
|
|
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_wrapper(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_wrapper(res);
|
|
}
|
|
|
|
static 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))
|
|
: (char *)my_malloc(PSI_NOT_INSTRUMENTED, src->alloced_len,
|
|
MYF(MY_WME))))
|
|
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_TRACE;
|
|
if (p_end) {
|
|
DBUG_PRINT("enter", ("p: '%.*s'", (int)(*p_end - p), p));
|
|
} else {
|
|
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 '%.*s' after $variable in expression",
|
|
(int)(expected_end - end - 1), end);
|
|
|
|
return;
|
|
}
|
|
|
|
if (*p == '`') {
|
|
var_query_set(v, p, p_end);
|
|
return;
|
|
}
|
|
|
|
{
|
|
/* Check if this is a "let $var= query_get_value()" */
|
|
const char *get_value_str = "query_get_value";
|
|
const size_t len = std::strlen(get_value_str);
|
|
if (std::strncmp(p, get_value_str, len) == 0) {
|
|
struct st_command command;
|
|
memset(&command, 0, sizeof(command));
|
|
command.query = const_cast<char *>(p);
|
|
command.first_word_len = len;
|
|
command.first_argument = command.query + len;
|
|
command.end = const_cast<char *>(*p_end);
|
|
var_set_query_get_value(&command, v);
|
|
return;
|
|
}
|
|
/* Check if this is a "let $var= convert_error()" */
|
|
const char *get_value_str1 = "convert_error";
|
|
const size_t len1 = std::strlen(get_value_str1);
|
|
if (std::strncmp(p, get_value_str1, len1) == 0) {
|
|
struct st_command command;
|
|
memset(&command, 0, sizeof(command));
|
|
command.query = const_cast<char *>(p);
|
|
command.first_word_len = len;
|
|
command.first_argument = command.query + len;
|
|
command.end = const_cast<char *>(*p_end);
|
|
var_set_convert_error(&command, v);
|
|
return;
|
|
}
|
|
}
|
|
|
|
NO_EVAL : {
|
|
size_t new_val_len =
|
|
(p_end && *p_end) ? static_cast<size_t>(*p_end - p) : std::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))
|
|
: (char *)my_malloc(PSI_NOT_INSTRUMENTED,
|
|
v->alloced_len + 1, MYF(MY_WME))))
|
|
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);
|
|
}
|
|
}
|
|
|
|
static int open_file(const char *name) {
|
|
char buff[FN_REFLEN];
|
|
size_t length;
|
|
DBUG_TRACE;
|
|
DBUG_PRINT("enter", ("name: %s", name));
|
|
|
|
bool file_exists = false;
|
|
/* Extract path from current file and try it as base first */
|
|
if (dirname_part(buff, cur_file->file_name, &length)) {
|
|
strxmov(buff, buff, name, NullS);
|
|
if (access(buff, F_OK) == 0) {
|
|
DBUG_PRINT("info", ("The file exists"));
|
|
name = buff;
|
|
file_exists = true;
|
|
}
|
|
}
|
|
|
|
if (!test_if_hard_path(name) && !file_exists) {
|
|
strxmov(buff, opt_basedir, name, NullS);
|
|
name = buff;
|
|
}
|
|
fn_format(buff, name, "", "", MY_UNPACK_FILENAME);
|
|
|
|
if (cur_file == file_stack_end) die("Source directives are nesting too deep");
|
|
cur_file++;
|
|
if (!(cur_file->file = fopen(buff, "rb"))) {
|
|
cur_file--;
|
|
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;
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
Source and execute the given file
|
|
|
|
SYNOPSIS
|
|
do_source()
|
|
query called command
|
|
|
|
DESCRIPTION
|
|
source <file_name>
|
|
|
|
Open the file <file_name> and execute it
|
|
|
|
*/
|
|
|
|
static 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_TRACE;
|
|
|
|
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);
|
|
}
|
|
|
|
static FILE *my_popen(DYNAMIC_STRING *ds_cmd, const char *mode,
|
|
struct st_command *command MY_ATTRIBUTE((unused))) {
|
|
#ifdef _WIN32
|
|
/*
|
|
--execw is for tests executing commands containing non-ASCII characters.
|
|
|
|
To correctly start such a program on Windows, we need to use the "wide"
|
|
version of popen, with prior translation of the command line from
|
|
the file character set to wide string. We use the current value
|
|
of --character_set as a file character set, so before using --execw
|
|
make sure to set --character_set properly.
|
|
|
|
If we use the non-wide version of popen, Windows internally
|
|
converts command line from the current ANSI code page to wide string.
|
|
In case when character set of the command line does not match the
|
|
current ANSI code page, non-ASCII characters get garbled in most cases.
|
|
|
|
On Linux, the command line passed to popen() is considered
|
|
as a binary string, no any internal to-wide and from-wide
|
|
character set conversion happens, so we don't need to do anything.
|
|
On Linux --execw is just a synonym to --exec.
|
|
|
|
For simplicity, assume that command line is limited to 4KB
|
|
(like in cmd.exe) and that mode at most 10 characters.
|
|
*/
|
|
if (command->type == Q_EXECW) {
|
|
wchar_t wcmd[4096];
|
|
wchar_t wmode[10];
|
|
const char *cmd = ds_cmd->str;
|
|
uint dummy_errors;
|
|
size_t len;
|
|
len = my_convert((char *)wcmd, sizeof(wcmd) - sizeof(wcmd[0]),
|
|
&my_charset_utf16le_bin, ds_cmd->str,
|
|
std::strlen(ds_cmd->str), charset_info, &dummy_errors);
|
|
wcmd[len / sizeof(wchar_t)] = 0;
|
|
len = my_convert((char *)wmode, sizeof(wmode) - sizeof(wmode[0]),
|
|
&my_charset_utf16le_bin, mode, std::strlen(mode),
|
|
charset_info, &dummy_errors);
|
|
wmode[len / sizeof(wchar_t)] = 0;
|
|
return _wpopen(wcmd, wmode);
|
|
}
|
|
#endif /* _WIN32 */
|
|
|
|
return popen(ds_cmd->str, mode);
|
|
}
|
|
|
|
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
|
|
/**
|
|
Replace CRLF sequence with LF in place.
|
|
|
|
This function is required as a workaround for a bug in the Microsoft
|
|
C runtime library introduced in Visual Studio 2015.
|
|
See bug#22608247 and bug#22811243
|
|
|
|
@param buf Null terminated buffer.
|
|
*/
|
|
static void replace_crlf_with_lf(char *buf) {
|
|
char *replace = buf;
|
|
while (*buf) {
|
|
*replace = *buf++;
|
|
if (!((*replace == '\x0D') && (*buf == '\x0A'))) {
|
|
replace++;
|
|
}
|
|
}
|
|
*replace = '\x0';
|
|
}
|
|
#endif
|
|
|
|
/// Execute the shell command using the popen() library call. References
|
|
/// to variables within the command are replaced with the corresponding
|
|
/// values. Use “\\$” to specify a literal “$” character.
|
|
///
|
|
/// 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.
|
|
///
|
|
/// @code
|
|
/// exec command [args]
|
|
/// @endcode
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
/// @param run_in_background Specifies if command should be run in background.
|
|
/// In such case we don't wait nor attempt to read the
|
|
/// output.
|
|
///
|
|
/// @note
|
|
/// It is recommended to use mysqltest command(s) like "remove_file"
|
|
/// instead of executing the shell commands using 'exec' command.
|
|
static void do_exec(struct st_command *command, bool run_in_background) {
|
|
DBUG_TRACE;
|
|
|
|
const char *cmd = command->first_argument;
|
|
DBUG_PRINT("enter", ("cmd: '%s'", cmd));
|
|
|
|
// Skip leading space
|
|
while (*cmd && my_isspace(charset_info, *cmd)) cmd++;
|
|
if (!*cmd) die("Missing argument in exec");
|
|
command->last_argument = command->end;
|
|
|
|
DYNAMIC_STRING ds_cmd;
|
|
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] && std::strncmp(cmd, "echo", 4) == 0) {
|
|
// Replace echo with our "builtin" echo
|
|
replace(&ds_cmd, "echo", 4, builtin_echo, std::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 (run_in_background) {
|
|
/* Add an invocation of "START /B" on Windows, append " &" on Linux*/
|
|
DYNAMIC_STRING ds_tmp;
|
|
#ifdef WIN32
|
|
init_dynamic_string(&ds_tmp, "START /B ", ds_cmd.length + 9, 256);
|
|
dynstr_append_mem(&ds_tmp, ds_cmd.str, ds_cmd.length);
|
|
#else
|
|
init_dynamic_string(&ds_tmp, ds_cmd.str, ds_cmd.length + 2, 256);
|
|
dynstr_append_mem(&ds_tmp, " &", 2);
|
|
#endif
|
|
dynstr_set(&ds_cmd, ds_tmp.str);
|
|
dynstr_free(&ds_tmp);
|
|
}
|
|
|
|
// exec command is interpreted externally and will not take newlines
|
|
while (replace(&ds_cmd, "\n", 1, " ", 1) == 0)
|
|
;
|
|
|
|
DBUG_PRINT("info",
|
|
("Executing '%s' as '%s'", command->first_argument, ds_cmd.str));
|
|
|
|
#ifdef WIN32
|
|
// Open pipe in binary mode as part of handling Microsoft _read bug.
|
|
// See bug#22608247 and bug#22811243
|
|
const char *mode = "rb";
|
|
#else
|
|
const char *mode = "r";
|
|
#endif
|
|
FILE *res_file;
|
|
if (!(res_file = my_popen(&ds_cmd, mode, command)) &&
|
|
command->abort_on_error) {
|
|
dynstr_free(&ds_cmd);
|
|
die("popen(\"%s\", \"r\") failed", command->first_argument);
|
|
}
|
|
|
|
if (!run_in_background) {
|
|
char buf[512];
|
|
std::string str;
|
|
while (std::fgets(buf, sizeof(buf), res_file)) {
|
|
if (std::strlen(buf) < 1) continue;
|
|
|
|
#ifdef WIN32
|
|
// Replace CRLF char with LF.
|
|
// See bug#22608247 and bug#22811243
|
|
DBUG_ASSERT(!std::strcmp(mode, "rb"));
|
|
replace_crlf_with_lf(buf);
|
|
#endif
|
|
if (trace_exec) {
|
|
fprintf(stdout, "%s", buf);
|
|
fflush(stdout);
|
|
}
|
|
if (disable_result_log) {
|
|
buf[std::strlen(buf) - 1] = 0;
|
|
DBUG_PRINT("exec_result", ("%s", buf));
|
|
} else {
|
|
// Read the file line by line. Check if the buffer read from the
|
|
// file ends with EOL character.
|
|
if ((buf[std::strlen(buf) - 1] != '\n' &&
|
|
std::strlen(buf) < (sizeof(buf) - 1)) ||
|
|
(buf[std::strlen(buf) - 1] == '\n')) {
|
|
// Found EOL
|
|
if (str.length()) {
|
|
// Temporary string exists, append the current buffer read
|
|
// to the temporary string.
|
|
str.append(buf);
|
|
replace_dynstr_append(&ds_res, str.c_str());
|
|
str.clear();
|
|
} else {
|
|
// Entire line is read at once
|
|
replace_dynstr_append(&ds_res, buf);
|
|
}
|
|
} else {
|
|
// The buffer read from the file doesn't end with EOL character,
|
|
// store it in a temporary string.
|
|
str.append(buf);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::uint32_t status = 0;
|
|
int error = pclose(res_file);
|
|
|
|
if (error > 0) {
|
|
#ifdef _WIN32
|
|
status = WEXITSTATUS(error);
|
|
#else
|
|
// Do the same as many shells here: show SIGKILL as 137
|
|
if (WIFEXITED(error))
|
|
status = WEXITSTATUS(error);
|
|
else if (WIFSIGNALED(error))
|
|
status = 0x80 + WTERMSIG(error);
|
|
#endif
|
|
}
|
|
|
|
if (error != 0 && command->abort_on_error) {
|
|
log_msg("exec of '%s' failed, error: %d, status: %d, errno: %d.",
|
|
ds_cmd.str, error, status, errno);
|
|
dynstr_free(&ds_cmd);
|
|
die("Command \"%s\" failed.\n\nOutput from before failure:\n%s",
|
|
command->first_argument, ds_res.str);
|
|
}
|
|
|
|
dynstr_free(&ds_cmd);
|
|
handle_command_error(command, status);
|
|
|
|
// Save error code
|
|
save_error_code(error);
|
|
}
|
|
|
|
enum enum_operator { DO_DEC, DO_INC };
|
|
|
|
/// Template function that frees memory of the dynamic string
|
|
/// passed to the function.
|
|
///
|
|
/// @param val Dynamic string whose memory needs to be freed.
|
|
template <typename T>
|
|
static void free_dynamic_strings(T *val) {
|
|
dynstr_free(val);
|
|
}
|
|
|
|
/// Frees the memory of dynamic strings passed to the function.
|
|
/// It accepts a variable number of dynamic strings, and through
|
|
/// recursion, frees the memory. The other template function
|
|
/// which calls dynstr_free() is called here.
|
|
///
|
|
/// @param first The dynamic string passed to the function which
|
|
/// gets freed using dynstr_free().
|
|
/// @param rest Rest of the dynamic strings which are passed to
|
|
/// the function, through recursion, end up being
|
|
/// freed by dynstr_free().
|
|
template <typename T1, typename... T2>
|
|
static void free_dynamic_strings(T1 *first, T2 *... rest) {
|
|
free_dynamic_strings(first);
|
|
free_dynamic_strings(rest...);
|
|
}
|
|
|
|
/*
|
|
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
|
|
|
|
*/
|
|
|
|
static 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 %.*s", static_cast<int>(command->first_word_len),
|
|
command->query);
|
|
if (*p != '$')
|
|
die("The argument to %.*s must be a variable (start with $)",
|
|
static_cast<int>(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 = const_cast<char *>(++p);
|
|
return 0;
|
|
}
|
|
|
|
/// Removes the file passed as the argument and retries a specified
|
|
/// number of times, if it is unsuccessful.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
static void do_remove_file(struct st_command *command) {
|
|
int error;
|
|
static DYNAMIC_STRING ds_filename;
|
|
static DYNAMIC_STRING ds_retry;
|
|
|
|
const struct command_arg rm_args[] = {
|
|
{"filename", ARG_STRING, true, &ds_filename, "File to delete"},
|
|
{"retry", ARG_STRING, false, &ds_retry, "Number of retries"}};
|
|
DBUG_TRACE;
|
|
|
|
check_command_args(command, command->first_argument, rm_args,
|
|
sizeof(rm_args) / sizeof(struct command_arg), ' ');
|
|
|
|
// Check if the retry value is passed, and if it is an integer
|
|
int retry = 0;
|
|
if (ds_retry.length) {
|
|
retry = get_int_val(ds_retry.str);
|
|
if (retry < 0) {
|
|
// In case of invalid retry, copy the value passed to print later
|
|
char buf[32];
|
|
strmake(buf, ds_retry.str, sizeof(buf) - 1);
|
|
free_dynamic_strings(&ds_filename, &ds_retry);
|
|
die("Invalid value '%s' for retry argument given to remove_file "
|
|
"command.",
|
|
buf);
|
|
}
|
|
}
|
|
|
|
DBUG_PRINT("info", ("removing file: %s", ds_filename.str));
|
|
error = my_delete(ds_filename.str, MYF(0)) != 0;
|
|
|
|
/*
|
|
If the remove command fails due to an environmental issue, the command can
|
|
be retried a specified number of times before throwing an error.
|
|
*/
|
|
for (int i = 0; error && (i < retry); i++) {
|
|
my_sleep(1000 * 1000);
|
|
error = my_delete(ds_filename.str, MYF(0)) != 0;
|
|
}
|
|
|
|
handle_command_error(command, error);
|
|
free_dynamic_strings(&ds_filename, &ds_retry);
|
|
}
|
|
|
|
/// Removes the files in the specified directory, by matching the
|
|
/// file name pattern. Retry of the command can happen optionally with
|
|
/// an interval of one second between each retry if the command fails.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
static void do_remove_files_wildcard(struct st_command *command) {
|
|
int error = 0;
|
|
uint i;
|
|
MY_DIR *dir_info;
|
|
FILEINFO *file;
|
|
char dir_separator[2];
|
|
static DYNAMIC_STRING ds_directory;
|
|
static DYNAMIC_STRING ds_wild;
|
|
static DYNAMIC_STRING ds_retry;
|
|
char dirname[FN_REFLEN];
|
|
|
|
const struct command_arg rm_args[] = {
|
|
{"directory", ARG_STRING, true, &ds_directory,
|
|
"Directory containing files to delete"},
|
|
{"pattern", ARG_STRING, true, &ds_wild, "File pattern to delete"},
|
|
{"retry", ARG_STRING, false, &ds_retry, "Number of retries"}};
|
|
DBUG_TRACE;
|
|
|
|
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);
|
|
|
|
// Check if the retry value is passed, and if it is an interger
|
|
int retry = 0;
|
|
if (ds_retry.length) {
|
|
retry = get_int_val(ds_retry.str);
|
|
if (retry < 0) {
|
|
// In case of invalid retry, copy the value passed to print later
|
|
char buf[32];
|
|
strmake(buf, ds_retry.str, sizeof(buf) - 1);
|
|
free_dynamic_strings(&ds_directory, &ds_wild, &ds_retry);
|
|
die("Invalid value '%s' for retry argument given to "
|
|
"remove_files_wildcard command.",
|
|
buf);
|
|
}
|
|
}
|
|
|
|
static DYNAMIC_STRING ds_file_to_remove;
|
|
DBUG_PRINT("info", ("listing directory: %s", dirname));
|
|
/* Note that my_dir sorts the list if not given any flags */
|
|
if (!(dir_info = my_dir(dirname, MYF(MY_DONT_SORT | MY_WANT_STAT)))) {
|
|
error = 1;
|
|
goto end;
|
|
}
|
|
init_dynamic_string(&ds_file_to_remove, dirname, 1024, 1024);
|
|
dir_separator[0] = FN_LIBCHAR;
|
|
dir_separator[1] = 0;
|
|
dynstr_append(&ds_file_to_remove, dir_separator);
|
|
|
|
size_t length;
|
|
/* Storing the length of the path to the file, so it can be reused */
|
|
length = ds_file_to_remove.length;
|
|
for (i = 0; i < (uint)dir_info->number_off_files; i++) {
|
|
ds_file_to_remove.length = length;
|
|
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 (wild_compare_full(file->name, std::strlen(file->name), ds_wild.str,
|
|
std::strlen(ds_wild.str), false, 0, '?', '*'))
|
|
continue;
|
|
/* Not required as the var ds_file_to_remove.length already has the
|
|
length in canonnicalized form */
|
|
/* ds_file_to_remove.length= ds_directory.length + 1;
|
|
ds_file_to_remove.str[ds_directory.length + 1]= 0; */
|
|
dynstr_append(&ds_file_to_remove, file->name);
|
|
DBUG_PRINT("info", ("removing file: %s", ds_file_to_remove.str));
|
|
error = my_delete(ds_file_to_remove.str, MYF(0)) != 0;
|
|
|
|
/*
|
|
If the remove command fails due to an environmental issue, the command
|
|
can be retried a specified number of times before throwing an error.
|
|
*/
|
|
for (int j = 0; error && (j < retry); j++) {
|
|
my_sleep(1000 * 1000);
|
|
error = my_delete(ds_file_to_remove.str, MYF(0)) != 0;
|
|
}
|
|
if (error) break;
|
|
}
|
|
my_dirend(dir_info);
|
|
|
|
end:
|
|
handle_command_error(command, error);
|
|
free_dynamic_strings(&ds_directory, &ds_wild, &ds_file_to_remove, &ds_retry);
|
|
}
|
|
|
|
/// Copy the source file to destination file. Copy will fail if the
|
|
/// destination file exists. Retry of the command can happen optionally with
|
|
/// an interval of one second between each retry if the command fails.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
static void do_copy_file(struct st_command *command) {
|
|
int error;
|
|
static DYNAMIC_STRING ds_from_file;
|
|
static DYNAMIC_STRING ds_to_file;
|
|
static DYNAMIC_STRING ds_retry;
|
|
|
|
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"},
|
|
{"retry", ARG_STRING, false, &ds_retry, "Number of retries"}};
|
|
DBUG_TRACE;
|
|
|
|
check_command_args(command, command->first_argument, copy_file_args,
|
|
sizeof(copy_file_args) / sizeof(struct command_arg), ' ');
|
|
|
|
// Check if the retry value is passed, and if it is an interger
|
|
int retry = 0;
|
|
if (ds_retry.length) {
|
|
retry = get_int_val(ds_retry.str);
|
|
if (retry < 0) {
|
|
// In case of invalid retry, copy the value passed to print later
|
|
char buf[32];
|
|
strmake(buf, ds_retry.str, sizeof(buf) - 1);
|
|
free_dynamic_strings(&ds_from_file, &ds_to_file, &ds_retry);
|
|
die("Invalid value '%s' for retry argument given to copy_file "
|
|
"command.",
|
|
buf);
|
|
}
|
|
}
|
|
|
|
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_HOLD_ORIGINAL_MODES)) != 0);
|
|
|
|
/*
|
|
If the copy command fails due to an environmental issue, the command can
|
|
be retried a specified number of times before throwing an error.
|
|
*/
|
|
for (int i = 0; error && (i < retry); i++) {
|
|
my_sleep(1000 * 1000);
|
|
error =
|
|
(my_copy(ds_from_file.str, ds_to_file.str,
|
|
MYF(MY_DONT_OVERWRITE_FILE | MY_HOLD_ORIGINAL_MODES)) != 0);
|
|
}
|
|
|
|
handle_command_error(command, error);
|
|
free_dynamic_strings(&ds_from_file, &ds_to_file, &ds_retry);
|
|
}
|
|
|
|
/*
|
|
SYNOPSIS
|
|
recursive_copy
|
|
ds_source - pointer to dynamic string containing source
|
|
directory informtion
|
|
ds_destination - pointer to dynamic string containing destination
|
|
directory informtion
|
|
|
|
DESCRIPTION
|
|
Recursive copy of <ds_source> to <ds_destination>
|
|
*/
|
|
|
|
static int recursive_copy(DYNAMIC_STRING *ds_source,
|
|
DYNAMIC_STRING *ds_destination) {
|
|
/* Note that my_dir sorts the list if not given any flags */
|
|
MY_DIR *src_dir_info =
|
|
my_dir(ds_source->str, MYF(MY_DONT_SORT | MY_WANT_STAT));
|
|
|
|
int error = 0;
|
|
|
|
/* Source directory exists */
|
|
if (src_dir_info) {
|
|
/* Note that my_dir sorts the list if not given any flags */
|
|
MY_DIR *dest_dir_info =
|
|
my_dir(ds_destination->str, MYF(MY_DONT_SORT | MY_WANT_STAT));
|
|
|
|
/* Create destination directory if it doesn't exist */
|
|
if (!dest_dir_info) {
|
|
error = my_mkdir(ds_destination->str, 0777, MYF(0)) != 0;
|
|
if (error) {
|
|
my_dirend(dest_dir_info);
|
|
goto end;
|
|
}
|
|
} else {
|
|
/* Extracting the source directory name */
|
|
if (ds_source->str[std::strlen(ds_source->str) - 1] == '/') {
|
|
strmake(ds_source->str, ds_source->str,
|
|
std::strlen(ds_source->str) - 1);
|
|
ds_source->length = ds_source->length - 1;
|
|
}
|
|
char *src_dir_name = strrchr(ds_source->str, '/');
|
|
|
|
/* Extracting the destination directory name */
|
|
if (ds_destination->str[std::strlen(ds_destination->str) - 1] == '/') {
|
|
strmake(ds_destination->str, ds_destination->str,
|
|
std::strlen(ds_destination->str) - 1);
|
|
ds_destination->length = ds_destination->length - 1;
|
|
}
|
|
char *dest_dir_name = strrchr(ds_destination->str, '/');
|
|
|
|
/*
|
|
Destination directory might not exist if source directory
|
|
name and destination directory name are not same.
|
|
|
|
For example, if source is "abc" and destintion is "def",
|
|
check for the existance of directory "def/abc". If it exists
|
|
then, copy the files from source directory(i.e "abc") to
|
|
destination directory(i.e "def/abc"), otherwise create a new
|
|
directory "abc" under "def" and copy the files from source to
|
|
destination directory.
|
|
*/
|
|
if (std::strcmp(src_dir_name, dest_dir_name)) {
|
|
dynstr_append(ds_destination, src_dir_name);
|
|
my_dirend(dest_dir_info);
|
|
dest_dir_info =
|
|
my_dir(ds_destination->str, MYF(MY_DONT_SORT | MY_WANT_STAT));
|
|
|
|
/* Create destination directory if it doesn't exist */
|
|
if (!dest_dir_info) {
|
|
error = my_mkdir(ds_destination->str, 0777, MYF(0)) != 0;
|
|
if (error) {
|
|
my_dirend(dest_dir_info);
|
|
goto end;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
char dir_separator[2] = {FN_LIBCHAR, 0};
|
|
dynstr_append(ds_source, dir_separator);
|
|
dynstr_append(ds_destination, dir_separator);
|
|
|
|
/*
|
|
Storing the length of source and destination
|
|
directory paths so it can be reused.
|
|
*/
|
|
size_t source_dir_length = ds_source->length;
|
|
size_t destination_dir_length = ds_destination->length;
|
|
;
|
|
|
|
for (uint i = 0; i < src_dir_info->number_off_files; i++) {
|
|
ds_source->length = source_dir_length;
|
|
ds_destination->length = destination_dir_length;
|
|
FILEINFO *file = src_dir_info->dir_entry + i;
|
|
|
|
/* Skip the names "." and ".." */
|
|
if (!std::strcmp(file->name, ".") || !std::strcmp(file->name, ".."))
|
|
continue;
|
|
|
|
dynstr_append(ds_source, file->name);
|
|
dynstr_append(ds_destination, file->name);
|
|
|
|
if (MY_S_ISDIR(file->mystat->st_mode))
|
|
error = (recursive_copy(ds_source, ds_destination) != 0) ? 1 : error;
|
|
else {
|
|
DBUG_PRINT("info", ("Copying file: %s to %s", ds_source->str,
|
|
ds_destination->str));
|
|
|
|
/* MY_HOLD_ORIGINAL_MODES prevents attempts to chown the file */
|
|
error = (my_copy(ds_source->str, ds_destination->str,
|
|
MYF(MY_HOLD_ORIGINAL_MODES)) != 0)
|
|
? 1
|
|
: error;
|
|
}
|
|
}
|
|
my_dirend(dest_dir_info);
|
|
}
|
|
/* Source directory does not exist or access denied */
|
|
else
|
|
error = 1;
|
|
|
|
end:
|
|
my_dirend(src_dir_info);
|
|
return error;
|
|
}
|
|
|
|
/*
|
|
SYNOPSIS
|
|
do_force_cpdir
|
|
command - command handle
|
|
|
|
DESCRIPTION
|
|
force-cpdir <from_directory> <to_directory>
|
|
Recursive copy of <from_directory> to <to_directory>.
|
|
Destination directory is created if it doesn't exist.
|
|
|
|
NOTE
|
|
Will fail if <from_directory> doesn't exist.
|
|
*/
|
|
|
|
static void do_force_cpdir(struct st_command *command) {
|
|
DBUG_TRACE;
|
|
|
|
static DYNAMIC_STRING ds_source;
|
|
static DYNAMIC_STRING ds_destination;
|
|
|
|
const struct command_arg copy_file_args[] = {
|
|
{"from_directory", ARG_STRING, true, &ds_source,
|
|
"Directory to copy from"},
|
|
{"to_directory", ARG_STRING, true, &ds_destination,
|
|
"Directory to copy to"}};
|
|
|
|
check_command_args(command, command->first_argument, copy_file_args,
|
|
sizeof(copy_file_args) / sizeof(struct command_arg), ' ');
|
|
|
|
DBUG_PRINT("info", ("Recursive copy files of %s to %s", ds_source.str,
|
|
ds_destination.str));
|
|
|
|
DBUG_PRINT("info", ("listing directory: %s", ds_source.str));
|
|
|
|
int error = 0;
|
|
|
|
/*
|
|
Throw an error if source directory path and
|
|
destination directory path are same.
|
|
*/
|
|
if (!std::strcmp(ds_source.str, ds_destination.str)) {
|
|
error = 1;
|
|
set_my_errno(EEXIST);
|
|
} else
|
|
error = recursive_copy(&ds_source, &ds_destination);
|
|
|
|
handle_command_error(command, error);
|
|
dynstr_free(&ds_source);
|
|
dynstr_free(&ds_destination);
|
|
}
|
|
|
|
/// Copy files from source directory to destination directory, by matching
|
|
/// a specified file name pattern.
|
|
///
|
|
/// Copy will fail if no files match the pattern. It will fail if source
|
|
/// directory is empty and/or there are no files in it. Copy will also
|
|
/// fail if source directory or destination directory or both do not
|
|
/// exist. Retry of the command can happen optionally with an interval of
|
|
/// one second between each retry if the command fails.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
static void do_copy_files_wildcard(struct st_command *command) {
|
|
static DYNAMIC_STRING ds_source;
|
|
static DYNAMIC_STRING ds_destination;
|
|
static DYNAMIC_STRING ds_wild;
|
|
static DYNAMIC_STRING ds_retry;
|
|
|
|
const struct command_arg copy_file_args[] = {
|
|
{"from_directory", ARG_STRING, true, &ds_source,
|
|
"Directory to copy from"},
|
|
{"to_directory", ARG_STRING, true, &ds_destination,
|
|
"Directory to copy to"},
|
|
{"pattern", ARG_STRING, true, &ds_wild, "File name pattern"},
|
|
{"retry", ARG_STRING, false, &ds_retry, "Number of retries"}};
|
|
DBUG_TRACE;
|
|
|
|
check_command_args(command, command->first_argument, copy_file_args,
|
|
sizeof(copy_file_args) / sizeof(struct command_arg), ' ');
|
|
|
|
DBUG_PRINT("info",
|
|
("Copy files of %s to %s", ds_source.str, ds_destination.str));
|
|
|
|
DBUG_PRINT("info", ("listing directory: %s", ds_source.str));
|
|
|
|
int error = 0;
|
|
|
|
// Check if the retry value is passed, and if it is an integer
|
|
int retry = 0;
|
|
if (ds_retry.length) {
|
|
retry = get_int_val(ds_retry.str);
|
|
if (retry < 0) {
|
|
// In case of invalid retry, copy the value passed to print later
|
|
char buf[32];
|
|
strmake(buf, ds_retry.str, sizeof(buf) - 1);
|
|
free_dynamic_strings(&ds_source, &ds_destination, &ds_wild, &ds_retry);
|
|
die("Invalid value '%s' for retry argument given to "
|
|
"copy_files_wildcard command.",
|
|
buf);
|
|
}
|
|
}
|
|
|
|
/* Note that my_dir sorts the list if not given any flags */
|
|
MY_DIR *dir_info = my_dir(ds_source.str, MYF(MY_DONT_SORT | MY_WANT_STAT));
|
|
|
|
/* Directory does not exist or access denied */
|
|
if (!dir_info) {
|
|
error = 1;
|
|
goto end;
|
|
}
|
|
|
|
/* The directory exists but is empty */
|
|
if (dir_info->number_off_files == 2) {
|
|
error = 1;
|
|
set_my_errno(ENOENT);
|
|
goto end;
|
|
}
|
|
|
|
char dir_separator[2];
|
|
dir_separator[0] = FN_LIBCHAR;
|
|
dir_separator[1] = 0;
|
|
dynstr_append(&ds_source, dir_separator);
|
|
dynstr_append(&ds_destination, dir_separator);
|
|
|
|
/* Storing the length of the path to the file, so it can be reused */
|
|
size_t source_file_length;
|
|
size_t dest_file_length;
|
|
dest_file_length = ds_destination.length;
|
|
source_file_length = ds_source.length;
|
|
uint match_count;
|
|
match_count = 0;
|
|
|
|
for (uint i = 0; i < dir_info->number_off_files; i++) {
|
|
ds_source.length = source_file_length;
|
|
ds_destination.length = dest_file_length;
|
|
FILEINFO *file = dir_info->dir_entry + i;
|
|
|
|
/*
|
|
Copy 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;
|
|
|
|
/* Copy only those files which the pattern matches */
|
|
if (wild_compare_full(file->name, std::strlen(file->name), ds_wild.str,
|
|
std::strlen(ds_wild.str), false, 0, '?', '*'))
|
|
continue;
|
|
|
|
match_count++;
|
|
dynstr_append(&ds_source, file->name);
|
|
dynstr_append(&ds_destination, file->name);
|
|
DBUG_PRINT("info",
|
|
("Copying file: %s to %s", ds_source.str, ds_destination.str));
|
|
|
|
/* MY_HOLD_ORIGINAL_MODES prevents attempts to chown the file */
|
|
error = (my_copy(ds_source.str, ds_destination.str,
|
|
MYF(MY_HOLD_ORIGINAL_MODES)) != 0);
|
|
|
|
/*
|
|
If the copy command fails due to an environmental issue, the command can
|
|
be retried a specified number of times before throwing an error.
|
|
*/
|
|
for (int j = 0; error && (j < retry); j++) {
|
|
my_sleep(1000 * 1000);
|
|
error =
|
|
(my_copy(ds_source.str, ds_destination.str,
|
|
MYF(MY_DONT_OVERWRITE_FILE | MY_HOLD_ORIGINAL_MODES)) != 0);
|
|
}
|
|
|
|
if (error) goto end;
|
|
}
|
|
|
|
/* Pattern did not match any files */
|
|
if (!match_count) {
|
|
error = 1;
|
|
set_my_errno(ENOENT);
|
|
}
|
|
|
|
end:
|
|
my_dirend(dir_info);
|
|
handle_command_error(command, error);
|
|
free_dynamic_strings(&ds_source, &ds_destination, &ds_wild, &ds_retry);
|
|
}
|
|
|
|
/*
|
|
SYNOPSIS
|
|
move_file_by_copy_delete
|
|
from path of source
|
|
to path of destination
|
|
|
|
DESCRIPTION
|
|
Move <from_file> to <to_file>
|
|
Auxiliary function for copying <from_file> to <to_file> followed by
|
|
deleting <to_file>.
|
|
*/
|
|
|
|
static int move_file_by_copy_delete(const char *from, const char *to) {
|
|
int error_copy, error_delete;
|
|
error_copy = (my_copy(from, to, MYF(MY_HOLD_ORIGINAL_MODES)) != 0);
|
|
if (error_copy) {
|
|
return error_copy;
|
|
}
|
|
|
|
error_delete = my_delete(from, MYF(0)) != 0;
|
|
|
|
/*
|
|
If deleting the source file fails, rollback by deleting the
|
|
redundant copy at the destinatiion.
|
|
*/
|
|
if (error_delete) {
|
|
my_delete(to, MYF(0));
|
|
}
|
|
return error_delete;
|
|
}
|
|
|
|
/// Moves a file to destination file. Retry of the command can happen
|
|
/// optionally with an interval of one second between each retry if
|
|
/// the command fails.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
static void do_move_file(struct st_command *command) {
|
|
int error;
|
|
static DYNAMIC_STRING ds_from_file;
|
|
static DYNAMIC_STRING ds_to_file;
|
|
static DYNAMIC_STRING ds_retry;
|
|
|
|
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"},
|
|
{"retry", ARG_STRING, false, &ds_retry, "Number of retries"}};
|
|
DBUG_TRACE;
|
|
|
|
check_command_args(command, command->first_argument, move_file_args,
|
|
sizeof(move_file_args) / sizeof(struct command_arg), ' ');
|
|
|
|
// Check if the retry value is passed, and if it is an interger
|
|
int retry = 0;
|
|
if (ds_retry.length) {
|
|
retry = get_int_val(ds_retry.str);
|
|
if (retry < 0) {
|
|
// In case of invalid retry, copy the value passed to print later
|
|
char buf[32];
|
|
strmake(buf, ds_retry.str, sizeof(buf) - 1);
|
|
free_dynamic_strings(&ds_from_file, &ds_to_file, &ds_retry);
|
|
die("Invalid value '%s' for retry argument given to move_file "
|
|
"command.",
|
|
buf);
|
|
}
|
|
}
|
|
|
|
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(0)) != 0);
|
|
|
|
/*
|
|
Use my_copy() followed by my_delete() for moving a file instead of
|
|
my_rename() when my_errno is EXDEV. This is because my_rename() fails
|
|
with the error "Invalid cross-device link" while moving a file between
|
|
locations having different filesystems in some operating systems.
|
|
*/
|
|
if (error && (my_errno() == EXDEV)) {
|
|
error = move_file_by_copy_delete(ds_from_file.str, ds_to_file.str);
|
|
}
|
|
|
|
/*
|
|
If the command fails due to an environmental issue, the command can be
|
|
retried a specified number of times before throwing an error.
|
|
*/
|
|
for (int i = 0; error && (i < retry); i++) {
|
|
my_sleep(1000 * 1000);
|
|
error = (my_rename(ds_from_file.str, ds_to_file.str, MYF(0)) != 0);
|
|
|
|
if (error && (my_errno() == EXDEV))
|
|
error = move_file_by_copy_delete(ds_from_file.str, ds_to_file.str);
|
|
}
|
|
|
|
handle_command_error(command, error);
|
|
free_dynamic_strings(&ds_from_file, &ds_to_file, &ds_retry);
|
|
}
|
|
|
|
/*
|
|
SYNOPSIS
|
|
do_chmod_file
|
|
command command handle
|
|
|
|
DESCRIPTION
|
|
chmod <octal> <file_name>
|
|
Change file permission of <file_name>
|
|
|
|
*/
|
|
|
|
static 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_TRACE;
|
|
|
|
check_command_args(command, command->first_argument, chmod_file_args,
|
|
sizeof(chmod_file_args) / sizeof(struct command_arg), ' ');
|
|
|
|
/* 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);
|
|
dynstr_free(&ds_mode);
|
|
dynstr_free(&ds_file);
|
|
}
|
|
|
|
/// Check if specified file exists. Retry of the command can happen
|
|
/// optionally with an interval of one second between each retry if
|
|
/// the command fails.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
static void do_file_exist(struct st_command *command) {
|
|
int error;
|
|
static DYNAMIC_STRING ds_filename;
|
|
static DYNAMIC_STRING ds_retry;
|
|
|
|
const struct command_arg file_exist_args[] = {
|
|
{"filename", ARG_STRING, true, &ds_filename, "File to check if it exist"},
|
|
{"retry", ARG_STRING, false, &ds_retry, "Number of retries"}};
|
|
DBUG_TRACE;
|
|
|
|
check_command_args(command, command->first_argument, file_exist_args,
|
|
sizeof(file_exist_args) / sizeof(struct command_arg), ' ');
|
|
|
|
// Check if the retry value is passed, and if it is an interger
|
|
int retry = 0;
|
|
if (ds_retry.length) {
|
|
retry = get_int_val(ds_retry.str);
|
|
if (retry < 0) {
|
|
// In case of invalid retry, copy the value passed to print later
|
|
char buf[32];
|
|
strmake(buf, ds_retry.str, sizeof(buf) - 1);
|
|
free_dynamic_strings(&ds_filename, &ds_retry);
|
|
die("Invalid value '%s' for retry argument given to file_exists "
|
|
"command.",
|
|
buf);
|
|
}
|
|
}
|
|
|
|
DBUG_PRINT("info", ("Checking for existence of file: %s", ds_filename.str));
|
|
error = (access(ds_filename.str, F_OK) != 0);
|
|
|
|
/*
|
|
If the file_exists command fails due to an environmental issue, the command
|
|
can be retried a specified number of times before throwing an error.
|
|
*/
|
|
for (int i = 0; error && (i < retry); i++) {
|
|
my_sleep(1000 * 1000);
|
|
error = (access(ds_filename.str, F_OK) != 0);
|
|
}
|
|
|
|
handle_command_error(command, error);
|
|
free_dynamic_strings(&ds_filename, &ds_retry);
|
|
}
|
|
|
|
/*
|
|
SYNOPSIS
|
|
do_mkdir
|
|
command called command
|
|
|
|
DESCRIPTION
|
|
mkdir <dir_name>
|
|
Create the directory <dir_name>
|
|
*/
|
|
|
|
static 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_TRACE;
|
|
|
|
check_command_args(command, command->first_argument, mkdir_args,
|
|
sizeof(mkdir_args) / sizeof(struct command_arg), ' ');
|
|
|
|
DBUG_PRINT("info", ("creating directory: %s", ds_dirname.str));
|
|
error = my_mkdir(ds_dirname.str, 0777, MYF(0)) != 0;
|
|
handle_command_error(command, error);
|
|
dynstr_free(&ds_dirname);
|
|
}
|
|
|
|
/*
|
|
SYNOPSIS
|
|
do_force_rmdir
|
|
command - command handle
|
|
ds_dirname - pointer to dynamic string containing directory informtion
|
|
|
|
DESCRIPTION
|
|
force-rmdir <dir_name>
|
|
Remove the directory <dir_name>
|
|
*/
|
|
|
|
void do_force_rmdir(struct st_command *command, DYNAMIC_STRING *ds_dirname) {
|
|
DBUG_TRACE;
|
|
|
|
char dir_name[FN_REFLEN + 1];
|
|
strncpy(dir_name, ds_dirname->str, sizeof(dir_name) - 1);
|
|
dir_name[FN_REFLEN] = '\0';
|
|
|
|
/* Note that my_dir sorts the list if not given any flags */
|
|
MY_DIR *dir_info = my_dir(ds_dirname->str, MYF(MY_DONT_SORT | MY_WANT_STAT));
|
|
|
|
if (dir_info && dir_info->number_off_files > 2) {
|
|
/* Storing the length of the path to the file, so it can be reused */
|
|
size_t length = ds_dirname->length;
|
|
|
|
/* Delete the directory recursively */
|
|
for (uint i = 0; i < dir_info->number_off_files; i++) {
|
|
FILEINFO *file = dir_info->dir_entry + i;
|
|
|
|
/* Skip the names "." and ".." */
|
|
if (!std::strcmp(file->name, ".") || !std::strcmp(file->name, ".."))
|
|
continue;
|
|
|
|
ds_dirname->length = length;
|
|
char dir_separator[2] = {FN_LIBCHAR, 0};
|
|
dynstr_append(ds_dirname, dir_separator);
|
|
dynstr_append(ds_dirname, file->name);
|
|
|
|
if (MY_S_ISDIR(file->mystat->st_mode)) /* It's a directory */
|
|
do_force_rmdir(command, ds_dirname);
|
|
else
|
|
/* It's a file */
|
|
my_delete(ds_dirname->str, MYF(0));
|
|
}
|
|
}
|
|
|
|
my_dirend(dir_info);
|
|
int error = rmdir(dir_name) != 0;
|
|
set_my_errno(errno);
|
|
handle_command_error(command, error);
|
|
}
|
|
|
|
/*
|
|
SYNOPSIS
|
|
do_rmdir
|
|
command called command
|
|
force Recursively delete a directory if the value is set to true,
|
|
otherwise delete an empty direcory
|
|
|
|
DESCRIPTION
|
|
rmdir <dir_name>
|
|
Remove the empty directory <dir_name>
|
|
*/
|
|
|
|
static void do_rmdir(struct st_command *command, bool force) {
|
|
int error;
|
|
static DYNAMIC_STRING ds_dirname;
|
|
const struct command_arg rmdir_args[] = {
|
|
{"dirname", ARG_STRING, true, &ds_dirname, "Directory to remove"}};
|
|
DBUG_TRACE;
|
|
|
|
check_command_args(command, command->first_argument, rmdir_args,
|
|
sizeof(rmdir_args) / sizeof(struct command_arg), ' ');
|
|
|
|
DBUG_PRINT("info", ("removing directory: %s", ds_dirname.str));
|
|
if (force)
|
|
do_force_rmdir(command, &ds_dirname);
|
|
else {
|
|
error = rmdir(ds_dirname.str) != 0;
|
|
set_my_errno(errno);
|
|
handle_command_error(command, error);
|
|
}
|
|
dynstr_free(&ds_dirname);
|
|
}
|
|
|
|
/*
|
|
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) {
|
|
uint i;
|
|
MY_DIR *dir_info;
|
|
FILEINFO *file;
|
|
DBUG_TRACE;
|
|
|
|
DBUG_PRINT("info", ("listing directory: %s", ds_dirname->str));
|
|
/* Note that my_dir sorts the list if not given any flags */
|
|
if (!(dir_info = my_dir(ds_dirname->str, MYF(0)))) return 1;
|
|
for (i = 0; i < (uint)dir_info->number_off_files; i++) {
|
|
file = dir_info->dir_entry + i;
|
|
if (file->name[0] == '.' &&
|
|
(file->name[1] == '\0' ||
|
|
(file->name[1] == '.' && file->name[2] == '\0')))
|
|
continue; /* . or .. */
|
|
if (ds_wild && ds_wild->length &&
|
|
wild_compare_full(file->name, std::strlen(file->name), ds_wild->str,
|
|
std::strlen(ds_wild->str), false, 0, '?', '*'))
|
|
continue;
|
|
replace_dynstr_append(ds, file->name);
|
|
dynstr_append(ds, "\n");
|
|
}
|
|
my_dirend(dir_info);
|
|
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_TRACE;
|
|
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);
|
|
dynstr_free(&ds_dirname);
|
|
dynstr_free(&ds_wild);
|
|
}
|
|
|
|
/*
|
|
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,
|
|
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_TRACE;
|
|
command->used_replace = 1;
|
|
|
|
check_command_args(command, command->first_argument, list_files_args,
|
|
sizeof(list_files_args) / sizeof(struct command_arg), ' ');
|
|
|
|
init_dynamic_string(&ds_content, "", 1024, 1024);
|
|
error = get_list_files(&ds_content, &ds_dirname, &ds_wild);
|
|
handle_command_error(command, error);
|
|
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);
|
|
}
|
|
|
|
/*
|
|
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.
|
|
*/
|
|
|
|
static int my_getc(FILE *file) {
|
|
if (line_buffer_pos == line_buffer) return fgetc(file);
|
|
return *--line_buffer_pos;
|
|
}
|
|
|
|
static void my_ungetc(int c) { *line_buffer_pos++ = (char)c; }
|
|
|
|
static void read_until_delimiter(DYNAMIC_STRING *ds,
|
|
DYNAMIC_STRING *ds_delimiter) {
|
|
char c;
|
|
DBUG_TRACE;
|
|
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 == '\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
|
|
*/
|
|
die("Trailing characters found after command");
|
|
}
|
|
|
|
if (feof(cur_file->file))
|
|
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));
|
|
}
|
|
|
|
static void do_write_file_command(struct st_command *command, 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_TRACE;
|
|
|
|
check_command_args(command, command->first_argument, write_file_args,
|
|
sizeof(write_file_args) / sizeof(struct command_arg), ' ');
|
|
|
|
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);
|
|
}
|
|
|
|
/*
|
|
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
|
|
|
|
*/
|
|
|
|
static 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
|
|
|
|
*/
|
|
|
|
static 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
|
|
|
|
*/
|
|
|
|
static 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_TRACE;
|
|
|
|
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);
|
|
dynstr_free(&ds_filename);
|
|
}
|
|
|
|
/// Compare the two files.
|
|
///
|
|
/// Success if 2 files are same, failure if the files are different or
|
|
/// either file does not exist.
|
|
///
|
|
/// @code
|
|
/// diff_files <file1> <file2>;
|
|
/// @endcode
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
static void do_diff_files(struct st_command *command) {
|
|
static DYNAMIC_STRING ds_filename1;
|
|
static DYNAMIC_STRING ds_filename2;
|
|
|
|
const struct command_arg diff_file_args[] = {
|
|
{"file1", ARG_STRING, true, &ds_filename1, "First file to diff"},
|
|
{"file2", ARG_STRING, true, &ds_filename2, "Second file to diff"}};
|
|
|
|
check_command_args(command, command->first_argument, diff_file_args,
|
|
sizeof(diff_file_args) / sizeof(struct command_arg), ' ');
|
|
|
|
if (access(ds_filename1.str, F_OK) != 0)
|
|
die("Command \"diff_files\" failed, file '%s' does not exist.",
|
|
ds_filename1.str);
|
|
|
|
if (access(ds_filename2.str, F_OK) != 0)
|
|
die("Command \"diff_files\" failed, file '%s' does not exist.",
|
|
ds_filename2.str);
|
|
|
|
int error = 0;
|
|
if ((error = compare_files(ds_filename1.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_filename1.str, ds_filename2.str);
|
|
if (log_file.write(ds_res.str, ds_res.length) || log_file.flush())
|
|
cleanup_and_exit(1);
|
|
dynstr_set(&ds_res, 0);
|
|
}
|
|
|
|
free_dynamic_strings(&ds_filename1, &ds_filename2);
|
|
handle_command_error(command, error);
|
|
}
|
|
|
|
static struct st_connection *find_connection_by_name(const char *name) {
|
|
struct st_connection *con;
|
|
for (con = connections; con < next_con; con++) {
|
|
if (!std::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.
|
|
|
|
*/
|
|
|
|
static void do_send_quit(struct st_command *command) {
|
|
char *p = command->first_argument;
|
|
const char *name;
|
|
struct st_connection *con;
|
|
|
|
DBUG_TRACE;
|
|
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);
|
|
}
|
|
|
|
/*
|
|
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.
|
|
|
|
*/
|
|
|
|
static void do_change_user(struct st_command *command) {
|
|
MYSQL *mysql = &cur_con->mysql;
|
|
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_TRACE;
|
|
|
|
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(curr_command, mysql_errno(mysql), mysql_error(mysql),
|
|
mysql_sqlstate(mysql), &ds_res);
|
|
mysql->reconnect = 1;
|
|
mysql_reconnect(&cur_con->mysql);
|
|
}
|
|
|
|
dynstr_free(&ds_user);
|
|
dynstr_free(&ds_passwd);
|
|
dynstr_free(&ds_db);
|
|
}
|
|
|
|
/*
|
|
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
|
|
*/
|
|
|
|
static void do_perl(struct st_command *command) {
|
|
int error;
|
|
File fd;
|
|
FILE *res_file;
|
|
char buf[FN_REFLEN + 10];
|
|
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_TRACE;
|
|
|
|
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_CREAT | O_RDWR, KEEP_FILE, MYF(MY_WME))) < 0)
|
|
die("Failed to create temporary file for perl command");
|
|
my_close(fd, MYF(0));
|
|
|
|
/* Compatibility for Perl 5.24 and newer. */
|
|
std::string script = "push @INC, \".\";\n";
|
|
script.append(ds_script.str, ds_script.length);
|
|
|
|
str_to_file(temp_file_path, &script[0], script.size());
|
|
|
|
/* Format the "perl <filename>" command */
|
|
snprintf(buf, sizeof(buf), "perl %s", temp_file_path);
|
|
|
|
if (!(res_file = popen(buf, "r")) && command->abort_on_error)
|
|
die("popen(\"%s\", \"r\") failed", buf);
|
|
|
|
while (fgets(buf, sizeof(buf), res_file)) {
|
|
if (disable_result_log) {
|
|
buf[std::strlen(buf) - 1] = 0;
|
|
DBUG_PRINT("exec_result", ("%s", buf));
|
|
} else {
|
|
replace_dynstr_append(&ds_res, buf);
|
|
}
|
|
}
|
|
error = pclose(res_file);
|
|
|
|
/* Remove the temporary file, but keep it if perl failed */
|
|
if (!error) my_delete(temp_file_path, MYF(0));
|
|
|
|
/* 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);
|
|
}
|
|
dynstr_free(&ds_delimiter);
|
|
}
|
|
|
|
/*
|
|
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
|
|
*/
|
|
|
|
static int do_echo(struct st_command *command) {
|
|
DYNAMIC_STRING ds_echo;
|
|
DBUG_TRACE;
|
|
|
|
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;
|
|
return 0;
|
|
}
|
|
|
|
static void do_wait_for_slave_to_stop(
|
|
struct st_command *c MY_ATTRIBUTE((unused))) {
|
|
static int SLAVE_POLL_INTERVAL = 300000;
|
|
MYSQL *mysql = &cur_con->mysql;
|
|
for (;;) {
|
|
MYSQL_RES *res = NULL;
|
|
MYSQL_ROW row;
|
|
int done;
|
|
|
|
if (mysql_query_wrapper(
|
|
mysql,
|
|
"SELECT 'Slave_running' as Variable_name,"
|
|
" IF(count(*)>0,'ON','OFF') as Value FROM"
|
|
" performance_schema.replication_applier_status ras,"
|
|
"performance_schema.replication_connection_status rcs WHERE "
|
|
"ras.SERVICE_STATE='ON' AND rcs.SERVICE_STATE='ON'") ||
|
|
!(res = mysql_store_result_wrapper(mysql)))
|
|
|
|
die("Query failed while probing slave for stop: %s", mysql_error(mysql));
|
|
|
|
if (!(row = mysql_fetch_row_wrapper(res)) || !row[1]) {
|
|
mysql_free_result_wrapper(res);
|
|
die("Strange result from query while probing slave for stop");
|
|
}
|
|
done = !std::strcmp(row[1], "OFF");
|
|
mysql_free_result_wrapper(res);
|
|
if (done) break;
|
|
my_sleep(SLAVE_POLL_INTERVAL);
|
|
}
|
|
return;
|
|
}
|
|
|
|
static void do_sync_with_master2(struct st_command *command, long offset) {
|
|
MYSQL_RES *res;
|
|
MYSQL_ROW row;
|
|
MYSQL *mysql = &cur_con->mysql;
|
|
char query_buf[FN_REFLEN + 128];
|
|
int timeout = 300; /* seconds */
|
|
|
|
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)", master_pos.file,
|
|
master_pos.pos + offset, timeout);
|
|
|
|
if (mysql_query_wrapper(mysql, query_buf))
|
|
die("failed in '%s': %d: %s", query_buf, mysql_errno(mysql),
|
|
mysql_error(mysql));
|
|
|
|
if (!(res = mysql_store_result_wrapper(mysql)))
|
|
die("mysql_store_result() returned NULL for '%s'", query_buf);
|
|
if (!(row = mysql_fetch_row_wrapper(res))) {
|
|
mysql_free_result_wrapper(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_wrapper(res);
|
|
|
|
if (!result_str || result < 0) {
|
|
/* master_pos_wait returned NULL or < 0 */
|
|
show_query(mysql, "SHOW MASTER STATUS");
|
|
show_query(mysql, "SHOW SLAVE STATUS");
|
|
show_query(mysql, "SHOW PROCESSLIST");
|
|
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("%.*s failed: '%s' returned NULL "
|
|
"indicating slave SQL thread failure",
|
|
static_cast<int>(command->first_word_len), command->query, query_buf);
|
|
}
|
|
|
|
if (result == -1)
|
|
die("%.*s failed: '%s' returned -1 "
|
|
"indicating timeout after %d seconds",
|
|
static_cast<int>(command->first_word_len), command->query, query_buf,
|
|
timeout);
|
|
else
|
|
die("%.*s failed: '%s' returned unknown result :%d",
|
|
static_cast<int>(command->first_word_len), command->query, query_buf,
|
|
result);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
static void do_sync_with_master(struct st_command *command) {
|
|
long offset = 0;
|
|
char *p = command->first_argument;
|
|
const char *offset_start = p;
|
|
if (*offset_start) {
|
|
for (; my_isdigit(charset_info, *p); p++) offset = offset * 10 + *p - '0';
|
|
|
|
if (*p && !my_isspace(charset_info, *p))
|
|
die("Invalid integer argument \"%s\"", offset_start);
|
|
command->last_argument = p;
|
|
}
|
|
do_sync_with_master2(command, offset);
|
|
return;
|
|
}
|
|
|
|
/*
|
|
Wait for ndb binlog injector to be up-to-date with all changes
|
|
done on the local mysql server
|
|
*/
|
|
|
|
static void ndb_wait_for_binlog_injector(void) {
|
|
MYSQL_RES *res;
|
|
MYSQL_ROW row;
|
|
MYSQL *mysql = &cur_con->mysql;
|
|
const char *query;
|
|
ulong have_ndbcluster;
|
|
if (mysql_query_wrapper(
|
|
mysql, query = "select count(*) from information_schema.engines"
|
|
" where engine = 'ndbcluster' and"
|
|
" support in ('YES', 'DEFAULT')"))
|
|
die("'%s' failed: %d %s", query, mysql_errno(mysql), mysql_error(mysql));
|
|
if (!(res = mysql_store_result_wrapper(mysql)))
|
|
die("mysql_store_result() returned NULL for '%s'", query);
|
|
if (!(row = mysql_fetch_row_wrapper(res)))
|
|
die("Query '%s' returned empty result", query);
|
|
|
|
have_ndbcluster = std::strcmp(row[0], "1") == 0;
|
|
mysql_free_result_wrapper(res);
|
|
|
|
if (!have_ndbcluster) {
|
|
return;
|
|
}
|
|
|
|
ulonglong start_epoch = 0, handled_epoch = 0, latest_trans_epoch = 0,
|
|
latest_handled_binlog_epoch = 0, start_handled_binlog_epoch = 0;
|
|
const int WaitSeconds = 150;
|
|
|
|
int count = 0;
|
|
int do_continue = 1;
|
|
while (do_continue) {
|
|
const char binlog[] = "binlog";
|
|
const char latest_trans_epoch_str[] = "latest_trans_epoch=";
|
|
const char latest_handled_binlog_epoch_str[] =
|
|
"latest_handled_binlog_epoch=";
|
|
if (count) my_sleep(100 * 1000); /* 100ms */
|
|
|
|
if (mysql_query_wrapper(mysql, query = "show engine ndb status"))
|
|
die("failed in '%s': %d %s", query, mysql_errno(mysql),
|
|
mysql_error(mysql));
|
|
|
|
if (!(res = mysql_store_result_wrapper(mysql)))
|
|
die("mysql_store_result() returned NULL for '%s'", query);
|
|
|
|
while ((row = mysql_fetch_row_wrapper(res))) {
|
|
if (std::strcmp(row[1], binlog) == 0) {
|
|
const char *status = row[2];
|
|
|
|
/* latest_trans_epoch */
|
|
while (*status && std::strncmp(status, latest_trans_epoch_str,
|
|
sizeof(latest_trans_epoch_str) - 1))
|
|
status++;
|
|
if (*status) {
|
|
status += sizeof(latest_trans_epoch_str) - 1;
|
|
latest_trans_epoch = my_strtoull(status, (char **)0, 10);
|
|
} else
|
|
die("result does not contain '%s' in '%s'", latest_trans_epoch_str,
|
|
query);
|
|
|
|
/* latest_handled_binlog */
|
|
while (*status &&
|
|
std::strncmp(status, latest_handled_binlog_epoch_str,
|
|
sizeof(latest_handled_binlog_epoch_str) - 1))
|
|
status++;
|
|
if (*status) {
|
|
status += sizeof(latest_handled_binlog_epoch_str) - 1;
|
|
latest_handled_binlog_epoch = my_strtoull(status, (char **)0, 10);
|
|
} else
|
|
die("result does not contain '%s' in '%s'",
|
|
latest_handled_binlog_epoch_str, query);
|
|
|
|
if (count == 0) {
|
|
start_epoch = latest_trans_epoch;
|
|
start_handled_binlog_epoch = latest_handled_binlog_epoch;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (!row) die("result does not contain '%s' in '%s'", binlog, query);
|
|
if (latest_handled_binlog_epoch > handled_epoch) count = 0;
|
|
handled_epoch = latest_handled_binlog_epoch;
|
|
count++;
|
|
if (latest_handled_binlog_epoch >= start_epoch)
|
|
do_continue = 0;
|
|
else if (count > (WaitSeconds * 10)) {
|
|
die("do_save_master_pos() timed out after %u s waiting for "
|
|
"last committed epoch to be applied by the "
|
|
"Ndb binlog injector. "
|
|
"Ndb epoch %llu/%llu to be handled. "
|
|
"Last handled epoch : %llu/%llu. "
|
|
"First handled epoch : %llu/%llu.",
|
|
WaitSeconds, start_epoch >> 32, start_epoch & 0xffffffff,
|
|
latest_handled_binlog_epoch >> 32,
|
|
latest_handled_binlog_epoch & 0xffffffff,
|
|
start_handled_binlog_epoch >> 32,
|
|
start_handled_binlog_epoch & 0xffffffff);
|
|
}
|
|
|
|
mysql_free_result_wrapper(res);
|
|
}
|
|
}
|
|
|
|
static int do_save_master_pos() {
|
|
MYSQL_RES *res;
|
|
MYSQL_ROW row;
|
|
MYSQL *mysql = &cur_con->mysql;
|
|
const char *query;
|
|
DBUG_TRACE;
|
|
/*
|
|
when ndb binlog is on, this call will wait until last updated epoch
|
|
(locally in the mysqld) has been received into the binlog
|
|
*/
|
|
ndb_wait_for_binlog_injector();
|
|
|
|
if (mysql_query_wrapper(mysql, query = "show master status"))
|
|
die("failed in 'show master status': %d %s", mysql_errno(mysql),
|
|
mysql_error(mysql));
|
|
|
|
if (!(res = mysql_store_result_wrapper(mysql)))
|
|
die("mysql_store_result() retuned NULL for '%s'", query);
|
|
if (!(row = mysql_fetch_row_wrapper(res)))
|
|
die("empty result in show master status");
|
|
my_stpnmov(master_pos.file, row[0], sizeof(master_pos.file) - 1);
|
|
master_pos.pos = strtoul(row[1], (char **)0, 10);
|
|
mysql_free_result_wrapper(res);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
Check if a variable name is valid or not.
|
|
|
|
SYNOPSIS
|
|
check_variable_name()
|
|
var_name - pointer to the beginning of variable name
|
|
var_name_end - pointer to the end of variable name
|
|
dollar_flag - flag to check whether variable name should start with '$'
|
|
*/
|
|
static void check_variable_name(const char *var_name, const char *var_name_end,
|
|
const bool dollar_flag) {
|
|
char save_var_name[MAX_VAR_NAME_LENGTH];
|
|
strmake(save_var_name, var_name, (var_name_end - var_name));
|
|
|
|
// Check if variable name should start with '$'
|
|
if (!dollar_flag && (*var_name != '$'))
|
|
die("Variable name '%s' should start with '$'", save_var_name);
|
|
|
|
if (*var_name == '$') var_name++;
|
|
|
|
// Check if variable name exists or not
|
|
if (var_name == var_name_end) die("Missing variable name.");
|
|
|
|
// Check for non alphanumeric character(s) in variable name
|
|
while ((var_name != var_name_end) && my_isvar(charset_info, *var_name))
|
|
var_name++;
|
|
|
|
if (var_name != var_name_end)
|
|
die("Invalid variable name '%s'", save_var_name);
|
|
}
|
|
|
|
/*
|
|
Check if the pointer points to an operator.
|
|
|
|
SYNOPSIS
|
|
is_operator()
|
|
op - character pointer to mathematical expression
|
|
*/
|
|
static bool is_operator(const char *op) {
|
|
if (*op == '+')
|
|
return true;
|
|
else if (*op == '-')
|
|
return true;
|
|
else if (*op == '*')
|
|
return true;
|
|
else if (*op == '/')
|
|
return true;
|
|
else if (*op == '%')
|
|
return true;
|
|
else if (*op == '&' && *(op + 1) == '&')
|
|
return true;
|
|
else if (*op == '|' && *(op + 1) == '|')
|
|
return true;
|
|
else if (*op == '&')
|
|
return true;
|
|
else if (*op == '|')
|
|
return true;
|
|
else if (*op == '^')
|
|
return true;
|
|
else if (*op == '>' && *(op + 1) == '>')
|
|
return true;
|
|
else if (*op == '<' && *(op + 1) == '<')
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
Perform basic mathematical operation.
|
|
|
|
SYNOPSIS
|
|
do_expr()
|
|
command - command handle
|
|
|
|
DESCRIPTION
|
|
expr $<var_name>= <operand1> <operator> <operand2>
|
|
Perform basic mathematical operation and store the result
|
|
in a variable($<var_name>). Both <operand1> and <operand2>
|
|
should be valid MTR variables.
|
|
|
|
'expr' command supports only binary operators that operates
|
|
on two operands and manipulates them to return a result.
|
|
|
|
Following mathematical operators are supported.
|
|
1 Arithmetic Operators
|
|
1.1 Addition
|
|
1.2 Subtraction
|
|
1.3 Multiplication
|
|
1.4 Division
|
|
1.5 Modulo
|
|
|
|
2 Logical Operators
|
|
2.1 Logical AND
|
|
2.2 Logical OR
|
|
|
|
3 Bitwise Operators
|
|
3.1 Binary AND
|
|
3.2 Binary OR
|
|
3.3 Binary XOR
|
|
3.4 Binary Left Shift
|
|
3.5 Binary Right Shift
|
|
|
|
NOTE
|
|
1. Non-integer operand is truncated to integer value for operations
|
|
that dont support non-integer operand.
|
|
2. If the result is an infinite value, then expr command will return
|
|
'inf' keyword to indicate the result is infinity.
|
|
3. Division by 0 will result in an infinite value and expr command
|
|
will return 'inf' keyword to indicate the result is infinity.
|
|
*/
|
|
static void do_expr(struct st_command *command) {
|
|
DBUG_TRACE;
|
|
|
|
const char *p = command->first_argument;
|
|
if (!*p) die("Missing arguments to expr command.");
|
|
|
|
// Find <var_name>
|
|
const char *var_name = p;
|
|
while (*p && (*p != '=') && !my_isspace(charset_info, *p)) p++;
|
|
const char *var_name_end = p;
|
|
check_variable_name(var_name, var_name_end, true);
|
|
|
|
// Skip spaces between <var_name> and '='
|
|
while (my_isspace(charset_info, *p)) p++;
|
|
|
|
if (*p++ != '=') die("Missing assignment operator in expr command.");
|
|
|
|
// Skip spaces after '='
|
|
while (*p && my_isspace(charset_info, *p)) p++;
|
|
|
|
// Save the mathematical expression in a variable
|
|
const char *expr = p;
|
|
|
|
// First operand in the expression
|
|
const char *operand_name = p;
|
|
while (*p && !is_operator(p) && !my_isspace(charset_info, *p)) p++;
|
|
const char *operand_name_end = p;
|
|
check_variable_name(operand_name, operand_name_end, false);
|
|
VAR *v1 = var_get(operand_name, &operand_name_end, 0, 0);
|
|
|
|
double operand1;
|
|
if ((my_isdigit(charset_info, *v1->str_val)) ||
|
|
((*v1->str_val == '-') && my_isdigit(charset_info, *(v1->str_val + 1))))
|
|
operand1 = strtod(v1->str_val, NULL);
|
|
else
|
|
die("Undefined/invalid first operand '$%s' in expr command.", v1->name);
|
|
|
|
// Skip spaces after the first operand
|
|
while (*p && my_isspace(charset_info, *p)) p++;
|
|
|
|
// Extract the operator
|
|
const char *operator_start = p;
|
|
while (*p && (*p != '$') &&
|
|
!(my_isspace(charset_info, *p) || my_isvar(charset_info, *p)))
|
|
p++;
|
|
|
|
char math_operator[3];
|
|
strmake(math_operator, operator_start, (p - operator_start));
|
|
if (!std::strlen(math_operator))
|
|
die("Missing mathematical operator in expr command.");
|
|
|
|
// Skip spaces after the operator
|
|
while (*p && my_isspace(charset_info, *p)) p++;
|
|
|
|
// Second operand in the expression
|
|
operand_name = p;
|
|
while (*p && !my_isspace(charset_info, *p)) p++;
|
|
operand_name_end = p;
|
|
check_variable_name(operand_name, operand_name_end, false);
|
|
VAR *v2 = var_get(operand_name, &operand_name_end, 0, 0);
|
|
|
|
double operand2;
|
|
if ((my_isdigit(charset_info, *v2->str_val)) ||
|
|
((*v2->str_val == '-') && my_isdigit(charset_info, *(v2->str_val + 1))))
|
|
operand2 = strtod(v2->str_val, NULL);
|
|
else
|
|
die("Undefined/invalid second operand '$%s' in expr command.", v2->name);
|
|
|
|
// Skip spaces at the end
|
|
while (*p && my_isspace(charset_info, *p)) p++;
|
|
|
|
// Check for any spurious text after the second operand
|
|
if (*p) die("Invalid mathematical expression '%s' in expr command.", expr);
|
|
|
|
double result;
|
|
// Arithmetic Operators
|
|
if (!std::strcmp(math_operator, "+"))
|
|
result = operand1 + operand2;
|
|
else if (!std::strcmp(math_operator, "-"))
|
|
result = operand1 - operand2;
|
|
else if (!std::strcmp(math_operator, "*"))
|
|
result = operand1 * operand2;
|
|
else if (!std::strcmp(math_operator, "/")) {
|
|
if (operand2 == 0.0) {
|
|
if (operand1 == 0.0)
|
|
result = std::numeric_limits<double>::quiet_NaN();
|
|
else
|
|
result = std::numeric_limits<double>::infinity();
|
|
} else
|
|
result = operand1 / operand2;
|
|
} else if (!std::strcmp(math_operator, "%"))
|
|
result = (int)operand1 % (int)operand2;
|
|
// Logical Operators
|
|
else if (!std::strcmp(math_operator, "&&"))
|
|
result = operand1 && operand2;
|
|
else if (!std::strcmp(math_operator, "||"))
|
|
result = operand1 || operand2;
|
|
// Bitwise Operators
|
|
else if (!std::strcmp(math_operator, "&"))
|
|
result = (int)operand1 & (int)operand2;
|
|
else if (!std::strcmp(math_operator, "|"))
|
|
result = (int)operand1 | (int)operand2;
|
|
else if (!std::strcmp(math_operator, "^"))
|
|
result = (int)operand1 ^ (int)operand2;
|
|
else if (!std::strcmp(math_operator, ">>"))
|
|
result = (int)operand1 >> (int)operand2;
|
|
else if (!std::strcmp(math_operator, "<<"))
|
|
result = (int)operand1 << (int)operand2;
|
|
else
|
|
die("Invalid operator '%s' in expr command", math_operator);
|
|
|
|
char buf[128];
|
|
size_t result_len;
|
|
// Check if result is an infinite value
|
|
if (std::isnan(result)) {
|
|
// Print 'nan' if result is Not a Number
|
|
result_len = snprintf(buf, sizeof(buf), "%s", "nan");
|
|
} else if (!std::isinf(result)) {
|
|
const char *format = (result < 1e10 && result > -1e10) ? "%f" : "%g";
|
|
result_len = snprintf(buf, sizeof(buf), format, result);
|
|
} else
|
|
// Print 'inf' if result is an infinite value
|
|
result_len = snprintf(buf, sizeof(buf), "%s", "inf");
|
|
|
|
if (result < 1e10 && result > -1e10) {
|
|
/*
|
|
Remove the trailing 0's i.e 2.0000000 need to be represented
|
|
as 2 for consistency, 2.0010000 also becomes 2.001.
|
|
*/
|
|
while (buf[result_len - 1] == '0') result_len--;
|
|
|
|
// Remove trailing '.' if exists
|
|
if (buf[result_len - 1] == '.') result_len--;
|
|
}
|
|
|
|
var_set(var_name, var_name_end, buf, buf + result_len);
|
|
command->last_argument = command->end;
|
|
}
|
|
|
|
/*
|
|
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
|
|
*/
|
|
|
|
static void do_let(struct st_command *command) {
|
|
const char *p = command->first_argument;
|
|
const char *var_name, *var_name_end;
|
|
DYNAMIC_STRING let_rhs_expr;
|
|
DBUG_TRACE;
|
|
|
|
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();
|
|
}
|
|
|
|
/*
|
|
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.
|
|
*/
|
|
|
|
static int do_sleep(struct st_command *command, bool real_sleep) {
|
|
int error = 0;
|
|
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;
|
|
const char *sleep_end = ds_sleep.str + ds_sleep.length;
|
|
while (my_isspace(charset_info, *p)) p++;
|
|
if (!*p)
|
|
die("Missing argument to %.*s", static_cast<int>(command->first_word_len),
|
|
command->query);
|
|
const char *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 %.*s \"%s\"",
|
|
static_cast<int>(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 %.*s \"%s\"",
|
|
static_cast<int>(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;
|
|
}
|
|
|
|
static 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));
|
|
if (!charset_info)
|
|
abort_not_supported_test("Test requires charset '%s'", charset_name);
|
|
}
|
|
|
|
/// Check if the bug number argument to disable_testcase is in a proper
|
|
/// format.
|
|
///
|
|
/// Bug number argument should follow 'BUG#XXXX' format
|
|
///
|
|
/// - Keyword 'BUG" is case-insensitive
|
|
/// - XXXX should contain only digits
|
|
///
|
|
/// @param bug_number String representing a bug number
|
|
///
|
|
/// @retval True if the bug number argument is in correct format, false
|
|
/// otherwise.
|
|
static bool validate_bug_number_argument(std::string bug_number) {
|
|
// Convert the string to lowercase characters
|
|
std::transform(bug_number.begin(), bug_number.end(), bug_number.begin(),
|
|
::tolower);
|
|
|
|
// Check if string representing a bug number starts 'BUG' keyword.
|
|
// Note: This keyword is case-inseinsitive.
|
|
if (bug_number.substr(0, 3).compare("bug") != 0) return false;
|
|
|
|
// Check if the string contains '#' after 'BUG' keyword
|
|
if (bug_number.at(3) != '#') return false;
|
|
|
|
// Check if the bug number string contains only digits after '#'
|
|
if (get_int_val(bug_number.substr(4).c_str()) == -1) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/// Disable or don't execute the statements or commands appear after
|
|
/// this command until the execution is enabled by 'enable_testcase'
|
|
/// command. If test cases are already disabled, then throw an error
|
|
/// and abort the test execution.
|
|
///
|
|
/// This command also takes a mandatory parameter specifying the bug
|
|
/// number.
|
|
///
|
|
/// @code
|
|
/// --disable_testcase BUG#XXXX
|
|
/// statements or commands
|
|
/// --enable_testcase
|
|
/// @endcode
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
static void do_disable_testcase(struct st_command *command) {
|
|
DYNAMIC_STRING ds_bug_number;
|
|
const struct command_arg disable_testcase_args[] = {
|
|
{"Bug number", ARG_STRING, true, &ds_bug_number, "Bug number"}};
|
|
|
|
check_command_args(command, command->first_argument, disable_testcase_args,
|
|
sizeof(disable_testcase_args) / sizeof(struct command_arg),
|
|
' ');
|
|
|
|
/// Check if the bug number argument to disable_testcase is in a
|
|
/// proper format.
|
|
if (validate_bug_number_argument(ds_bug_number.str) == 0) {
|
|
free_dynamic_strings(&ds_bug_number);
|
|
die("Bug number mentioned in '%s' command is not in a correct format. "
|
|
"It should be 'BUG#XXXX', where keyword 'BUG' is case-insensitive "
|
|
"and 'XXXX' should contain only digits.",
|
|
command->query);
|
|
}
|
|
|
|
testcase_disabled = 1;
|
|
free_dynamic_strings(&ds_bug_number);
|
|
}
|
|
|
|
/**
|
|
Check if process is active.
|
|
|
|
@param pid Process id.
|
|
|
|
@return true if process is active, false otherwise.
|
|
*/
|
|
static bool is_process_active(int pid) {
|
|
#ifdef _WIN32
|
|
DWORD exit_code;
|
|
HANDLE proc;
|
|
proc = OpenProcess(PROCESS_QUERY_INFORMATION, false, pid);
|
|
if (proc == NULL) return false; /* Process could not be found. */
|
|
|
|
if (!GetExitCodeProcess(proc, &exit_code)) exit_code = 0;
|
|
|
|
CloseHandle(proc);
|
|
if (exit_code != STILL_ACTIVE)
|
|
return false; /* Error or process has terminated. */
|
|
|
|
return true;
|
|
#else
|
|
return (kill(pid, 0) == 0);
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
kill process.
|
|
|
|
@param pid Process id.
|
|
|
|
@return true if process is terminated, false otherwise.
|
|
*/
|
|
static bool kill_process(int pid) {
|
|
bool killed = true;
|
|
#ifdef _WIN32
|
|
HANDLE proc;
|
|
proc = OpenProcess(PROCESS_TERMINATE, false, pid);
|
|
if (proc == NULL) return true; /* Process could not be found. */
|
|
|
|
if (!TerminateProcess(proc, 201)) killed = false;
|
|
|
|
CloseHandle(proc);
|
|
#else
|
|
killed = (kill(pid, SIGKILL) == 0);
|
|
#endif
|
|
return killed;
|
|
}
|
|
|
|
/**
|
|
Abort process.
|
|
|
|
@param pid Process id.
|
|
@param path Path to create minidump file in.
|
|
*/
|
|
static void abort_process(int pid, const char *path MY_ATTRIBUTE((unused))) {
|
|
#ifdef _WIN32
|
|
HANDLE proc;
|
|
proc = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
|
|
verbose_msg("Aborting pid %d (handle: %p)\n", pid, proc);
|
|
if (proc != NULL) {
|
|
char name[FN_REFLEN], *end;
|
|
BOOL is_debugged;
|
|
|
|
if (path) {
|
|
/* Append mysqld.<pid>.dmp to the path. */
|
|
strncpy(name, path, sizeof(name) - 1);
|
|
name[sizeof(name) - 1] = '\0';
|
|
end = name + std::strlen(name);
|
|
/* Make sure "/mysqld.nnnnnnnnnn.dmp" fits */
|
|
if ((end - name) < (sizeof(name) - 23)) {
|
|
if (!is_directory_separator(end[-1])) {
|
|
end[0] = FN_LIBCHAR2; // datadir path normally uses '/'.
|
|
end++;
|
|
}
|
|
snprintf(end, sizeof(name) + name - end - 1, "mysqld.%d.dmp", pid);
|
|
|
|
verbose_msg("Creating minidump.\n");
|
|
my_create_minidump(name, proc, pid);
|
|
} else
|
|
die("Path too long for creating minidump!\n");
|
|
}
|
|
/* If running in a debugger, send a break, otherwise terminate. */
|
|
if (CheckRemoteDebuggerPresent(proc, &is_debugged) && is_debugged) {
|
|
if (!DebugBreakProcess(proc)) {
|
|
DWORD err = GetLastError();
|
|
verbose_msg("DebugBreakProcess failed: %d\n", err);
|
|
} else
|
|
verbose_msg("DebugBreakProcess succeeded!\n");
|
|
CloseHandle(proc);
|
|
} else {
|
|
CloseHandle(proc);
|
|
(void)kill_process(pid);
|
|
}
|
|
} else {
|
|
DWORD err = GetLastError();
|
|
verbose_msg("OpenProcess failed: %d\n", err);
|
|
}
|
|
#else
|
|
kill(pid, SIGABRT);
|
|
#endif
|
|
}
|
|
|
|
/// Shutdown or kill the server. If timeout is set to 0 the server is
|
|
/// killed or terminated immediately. Otherwise the shutdown command
|
|
/// is first sent and then it waits for the server to terminate within
|
|
/// 'timeout' seconds. If it has not terminated before 'timeout'
|
|
/// seconds the command will fail.
|
|
///
|
|
/// @note
|
|
/// Currently only works with local server
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command. Optionally
|
|
/// including a timeout else the default of 60 seconds
|
|
static void do_shutdown_server(struct st_command *command) {
|
|
static DYNAMIC_STRING ds_timeout;
|
|
const struct command_arg shutdown_args[] = {
|
|
{"timeout", ARG_STRING, false, &ds_timeout,
|
|
"Timeout before killing server"}};
|
|
|
|
check_command_args(command, command->first_argument, shutdown_args,
|
|
sizeof(shutdown_args) / sizeof(struct command_arg), ' ');
|
|
if (opt_offload_count_file) {
|
|
// Save the value of secondary engine execution status
|
|
// before shutting down the server.
|
|
if (secondary_engine->offload_count(&cur_con->mysql, "after"))
|
|
cleanup_and_exit(1);
|
|
}
|
|
|
|
long timeout = 60;
|
|
|
|
if (ds_timeout.length) {
|
|
char *endptr;
|
|
timeout = std::strtol(ds_timeout.str, &endptr, 10);
|
|
if (*endptr != '\0')
|
|
die("Illegal argument for timeout: '%s'", ds_timeout.str);
|
|
}
|
|
|
|
dynstr_free(&ds_timeout);
|
|
|
|
MYSQL *mysql = &cur_con->mysql;
|
|
std::string ds_file_name;
|
|
|
|
// Get the servers pid_file name and use it to read pid
|
|
if (query_get_string(mysql, "SHOW VARIABLES LIKE 'pid_file'", 1,
|
|
&ds_file_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_file_name.c_str(), O_RDONLY, MYF(0))) < 0)
|
|
die("Failed to open file '%s'", ds_file_name.c_str());
|
|
|
|
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));
|
|
|
|
int pid = std::atoi(buff);
|
|
if (pid == 0) die("Pidfile didn't contain a valid number");
|
|
|
|
int error = 0;
|
|
if (timeout) {
|
|
// Check if we should generate a minidump on timeout.
|
|
if (query_get_string(mysql, "SHOW VARIABLES LIKE 'core_file'", 1,
|
|
&ds_file_name) ||
|
|
std::strcmp("ON", ds_file_name.c_str())) {
|
|
} else {
|
|
// Get the data dir and use it as path for a minidump if needed.
|
|
if (query_get_string(mysql, "SHOW VARIABLES LIKE 'datadir'", 1,
|
|
&ds_file_name))
|
|
die("Failed to get datadir from server");
|
|
}
|
|
|
|
const char *var_name = "$MTR_MANUAL_DEBUG";
|
|
VAR *var = var_get(var_name, &var_name, 0, 0);
|
|
if (var->int_val) {
|
|
if (!kill_process(pid) && is_process_active(pid)) error = 3;
|
|
} else {
|
|
// Tell server to shutdown if timeout > 0.
|
|
if (timeout > 0 && mysql_query_wrapper(mysql, "shutdown")) {
|
|
// Failed to issue shutdown command.
|
|
error = 1;
|
|
goto end;
|
|
}
|
|
|
|
// Check that server dies
|
|
do {
|
|
if (!is_process_active(pid)) {
|
|
DBUG_PRINT("info", ("Process %d does not exist anymore", pid));
|
|
goto end;
|
|
}
|
|
if (timeout > 0) {
|
|
DBUG_PRINT("info", ("Sleeping, timeout: %ld", timeout));
|
|
my_sleep(1000000L);
|
|
}
|
|
} while (timeout-- > 0);
|
|
|
|
error = 2;
|
|
|
|
// Abort to make it easier to find the hang/problem.
|
|
abort_process(pid, ds_file_name.c_str());
|
|
}
|
|
} else {
|
|
// timeout value is 0, kill the server
|
|
DBUG_PRINT("info", ("Killing server, pid: %d", pid));
|
|
|
|
// kill_process can fail (bad privileges, non existing process on
|
|
// *nix etc), so also check if the process is active before setting
|
|
// error.
|
|
if (!kill_process(pid) && is_process_active(pid)) error = 3;
|
|
}
|
|
|
|
end:
|
|
if (error) handle_command_error(command, error);
|
|
}
|
|
|
|
/// Evaluate the warning list argument specified with either
|
|
/// disable_warnings or enable_warnings command and replace the
|
|
/// variables with actual values if there exist any.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds
|
|
/// the arguments and information for the command.
|
|
/// @param ds_warnings DYNAMIC_STRING object containing the argument.
|
|
///
|
|
/// @retval Evaluated string after replacing the variables with values.
|
|
const char *eval_warning_argument(struct st_command *command,
|
|
DYNAMIC_STRING *ds_warnings) {
|
|
dynstr_trunc(ds_warnings, ds_warnings->length);
|
|
do_eval(ds_warnings, command->first_argument, command->end, false);
|
|
return ds_warnings->str;
|
|
}
|
|
|
|
/// Check if second argument "ONCE" to disable_warnings or enable_warnings
|
|
/// command is specified. If yes, filter out the keyword "ONCE" from the
|
|
/// argument string.
|
|
///
|
|
/// @param ds_property DYNAMIC_STRING object containing the second argument
|
|
/// @param warn_argument String to store the argument string containing only
|
|
/// the list of warnings.
|
|
///
|
|
/// @retval True if the second argument is specified, false otherwise.
|
|
static bool check_and_filter_once_property(DYNAMIC_STRING ds_property,
|
|
std::string *warn_argument) {
|
|
if (ds_property.length) {
|
|
// Second argument exists, and it should be "ONCE" keyword.
|
|
if (std::strcmp(ds_property.str, "ONCE"))
|
|
die("Second argument to '%s' command should always be \"ONCE\" keyword.",
|
|
command_names[curr_command->type - 1]);
|
|
|
|
// Filter out the keyword and save only the warnings.
|
|
std::size_t position = warn_argument->find(" ONCE");
|
|
DBUG_ASSERT(position != std::string::npos);
|
|
warn_argument->erase(position, 5);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// Handle disabling of warnings.
|
|
///
|
|
/// * If all warnings are disabled, don't add the warning to disabled
|
|
/// warning list.
|
|
/// * If there exist enabled warnings, remove the disabled warning from
|
|
/// the list of enabled warnings.
|
|
/// * If all the warnings are enabled or if there exist disabled warnings,
|
|
/// add or append the new warning to the list of disabled warnings.
|
|
///
|
|
/// @param warning_code Warning code
|
|
/// @param warning Warning string
|
|
/// @param once_prop Flag specifying whether a property should be set
|
|
/// for next statement only.
|
|
static void handle_disable_warnings(std::uint32_t warning_code,
|
|
std::string warning, bool once_prop) {
|
|
if (enabled_warnings->count()) {
|
|
// Remove the warning from list of enabled warnings.
|
|
enabled_warnings->remove_warning(warning_code, once_prop);
|
|
} else if (!disable_warnings || disabled_warnings->count()) {
|
|
// Add the warning to list of expected warnings only if all the
|
|
// warnings are not disabled.
|
|
disabled_warnings->add_warning(warning_code, warning.c_str(), once_prop);
|
|
}
|
|
}
|
|
|
|
/// Handle enabling of warnings.
|
|
///
|
|
/// * If all the warnings are enabled, don't add the warning to enabled
|
|
/// warning list.
|
|
/// * If there exist disabled warnings, remove the enabled warning from
|
|
/// the list of disabled warnings.
|
|
/// * If all the warnings are disabled or if there exist enabled warnings,
|
|
/// add or append the new warning to the list of enabled warnings.
|
|
///
|
|
/// @param warning_code Warning code
|
|
/// @param warning Warning string
|
|
/// @param once_prop Flag specifying whether a property should be set
|
|
/// for next statement only.
|
|
static void handle_enable_warnings(std::uint32_t warning_code,
|
|
std::string warning, bool once_prop) {
|
|
if (disabled_warnings->count()) {
|
|
// Remove the warning from list of disabled warnings.
|
|
disabled_warnings->remove_warning(warning_code, once_prop);
|
|
} else if (disabled_warnings) {
|
|
// All the warnings are disabled, enable only the warnings specified
|
|
// in the argument.
|
|
enabled_warnings->add_warning(warning_code, warning.c_str(), once_prop);
|
|
}
|
|
}
|
|
|
|
/// Get an error code corresponding to a warning name. The warning name
|
|
/// specified is in symbolic error name format.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
/// @param warn_argument String containing warning argument
|
|
/// @param once_prop Flag specifying whether a property should be set for
|
|
/// next statement only.
|
|
static void get_warning_codes(struct st_command *command,
|
|
std::string warn_argument, bool once_prop) {
|
|
std::string warning;
|
|
std::stringstream warnings(warn_argument);
|
|
|
|
if (!my_isalnum(charset_info, warn_argument.back())) {
|
|
die("Invalid argument '%s' to '%s' command.", command->first_argument,
|
|
command_names[command->type - 1]);
|
|
}
|
|
|
|
while (std::getline(warnings, warning, ',')) {
|
|
// Remove any space in a string representing a warning.
|
|
warning.erase(remove_if(warning.begin(), warning.end(), isspace),
|
|
warning.end());
|
|
|
|
// Check if a warning name is a valid symbolic error name.
|
|
if (warning.front() == 'E' || warning.front() == 'W') {
|
|
int warning_code = get_errcode_from_name(warning);
|
|
if (warning_code == -1)
|
|
die("Unknown SQL error name '%s'.", warning.c_str());
|
|
if (command->type == Q_DISABLE_WARNINGS)
|
|
handle_disable_warnings(warning_code, warning, once_prop);
|
|
else if (command->type == Q_ENABLE_WARNINGS) {
|
|
handle_enable_warnings(warning_code, warning, once_prop);
|
|
// If disable_warnings flag is set, and there are no disabled or
|
|
// enabled warnings, set the disable_warnings flag to 0.
|
|
if (disable_warnings) {
|
|
if (!disabled_warnings->count() && !enabled_warnings->count())
|
|
set_property(command, P_WARN, 0);
|
|
}
|
|
}
|
|
} else {
|
|
// Invalid argument, should only consist of warnings specified in
|
|
// symbolic error name format.
|
|
die("Invalid argument '%s' to '%s' command, list of disabled or enabled "
|
|
"warnings may only consist of symbolic error names.",
|
|
command->first_argument, command_names[command->type - 1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parse the warning list argument specified with disable_warnings or
|
|
/// enable_warnings command. Check if the second argument "ONCE" is
|
|
/// specified, if yes, set once_property flag.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
///
|
|
/// @retval True if second argument "ONCE" is specified, false otherwise.
|
|
static bool parse_warning_list_argument(struct st_command *command) {
|
|
DYNAMIC_STRING ds_warnings, ds_property;
|
|
const struct command_arg warning_args[] = {
|
|
{"Warnings", ARG_STRING, false, &ds_warnings,
|
|
"Comma separated list of warnings to be disabled or enabled."},
|
|
{"Property", ARG_STRING, false, &ds_property,
|
|
"Keyword \"ONCE\" repesenting the property should be set for next "
|
|
"statement only."}};
|
|
|
|
check_command_args(command, command->first_argument, warning_args,
|
|
sizeof(warning_args) / sizeof(struct command_arg), ' ');
|
|
|
|
// Waning list argument can't be an empty string.
|
|
if (!ds_warnings.length)
|
|
die("Warning list argument to command '%s' can't be an empty string.",
|
|
command_names[command->type - 1]);
|
|
|
|
// Evaluate warning list argument and replace the variables with
|
|
// actual values
|
|
std::string warn_argument = eval_warning_argument(command, &ds_warnings);
|
|
|
|
// Set once_prop flag to true if keyword "ONCE" is specified as an
|
|
// argument to a disable_warnings or a enable_warnings command.
|
|
// Filter this keyword from the argument string and save only the
|
|
// list of warnings.
|
|
bool once_prop = check_and_filter_once_property(ds_property, &warn_argument);
|
|
|
|
// Free all the DYNAMIC_STRING objects created
|
|
free_dynamic_strings(&ds_warnings, &ds_property);
|
|
|
|
// Get warning codes
|
|
get_warning_codes(command, warn_argument, once_prop);
|
|
|
|
return once_prop;
|
|
}
|
|
|
|
/// Create a list of disabled warnings that should be suppressed for
|
|
/// the next statements until enabled by enable_warnings command.
|
|
///
|
|
/// disable_warnings command can take an optional argument specifying
|
|
/// a warning or a comma separated list of warnings to be disabled.
|
|
/// The warnings specified should be in symbolic error name format.
|
|
///
|
|
/// disable_warnings command can also take a second optional argument,
|
|
/// which when specified will suppress the warnings for next statement
|
|
/// only. The second argument must be a "ONCE" keyword.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
static void do_disable_warnings(struct st_command *command) {
|
|
// Revert the previously set properties
|
|
if (once_property) revert_properties();
|
|
|
|
// Check if disable_warnings command has warning list argument.
|
|
if (!*command->first_argument) {
|
|
// disable_warnings without an argument, disable all the warnings.
|
|
if (disabled_warnings->count()) disabled_warnings->clear_list();
|
|
if (enabled_warnings->count()) enabled_warnings->clear_list();
|
|
|
|
// Update the environment variables containing the list of disabled
|
|
// and enabled warnings.
|
|
update_disabled_enabled_warnings_list_var();
|
|
|
|
// Set 'disable_warnings' property value to 1
|
|
set_property(command, P_WARN, 1);
|
|
return;
|
|
} else {
|
|
// Parse the warning list argument specified with disable_warnings
|
|
// command.
|
|
parse_warning_list_argument(command);
|
|
|
|
// Update the environment variables containing the list of disabled
|
|
// and enabled warnings.
|
|
update_disabled_enabled_warnings_list_var();
|
|
|
|
// Set 'disable_warnings' property value to 1
|
|
set_property(command, P_WARN, 1);
|
|
}
|
|
|
|
command->last_argument = command->end;
|
|
}
|
|
|
|
/// Create a list of enabled warnings that should be enabled for the
|
|
/// next statements until disabled by disable_warnings command.
|
|
///
|
|
/// enable_warnings command can take an optional argument specifying
|
|
/// a warning or a comma separated list of warnings to be enabled. The
|
|
/// warnings specified should be in symbolic error name format.
|
|
///
|
|
/// enable_warnings command can also take a second optional argument,
|
|
/// which when specified will enable the warnings for next statement
|
|
/// only. The second argument must be a "ONCE" keyword.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
static void do_enable_warnings(struct st_command *command) {
|
|
// Revert the previously set properties
|
|
if (once_property) revert_properties();
|
|
|
|
bool once_prop = false;
|
|
if (!*command->first_argument) {
|
|
// enable_warnings without an argument, enable all the warnings.
|
|
if (disabled_warnings->count()) disabled_warnings->clear_list();
|
|
if (enabled_warnings->count()) enabled_warnings->clear_list();
|
|
|
|
// Update the environment variables containing the list of disabled
|
|
// and enabled warnings.
|
|
update_disabled_enabled_warnings_list_var();
|
|
|
|
// Set 'disable_warnings' property value to 0
|
|
set_property(command, P_WARN, 0);
|
|
} else {
|
|
// Parse the warning list argument specified with enable_warnings command.
|
|
once_prop = parse_warning_list_argument(command);
|
|
|
|
// Update the environment variables containing the list of disabled and
|
|
// enabled warnings.
|
|
update_disabled_enabled_warnings_list_var();
|
|
}
|
|
|
|
// Call set_once_property() to set once_propetry flag.
|
|
if (disable_warnings && once_prop) set_once_property(P_WARN, 1);
|
|
|
|
command->last_argument = command->end;
|
|
}
|
|
|
|
/// Create a list of error values that the next statement is expected
|
|
/// to return. Each error must be an error number or an SQLSTATE value
|
|
/// or a symbolic error name representing an error.
|
|
///
|
|
/// SQLSTATE value must start with 'S'. It is also possible to specify
|
|
/// client errors with 'error' command.
|
|
///
|
|
/// @code
|
|
/// --error 1064
|
|
/// --error S42S01
|
|
/// --error ER_TABLE_EXISTS_ERROR,ER_PARSE_ERROR
|
|
/// --error CR_SERVER_GONE_ERROR
|
|
/// @endcode
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
static void do_error(struct st_command *command) {
|
|
if (!*command->first_argument) die("Missing argument(s) to 'error' command.");
|
|
|
|
// Check if error command ends with a comment
|
|
char *end = command->first_argument + std::strlen(command->first_argument);
|
|
while (std::strlen(command->first_argument) > std::strlen(end)) {
|
|
end--;
|
|
if (*end == '#') break;
|
|
}
|
|
|
|
if (std::strlen(command->first_argument) == std::strlen(end))
|
|
end = command->first_argument + std::strlen(command->first_argument);
|
|
|
|
std::string error;
|
|
std::stringstream errors(std::string(command->first_argument, end));
|
|
|
|
// Get error codes
|
|
while (std::getline(errors, error, ',')) {
|
|
// Remove any space from the string representing an error.
|
|
error.erase(remove_if(error.begin(), error.end(), isspace), error.end());
|
|
|
|
// Code to handle a variable containing an error.
|
|
if (error.front() == '$') {
|
|
const char *varname_end = NULL;
|
|
VAR *var = var_get(error.c_str(), &varname_end, 0, 0);
|
|
error.assign(var->str_val);
|
|
}
|
|
|
|
if (error.front() == 'S') {
|
|
// SQLSTATE string
|
|
// * Must be SQLSTATE_LENGTH long
|
|
// * May contain only digits[0-9] and _uppercase_ letters
|
|
|
|
// Step pass 'S' character
|
|
error = error.substr(1, error.length());
|
|
|
|
if (error.length() != SQLSTATE_LENGTH)
|
|
die("The sqlstate must be exactly %d chars long.", SQLSTATE_LENGTH);
|
|
|
|
// Check the validity of an SQLSTATE string.
|
|
for (std::size_t i = 0; i < error.length(); i++) {
|
|
if (!my_isdigit(charset_info, error[i]) &&
|
|
!my_isupper(charset_info, error[i]))
|
|
die("The sqlstate may only consist of digits[0-9] and _uppercase_ "
|
|
"letters.");
|
|
}
|
|
expected_errors->add_error(error.c_str(), ERR_SQLSTATE);
|
|
} else if (error.front() == 's') {
|
|
die("The sqlstate definition must start with an uppercase S.");
|
|
}
|
|
// Checking for both server error names as well as client error names.
|
|
else if (error.front() == 'C' || error.front() == 'E') {
|
|
// Code to handle --error <error_string>.
|
|
int error_code = get_errcode_from_name(error);
|
|
if (error_code == -1) die("Unknown SQL error name '%s'.", error.c_str());
|
|
expected_errors->add_error(error_code, ERR_ERRNO);
|
|
} else if (error.front() == 'c' || error.front() == 'e') {
|
|
die("The error name definition must start with an uppercase C or E.");
|
|
} else {
|
|
// Check that the string passed to error command contains only digits.
|
|
int error_code = get_int_val(error.c_str());
|
|
if (error_code == -1)
|
|
die("Invalid argument '%s' to 'error' command, the argument may "
|
|
"consist of either symbolic error names or error codes.",
|
|
command->first_argument);
|
|
expected_errors->add_error((std::uint32_t)error_code, ERR_ERRNO);
|
|
}
|
|
|
|
if (expected_errors->count() >= MAX_ERROR_COUNT)
|
|
die("Too many errorcodes specified.");
|
|
}
|
|
|
|
command->last_argument = command->end;
|
|
}
|
|
|
|
/*
|
|
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, const char **from_ptr,
|
|
struct st_command *command) {
|
|
char c, sep;
|
|
char *to = *to_ptr, *start = to;
|
|
const char *from = *from_ptr;
|
|
DBUG_TRACE;
|
|
|
|
/* 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 == end + 1) {
|
|
DBUG_PRINT("info", ("var: '%s' -> '%s'", start, var->str_val));
|
|
return var->str_val; /* return found variable value */
|
|
}
|
|
}
|
|
return start;
|
|
}
|
|
|
|
static void set_reconnect(MYSQL *mysql, int val) {
|
|
bool reconnect = val;
|
|
DBUG_TRACE;
|
|
DBUG_PRINT("info", ("val: %d", val));
|
|
mysql_options(mysql, MYSQL_OPT_RECONNECT, (char *)&reconnect);
|
|
}
|
|
|
|
/**
|
|
Change the current connection to the given st_connection, and update
|
|
$mysql_get_server_version and $CURRENT_CONNECTION accordingly.
|
|
*/
|
|
static 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);
|
|
}
|
|
|
|
static void select_connection_name(const char *name) {
|
|
DBUG_TRACE;
|
|
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);
|
|
}
|
|
}
|
|
|
|
static void select_connection(struct st_command *command) {
|
|
DBUG_TRACE;
|
|
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);
|
|
}
|
|
|
|
static void do_close_connection(struct st_command *command) {
|
|
DBUG_TRACE;
|
|
|
|
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));
|
|
if (command->type == Q_DIRTY_CLOSE) {
|
|
if (con->mysql.net.vio) {
|
|
vio_delete(con->mysql.net.vio);
|
|
con->mysql.net.vio = 0;
|
|
end_server(&con->mysql);
|
|
}
|
|
}
|
|
if (con->stmt) mysql_stmt_close(con->stmt);
|
|
con->stmt = 0;
|
|
|
|
mysql_close(&con->mysql);
|
|
|
|
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");
|
|
|
|
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);
|
|
}
|
|
|
|
/*
|
|
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.
|
|
|
|
*/
|
|
|
|
static 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_TRACE;
|
|
|
|
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");
|
|
mysql_options(mysql, MYSQL_OPT_CAN_HANDLE_EXPIRED_PASSWORDS,
|
|
&can_handle_expired_passwords);
|
|
while (!mysql_real_connect_wrapper(
|
|
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 ||
|
|
mysql_errno(mysql) == CR_NAMEDPIPEOPEN_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.");
|
|
}
|
|
|
|
/// Connect to a server and handle connection errors in case they
|
|
/// occur.
|
|
///
|
|
/// 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 an 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.
|
|
///
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
/// @param con Connection structure to be used
|
|
/// @param host Host name
|
|
/// @param user User name
|
|
/// @param pass Password
|
|
/// @param db Database name
|
|
/// @param port Port number
|
|
/// @param sock Socket value
|
|
///
|
|
/// @retval 1 if connection succeeds, 0 otherwise
|
|
static 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) {
|
|
DYNAMIC_STRING *ds;
|
|
int failed_attempts = 0;
|
|
|
|
ds = &ds_res;
|
|
|
|
/* Only log if an error is expected */
|
|
if (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);
|
|
}
|
|
/* Simlified 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");
|
|
mysql_options(con, MYSQL_OPT_CAN_HANDLE_EXPIRED_PASSWORDS,
|
|
&can_handle_expired_passwords);
|
|
while (!mysql_real_connect_wrapper(con, host, user, pass, 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 */
|
|
}
|
|
|
|
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
|
|
* SHM - use shared memory if available
|
|
* PIPE - use named pipe if available
|
|
* SOCKET - use socket protocol
|
|
* TCP - use tcp protocol
|
|
*/
|
|
|
|
static void do_connect(struct st_command *command) {
|
|
int con_port = opt_port;
|
|
char *con_options;
|
|
bool con_ssl = 0, con_compress = 0;
|
|
bool con_pipe = 0, con_shm = 0, con_cleartext_enable = 0;
|
|
struct st_connection *con_slot;
|
|
#if defined(HAVE_OPENSSL)
|
|
uint save_opt_ssl_mode = opt_ssl_mode;
|
|
#endif
|
|
|
|
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;
|
|
static DYNAMIC_STRING ds_shm;
|
|
static DYNAMIC_STRING ds_compression_algorithm;
|
|
static DYNAMIC_STRING ds_zstd_compression_level;
|
|
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"},
|
|
{"passsword", 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"},
|
|
{"default_compression_algorithm", ARG_STRING, false,
|
|
&ds_compression_algorithm, "Default compression algorithm to use"},
|
|
{"default_zstd_compression_level", ARG_STRING, false,
|
|
&ds_zstd_compression_level,
|
|
"Default compression level to use "
|
|
"when using zstd compression."}};
|
|
|
|
DBUG_TRACE;
|
|
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);
|
|
}
|
|
|
|
/* Shared memory */
|
|
init_dynamic_string(&ds_shm, ds_sock.str, 0, 0);
|
|
|
|
/* Sock */
|
|
if (ds_sock.length) {
|
|
/*
|
|
If the socket is specified just as a name without path
|
|
append tmpdir in front
|
|
*/
|
|
if (*ds_sock.str != FN_LIBCHAR) {
|
|
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;
|
|
bool con_socket = 0, con_tcp = 0;
|
|
while (*con_options) {
|
|
/* Step past any spaces in beginning of option */
|
|
while (*con_options && my_isspace(charset_info, *con_options))
|
|
con_options++;
|
|
|
|
/* Find end of this option */
|
|
char *end = con_options;
|
|
while (*end && !my_isspace(charset_info, *end)) end++;
|
|
|
|
size_t con_option_len = end - con_options;
|
|
char cur_con_option[10];
|
|
strmake(cur_con_option, con_options, con_option_len);
|
|
|
|
if (!std::strcmp(cur_con_option, "SSL"))
|
|
con_ssl = 1;
|
|
else if (!std::strcmp(cur_con_option, "COMPRESS"))
|
|
con_compress = 1;
|
|
else if (!std::strcmp(cur_con_option, "PIPE"))
|
|
con_pipe = 1;
|
|
else if (!std::strcmp(cur_con_option, "SHM"))
|
|
con_shm = 1;
|
|
else if (!std::strcmp(cur_con_option, "CLEARTEXT"))
|
|
con_cleartext_enable = 1;
|
|
else if (!std::strcmp(cur_con_option, "SOCKET"))
|
|
con_socket = 1;
|
|
else if (!std::strcmp(cur_con_option, "TCP"))
|
|
con_tcp = 1;
|
|
else
|
|
die("Illegal option to connect: %s", cur_con_option);
|
|
|
|
/* 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);
|
|
}
|
|
|
|
if (!mysql_init(&con_slot->mysql)) die("Failed on mysql_init()");
|
|
|
|
if (opt_connect_timeout)
|
|
mysql_options(&con_slot->mysql, MYSQL_OPT_CONNECT_TIMEOUT,
|
|
(void *)&opt_connect_timeout);
|
|
|
|
if (opt_compress || con_compress)
|
|
mysql_options(&con_slot->mysql, MYSQL_OPT_COMPRESS, NullS);
|
|
mysql_options(&con_slot->mysql, MYSQL_OPT_LOCAL_INFILE, 0);
|
|
mysql_options(&con_slot->mysql, MYSQL_SET_CHARSET_NAME, charset_info->csname);
|
|
if (opt_charsets_dir)
|
|
mysql_options(&con_slot->mysql, MYSQL_SET_CHARSET_DIR, opt_charsets_dir);
|
|
|
|
if (ds_compression_algorithm.length)
|
|
mysql_options(&con_slot->mysql, MYSQL_OPT_COMPRESSION_ALGORITHMS,
|
|
ds_compression_algorithm.str);
|
|
else if (opt_compress_algorithm)
|
|
mysql_options(&con_slot->mysql, MYSQL_OPT_COMPRESSION_ALGORITHMS,
|
|
opt_compress_algorithm);
|
|
if (ds_zstd_compression_level.length) {
|
|
char *end = nullptr;
|
|
opt_zstd_compress_level = strtol(ds_zstd_compression_level.str, &end, 10);
|
|
}
|
|
mysql_options(&con_slot->mysql, MYSQL_OPT_ZSTD_COMPRESSION_LEVEL,
|
|
&opt_zstd_compress_level);
|
|
|
|
#if defined(HAVE_OPENSSL)
|
|
/*
|
|
If mysqltest --ssl-mode option is set to DISABLED
|
|
and connect(.., SSL) command used, set proper opt_ssl_mode.
|
|
|
|
So, SSL connection is used either:
|
|
a) mysqltest --ssl-mode option is NOT set to DISABLED or
|
|
b) connect(.., SSL) command used.
|
|
*/
|
|
if (opt_ssl_mode == SSL_MODE_DISABLED && con_ssl) {
|
|
opt_ssl_mode =
|
|
(opt_ssl_ca || opt_ssl_capath) ? SSL_MODE_VERIFY_CA : SSL_MODE_REQUIRED;
|
|
}
|
|
#else
|
|
/* keep the compiler happy about con_ssl */
|
|
con_ssl = con_ssl ? true : false;
|
|
#endif
|
|
if (SSL_SET_OPTIONS(&con_slot->mysql)) die("%s", SSL_SET_OPTIONS_ERROR);
|
|
#if defined(HAVE_OPENSSL)
|
|
opt_ssl_mode = save_opt_ssl_mode;
|
|
#endif
|
|
|
|
if (con_pipe && !con_ssl) {
|
|
opt_protocol = MYSQL_PROTOCOL_PIPE;
|
|
}
|
|
|
|
if (opt_compress || con_compress) {
|
|
enable_async_client = false;
|
|
}
|
|
|
|
if (opt_protocol) {
|
|
mysql_options(&con_slot->mysql, MYSQL_OPT_PROTOCOL, (char *)&opt_protocol);
|
|
/*
|
|
Resetting the opt_protocol value to 0 to avoid the
|
|
possible failure in the next connect() command.
|
|
*/
|
|
opt_protocol = 0;
|
|
}
|
|
|
|
if (con_shm) {
|
|
uint protocol = MYSQL_PROTOCOL_MEMORY;
|
|
if (!ds_shm.length) die("Missing shared memory base name");
|
|
|
|
mysql_options(&con_slot->mysql, MYSQL_SHARED_MEMORY_BASE_NAME, ds_shm.str);
|
|
mysql_options(&con_slot->mysql, MYSQL_OPT_PROTOCOL, &protocol);
|
|
} else if (shared_memory_base_name) {
|
|
mysql_options(&con_slot->mysql, MYSQL_SHARED_MEMORY_BASE_NAME,
|
|
shared_memory_base_name);
|
|
}
|
|
|
|
if (con_socket) {
|
|
uint protocol = MYSQL_PROTOCOL_SOCKET;
|
|
mysql_options(&con_slot->mysql, MYSQL_OPT_PROTOCOL, &protocol);
|
|
}
|
|
|
|
if (con_tcp) {
|
|
uint protocol = MYSQL_PROTOCOL_TCP;
|
|
mysql_options(&con_slot->mysql, MYSQL_OPT_PROTOCOL, &protocol);
|
|
}
|
|
|
|
/* Use default db name */
|
|
if (ds_database.length == 0) dynstr_set(&ds_database, opt_db);
|
|
|
|
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);
|
|
|
|
/* Set server public_key */
|
|
set_server_public_key(&con_slot->mysql);
|
|
|
|
set_get_server_public_key_option(&con_slot->mysql);
|
|
|
|
if (con_cleartext_enable)
|
|
mysql_options(&con_slot->mysql, MYSQL_ENABLE_CLEARTEXT_PLUGIN,
|
|
(char *)&con_cleartext_enable);
|
|
|
|
/* Special database to allow one to connect without a database name */
|
|
if (ds_database.length && !std::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)) {
|
|
DBUG_PRINT("info", ("Inserting connection %s in connection pool",
|
|
ds_connection_name.str));
|
|
my_free(con_slot->name);
|
|
if (!(con_slot->name = my_strdup(PSI_NOT_INSTRUMENTED,
|
|
ds_connection_name.str, MYF(MY_WME))))
|
|
die("Out of memory");
|
|
con_slot->name_len = std::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 */
|
|
}
|
|
|
|
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);
|
|
dynstr_free(&ds_shm);
|
|
dynstr_free(&ds_compression_algorithm);
|
|
dynstr_free(&ds_zstd_compression_level);
|
|
}
|
|
|
|
static 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 */
|
|
strcpy(delimiter, cur_block->delim);
|
|
delimiter_length = std::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 };
|
|
|
|
static 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.
|
|
*/
|
|
|
|
static void do_block(enum block_cmd cmd, struct st_command *command) {
|
|
const char *p = command->first_argument;
|
|
const char *expr_start;
|
|
const char *expr_end;
|
|
VAR v;
|
|
const char *cmd_name = (cmd == cmd_while ? "while" : "if");
|
|
bool not_expr = false;
|
|
DBUG_TRACE;
|
|
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';
|
|
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 = 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 '%.*s' 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 = !std::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 = (std::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 beging 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 */
|
|
strcpy(cur_block->delim, delimiter);
|
|
}
|
|
|
|
DBUG_PRINT("info", ("OK: %d", cur_block->ok));
|
|
|
|
var_free()(&v);
|
|
}
|
|
|
|
static void do_delimiter(struct st_command *command) {
|
|
char *p = command->first_argument;
|
|
DBUG_TRACE;
|
|
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");
|
|
|
|
strmake(delimiter, p, sizeof(delimiter) - 1);
|
|
delimiter_length = std::strlen(delimiter);
|
|
|
|
DBUG_PRINT("exit", ("delimiter: %s", delimiter));
|
|
command->last_argument = p + delimiter_length;
|
|
}
|
|
|
|
/*
|
|
do_reset_connection
|
|
|
|
DESCRIPTION
|
|
Reset the current session.
|
|
*/
|
|
static void do_reset_connection() {
|
|
MYSQL *mysql = &cur_con->mysql;
|
|
|
|
DBUG_TRACE;
|
|
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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
static bool end_of_query(int c) {
|
|
return match_delimiter(c, delimiter, delimiter_length);
|
|
}
|
|
|
|
/*
|
|
Read one "line" from the file
|
|
|
|
SYNOPSIS
|
|
read_line
|
|
buf buffer for the read line
|
|
size size of the buffer i.e max size to read
|
|
|
|
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 int read_line(char *buf, int size) {
|
|
char c, last_quote = 0, last_char = 0;
|
|
char *p = buf, *buf_end = buf + size - 1;
|
|
int skip_char = 0;
|
|
int query_comment = 0, query_comment_start = 0, query_comment_end = 0;
|
|
bool have_slash = false;
|
|
|
|
enum {
|
|
R_NORMAL,
|
|
R_Q,
|
|
R_SLASH_IN_Q,
|
|
R_COMMENT,
|
|
R_LINE_START
|
|
} state = R_LINE_START;
|
|
DBUG_TRACE;
|
|
|
|
start_lineno = cur_file->lineno;
|
|
DBUG_PRINT("info", ("Starting to read at lineno: %d", start_lineno));
|
|
for (; p < buf_end;) {
|
|
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));
|
|
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 != 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));
|
|
return 0;
|
|
} else if ((c == '{' &&
|
|
(!charset_info->coll->strnncoll(
|
|
charset_info, (const uchar *)"while", 5, (uchar *)buf,
|
|
std::min<ptrdiff_t>(5, p - buf), 0) ||
|
|
!charset_info->coll->strnncoll(
|
|
charset_info, (const uchar *)"if", 2, (uchar *)buf,
|
|
std::min<ptrdiff_t>(2, p - 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));
|
|
return 0;
|
|
} else if (c == '\'' || c == '"' || c == '`') {
|
|
if (!have_slash) {
|
|
last_quote = c;
|
|
state = R_Q;
|
|
}
|
|
} else if (c == '/') {
|
|
if ((query_comment_start == 0) && (query_comment == 0))
|
|
query_comment_start = 1;
|
|
else if (query_comment_end == 1) {
|
|
query_comment = 0;
|
|
query_comment_end = 0;
|
|
}
|
|
} else if (c == '*') {
|
|
if ((query_comment == 1) && (query_comment_end == 0))
|
|
query_comment_end = 1;
|
|
else if (query_comment_start == 1)
|
|
query_comment = 1;
|
|
} else if ((c == '+') || (c == '!')) {
|
|
if ((query_comment_start == 1) && (query_comment == 1)) {
|
|
query_comment_start = 0;
|
|
query_comment = 0;
|
|
}
|
|
} else if (query_comment_start == 1)
|
|
query_comment_start = 0;
|
|
else if (query_comment_end == 1)
|
|
query_comment_end = 0;
|
|
|
|
have_slash = (c == '\\');
|
|
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));
|
|
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;
|
|
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 begining 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));
|
|
return 0;
|
|
} else if (c == '}') {
|
|
/* A "}" need to be by itself in the begining of a line to terminate
|
|
*/
|
|
*p++ = c;
|
|
*p = 0;
|
|
DBUG_PRINT("exit", ("Found '}' in begining of a line at line: %d",
|
|
cur_file->lineno));
|
|
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 (c == '\\')
|
|
state = R_SLASH_IN_Q;
|
|
else if (query_comment)
|
|
state = R_NORMAL;
|
|
break;
|
|
|
|
case R_SLASH_IN_Q:
|
|
state = R_Q;
|
|
break;
|
|
}
|
|
|
|
last_char = c;
|
|
|
|
if (!skip_char) {
|
|
/* Could be a multibyte character */
|
|
/* This code is based on the code in "sql_load.cc" */
|
|
uint charlen;
|
|
if (my_mbmaxlenlen(charset_info) == 1)
|
|
charlen = my_mbcharlen(charset_info, (unsigned char)c);
|
|
else {
|
|
if (!(charlen = my_mbcharlen(charset_info, (unsigned char)c))) {
|
|
int c1 = my_getc(cur_file->file);
|
|
if (c1 == EOF) {
|
|
*p++ = c;
|
|
goto found_eof;
|
|
}
|
|
|
|
charlen =
|
|
my_mbcharlen_2(charset_info, (unsigned char)c, (unsigned char)c1);
|
|
my_ungetc(c1);
|
|
}
|
|
}
|
|
if (charlen == 0) return 1;
|
|
/* We give up if multibyte character is started but not */
|
|
/* completed before we pass buf_end */
|
|
if ((charlen > 1) && (p + charlen) <= buf_end) {
|
|
char *mb_start = p;
|
|
|
|
*p++ = c;
|
|
|
|
for (uint i = 1; i < charlen; i++) {
|
|
c = my_getc(cur_file->file);
|
|
if (feof(cur_file->file)) goto found_eof;
|
|
*p++ = c;
|
|
}
|
|
if (!my_ismbchar(charset_info, mb_start, p)) {
|
|
/* 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);
|
|
}
|
|
} else
|
|
*p++ = c;
|
|
}
|
|
}
|
|
die("The input buffer is too small for this query.x\n"
|
|
"check your query or increase MAX_QUERY and recompile");
|
|
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
|
|
|
|
*/
|
|
|
|
static 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_TRACE;
|
|
DBUG_PRINT("enter", ("line: %s", line));
|
|
|
|
/* Check for extra delimiter */
|
|
if (*p && !std::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);
|
|
}
|
|
}
|
|
|
|
static void check_eol_junk(const char *eol) {
|
|
const char *p = eol;
|
|
DBUG_TRACE;
|
|
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);
|
|
}
|
|
|
|
static bool is_delimiter(const char *p) {
|
|
uint match = 0;
|
|
char *delim = delimiter;
|
|
while (*p && *p == *delim++) {
|
|
match++;
|
|
p++;
|
|
}
|
|
|
|
return (match == delimiter_length);
|
|
}
|
|
|
|
// 256K -- a test in sp-big is >128K
|
|
#define MAX_QUERY (256 * 1024 * 2)
|
|
static char read_command_buf[MAX_QUERY];
|
|
|
|
/// Create a command from a set of lines.
|
|
///
|
|
/// 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.
|
|
///
|
|
/// @param [in] command_ptr pointer where to return the new query
|
|
///
|
|
/// @retval 0 on success, else 1
|
|
static int read_command(struct st_command **command_ptr) {
|
|
char *p = read_command_buf;
|
|
DBUG_TRACE;
|
|
|
|
if (parser.current_line < parser.read_lines) {
|
|
*command_ptr = q_lines->at(parser.current_line);
|
|
// Assign the current command line number
|
|
start_lineno = (*command_ptr)->lineno;
|
|
return 0;
|
|
}
|
|
|
|
struct st_command *command;
|
|
if (!(*command_ptr = command = (struct st_command *)my_malloc(
|
|
PSI_NOT_INSTRUMENTED, sizeof(*command),
|
|
MYF(MY_WME | MY_ZEROFILL))) ||
|
|
q_lines->push_back(command))
|
|
die("Out of memory");
|
|
command->type = Q_UNKNOWN;
|
|
|
|
read_command_buf[0] = 0;
|
|
if (read_line(read_command_buf, sizeof(read_command_buf))) {
|
|
check_eol_junk(read_command_buf);
|
|
return 1;
|
|
}
|
|
|
|
// Set the line number for the command
|
|
command->lineno = start_lineno;
|
|
|
|
if (opt_result_format_version == 1) convert_to_format_v1(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;
|
|
// Skip past '--'
|
|
p += 2;
|
|
} 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", static_cast<int>(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 = (command->end - command->query);
|
|
parser.read_lines++;
|
|
return 0;
|
|
}
|
|
|
|
static struct my_option my_long_options[] = {
|
|
#include "caching_sha2_passwordopt-longopts.h"
|
|
#include "sslopt-longopts.h"
|
|
{"basedir", 'b', "Basedir for tests.", &opt_basedir, &opt_basedir, 0,
|
|
GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
|
|
{"character-sets-dir", OPT_CHARSETS_DIR,
|
|
"Directory for character set files.", &opt_charsets_dir, &opt_charsets_dir,
|
|
0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
|
|
{"colored-diff", OPT_COLORED_DIFF, "Colorize the diff outout.",
|
|
&opt_colored_diff, &opt_colored_diff, 0, GET_BOOL, NO_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},
|
|
{"connect_timeout", OPT_CONNECT_TIMEOUT,
|
|
"Number of seconds before connection timeout.", &opt_connect_timeout,
|
|
&opt_connect_timeout, 0, GET_UINT, REQUIRED_ARG, 120, 0, 3600 * 12, 0, 0,
|
|
0},
|
|
{"cursor-protocol", OPT_CURSOR_PROTOCOL,
|
|
"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},
|
|
{"debug-check", OPT_DEBUG_CHECK,
|
|
"This is a non-debug version. Catch this and exit.", 0, 0, 0, GET_DISABLED,
|
|
NO_ARG, 0, 0, 0, 0, 0, 0},
|
|
{"debug-info", OPT_DEBUG_INFO,
|
|
"This is a non-debug version. Catch this and exit.", 0, 0, 0, GET_DISABLED,
|
|
NO_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},
|
|
{"debug-check", OPT_DEBUG_CHECK,
|
|
"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", OPT_DEBUG_INFO, "Print some debug info at exit.",
|
|
&debug_info_flag, &debug_info_flag, 0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0},
|
|
#endif
|
|
{"default-character-set", OPT_DEFAULT_CHARSET,
|
|
"Set the default character set.", &default_charset, &default_charset, 0,
|
|
GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
|
|
{"explain-protocol", OPT_EXPLAIN_PROTOCOL,
|
|
"Explain all SELECT/INSERT/REPLACE/UPDATE/DELETE statements",
|
|
&explain_protocol, &explain_protocol, 0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0,
|
|
0},
|
|
{"help", '?', "Display this help and exit.", 0, 0, 0, GET_NO_ARG, 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},
|
|
{"include", 'i', "Include SQL before each test case.", &opt_include,
|
|
&opt_include, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
|
|
{"json-explain-protocol", OPT_JSON_EXPLAIN_PROTOCOL,
|
|
"Explain all SELECT/INSERT/REPLACE/UPDATE/DELETE statements with "
|
|
"FORMAT=JSON",
|
|
&json_explain_protocol, &json_explain_protocol, 0, GET_BOOL, NO_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", OPT_MARK_PROGRESS,
|
|
"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", OPT_MAX_CONNECT_RETRIES,
|
|
"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", OPT_MAX_CONNECTIONS,
|
|
"Max number of open connections to server", &opt_max_connections,
|
|
&opt_max_connections, 0, GET_INT, REQUIRED_ARG, 128, 8, 5120, 0, 0, 0},
|
|
{"no-skip", OPT_NO_SKIP, "Force the test to run without skip.", &no_skip,
|
|
&no_skip, 0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0},
|
|
{"no-skip-exclude-list", 'n',
|
|
"Contains comma seperated list of to be excluded inc files.",
|
|
&excluded_string, &excluded_string, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0,
|
|
0, 0},
|
|
{"offload-count-file", OPT_OFFLOAD_COUNT_FILE, "Offload count report file",
|
|
&opt_offload_count_file, &opt_offload_count_file, 0, GET_STR, REQUIRED_ARG,
|
|
0, 0, 0, 0, 0, 0},
|
|
{"opt-trace-protocol", OPT_TRACE_PROTOCOL,
|
|
"Trace DML statements with optimizer trace", &opt_trace_protocol,
|
|
&opt_trace_protocol, 0, GET_BOOL, NO_ARG, 0, 0, 0, 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},
|
|
{"plugin_dir", OPT_PLUGIN_DIR, "Directory for client-side plugins.",
|
|
&opt_plugin_dir, &opt_plugin_dir, 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},
|
|
{"protocol", OPT_MYSQL_PROTOCOL,
|
|
"The protocol of connection (tcp,socket,pipe,memory).", 0, 0, 0, GET_STR,
|
|
REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
|
|
{"ps-protocol", OPT_PS_PROTOCOL,
|
|
"Use prepared-statement protocol for communication.", &ps_protocol,
|
|
&ps_protocol, 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},
|
|
#ifdef _WIN32
|
|
{"safe-process-pid", OPT_SAFEPROCESS_PID, "PID of safeprocess.",
|
|
&opt_safe_process_pid, &opt_safe_process_pid, 0, GET_INT, REQUIRED_ARG, 0,
|
|
0, 0, 0, 0, 0},
|
|
#endif
|
|
{"shared-memory-base-name", OPT_SHARED_MEMORY_BASE_NAME,
|
|
"Base name of shared memory.", &shared_memory_base_name,
|
|
&shared_memory_base_name, 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", OPT_SP_PROTOCOL, "Use stored procedures for select.",
|
|
&sp_protocol, &sp_protocol, 0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0},
|
|
{"tail-lines", OPT_TAIL_LINES,
|
|
"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},
|
|
{"trace-exec", OPT_TRACE_EXEC, "Print output from exec to stdout.",
|
|
&trace_exec, &trace_exec, 0, GET_BOOL, NO_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", OPT_VIEW_PROTOCOL, "Use views for select.",
|
|
&view_protocol, &view_protocol, 0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0},
|
|
{"async-client", '*', "Use async client.", &use_async_client,
|
|
&use_async_client, 0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0},
|
|
{"compression-algorithms", 0,
|
|
"Use compression algorithm in server/client protocol. Valid values "
|
|
"are any combination of 'zstd','zlib','uncompressed'.",
|
|
&opt_compress_algorithm, &opt_compress_algorithm, 0, GET_STR, REQUIRED_ARG,
|
|
0, 0, 0, 0, 0, 0},
|
|
{"zstd-compression-level", 0,
|
|
"Use this compression level in the client/server protocol, in case "
|
|
"--compression-algorithms=zstd. Valid range is between 1 and 22, "
|
|
"inclusive. Default is 3.",
|
|
&opt_zstd_compress_level, &opt_zstd_compress_level, 0, GET_UINT,
|
|
REQUIRED_ARG, 3, 1, 22, 0, 0, 0},
|
|
|
|
{0, 0, 0, 0, 0, 0, GET_NO_ARG, NO_ARG, 0, 0, 0, 0, 0, 0}};
|
|
|
|
static void usage() {
|
|
print_version();
|
|
puts(ORACLE_WELCOME_COPYRIGHT_NOTICE("2000"));
|
|
printf(
|
|
"Runs a test against the mysql server and compares output with a results "
|
|
"file.\n\n");
|
|
printf("Usage: %s [OPTIONS] [database] < test_file\n", my_progname);
|
|
my_print_help(my_long_options);
|
|
printf(
|
|
" --no-defaults Don't read default options from any options "
|
|
"file.\n");
|
|
my_print_variables(my_long_options);
|
|
}
|
|
|
|
static bool get_one_option(int optid, const struct my_option *opt,
|
|
char *argument) {
|
|
switch (optid) {
|
|
case '#':
|
|
#ifndef DBUG_OFF
|
|
DBUG_PUSH(argument ? argument : "d:t:S:i:O,/tmp/mysqltest.trace");
|
|
debug_check_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) {
|
|
// Don't require password
|
|
static char empty_password[] = {'\0'};
|
|
DBUG_ASSERT(empty_password[0] ==
|
|
'\0'); // Check that it has not been overwritten
|
|
argument = empty_password;
|
|
}
|
|
if (argument) {
|
|
my_free(opt_pass);
|
|
opt_pass = my_strdup(PSI_NOT_INSTRUMENTED, argument, MYF(MY_FAE));
|
|
while (*argument) *argument++ = 'x'; /* Destroy argument */
|
|
tty_password = 0;
|
|
} else
|
|
tty_password = 1;
|
|
break;
|
|
#include "sslopt-case.h"
|
|
|
|
case 't':
|
|
my_stpnmov(TMPDIR, argument, sizeof(TMPDIR));
|
|
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 OPT_RESULT_FORMAT_VERSION:
|
|
set_result_format_version(opt_result_format_version);
|
|
break;
|
|
case 'V':
|
|
print_version();
|
|
exit(0);
|
|
case OPT_MYSQL_PROTOCOL:
|
|
opt_protocol =
|
|
find_type_or_exit(argument, &sql_protocol_typelib, opt->name);
|
|
break;
|
|
case '?':
|
|
usage();
|
|
exit(0);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
Test case or the result file names may use alphanumeric characters
|
|
(A-Z, a-z, 0-9), dash ('-') or underscore ('_'), but should not
|
|
start with dash or underscore.
|
|
|
|
Check if a file name conatins any other special characters. If yes,
|
|
throw an error and abort the test run.
|
|
|
|
@param[in] file_name File name
|
|
*/
|
|
|
|
static void validate_filename(const char *file_name) {
|
|
const char *fname = strrchr(file_name, '/');
|
|
|
|
if (fname == NULL) {
|
|
if (is_windows) {
|
|
fname = strrchr(file_name, '\\');
|
|
|
|
if (fname == NULL)
|
|
fname = file_name;
|
|
else
|
|
fname++;
|
|
} else
|
|
fname = file_name;
|
|
} else
|
|
fname++;
|
|
|
|
file_name = fname;
|
|
|
|
// Check if first character in the file name is a alphanumeric character
|
|
if (!my_isalnum(charset_info, file_name[0])) {
|
|
die("Invalid file name '%s', first character must be alpha-numeric.",
|
|
file_name);
|
|
} else
|
|
file_name++;
|
|
|
|
// Skip extension('.test' or '.result' or '.inc' etc) in the file name
|
|
const char *file_name_end = strrchr(file_name, '.');
|
|
|
|
while (*file_name && (file_name != file_name_end) &&
|
|
(file_name[0] == '-' || file_name[0] == '_' ||
|
|
my_isalnum(charset_info, file_name[0]))) {
|
|
file_name++;
|
|
}
|
|
|
|
if (file_name != file_name_end) {
|
|
die("Invalid file name '%s'. Test or result file name should "
|
|
"consist of only alpha-numeric characters, dash (-) or "
|
|
"underscore (_), but should not start with dash or "
|
|
"underscore.",
|
|
fname);
|
|
}
|
|
}
|
|
|
|
static int parse_args(int argc, char **argv) {
|
|
if (load_defaults("my", load_default_groups, &argc, &argv, &argv_alloc))
|
|
exit(1);
|
|
|
|
if ((handle_options(&argc, &argv, my_long_options, get_one_option))) exit(1);
|
|
|
|
// Check for special characters in test case file name
|
|
if (cur_file->file_name) validate_filename(cur_file->file_name);
|
|
|
|
// Check for special characters in result file name
|
|
if (result_file_name) validate_filename(result_file_name);
|
|
|
|
if (argc > 1) {
|
|
usage();
|
|
exit(1);
|
|
}
|
|
|
|
if (argc == 1) opt_db = *argv;
|
|
if (tty_password) opt_pass = get_tty_password(NullS);
|
|
if (debug_info_flag) my_end_arg = MY_CHECK_ERROR | MY_GIVE_INFO;
|
|
if (debug_check_flag) my_end_arg = MY_CHECK_ERROR;
|
|
|
|
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, 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);
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
|
|
typedef Prealloced_array<const char *, 16> Patterns;
|
|
Patterns *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_CHARSETSDIR",
|
|
"$MYSQL_LIBDIR", "./test/", ".ibd",
|
|
".\\ibdata", ".\\ibtmp", ".\\undo"};
|
|
int num_paths = sizeof(paths) / sizeof(char *);
|
|
int i;
|
|
char *p;
|
|
|
|
DBUG_TRACE;
|
|
|
|
patterns = new Patterns(PSI_NOT_INSTRUMENTED);
|
|
|
|
/* 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 (std::strlen(p) == 0) {
|
|
my_free(p);
|
|
continue;
|
|
}
|
|
|
|
if (patterns->push_back(p)) die("Out of memory");
|
|
|
|
DBUG_PRINT("info", ("p: %s", p));
|
|
while (*p) {
|
|
if (*p == '/') *p = '\\';
|
|
p++;
|
|
}
|
|
}
|
|
}
|
|
|
|
void free_win_path_patterns() {
|
|
uint i = 0;
|
|
const char **pat;
|
|
for (pat = patterns->begin(); pat != patterns->end(); ++pat) {
|
|
my_free(const_cast<char *>(*pat));
|
|
}
|
|
delete patterns;
|
|
patterns = NULL;
|
|
}
|
|
|
|
/*
|
|
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(const char *val, size_t len) {
|
|
DBUG_TRACE;
|
|
const char **pat;
|
|
for (pat = patterns->begin(); pat != patterns->end(); ++pat) {
|
|
char *p;
|
|
DBUG_PRINT("info", ("pattern: %s", *pat));
|
|
|
|
/* Find and fix each path in this string */
|
|
p = const_cast<char *>(val);
|
|
while (p = strstr(p, *pat)) {
|
|
DBUG_PRINT("info", ("Found %s in val p: %s", *pat, p));
|
|
/* Found the pattern. Back up to the start of this path */
|
|
while (p > val && !my_isspace(charset_info, *(p - 1))) {
|
|
p--;
|
|
}
|
|
|
|
while (*p && !my_isspace(charset_info, *p)) {
|
|
if (*p == '\\') *p = '/';
|
|
p++;
|
|
}
|
|
DBUG_PRINT("info", ("Converted \\ to / in %s", val));
|
|
}
|
|
}
|
|
DBUG_PRINT("exit", (" val: %s, len: %d", val, len));
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
Append the result for one field to the dynamic string ds
|
|
*/
|
|
|
|
static void append_field(DYNAMIC_STRING *ds, uint col_idx, MYSQL_FIELD *field,
|
|
char *val, size_t len, bool is_null) {
|
|
char null[] = "NULL";
|
|
|
|
if (col_idx < max_replace_column && replace_column[col_idx]) {
|
|
val = replace_column[col_idx];
|
|
len = std::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 && std::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, std::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'
|
|
*/
|
|
|
|
static 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_wrapper(res))) {
|
|
uint i;
|
|
lengths = mysql_fetch_lengths(res);
|
|
for (i = 0; i < num_fields; i++) {
|
|
/* looks ugly , but put here to convince parfait */
|
|
assert(lengths);
|
|
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'
|
|
*/
|
|
|
|
static void append_stmt_result(DYNAMIC_STRING *ds, MYSQL_STMT *stmt,
|
|
MYSQL_FIELD *fields, uint num_fields) {
|
|
MYSQL_BIND *my_bind;
|
|
bool *is_null;
|
|
ulong *length;
|
|
uint i;
|
|
|
|
/* 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 = (bool *)my_malloc(PSI_NOT_INSTRUMENTED, num_fields * sizeof(bool),
|
|
MYF(MY_WME | MY_FAE));
|
|
|
|
/* Allocate data for the result of each field */
|
|
for (i = 0; i < num_fields; i++) {
|
|
size_t 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 = static_cast<ulong>(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 (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);
|
|
}
|
|
|
|
int rc;
|
|
if ((rc = mysql_stmt_fetch(stmt)) != MYSQL_NO_DATA)
|
|
die("fetch didn't end with MYSQL_NO_DATA from statement: %d: %s; rc=%d",
|
|
mysql_stmt_errno(stmt), mysql_stmt_error(stmt), rc);
|
|
|
|
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
|
|
*/
|
|
|
|
static 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);
|
|
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
|
|
*/
|
|
|
|
static 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
@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) {
|
|
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)) {
|
|
/*
|
|
Append the type information. Please update the definition of APPEND_TYPE
|
|
when any changes are made to enum_session_state_type.
|
|
*/
|
|
APPEND_TYPE(type);
|
|
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-- ");
|
|
dynstr_append_mem(ds, data, data_length);
|
|
}
|
|
dynstr_append(ds, "\n\n");
|
|
}
|
|
}
|
|
|
|
/*
|
|
Display the table headings with the names tab separated
|
|
*/
|
|
|
|
static void append_table_headings(DYNAMIC_STRING *ds, MYSQL_FIELD *field,
|
|
uint num_fields) {
|
|
uint col_idx;
|
|
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);
|
|
}
|
|
|
|
/// Check whether a given warning is in list of disabled or enabled warnings.
|
|
///
|
|
/// @param warnings List of either disabled or enabled warnings.
|
|
/// @param error Error number
|
|
/// @param warning_found Boolean value, should be set to true if warning
|
|
/// is found in the list, false otherwise.
|
|
///
|
|
/// @retval True if the given warning is present in the list, and
|
|
/// ignore flag for that warning is not set, false otherwise.
|
|
static bool match_warnings(Expected_warnings *warnings, std::uint32_t error,
|
|
bool *warning_found) {
|
|
bool match_found = false;
|
|
std::vector<std::unique_ptr<Warning>>::iterator warning = warnings->begin();
|
|
|
|
for (; warning != warnings->end(); warning++) {
|
|
if ((*warning)->warning_code() == error) {
|
|
*warning_found = true;
|
|
if (!(*warning)->ignore_warning()) {
|
|
match_found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return match_found;
|
|
}
|
|
|
|
/// Handle one warning which occurred during execution of a query.
|
|
///
|
|
/// @param ds DYNAMIC_STRING object to store the warnings.
|
|
/// @param warning Warning string
|
|
///
|
|
/// @retval True if a warning is found in the list of disabled or enabled
|
|
/// warnings, false otherwise.
|
|
static bool handle_one_warning(DYNAMIC_STRING *ds, std::string warning) {
|
|
// Each line of show warnings output contains information about
|
|
// error level, error code and the error/warning message separated
|
|
// by '\t'. Parse each line from the show warnings output to
|
|
// extract the error code and compare it with list of expected
|
|
// warnings.
|
|
bool warning_found = false;
|
|
std::string error_code;
|
|
std::stringstream warn_msg(warning);
|
|
|
|
while (std::getline(warn_msg, error_code, '\t')) {
|
|
int errcode = get_int_val(error_code.c_str());
|
|
if (errcode != -1) {
|
|
if (disabled_warnings->count()) {
|
|
// Print the warning if it doesn't match with any of the
|
|
// disabled warnings.
|
|
if (!match_warnings(disabled_warnings, errcode, &warning_found)) {
|
|
dynstr_append_mem(ds, warning.c_str(), warning.length());
|
|
dynstr_append_mem(ds, "\n", 1);
|
|
}
|
|
} else if (enabled_warnings->count()) {
|
|
if (match_warnings(enabled_warnings, errcode, &warning_found)) {
|
|
dynstr_append_mem(ds, warning.c_str(), warning.length());
|
|
dynstr_append_mem(ds, "\n", 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return warning_found;
|
|
}
|
|
|
|
/// Handle warnings which occurred during execution of a query.
|
|
///
|
|
/// @param ds DYNAMIC_STRING object to store the warnings.
|
|
/// @param ds_warnings String containing all the generated warnings.
|
|
static void handle_warnings(DYNAMIC_STRING *ds, const char *ds_warnings) {
|
|
bool warning_found = false;
|
|
std::string warning;
|
|
std::stringstream warnings(ds_warnings);
|
|
|
|
// Set warning_found only if at least one of the warning exist
|
|
// in expected list of warnings.
|
|
while (std::getline(warnings, warning))
|
|
if (handle_one_warning(ds, warning)) warning_found = true;
|
|
|
|
// Throw an error and abort the test run if a query generates warnings
|
|
// which are not listed as expected.
|
|
if (!warning_found) {
|
|
std::string warning_list;
|
|
|
|
if (disabled_warnings->count())
|
|
warning_list = disabled_warnings->warnings_list();
|
|
else if (enabled_warnings->count())
|
|
warning_list = enabled_warnings->warnings_list();
|
|
|
|
die("Query '%s' didn't generate any of the expected warning(s) '%s'.",
|
|
curr_command->query, warning_list.c_str());
|
|
}
|
|
}
|
|
|
|
/// Fetch warnings generated by server while executing a query and
|
|
/// append them to warnings buffer 'ds'.
|
|
///
|
|
/// @param ds DYNAMIC_STRING object to store the warnings
|
|
/// @param mysql mysql handle object
|
|
///
|
|
/// @retval Number of warnings appended to ds
|
|
static int append_warnings(DYNAMIC_STRING *ds, MYSQL *mysql) {
|
|
unsigned int count;
|
|
if (!(count = mysql_warning_count(mysql))) return 0;
|
|
|
|
// 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));
|
|
|
|
MYSQL_RES *warn_res;
|
|
if (mysql_real_query_wrapper(mysql, "SHOW WARNINGS", 13))
|
|
die("Error running query \"SHOW WARNINGS\": %s", mysql_error(mysql));
|
|
|
|
if (!(warn_res = mysql_store_result_wrapper(mysql)))
|
|
die("Warning count is %u but didn't get any warnings", count);
|
|
|
|
DYNAMIC_STRING ds_warnings;
|
|
init_dynamic_string(&ds_warnings, "", 1024, 1024);
|
|
append_result(&ds_warnings, warn_res);
|
|
mysql_free_result_wrapper(warn_res);
|
|
|
|
if (disable_warnings &&
|
|
(disabled_warnings->count() || enabled_warnings->count()))
|
|
handle_warnings(ds, ds_warnings.str);
|
|
else if (!disable_warnings)
|
|
dynstr_append_mem(ds, ds_warnings.str, ds_warnings.length);
|
|
|
|
dynstr_free(&ds_warnings);
|
|
return ds->length;
|
|
}
|
|
|
|
/// Run query using MySQL C API
|
|
///
|
|
/// @param cn Connection object
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
/// @param flags Flags indicating if we should SEND and/or REAP.
|
|
/// @param query Query string
|
|
/// @param query_len Length of the query string
|
|
/// @param ds Output buffer to store the query result.
|
|
/// @param ds_warnings Buffer to store the warnings generated while
|
|
/// executing the query.
|
|
static 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) {
|
|
int error = 0;
|
|
std::uint32_t counter = 0;
|
|
MYSQL *mysql = &cn->mysql;
|
|
MYSQL_RES *res = 0;
|
|
|
|
if (flags & QUERY_SEND_FLAG) {
|
|
/* Send the query */
|
|
if (mysql_send_query_wrapper(&cn->mysql, query,
|
|
static_cast<ulong>(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;
|
|
return;
|
|
}
|
|
|
|
do {
|
|
/*
|
|
When on first result set, call mysql_read_query_result_wrapper to
|
|
retrieve answer to the query sent earlier
|
|
*/
|
|
if ((counter == 0) && mysql_read_query_result_wrapper(&cn->mysql)) {
|
|
/* 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_wrapper(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);
|
|
std::uint32_t 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 || disabled_warnings->count() ||
|
|
enabled_warnings->count()) &&
|
|
!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_wrapper(res);
|
|
res = 0;
|
|
}
|
|
counter++;
|
|
} while (!(error = mysql_next_result_wrapper(mysql)));
|
|
if (error > 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;
|
|
}
|
|
|
|
// Successful and there are no more results.
|
|
DBUG_ASSERT(error == -1);
|
|
|
|
// If we come here the query is both executed and read successfully.
|
|
handle_no_error(command);
|
|
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));
|
|
}
|
|
|
|
/// Run query using prepared statement C API
|
|
///
|
|
/// @param mysql mysql handle
|
|
/// @param command Pointer to the st_command structure which holds the
|
|
/// arguments and information for the command.
|
|
/// @param query Query string
|
|
/// @param query_len Length of the query string
|
|
/// @param ds Output buffer to store the query result.
|
|
/// @param ds_warnings Buffer to store the warnings generated while
|
|
/// executing the query.
|
|
static void run_query_stmt(MYSQL *mysql, struct st_command *command,
|
|
const char *query, size_t query_len,
|
|
DYNAMIC_STRING *ds, DYNAMIC_STRING *ds_warnings) {
|
|
// Init a new stmt if it's not already one created for this connection.
|
|
MYSQL_STMT *stmt;
|
|
if (!(stmt = cur_con->stmt)) {
|
|
if (!(stmt = mysql_stmt_init(mysql))) die("unable to init stmt structure");
|
|
cur_con->stmt = stmt;
|
|
}
|
|
|
|
DYNAMIC_STRING ds_prepare_warnings;
|
|
DYNAMIC_STRING ds_execute_warnings;
|
|
|
|
// Init dynamic strings for warnings.
|
|
if (!disable_warnings || disabled_warnings->count() ||
|
|
enabled_warnings->count()) {
|
|
init_dynamic_string(&ds_prepare_warnings, NULL, 0, 256);
|
|
init_dynamic_string(&ds_execute_warnings, NULL, 0, 256);
|
|
}
|
|
|
|
// Note that here 'res' is meta data result set
|
|
MYSQL_RES *res = NULL;
|
|
int err = 0;
|
|
|
|
// Prepare the query
|
|
if (mysql_stmt_prepare(stmt, query, static_cast<ulong>(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 || disabled_warnings->count() ||
|
|
enabled_warnings->count())
|
|
append_warnings(&ds_prepare_warnings, mysql);
|
|
|
|
// No need to call mysql_stmt_bind_param() because we have no
|
|
// parameter markers.
|
|
if (cursor_protocol_enabled) {
|
|
// Use cursor when retrieving result.
|
|
unsigned long 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));
|
|
}
|
|
|
|
// Execute the query
|
|
if (mysql_stmt_execute(stmt)) {
|
|
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 || disabled_warnings->count() ||
|
|
enabled_warnings->count()))
|
|
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
|
|
{
|
|
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));
|
|
}
|
|
|
|
do {
|
|
// 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);
|
|
std::uint32_t 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);
|
|
|
|
// Free normal result set with meta data
|
|
mysql_free_result_wrapper(res);
|
|
|
|
// Clear prepare warnings if there are execute warnings,
|
|
// since they are probably duplicated.
|
|
if (ds_execute_warnings.length || mysql->warning_count)
|
|
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_affected_rows(stmt->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 || disabled_warnings->count() ||
|
|
enabled_warnings->count()) &&
|
|
!mysql_more_results(stmt->mysql)) {
|
|
// Get the warnings from execute. Append warnings to ds,
|
|
// if there are any.
|
|
append_warnings(&ds_execute_warnings, mysql);
|
|
if (ds_execute_warnings.length || ds_prepare_warnings.length ||
|
|
ds_warnings->length) {
|
|
dynstr_append_mem(ds, "Warnings:\n", 10);
|
|
|
|
// Append warnings if exist any
|
|
if (ds_warnings->length)
|
|
dynstr_append_mem(ds, ds_warnings->str, ds_warnings->length);
|
|
|
|
// Append prepare warnings if exist any
|
|
if (ds_prepare_warnings.length)
|
|
dynstr_append_mem(ds, ds_prepare_warnings.str,
|
|
ds_prepare_warnings.length);
|
|
|
|
// Append execute warnings if exist any
|
|
if (ds_execute_warnings.length)
|
|
dynstr_append_mem(ds, ds_execute_warnings.str,
|
|
ds_execute_warnings.length);
|
|
}
|
|
}
|
|
}
|
|
} while ((err = mysql_stmt_next_result(stmt)) == 0);
|
|
|
|
if (err > 0) {
|
|
// We got an error from mysql_stmt_next_result, maybe expected.
|
|
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);
|
|
|
|
end:
|
|
if (!disable_warnings || disabled_warnings->count() ||
|
|
enabled_warnings->count()) {
|
|
dynstr_free(&ds_prepare_warnings);
|
|
dynstr_free(&ds_execute_warnings);
|
|
}
|
|
revert_properties();
|
|
|
|
// 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));
|
|
|
|
// Close the statement if no reconnect, need new prepare.
|
|
if (mysql->reconnect) {
|
|
mysql_stmt_close(stmt);
|
|
cur_con->stmt = NULL;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Create a util connection if one does not already exists
|
|
and use that to run the query
|
|
This is done to avoid implict commit when creating/dropping objects such
|
|
as view, sp etc.
|
|
*/
|
|
|
|
static int util_query(MYSQL *org_mysql, const char *query) {
|
|
MYSQL *mysql;
|
|
DBUG_TRACE;
|
|
|
|
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);
|
|
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;
|
|
}
|
|
|
|
return mysql_query_wrapper(mysql, query);
|
|
}
|
|
|
|
/*
|
|
Run query
|
|
|
|
SYNPOSIS
|
|
run_query()
|
|
mysql mysql handle
|
|
command currrent 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
|
|
*/
|
|
|
|
static 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_sorted;
|
|
DYNAMIC_STRING ds_warnings;
|
|
DYNAMIC_STRING eval_query;
|
|
const char *query;
|
|
size_t query_len;
|
|
bool view_created = 0, sp_created = 0;
|
|
bool complete_query =
|
|
((flags & QUERY_SEND_FLAG) && (flags & QUERY_REAP_FLAG));
|
|
DBUG_TRACE;
|
|
dynstr_set(&ds_result, "");
|
|
|
|
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) {
|
|
init_dynamic_string(&eval_query, "", command->query_len + 256, 1024);
|
|
do_eval(&eval_query, command->query, command->end, false);
|
|
query = eval_query.str;
|
|
query_len = eval_query.length;
|
|
} else {
|
|
query = command->query;
|
|
query_len = std::strlen(query);
|
|
}
|
|
|
|
/*
|
|
Create a temporary dynamic string to contain the
|
|
output from this query.
|
|
*/
|
|
if (command->output_file[0])
|
|
ds = &ds_result;
|
|
else
|
|
ds = &ds_res;
|
|
|
|
/*
|
|
Log the query into the output buffer
|
|
*/
|
|
if (!disable_query_log && (flags & QUERY_SEND_FLAG)) {
|
|
replace_dynstr_append_mem(ds, query, query_len);
|
|
dynstr_append_mem(ds, delimiter, delimiter_length);
|
|
dynstr_append_mem(ds, "\n", 1);
|
|
}
|
|
|
|
if (view_protocol_enabled && complete_query &&
|
|
search_protocol_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 = "SELECT * FROM mysqltest_tmp_v";
|
|
query_len = std::strlen(query);
|
|
|
|
/*
|
|
Collect warnings from create of the view that should otherwise
|
|
have been produced when the SELECT was executed
|
|
*/
|
|
append_warnings(&ds_warnings, cur_con->util_mysql);
|
|
}
|
|
|
|
dynstr_free(&query_str);
|
|
}
|
|
|
|
if (sp_protocol_enabled && complete_query &&
|
|
search_protocol_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 = "CALL mysqltest_tmp_sp()";
|
|
query_len = std::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 &&
|
|
search_protocol_re(&ps_re, query))
|
|
run_query_stmt(mysql, 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 (command->type == Q_EVAL || command->type == Q_SEND_EVAL)
|
|
dynstr_free(&eval_query);
|
|
|
|
if (display_result_sorted) {
|
|
/* Sort the result set and append it to result */
|
|
dynstr_append_sorted(save_ds, &ds_sorted, start_sort_column);
|
|
ds = save_ds;
|
|
dynstr_free(&ds_sorted);
|
|
}
|
|
|
|
if (sp_created) {
|
|
if (util_query(mysql, "DROP PROCEDURE mysqltest_tmp_sp "))
|
|
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 "))
|
|
die("Failed to drop view: %d: %s", mysql_errno(mysql),
|
|
mysql_error(mysql));
|
|
}
|
|
if (command->output_file[0]) {
|
|
/* An output file was specified for _this_ query */
|
|
str_to_file2(command->output_file, ds_result.str, ds_result.length, false);
|
|
command->output_file[0] = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
Display the optimizer trace produced by the last executed statement.
|
|
*/
|
|
static void display_opt_trace(struct st_connection *cn,
|
|
struct st_command *command, int flags) {
|
|
if (!disable_query_log && opt_trace_protocol_enabled && !cn->pending &&
|
|
!expected_errors->count() &&
|
|
search_protocol_re(&opt_trace_re, command->query)) {
|
|
st_command save_command = *command;
|
|
DYNAMIC_STRING query_str;
|
|
init_dynamic_string(&query_str,
|
|
"SELECT trace FROM information_schema.optimizer_trace"
|
|
" /* injected by --opt-trace-protocol */",
|
|
128, 128);
|
|
|
|
command->query = query_str.str;
|
|
command->query_len = query_str.length;
|
|
command->end = strend(command->query);
|
|
|
|
/* Sorted trace is not readable at all, don't bother to lower case */
|
|
/* No need to keep old values, will be reset anyway */
|
|
display_result_sorted = false;
|
|
display_result_lower = false;
|
|
run_query(cn, command, flags);
|
|
|
|
dynstr_free(&query_str);
|
|
*command = save_command;
|
|
}
|
|
}
|
|
|
|
static void run_explain(struct st_connection *cn, struct st_command *command,
|
|
int flags, bool json) {
|
|
if ((flags & QUERY_REAP_FLAG) && !expected_errors->count() &&
|
|
search_protocol_re(&explain_re, command->query)) {
|
|
st_command save_command = *command;
|
|
DYNAMIC_STRING query_str;
|
|
DYNAMIC_STRING ds_warning_messages;
|
|
|
|
init_dynamic_string(&ds_warning_messages, "", 0, 2048);
|
|
init_dynamic_string(&query_str, json ? "EXPLAIN FORMAT=JSON " : "EXPLAIN ",
|
|
256, 256);
|
|
dynstr_append_mem(&query_str, command->query,
|
|
command->end - command->query);
|
|
|
|
command->query = query_str.str;
|
|
command->query_len = query_str.length;
|
|
command->end = strend(command->query);
|
|
|
|
run_query(cn, command, flags);
|
|
|
|
dynstr_free(&query_str);
|
|
dynstr_free(&ds_warning_messages);
|
|
|
|
*command = save_command;
|
|
}
|
|
}
|
|
|
|
static void get_command_type(struct st_command *command) {
|
|
char save;
|
|
uint type;
|
|
DBUG_TRACE;
|
|
|
|
if (*command->query == '}') {
|
|
command->type = Q_END_BLOCK;
|
|
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 */
|
|
die("Found line '%s' beginning with -- that didn't contain "
|
|
"a valid mysqltest command, check your syntax or "
|
|
"use # if you intended to write a comment",
|
|
command->query);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Record how many milliseconds it took to execute the test file
|
|
/// up until the current line and write it to .progress file.
|
|
///
|
|
/// @param progress_file Logfile object to store the progress information
|
|
/// @param line Line number of the progress file where the progress
|
|
/// information should be recorded.
|
|
static void mark_progress(Logfile progress_file, int line) {
|
|
static unsigned long long int progress_start = 0;
|
|
unsigned long long int timer = timer_now();
|
|
|
|
if (!progress_start) progress_start = timer;
|
|
timer = timer - progress_start;
|
|
|
|
std::string str_progress;
|
|
|
|
// Milliseconds since start
|
|
std::string str_timer = std::to_string(timer);
|
|
str_progress.append(str_timer);
|
|
str_progress.append("\t");
|
|
|
|
// Parse the line number
|
|
std::string str_line = std::to_string(line);
|
|
str_progress.append(str_line);
|
|
str_progress.append("\t");
|
|
|
|
// Filename
|
|
str_progress.append(cur_file->file_name);
|
|
str_progress.append(":");
|
|
|
|
// Line in file
|
|
str_line = std::to_string(cur_file->lineno);
|
|
str_progress.append(str_line);
|
|
str_progress.append("\n");
|
|
|
|
if (progress_file.write(str_progress.c_str(), str_progress.length()) ||
|
|
progress_file.flush()) {
|
|
cleanup_and_exit(1);
|
|
}
|
|
}
|
|
|
|
#ifdef HAVE_STACKTRACE
|
|
static void dump_backtrace() {
|
|
struct st_connection *conn = cur_con;
|
|
|
|
fprintf(stderr, "mysqltest: ");
|
|
|
|
// Print the query and the line number
|
|
if (start_lineno > 0) fprintf(stderr, "At line %u: ", start_lineno);
|
|
fprintf(stderr, "%s\n", curr_command->query);
|
|
|
|
// Print the file stack
|
|
if (cur_file && cur_file != file_stack) {
|
|
fprintf(stderr, "In included ");
|
|
print_file_stack();
|
|
}
|
|
|
|
if (conn) fprintf(stderr, "conn->name: %s\n", conn->name);
|
|
|
|
fprintf(stderr, "Attempting backtrace.\n");
|
|
fflush(stderr);
|
|
my_print_stacktrace(NULL, my_thread_stack_size);
|
|
}
|
|
|
|
#else
|
|
static void dump_backtrace() { fputs("Backtrace not available.\n", stderr); }
|
|
|
|
#endif
|
|
|
|
static void 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
|
|
// Shouldn't get here but just in case
|
|
exit(1);
|
|
#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;
|
|
|
|
mysqltest_thread = OpenThread(THREAD_ALL_ACCESS, FALSE, GetCurrentThreadId());
|
|
if (mysqltest_thread == NULL)
|
|
die("OpenThread failed, err = %d.", GetLastError());
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/// Function to handle the stacktrace request event.
|
|
///
|
|
/// - Suspend the thread running the test
|
|
/// - Fetch CONTEXT record from the thread handle
|
|
/// - Initialize EXCEPTION_RECORD structure
|
|
/// - Use EXCEPTION_POINTERS and EXCEPTION_RECORD to set EXCEPTION_POINTERS
|
|
/// structure
|
|
/// - Call exception_filter() method to generate to stack trace
|
|
/// - Resume the suspended test thread
|
|
static void handle_wait_stacktrace_request_event() {
|
|
fprintf(stderr, "Test case timeout failure.\n");
|
|
|
|
// Suspend the thread running the test
|
|
if (SuspendThread(mysqltest_thread) == -1) {
|
|
DWORD error = GetLastError();
|
|
CloseHandle(mysqltest_thread);
|
|
die("Error suspending thread, err = %d.\n", error);
|
|
}
|
|
|
|
// Fetch the thread context
|
|
CONTEXT test_thread_ctx = {0};
|
|
test_thread_ctx.ContextFlags = CONTEXT_FULL;
|
|
|
|
if (GetThreadContext(mysqltest_thread, &test_thread_ctx) == FALSE) {
|
|
DWORD error = GetLastError();
|
|
CloseHandle(mysqltest_thread);
|
|
die("Error while fetching thread conext information, err = %d.\n", error);
|
|
}
|
|
|
|
EXCEPTION_POINTERS exp = {0};
|
|
exp.ContextRecord = &test_thread_ctx;
|
|
|
|
// Set up an Exception record with EXCEPTION_BREAKPOINT code
|
|
EXCEPTION_RECORD exc_rec = {0};
|
|
exc_rec.ExceptionCode = EXCEPTION_BREAKPOINT;
|
|
exp.ExceptionRecord = &exc_rec;
|
|
|
|
exception_filter(&exp);
|
|
|
|
// Resume the suspended test thread
|
|
if (ResumeThread(mysqltest_thread) == -1) {
|
|
DWORD error = GetLastError();
|
|
CloseHandle(mysqltest_thread);
|
|
die("Error resuming thread, err = %d.\n", error);
|
|
}
|
|
|
|
my_set_exception_pointers(nullptr);
|
|
}
|
|
|
|
/// Thread waiting for timeout event to occur. If the event occurs,
|
|
/// this method will trigger signal_handler() function.
|
|
static void wait_stacktrace_request_event() {
|
|
DWORD wait_res = WaitForSingleObject(stacktrace_request_event, INFINITE);
|
|
switch (wait_res) {
|
|
case WAIT_OBJECT_0:
|
|
handle_wait_stacktrace_request_event();
|
|
break;
|
|
default:
|
|
die("Unexpected result %d from WaitForSingleObject.", wait_res);
|
|
break;
|
|
}
|
|
CloseHandle(stacktrace_request_event);
|
|
}
|
|
|
|
/// Create an event name from the safeprocess PID value of the form
|
|
/// mysqltest[%d]stacktrace and spawn thread waiting for that event
|
|
/// to occur.
|
|
///
|
|
/// When this event occurs, signal_handler() method is called and
|
|
/// stacktrace for the mysqltest client process is printed in the
|
|
/// log file.
|
|
static void create_stacktrace_request_event() {
|
|
char event_name[64];
|
|
std::sprintf(event_name, "mysqltest[%d]stacktrace", opt_safe_process_pid);
|
|
|
|
// Create an event for the signal handler
|
|
if ((stacktrace_request_event = CreateEvent(NULL, TRUE, FALSE, event_name)) ==
|
|
NULL)
|
|
die("Failed to create timeout_event.");
|
|
|
|
wait_for_stacktrace_request_event_thread =
|
|
std::thread(wait_stacktrace_request_event);
|
|
}
|
|
|
|
#else /* _WIN32 */
|
|
|
|
static void init_signal_handling(void) {
|
|
struct sigaction sa;
|
|
DBUG_TRACE;
|
|
|
|
#ifdef HAVE_STACKTRACE
|
|
my_init_stacktrace();
|
|
#endif
|
|
|
|
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);
|
|
}
|
|
|
|
#endif /* !_WIN32 */
|
|
|
|
int main(int argc, char **argv) {
|
|
struct st_command *command;
|
|
bool abort_flag = 0;
|
|
int q_send_flag = 0;
|
|
uint command_executed = 0, last_command_executed = 0;
|
|
char output_file[FN_REFLEN];
|
|
MY_INIT(argv[0]);
|
|
|
|
output_file[0] = 0;
|
|
TMPDIR[0] = 0;
|
|
|
|
init_signal_handling();
|
|
|
|
/* 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;
|
|
|
|
q_lines = new Q_lines(PSI_NOT_INSTRUMENTED);
|
|
|
|
var_hash =
|
|
new collation_unordered_map<std::string, std::unique_ptr<VAR, var_free>>(
|
|
charset_info, PSI_NOT_INSTRUMENTED);
|
|
|
|
{
|
|
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
|
|
|
|
init_dynamic_string(&ds_res, "", 2048, 2048);
|
|
init_dynamic_string(&ds_result, "", 1024, 1024);
|
|
|
|
parse_args(argc, argv);
|
|
|
|
#ifdef _WIN32
|
|
// Create an event to request stack trace when timeout occurs
|
|
if (opt_safe_process_pid) create_stacktrace_request_event();
|
|
#endif
|
|
|
|
/* 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_ZEROFILL));
|
|
connections_end = connections + opt_max_connections + 1;
|
|
next_con = connections + 1;
|
|
|
|
var_set_int("$PS_PROTOCOL", ps_protocol);
|
|
var_set_int("$SP_PROTOCOL", sp_protocol);
|
|
var_set_int("$VIEW_PROTOCOL", view_protocol);
|
|
var_set_int("$OPT_TRACE_PROTOCOL", opt_trace_protocol);
|
|
var_set_int("$EXPLAIN_PROTOCOL", explain_protocol);
|
|
var_set_int("$JSON_EXPLAIN_PROTOCOL", json_explain_protocol);
|
|
var_set_int("$CURSOR_PROTOCOL", cursor_protocol);
|
|
|
|
var_set_int("$ENABLE_QUERY_LOG", 1);
|
|
var_set_int("$ENABLE_ABORT_ON_ERROR", 1);
|
|
var_set_int("$ENABLE_RESULT_LOG", 1);
|
|
var_set_int("$ENABLE_CONNECT_LOG", 0);
|
|
var_set_int("$ENABLE_WARNINGS", 1);
|
|
var_set_int("$ENABLE_INFO", 0);
|
|
var_set_int("$ENABLE_METADATA", 0);
|
|
var_set_int("$ENABLE_ASYNC_CLIENT", 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(0, nullptr, nullptr))
|
|
die("Can't initialize MySQL 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;
|
|
}
|
|
|
|
if (log_file.open(opt_logdir, result_file_name, ".log")) cleanup_and_exit(1);
|
|
|
|
verbose_msg("Logging to '%s'.", log_file.file_name());
|
|
enable_async_client = use_async_client;
|
|
|
|
// Creating a log file using current file name if result file doesn't exist.
|
|
if (result_file_name) {
|
|
if (log_file.open(opt_logdir, result_file_name, ".log"))
|
|
cleanup_and_exit(1);
|
|
} else {
|
|
if (std::strcmp(cur_file->file_name, "<stdin>")) {
|
|
if (log_file.open(opt_logdir, cur_file->file_name, ".log"))
|
|
cleanup_and_exit(1);
|
|
} else {
|
|
if (log_file.open(opt_logdir, "stdin", ".log")) cleanup_and_exit(1);
|
|
}
|
|
}
|
|
|
|
// File to store the progress
|
|
Logfile progress_file;
|
|
|
|
if (opt_mark_progress) {
|
|
if (result_file_name) {
|
|
if (progress_file.open(opt_logdir, result_file_name, ".progress"))
|
|
cleanup_and_exit(1);
|
|
} else {
|
|
if (std::strcmp(cur_file->file_name, "<stdin>")) {
|
|
if (progress_file.open(opt_logdir, cur_file->file_name, ".progress"))
|
|
cleanup_and_exit(1);
|
|
} else {
|
|
if (progress_file.open(opt_logdir, "stdin", ".progress"))
|
|
cleanup_and_exit(1);
|
|
}
|
|
}
|
|
verbose_msg("Tracing progress in '%s'.", progress_file.file_name());
|
|
}
|
|
|
|
var_set_string("MYSQLTEST_FILE", cur_file->file_name);
|
|
|
|
/* Cursor protcol implies ps protocol */
|
|
if (cursor_protocol) ps_protocol = 1;
|
|
|
|
ps_protocol_enabled = ps_protocol;
|
|
sp_protocol_enabled = sp_protocol;
|
|
view_protocol_enabled = view_protocol;
|
|
opt_trace_protocol_enabled = opt_trace_protocol;
|
|
explain_protocol_enabled = explain_protocol;
|
|
json_explain_protocol_enabled = json_explain_protocol;
|
|
cursor_protocol_enabled = cursor_protocol;
|
|
|
|
st_connection *con = connections;
|
|
if (!(mysql_init(&con->mysql))) 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_OPT_LOCAL_INFILE, 0);
|
|
if (std::strcmp(default_charset, charset_info->csname) &&
|
|
!(charset_info =
|
|
get_charset_by_csname(default_charset, MY_CS_PRIMARY, MYF(MY_WME))))
|
|
die("Invalid character set specified.");
|
|
mysql_options(&con->mysql, MYSQL_SET_CHARSET_NAME, charset_info->csname);
|
|
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 defined(HAVE_OPENSSL)
|
|
/* Turn on VERIFY_IDENTITY mode only if host=="localhost". */
|
|
if (opt_ssl_mode == SSL_MODE_VERIFY_IDENTITY) {
|
|
if (!opt_host || std::strcmp(opt_host, "localhost"))
|
|
opt_ssl_mode = SSL_MODE_VERIFY_CA;
|
|
}
|
|
#endif
|
|
if (opt_compress) {
|
|
enable_async_client = false;
|
|
}
|
|
if (SSL_SET_OPTIONS(&con->mysql)) die("%s", SSL_SET_OPTIONS_ERROR);
|
|
#if defined(_WIN32)
|
|
if (shared_memory_base_name)
|
|
mysql_options(&con->mysql, MYSQL_SHARED_MEMORY_BASE_NAME,
|
|
shared_memory_base_name);
|
|
|
|
if (opt_ssl_mode == SSL_MODE_DISABLED)
|
|
mysql_options(&con->mysql, MYSQL_OPT_PROTOCOL,
|
|
(char *)&opt_protocol_for_default_connection);
|
|
#endif
|
|
|
|
if (!(con->name = my_strdup(PSI_NOT_INSTRUMENTED, "default", MYF(MY_WME))))
|
|
die("Out of memory");
|
|
|
|
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_include) {
|
|
open_file(opt_include);
|
|
}
|
|
|
|
if (opt_offload_count_file) {
|
|
secondary_engine = new Secondary_engine();
|
|
// Save the initial value of secondary engine execution status.
|
|
if (secondary_engine->offload_count(&cur_con->mysql, "before"))
|
|
cleanup_and_exit(1);
|
|
}
|
|
|
|
verbose_msg("Start processing test commands from '%s' ...",
|
|
cur_file->file_name);
|
|
while (!read_command(&command) && !abort_flag) {
|
|
int current_line_inc = 1, processed = 0;
|
|
if (command->type == Q_UNKNOWN || command->type == Q_COMMENT_WITH_COMMAND)
|
|
get_command_type(command);
|
|
|
|
if (command->type == Q_ERROR && expected_errors->count())
|
|
// Delete all the error codes from previous 'error' command.
|
|
expected_errors->clear_list();
|
|
|
|
if (testcase_disabled && command->type != Q_ENABLE_TESTCASE &&
|
|
command->type != Q_DISABLE_TESTCASE) {
|
|
// Test case 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 = (expected_errors->count() == 0 && abort_on_error);
|
|
|
|
/* delimiter needs to be executed so we can continue to parse */
|
|
bool ok_to_do = cur_block->ok || command->type == Q_DELIMITER;
|
|
|
|
/*
|
|
'source' command needs to be "done" the first time if it 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 && command->type == Q_SOURCE) {
|
|
for (struct st_block *stb = cur_block - 1; stb >= block_stack; stb--) {
|
|
if (stb->cmd == cmd_while) {
|
|
ok_to_do = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Some commands need to be parsed in false context also to
|
|
avoid any parsing errors.
|
|
*/
|
|
if (!ok_to_do &&
|
|
(command->type == Q_APPEND_FILE || command->type == Q_PERL ||
|
|
command->type == Q_WRITE_FILE)) {
|
|
ok_to_do = 1;
|
|
}
|
|
|
|
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_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:
|
|
do_enable_warnings(command);
|
|
break;
|
|
case Q_DISABLE_WARNINGS:
|
|
do_disable_warnings(command);
|
|
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_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_REMOVE_FILE:
|
|
do_remove_file(command);
|
|
break;
|
|
case Q_REMOVE_FILES_WILDCARD:
|
|
do_remove_files_wildcard(command);
|
|
break;
|
|
case Q_COPY_FILES_WILDCARD:
|
|
do_copy_files_wildcard(command);
|
|
break;
|
|
case Q_MKDIR:
|
|
do_mkdir(command);
|
|
break;
|
|
case Q_RMDIR:
|
|
do_rmdir(command, 0);
|
|
break;
|
|
case Q_FORCE_RMDIR:
|
|
do_rmdir(command, 1);
|
|
break;
|
|
case Q_FORCE_CPDIR:
|
|
do_force_cpdir(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;
|
|
start_sort_column = 0;
|
|
break;
|
|
case Q_PARTIALLY_SORTED_RESULT:
|
|
/*
|
|
Turn on sorting of result set, will be reset after next
|
|
command
|
|
*/
|
|
display_result_sorted = true;
|
|
start_sort_column = atoi(command->first_argument);
|
|
command->last_argument = command->end;
|
|
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_EXPR:
|
|
do_expr(command);
|
|
break;
|
|
case Q_EVAL:
|
|
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: {
|
|
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' or 'send_eval'
|
|
flags = QUERY_SEND_FLAG;
|
|
if (q_send_flag == 2)
|
|
// Last command was an empty 'send_eval' command. Set the command
|
|
// type to Q_SEND_EVAL so that the variable gets replaced with its
|
|
// value before executing.
|
|
command->type = Q_SEND_EVAL;
|
|
q_send_flag = 0;
|
|
} else if (command->type == Q_REAP) {
|
|
flags = QUERY_REAP_FLAG;
|
|
}
|
|
|
|
/* Check for special property for this query */
|
|
display_result_vertically |= (command->type == Q_QUERY_VERTICAL);
|
|
|
|
/*
|
|
We run EXPLAIN _before_ the query. If query is UPDATE/DELETE is
|
|
matters: a DELETE may delete rows, and then EXPLAIN DELETE will
|
|
usually terminate quickly with "no matching rows". To make it more
|
|
interesting, EXPLAIN is now first.
|
|
*/
|
|
if (explain_protocol_enabled) run_explain(cur_con, command, flags, 0);
|
|
if (json_explain_protocol_enabled)
|
|
run_explain(cur_con, command, flags, 1);
|
|
|
|
if (*output_file) {
|
|
strmake(command->output_file, output_file, sizeof(output_file) - 1);
|
|
*output_file = 0;
|
|
}
|
|
run_query(cur_con, command, flags);
|
|
display_opt_trace(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' or 'send_eval' command without arguments, it
|
|
// indicates that _next_ query should be send only.
|
|
if (command->type == Q_SEND)
|
|
q_send_flag = 1;
|
|
else if (command->type == Q_SEND_EVAL)
|
|
q_send_flag = 2;
|
|
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_ERROR:
|
|
do_error(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_REPLACE_NUMERIC_ROUND:
|
|
do_get_replace_numeric_round(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));
|
|
break;
|
|
case Q_RESET_CONNECTION:
|
|
do_reset_connection();
|
|
break;
|
|
case Q_SEND_SHUTDOWN:
|
|
if (opt_offload_count_file) {
|
|
// Save the value of secondary engine execution status
|
|
// before shutting down the server.
|
|
if (secondary_engine->offload_count(&cur_con->mysql, "after"))
|
|
cleanup_and_exit(1);
|
|
}
|
|
|
|
handle_command_error(
|
|
command, mysql_query_wrapper(&cur_con->mysql, "shutdown"));
|
|
break;
|
|
case Q_SHUTDOWN_SERVER:
|
|
do_shutdown_server(command);
|
|
break;
|
|
case Q_EXEC:
|
|
case Q_EXECW:
|
|
do_exec(command, false);
|
|
command_executed++;
|
|
break;
|
|
case Q_EXEC_BACKGROUND:
|
|
do_exec(command, true);
|
|
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_RECONNECT:
|
|
set_reconnect(&cur_con->mysql, 0);
|
|
break;
|
|
case Q_ENABLE_RECONNECT:
|
|
set_reconnect(&cur_con->mysql, 1);
|
|
enable_async_client = false;
|
|
/* Close any open statements - no reconnect, need new prepare */
|
|
close_statements();
|
|
break;
|
|
case Q_ENABLE_ASYNC_CLIENT:
|
|
set_property(command, P_ASYNC, 1);
|
|
break;
|
|
case Q_DISABLE_ASYNC_CLIENT:
|
|
set_property(command, P_ASYNC, 0);
|
|
break;
|
|
case Q_DISABLE_TESTCASE:
|
|
if (testcase_disabled == 0)
|
|
do_disable_testcase(command);
|
|
else
|
|
die("Test case is already disabled.");
|
|
break;
|
|
case Q_ENABLE_TESTCASE:
|
|
// Ensure we don't get testcase_disabled < 0 as this would
|
|
// accidentally disable code we don't want to have disabled.
|
|
if (testcase_disabled == 1)
|
|
testcase_disabled = 0;
|
|
else
|
|
die("Test case is already enabled.");
|
|
break;
|
|
case Q_DIE:
|
|
/* Abort test with error code and error message */
|
|
die("%s", command->first_argument);
|
|
break;
|
|
case Q_EXIT:
|
|
/* Stop processing any more commands */
|
|
abort_flag = 1;
|
|
break;
|
|
case Q_SKIP: {
|
|
DYNAMIC_STRING ds_skip_msg;
|
|
init_dynamic_string(&ds_skip_msg, 0, command->query_len, 256);
|
|
|
|
// Evaluate the skip message
|
|
do_eval(&ds_skip_msg, command->first_argument, command->end, false);
|
|
|
|
char skip_msg[FN_REFLEN];
|
|
strmake(skip_msg, ds_skip_msg.str, FN_REFLEN - 1);
|
|
dynstr_free(&ds_skip_msg);
|
|
|
|
if (!no_skip) {
|
|
// --no-skip option is disabled, skip the test case
|
|
abort_not_supported_test("%s", skip_msg);
|
|
} else {
|
|
const char *path = cur_file->file_name;
|
|
const char *fn = get_filename_from_path(path);
|
|
|
|
// Check if the file is in excluded list
|
|
if (excluded_string && strstr(excluded_string, fn)) {
|
|
// File is present in excluded list, skip the test case
|
|
abort_not_supported_test("%s", skip_msg);
|
|
} else {
|
|
// File is not present in excluded list, ignore the skip
|
|
// and continue running the test case
|
|
command->last_argument = command->end;
|
|
}
|
|
}
|
|
} break;
|
|
case Q_OUTPUT: {
|
|
static DYNAMIC_STRING ds_to_file;
|
|
const struct command_arg output_file_args[] = {
|
|
{"to_file", ARG_STRING, true, &ds_to_file, "Output filename"}};
|
|
check_command_args(command, command->first_argument, output_file_args,
|
|
1, ' ');
|
|
strmake(output_file, ds_to_file.str, FN_REFLEN);
|
|
dynstr_free(&ds_to_file);
|
|
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 &&
|
|
command->type != Q_IF && command->type != Q_END_BLOCK) {
|
|
// As soon as any non "error" command or comment has been executed,
|
|
// the array with expected errors should be cleared
|
|
expected_errors->clear_list();
|
|
}
|
|
|
|
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(progress_file, parser.current_line);
|
|
|
|
// Write result from command to log file immediately.
|
|
if (log_file.write(ds_res.str, ds_res.length) || log_file.flush())
|
|
cleanup_and_exit(1);
|
|
|
|
if (!result_file_name) {
|
|
std::fwrite(ds_res.str, 1, ds_res.length, stdout);
|
|
std::fflush(stdout);
|
|
}
|
|
|
|
dynstr_set(&ds_res, 0);
|
|
}
|
|
|
|
log_file.close();
|
|
|
|
if (opt_mark_progress) progress_file.close();
|
|
|
|
start_lineno = 0;
|
|
verbose_msg("... Done processing test commands.");
|
|
|
|
if (testcase_disabled) die("Test ended with test case execution disabled.");
|
|
|
|
if (disable_warnings || disabled_warnings->count())
|
|
die("The test didn't enable all the disabled warnings, enable "
|
|
"all of them before end of the test.");
|
|
|
|
bool empty_result = false;
|
|
|
|
/*
|
|
The whole test has been executed _sucessfully_.
|
|
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 suceeded! 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 = longlong2str(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_micro_time() / 1000; }
|
|
|
|
/*
|
|
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) {
|
|
const char *from = command->first_argument;
|
|
char *buff, *start;
|
|
DBUG_TRACE;
|
|
|
|
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, std::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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/*
|
|
Functions to round numeric results.
|
|
|
|
SYNOPSIS
|
|
do_get_replace_numeric_round()
|
|
command - command handle
|
|
|
|
DESCRIPTION
|
|
replace_numeric_round <precision>
|
|
|
|
where precision is the number of digits after the decimal point
|
|
that the result will be rounded off to. The precision can only
|
|
be a number between 0 and 16.
|
|
eg. replace_numeric_round 10;
|
|
Numbers which are > 1e10 or < -1e10 are represented using the
|
|
exponential notation after they are rounded off.
|
|
Trailing zeroes after the decimal point are removed from the
|
|
numbers.
|
|
If the precision is 0, then the value is rounded off to the
|
|
nearest whole number.
|
|
*/
|
|
void do_get_replace_numeric_round(struct st_command *command) {
|
|
DYNAMIC_STRING ds_round;
|
|
const struct command_arg numeric_arg = {
|
|
"precision", ARG_STRING, true, &ds_round, "Number of decimal precision"};
|
|
DBUG_TRACE;
|
|
|
|
check_command_args(command, command->first_argument, &numeric_arg,
|
|
sizeof(numeric_arg) / sizeof(struct command_arg), ' ');
|
|
|
|
// Parse the argument string to get the precision
|
|
long int v = 0;
|
|
if (str2int(ds_round.str, 10, 0, REPLACE_ROUND_MAX, &v) == NullS)
|
|
die("A number between 0 and %d is required for the precision "
|
|
"in replace_numeric_round",
|
|
REPLACE_ROUND_MAX);
|
|
|
|
glob_replace_numeric_round = (int)v;
|
|
dynstr_free(&ds_round);
|
|
}
|
|
|
|
void free_replace_numeric_round() { glob_replace_numeric_round = -1; }
|
|
|
|
/*
|
|
Round the digits after the decimal point to the specified precision
|
|
by iterating through the result set element, identifying the part to
|
|
be rounded off, and rounding that part off.
|
|
*/
|
|
void replace_numeric_round_append(int round, DYNAMIC_STRING *result,
|
|
const char *from, size_t len) {
|
|
while (len > 0) {
|
|
// Move pointer to the start of the numeric values
|
|
size_t size = strcspn(from, "0123456789");
|
|
if (size > 0) {
|
|
dynstr_append_mem(result, from, size);
|
|
from += size;
|
|
len -= size;
|
|
}
|
|
|
|
/*
|
|
Move the pointer to the end of the numeric values and the
|
|
the start of the non-numeric values such as "." and "e"
|
|
*/
|
|
size = strspn(from, "0123456789");
|
|
int r = round;
|
|
|
|
/*
|
|
If result from one of the rows of the result set is null,
|
|
break the loop
|
|
*/
|
|
if (*(from + size) == 0) {
|
|
dynstr_append_mem(result, from, size);
|
|
break;
|
|
}
|
|
|
|
switch (*(from + size)) {
|
|
// double/float
|
|
case '.':
|
|
size_t size1;
|
|
size1 = strspn(from + size + 1, "0123456789");
|
|
|
|
/*
|
|
Restrict rounding to less than the
|
|
the existing precision to avoid 1.2 being replaced
|
|
to 1.2000000
|
|
*/
|
|
if (size1 < (size_t)r) r = size1;
|
|
// fallthrough: all cases till next break are executed
|
|
case 'e':
|
|
case 'E':
|
|
if (isdigit(*(from + size + 1))) {
|
|
char *end;
|
|
double val = strtod(from, &end);
|
|
if (end != NULL) {
|
|
const char *format = (val < 1e10 && val > -1e10) ? "%.*f" : "%.*e";
|
|
char buf[40];
|
|
|
|
size = snprintf(buf, sizeof(buf), format, r, val);
|
|
if (val < 1e10 && val > -1e10 && r > 0) {
|
|
/*
|
|
2.0000000 need to be represented as 2 for consistency
|
|
2.0010000 also becomes 2.001
|
|
*/
|
|
while (buf[size - 1] == '0') size--;
|
|
|
|
// don't leave 100. trailing
|
|
if (buf[size - 1] == '.') size--;
|
|
}
|
|
dynstr_append_mem(result, buf, size);
|
|
len -= (end - from);
|
|
from = end;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
This is because strtod didn't convert or there wasn't digits after
|
|
[.eE] so output without changing
|
|
*/
|
|
dynstr_append_mem(result, from, size);
|
|
from += size;
|
|
len -= size;
|
|
break;
|
|
// int
|
|
default:
|
|
dynstr_append_mem(result, from, size);
|
|
from += size;
|
|
len -= size;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/****************************************************************************/
|
|
/*
|
|
Replace functions
|
|
*/
|
|
|
|
/* Definitions for replace result */
|
|
|
|
struct POINTER_ARRAY { /* when using array-strings */
|
|
TYPELIB typelib; /* Pointer to strings */
|
|
uchar *str{nullptr}; /* Strings is here */
|
|
uint8 *flag{nullptr}; /* Flag about each var. */
|
|
uint array_allocs{0}, max_count{0}, length{0}, max_length{0};
|
|
};
|
|
|
|
REPLACE *init_replace(const char **from, const char **to, uint count,
|
|
const 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;
|
|
const char *from = command->first_argument;
|
|
char *buff, *start;
|
|
char word_end_chars[256], *pos;
|
|
POINTER_ARRAY to_array, from_array;
|
|
DBUG_TRACE;
|
|
|
|
free_replace();
|
|
|
|
if (!*from) die("Missing argument in %s", command->query);
|
|
start = buff = (char *)my_malloc(PSI_NOT_INSTRUMENTED, std::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);
|
|
#ifdef _WIN32
|
|
fix_win_paths(to, from - to);
|
|
#endif
|
|
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(
|
|
from_array.typelib.type_names, 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;
|
|
}
|
|
|
|
void free_replace() {
|
|
DBUG_TRACE;
|
|
my_free(glob_replace);
|
|
glob_replace = NULL;
|
|
}
|
|
|
|
struct REPLACE {
|
|
int found;
|
|
REPLACE *next[256];
|
|
};
|
|
|
|
struct REPLACE_STRING {
|
|
int found;
|
|
const char *replace_string;
|
|
uint to_offset;
|
|
int from_offset;
|
|
};
|
|
|
|
void replace_strings_append(REPLACE *rep, DYNAMIC_STRING *ds, const char *str,
|
|
size_t len MY_ATTRIBUTE((unused))) {
|
|
REPLACE *rep_pos;
|
|
REPLACE_STRING *rep_str;
|
|
const char *start, *from;
|
|
DBUG_TRACE;
|
|
|
|
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));
|
|
return;
|
|
}
|
|
|
|
/* Found a string that needs to be replaced */
|
|
DBUG_PRINT("info", ("found: %d, to_offset: %d, 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,
|
|
std::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"));
|
|
return;
|
|
}
|
|
DBUG_ASSERT(from <= str + len);
|
|
start = from;
|
|
rep_pos = rep;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Regex replace functions
|
|
*/
|
|
|
|
/*
|
|
Finds the next (non-escaped) '/' in the expression.
|
|
(If the character '/' is needed, it can be escaped using '\'.)
|
|
*/
|
|
|
|
#define PARSE_REGEX_ARG \
|
|
while (p < expr_end) { \
|
|
char c = *p; \
|
|
if (c == '/') { \
|
|
if (last_c == '\\') { \
|
|
buf_p[-1] = '/'; \
|
|
} else { \
|
|
*buf_p++ = 0; \
|
|
break; \
|
|
} \
|
|
} else \
|
|
*buf_p++ = c; \
|
|
\
|
|
last_c = c; \
|
|
p++; \
|
|
}
|
|
|
|
/**
|
|
Initializes the regular substitution expression to be used in the
|
|
result output of test.
|
|
|
|
@param expr Pointer to string having regular expression to be used
|
|
for substitution.
|
|
@retval st_replace_regex structure with pairs of substitutions.
|
|
*/
|
|
static struct st_replace_regex *init_replace_regex(const char *expr) {
|
|
struct st_replace_regex *res;
|
|
char *buf;
|
|
const char *expr_end;
|
|
const char *p;
|
|
char *buf_p;
|
|
size_t expr_len = std::strlen(expr);
|
|
char last_c = 0;
|
|
struct st_regex reg;
|
|
|
|
/* my_malloc() will die on fail with MY_FAE */
|
|
void *rawmem = my_malloc(PSI_NOT_INSTRUMENTED, sizeof(*res) + expr_len,
|
|
MYF(MY_FAE + MY_WME));
|
|
res = new (rawmem) st_replace_regex;
|
|
|
|
buf = (char *)res + sizeof(*res);
|
|
expr_end = expr + expr_len;
|
|
p = expr;
|
|
buf_p = buf;
|
|
|
|
/* for each regexp substitution statement */
|
|
while (p < expr_end) {
|
|
memset(®, 0, sizeof(reg));
|
|
/* find the start of the statement */
|
|
while (p < expr_end) {
|
|
if (*p == '/') break;
|
|
p++;
|
|
}
|
|
|
|
if (p == expr_end || ++p == expr_end) {
|
|
if (res->regex_arr.size())
|
|
break;
|
|
else
|
|
goto err;
|
|
}
|
|
/* we found the start */
|
|
reg.pattern = buf_p;
|
|
|
|
/* Find first argument -- pattern string to be removed */
|
|
PARSE_REGEX_ARG
|
|
|
|
if (p == expr_end || ++p == expr_end) goto err;
|
|
|
|
/* buf_p now points to the replacement pattern terminated with \0 */
|
|
reg.replace = buf_p;
|
|
|
|
/* Find second argument -- replace string to replace pattern */
|
|
PARSE_REGEX_ARG
|
|
|
|
if (p == expr_end) goto err;
|
|
|
|
/* skip the ending '/' in the statement */
|
|
p++;
|
|
|
|
/* Check if we should do matching case insensitive */
|
|
if (p < expr_end && *p == 'i') reg.icase = 1;
|
|
|
|
/* done parsing the statement, now place it in regex_arr */
|
|
if (res->regex_arr.push_back(reg)) die("Out of memory");
|
|
}
|
|
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;
|
|
|
|
err:
|
|
my_free(res);
|
|
die("Error parsing replace_regex \"%s\"", expr);
|
|
return 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) {
|
|
const char *expr = command->first_argument;
|
|
free_replace_regex();
|
|
/* Allow variable for the *entire* list of replacements */
|
|
if (*expr == '$') {
|
|
VAR *val = var_get(expr, NULL, 0, 1);
|
|
expr = val ? val->str_val : NULL;
|
|
}
|
|
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) {
|
|
my_free(glob_replace_regex->even_buf);
|
|
my_free(glob_replace_regex->odd_buf);
|
|
glob_replace_regex->~st_replace_regex();
|
|
my_free(glob_replace_regex);
|
|
glob_replace_regex = NULL;
|
|
}
|
|
}
|
|
|
|
#ifndef WORD_BIT
|
|
#define WORD_BIT (8 * sizeof(uint))
|
|
#endif
|
|
|
|
#define SET_MALLOC_HUNC 64
|
|
#define LAST_CHAR_CODE 259
|
|
|
|
struct 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 convinience */
|
|
};
|
|
|
|
struct 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;
|
|
};
|
|
|
|
struct FOUND_SET {
|
|
uint table_offset;
|
|
int found_offset;
|
|
};
|
|
|
|
struct FOLLOWS {
|
|
int chr;
|
|
uint table_offset;
|
|
uint len;
|
|
};
|
|
|
|
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(const char *pos);
|
|
uint end_of_word(const char *pos);
|
|
|
|
static uint found_sets = 0;
|
|
|
|
static uint replace_len(const char *str) {
|
|
uint len = 0;
|
|
while (*str) {
|
|
str++;
|
|
len++;
|
|
}
|
|
return len;
|
|
}
|
|
|
|
/* Init a replace structure for further calls */
|
|
|
|
REPLACE *init_replace(const char **from, const char **to, uint count,
|
|
const 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];
|
|
const char *pos, **to_array;
|
|
char *to_pos;
|
|
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_TRACE;
|
|
|
|
/* 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;
|
|
return 0;
|
|
}
|
|
states += len + 1;
|
|
result_len += (uint)std::strlen(to[i]) + 1;
|
|
if (len > max_length) max_length = len;
|
|
}
|
|
memset(is_word_end, 0, 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)) return 0;
|
|
found_sets = 0;
|
|
if (!(found_set = (FOUND_SET *)my_malloc(
|
|
PSI_NOT_INSTRUMENTED, sizeof(FOUND_SET) * max_length * count,
|
|
MYF(MY_WME)))) {
|
|
free_sets(&sets);
|
|
return 0;
|
|
}
|
|
(void)make_new_set(&sets); /* Set starting set */
|
|
make_sets_invisible(&sets); /* Hide previus 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)))) {
|
|
free_sets(&sets);
|
|
my_free(found_set);
|
|
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 */
|
|
memset(used_chars, 0, 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);
|
|
assert(new_set);
|
|
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_ZEROFILL)))) {
|
|
rep_str = (REPLACE_STRING *)(replace + sets.count);
|
|
to_array = pointer_cast<const 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 = my_stpcpy(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 = !memcmp(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));
|
|
return replace;
|
|
}
|
|
|
|
int init_sets(REP_SETS *sets, uint states) {
|
|
memset(sets, 0, 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))))
|
|
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_free(sets->set);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Make help sets invisible for nicer codeing */
|
|
|
|
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++;
|
|
memset(set->bits, 0, sizeof(uint) * sets->size_of_bits);
|
|
memset(&set->next[0], 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,
|
|
(uchar *)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, (uchar *)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((uchar *)to->bits, (uchar *)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] & ~((1 << ((lastpos + 1) % WORD_BIT)) - 1U);
|
|
|
|
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(const char *pos) {
|
|
return (((!memcmp(pos, "\\b", 2) && pos[2]) || !memcmp(pos, "\\^", 2)) ? 1
|
|
: 0);
|
|
}
|
|
|
|
uint end_of_word(const char *pos) {
|
|
const 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_TRACE;
|
|
|
|
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))))
|
|
return -1;
|
|
if (!(pa->str = (uchar *)my_malloc(PSI_NOT_INSTRUMENTED,
|
|
(uint)(PS_MALLOC - MALLOC_OVERHEAD),
|
|
MYF(MY_WME)))) {
|
|
my_free(pa->typelib.type_names);
|
|
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)std::strlen(name) + 1;
|
|
if (pa->length + length >= pa->max_length) {
|
|
if (!(new_pos = (uchar *)my_realloc(PSI_NOT_INSTRUMENTED, (uchar *)pa->str,
|
|
(uint)(pa->length + length + PS_MALLOC),
|
|
MYF(MY_WME))))
|
|
return 1;
|
|
if (new_pos != pa->str) {
|
|
ptrdiff_t diff = new_pos - pa->str;
|
|
for (i = 0; i < pa->typelib.count; i++)
|
|
pa->typelib.type_names[i] = pa->typelib.type_names[i] + diff;
|
|
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, (uchar *)pa->typelib.type_names,
|
|
(uint)len / (sizeof(uchar *) + sizeof(*pa->flag)) *
|
|
(sizeof(uchar *) + sizeof(*pa->flag)),
|
|
MYF(MY_WME))))
|
|
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((uchar *)pa->flag, (char *)(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)my_stpcpy((char *)pa->str + pa->length, name);
|
|
pa->length += length;
|
|
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) {
|
|
#ifdef _WIN32
|
|
fix_win_paths(val, len);
|
|
#endif
|
|
|
|
if (display_result_lower) {
|
|
/* Convert to lower case, and do this first */
|
|
my_casedn_str(charset_info, const_cast<char *>(val));
|
|
}
|
|
|
|
if (glob_replace_regex) {
|
|
size_t orig_len = len;
|
|
// Regex replace
|
|
if (!multi_reg_replace(glob_replace_regex, const_cast<char *>(val), &len)) {
|
|
val = glob_replace_regex->buf;
|
|
} else {
|
|
len = orig_len;
|
|
}
|
|
}
|
|
|
|
DYNAMIC_STRING ds_temp;
|
|
init_dynamic_string(&ds_temp, "", 512, 512);
|
|
|
|
/* Store result from replace_result in ds_temp */
|
|
if (glob_replace) {
|
|
/* Normal replace */
|
|
replace_strings_append(glob_replace, &ds_temp, val, len);
|
|
}
|
|
|
|
/*
|
|
Call the replace_numeric_round function with the specified
|
|
precision. It may be used along with replace_result, so use the
|
|
output from replace_result as the input for replace_numeric_round.
|
|
*/
|
|
if (glob_replace_numeric_round >= 0) {
|
|
/* Copy the result from replace_result if it was used, into buffer */
|
|
if (ds_temp.length > 0) {
|
|
char buffer[512];
|
|
strcpy(buffer, ds_temp.str);
|
|
dynstr_free(&ds_temp);
|
|
init_dynamic_string(&ds_temp, "", 512, 512);
|
|
replace_numeric_round_append(glob_replace_numeric_round, &ds_temp, buffer,
|
|
std::strlen(buffer));
|
|
} else
|
|
replace_numeric_round_append(glob_replace_numeric_round, &ds_temp, val,
|
|
len);
|
|
}
|
|
|
|
if (!glob_replace && glob_replace_numeric_round < 0)
|
|
dynstr_append_mem(ds, val, len);
|
|
else
|
|
dynstr_append_mem(ds, ds_temp.str, std::strlen(ds_temp.str));
|
|
dynstr_free(&ds_temp);
|
|
}
|
|
|
|
/* 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, std::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
|
|
start_sort_column - column to start sorting from (0 for sorting
|
|
the entire line); a stable sort will be used
|
|
*/
|
|
|
|
class Comp_lines {
|
|
public:
|
|
bool operator()(const char *a, const char *b) {
|
|
return std::strcmp(a, b) < 0;
|
|
}
|
|
};
|
|
|
|
static size_t length_of_n_first_columns(std::string str,
|
|
int start_sort_column) {
|
|
std::stringstream columns(str);
|
|
std::string temp;
|
|
size_t size_of_columns = 0;
|
|
|
|
int i = 0;
|
|
while (getline(columns, temp, '\t') && i < start_sort_column) {
|
|
size_of_columns = size_of_columns + temp.length();
|
|
i++;
|
|
}
|
|
|
|
return size_of_columns;
|
|
}
|
|
|
|
void dynstr_append_sorted(DYNAMIC_STRING *ds, DYNAMIC_STRING *ds_input,
|
|
int start_sort_column) {
|
|
char *start = ds_input->str;
|
|
char *end = ds_input->str + ds_input->length;
|
|
std::vector<std::string> sorted;
|
|
DBUG_TRACE;
|
|
|
|
if (!*start) return; /* No input */
|
|
|
|
/* 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);
|
|
|
|
/*
|
|
Traverse through the result set from start to end to avoid
|
|
ignoring null characters (0x00), and insert line by line
|
|
into array.
|
|
*/
|
|
size_t first_unsorted_row = 0;
|
|
while (start < end) {
|
|
char *line_end = (char *)start;
|
|
|
|
/* Find end of line */
|
|
while (*line_end != '\n') line_end++;
|
|
*line_end = 0;
|
|
|
|
std::string result_row = std::string(start, line_end - start);
|
|
if (!sorted.empty() && start_sort_column > 0) {
|
|
/*
|
|
If doing partial sorting, and the prefix is different from that of the
|
|
previous line, the group is done. Sort it and start another one.
|
|
*/
|
|
size_t prev_line_prefix_len =
|
|
length_of_n_first_columns(sorted.back(), start_sort_column);
|
|
if (sorted.back().compare(0, prev_line_prefix_len, result_row, 0,
|
|
prev_line_prefix_len) != 0) {
|
|
std::sort(sorted.begin() + first_unsorted_row, sorted.end());
|
|
first_unsorted_row = sorted.size();
|
|
}
|
|
}
|
|
|
|
/* Insert line into the array */
|
|
sorted.push_back(result_row);
|
|
start = line_end + 1;
|
|
}
|
|
|
|
/* Sort array */
|
|
std::stable_sort(sorted.begin() + first_unsorted_row, sorted.end());
|
|
|
|
/* Create new result */
|
|
for (auto i : sorted) {
|
|
dynstr_append_mem(ds, i.c_str(), i.length());
|
|
dynstr_append(ds, "\n");
|
|
}
|
|
}
|
|
|