diff options
Diffstat (limited to 'client/mysql_upgrade.c')
-rw-r--r-- | client/mysql_upgrade.c | 1385 |
1 files changed, 1385 insertions, 0 deletions
diff --git a/client/mysql_upgrade.c b/client/mysql_upgrade.c new file mode 100644 index 00000000..e49b0fd0 --- /dev/null +++ b/client/mysql_upgrade.c @@ -0,0 +1,1385 @@ +/* + Copyright (c) 2006, 2013, Oracle and/or its affiliates. + Copyright (c) 2010, 2017, MariaDB + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA +*/ + +#include "client_priv.h" +#include <sslopt-vars.h> +#include <../scripts/mysql_fix_privilege_tables_sql.c> + +#include <welcome_copyright_notice.h> /* ORACLE_WELCOME_COPYRIGHT_NOTICE */ + +#define VER "1.4" + +#ifdef HAVE_SYS_WAIT_H +#include <sys/wait.h> +#endif + +#ifndef WEXITSTATUS +# ifdef __WIN__ +# define WEXITSTATUS(stat_val) (stat_val) +# else +# define WEXITSTATUS(stat_val) ((unsigned)(stat_val) >> 8) +# endif +#endif + +static int phase = 0; +static const int phases_total = 7; +static char mysql_path[FN_REFLEN]; +static char mysqlcheck_path[FN_REFLEN]; + +static my_bool opt_force, opt_verbose, debug_info_flag, debug_check_flag, + opt_systables_only, opt_version_check; +static my_bool opt_not_used, opt_silent; +static uint my_end_arg= 0; +static char *opt_user= (char*)"root"; + +static my_bool upgrade_from_mysql; + +static DYNAMIC_STRING ds_args; +static DYNAMIC_STRING conn_args; +static DYNAMIC_STRING ds_plugin_data_types; + +static char *opt_password= 0; +static char *opt_plugin_dir= 0, *opt_default_auth= 0; + +static char *cnf_file_path= 0, defaults_file[FN_REFLEN + 32]; + +static my_bool tty_password= 0; + +static char opt_tmpdir[FN_REFLEN] = ""; + +#ifndef DBUG_OFF +static char *default_dbug_option= (char*) "d:t:O,/tmp/mariadb-upgrade.trace"; +#endif + +static char **defaults_argv; + +static my_bool not_used; /* Can't use GET_BOOL without a value pointer */ + +char upgrade_from_version[sizeof("10.20.456-MariaDB")+1]; + +static my_bool opt_write_binlog; + +#define OPT_SILENT OPT_MAX_CLIENT_OPTION + +static struct my_option my_long_options[]= +{ + {"help", '?', "Display this help message and exit.", 0, 0, 0, GET_NO_ARG, + NO_ARG, 0, 0, 0, 0, 0, 0}, + {"basedir", 'b', + "Not used by mysql_upgrade. Only for backward compatibility.", + 0, 0, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"character-sets-dir", OPT_CHARSETS_DIR, + "Not used by mysql_upgrade. Only for backward compatibility.", + 0, 0, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0 }, + {"compress", OPT_COMPRESS, + "Not used by mysql_upgrade. Only for backward compatibility.", + ¬_used, ¬_used, 0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"datadir", 'd', + "Not used by mysql_upgrade. Only for backward compatibility.", + 0, 0, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, +#ifdef DBUG_OFF + {"debug", '#', "This is a non-debug version. Catch this and exit.", + 0, 0, 0, GET_DISABLED, OPT_ARG, 0, 0, 0, 0, 0, 0}, +#else + {"debug", '#', "Output debug log.", &default_dbug_option, + &default_dbug_option, 0, GET_STR, OPT_ARG, 0, 0, 0, 0, 0, 0}, +#endif + {"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", 'T', "Print some debug info at exit.", &debug_info_flag, + &debug_info_flag, 0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"default-character-set", OPT_DEFAULT_CHARSET, + "Not used by mysql_upgrade. Only for backward compatibility.", + 0, 0, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"default_auth", OPT_DEFAULT_AUTH, + "Default authentication client-side plugin to use.", + &opt_default_auth, &opt_default_auth, 0, + GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"force", 'f', "Force execution of mysqlcheck even if mysql_upgrade " + "has already been executed for the current version of MariaDB.", + &opt_force, &opt_force, 0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"host", 'h', "Connect to host.", 0, + 0, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, +#define PASSWORD_OPT 12 + {"password", 'p', + "Password to use when connecting to server. If password is not given," + " it's solicited on the tty.", &opt_password,&opt_password, + 0, GET_STR, OPT_ARG, 0, 0, 0, 0, 0, 0}, +#ifdef __WIN__ + {"pipe", 'W', "Use named pipes to connect to server.", 0, 0, 0, + GET_NO_ARG, NO_ARG, 0, 0, 0, 0, 0, 0}, +#endif + {"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) ").", + 0, 0, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"protocol", OPT_MYSQL_PROTOCOL, + "The protocol to use for connection (tcp, socket, pipe).", + 0, 0, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"silent", OPT_SILENT, "Print less information", &opt_silent, + &opt_silent, 0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"socket", 'S', "The socket file to use for connection.", + 0, 0, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, +#include <sslopt-longopts.h> + {"tmpdir", 't', "Directory for temporary files.", + 0, 0, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"upgrade-system-tables", 's', "Only upgrade the system tables in the mysql database. Tables in other databases are not checked or touched.", + &opt_systables_only, &opt_systables_only, 0, + GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, +#define USER_OPT (array_elements(my_long_options) - 6) + {"user", 'u', "User for login.", &opt_user, + &opt_user, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"verbose", 'v', "Display more output about the process; Using it twice will print connection argument; Using it 3 times will print out all CHECK, RENAME and ALTER TABLE during the check phase.", + &opt_not_used, &opt_not_used, 0, GET_BOOL, NO_ARG, 1, 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}, + {"version-check", 'k', + "Run this program only if its \'server version\' " + "matches the version of the server to which it's connecting. " + "Note: the \'server version\' of the program is the version of the MariaDB " + "server with which it was built/distributed.", + &opt_version_check, &opt_version_check, 0, + GET_BOOL, NO_ARG, 1, 0, 0, 0, 0, 0}, + {"write-binlog", OPT_WRITE_BINLOG, "All commands including those " + "issued by mysqlcheck are written to the binary log.", + &opt_write_binlog, &opt_write_binlog, 0, GET_BOOL, NO_ARG, + 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, GET_NO_ARG, NO_ARG, 0, 0, 0, 0, 0, 0} +}; + + +static const char *load_default_groups[]= +{ + "client", /* Read settings how to connect to server */ + "mysql_upgrade", /* Read special settings for mysql_upgrade */ + "mariadb-upgrade", /* Read special settings for mysql_upgrade */ + "client-server", /* Reads settings common between client & server */ + "client-mariadb", /* Read mariadb unique client settings */ + 0 +}; + +static void free_used_memory(void) +{ + /* Free memory allocated by 'load_defaults' */ + if (defaults_argv) + free_defaults(defaults_argv); + + dynstr_free(&ds_args); + dynstr_free(&conn_args); + dynstr_free(&ds_plugin_data_types); + if (cnf_file_path) + my_delete(cnf_file_path, MYF(MY_WME)); +} + + +static void die(const char *fmt, ...) +{ + va_list args; + DBUG_ENTER("die"); + + /* Print the error message */ + fflush(stdout); + va_start(args, fmt); + if (fmt) + { + fprintf(stderr, "FATAL ERROR: "); + vfprintf(stderr, fmt, args); + fprintf(stderr, "\n"); + fflush(stderr); + } + va_end(args); + + free_used_memory(); + my_end(my_end_arg); + exit(1); +} + + +static void verbose(const char *fmt, ...) +{ + va_list args; + + if (opt_silent) + return; + + /* Print the verbose message */ + va_start(args, fmt); + if (fmt) + { + vfprintf(stdout, fmt, args); + fprintf(stdout, "\n"); + fflush(stdout); + } + va_end(args); +} + + +/* + Add one option - passed to mysql_upgrade on command line + or by defaults file(my.cnf) - to a dynamic string, in + this way we pass the same arguments on to mysql and mysql_check +*/ + +static void add_one_option_cmd_line(DYNAMIC_STRING *ds, + const struct my_option *opt, + const char* arg) +{ + dynstr_append(ds, "--"); + dynstr_append(ds, opt->name); + if (arg) + { + dynstr_append(ds, "="); + dynstr_append_os_quoted(ds, arg, NullS); + } + dynstr_append(ds, " "); +} + +static void add_one_option_cnf_file(DYNAMIC_STRING *ds, + const struct my_option *opt, + const char* arg) +{ + dynstr_append(ds, opt->name); + if (arg) + { + dynstr_append(ds, "="); + dynstr_append_os_quoted(ds, arg, NullS); + } + dynstr_append(ds, "\n"); +} + +static my_bool +get_one_option(const struct my_option *opt, const char *argument, + const char *filename __attribute__((unused))) +{ + my_bool add_option= TRUE; + + switch (opt->id) { + + case '?': + printf("%s Ver %s Distrib %s, for %s (%s)\n", + my_progname, VER, MYSQL_SERVER_VERSION, SYSTEM_TYPE, MACHINE_TYPE); + puts(ORACLE_WELCOME_COPYRIGHT_NOTICE("2000")); + puts("MariaDB utility for upgrading databases to new MariaDB versions."); + print_defaults("my", load_default_groups); + puts(""); + my_print_help(my_long_options); + my_print_variables(my_long_options); + die(0); + break; + + case '#': + DBUG_PUSH(argument ? argument : default_dbug_option); + add_option= FALSE; + debug_check_flag= 1; + break; + + case 'p': + if (argument == disabled_my_option) + argument= (char*) ""; /* Don't require password */ + add_option= FALSE; + if (argument) + { + /* + One should not really change the argument, but we make an + exception for passwords + */ + char *start= (char*) argument; + /* Add password to ds_args before overwriting the arg with x's */ + add_one_option_cnf_file(&ds_args, opt, argument); + while (*argument) + *(char*)argument++= 'x'; /* Destroy argument */ + if (*start) + start[1]= 0; + tty_password= 0; + } + else + tty_password= 1; + break; + + case 't': + strnmov(opt_tmpdir, argument, sizeof(opt_tmpdir)); + add_option= FALSE; + break; + + case 'b': /* --basedir */ + case 'd': /* --datadir */ + fprintf(stderr, "%s: the '--%s' option is always ignored\n", + my_progname, opt->id == 'b' ? "basedir" : "datadir"); + /* FALLTHROUGH */ + + case 'k': /* --version-check */ + case 'v': /* --verbose */ + opt_verbose++; + if (argument == disabled_my_option) + { + opt_verbose= 0; + opt_silent= 1; + } + add_option= 0; + break; + case 'V': + printf("%s Ver %s Distrib %s, for %s (%s)\n", + my_progname, VER, MYSQL_SERVER_VERSION, SYSTEM_TYPE, MACHINE_TYPE); + die(0); + break; + case OPT_SILENT: + opt_verbose= 0; + add_option= 0; + break; + case 'f': /* --force */ + case 's': /* --upgrade-system-tables */ + case OPT_WRITE_BINLOG: /* --write-binlog */ + add_option= FALSE; + break; + + case 'h': /* --host */ + case 'W': /* --pipe */ + case 'P': /* --port */ + case 'S': /* --socket */ + case OPT_MYSQL_PROTOCOL: /* --protocol */ + case OPT_PLUGIN_DIR: /* --plugin-dir */ + case OPT_DEFAULT_AUTH: /* --default-auth */ + add_one_option_cmd_line(&conn_args, opt, argument); + break; + } + + if (add_option) + { + /* + This is an option that is accepted by mysql_upgrade just so + it can be passed on to "mysql" and "mysqlcheck" + Save it in the ds_args string + */ + add_one_option_cnf_file(&ds_args, opt, argument); + } + return 0; +} + + +/** + Run a command using the shell, storing its output in the supplied dynamic + string. +*/ +static int run_command(char* cmd, + DYNAMIC_STRING *ds_res) +{ + char buf[512]= {0}; + FILE *res_file; + int error; + + if (opt_verbose >= 4) + puts(cmd); + + if (!(res_file= my_popen(cmd, "r"))) + die("popen(\"%s\", \"r\") failed", cmd); + + while (fgets(buf, sizeof(buf), res_file)) + { +#ifdef _WIN32 + /* Strip '\r' off newlines. */ + size_t len = strlen(buf); + if (len > 1 && buf[len - 2] == '\r' && buf[len - 1] == '\n') + { + buf[len - 2] = '\n'; + buf[len - 1] = 0; + } +#endif + 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= my_pclose(res_file); + return WEXITSTATUS(error); +} + + +static int run_tool(char *tool_path, DYNAMIC_STRING *ds_res, ...) +{ + int ret; + const char* arg; + va_list args; + DYNAMIC_STRING ds_cmdline; + + DBUG_ENTER("run_tool"); + DBUG_PRINT("enter", ("tool_path: %s", tool_path)); + + if (init_dynamic_string(&ds_cmdline, IF_WIN("\"", ""), FN_REFLEN, FN_REFLEN)) + die("Out of memory"); + + dynstr_append_os_quoted(&ds_cmdline, tool_path, NullS); + dynstr_append(&ds_cmdline, " "); + + va_start(args, ds_res); + + while ((arg= va_arg(args, char *))) + { + /* Options should already be os quoted */ + dynstr_append(&ds_cmdline, arg); + dynstr_append(&ds_cmdline, " "); + } + + va_end(args); + +#ifdef __WIN__ + dynstr_append(&ds_cmdline, "\""); +#endif + + DBUG_PRINT("info", ("Running: %s", ds_cmdline.str)); + ret= run_command(ds_cmdline.str, ds_res); + DBUG_PRINT("exit", ("ret: %d", ret)); + dynstr_free(&ds_cmdline); + DBUG_RETURN(ret); +} + + +/** + Look for the filename of given tool, with the presumption that it is in the + same directory as mysql_upgrade and that the same executable-searching + mechanism will be used when we run our sub-shells with popen() later. +*/ +static void find_tool(char *tool_executable_name, const char *tool_name, + const char *self_name) +{ + char *last_fn_libchar; + DYNAMIC_STRING ds_tmp; + DBUG_ENTER("find_tool"); + DBUG_PRINT("enter", ("progname: %s", my_progname)); + + if (init_dynamic_string(&ds_tmp, "", 32, 32)) + die("Out of memory"); + + last_fn_libchar= strrchr(self_name, FN_LIBCHAR); + + if (last_fn_libchar == NULL) + { + /* + mysql_upgrade was found by the shell searching the path. A sibling + next to us should be found the same way. + */ + strncpy(tool_executable_name, tool_name, FN_REFLEN); + } + else + { + int len; + + /* + mysql_upgrade was run absolutely or relatively. We can find a sibling + by replacing our name after the LIBCHAR with the new tool name. + */ + + /* + When running in a not yet installed build and using libtool, + the program(mysql_upgrade) will be in .libs/ and executed + through a libtool wrapper in order to use the dynamic libraries + from this build. The same must be done for the tools(mysql and + mysqlcheck). Thus if path ends in .libs/, step up one directory + and execute the tools from there + */ + if (((last_fn_libchar - 6) >= self_name) && + (strncmp(last_fn_libchar - 5, ".libs", 5) == 0) && + (*(last_fn_libchar - 6) == FN_LIBCHAR)) + { + DBUG_PRINT("info", ("Chopping off \".libs\" from end of path")); + last_fn_libchar -= 6; + } + + len= (int)(last_fn_libchar - self_name); + + my_snprintf(tool_executable_name, FN_REFLEN, "%.*b%c%s", + len, self_name, FN_LIBCHAR, tool_name); + } + + if (opt_verbose) + verbose("Looking for '%s' as: %s", tool_name, tool_executable_name); + + /* + Make sure it can be executed + */ + if (run_tool(tool_executable_name, + &ds_tmp, /* Get output from command, discard*/ + "--no-defaults", + "--help", + "2>&1", + IF_WIN("> NUL", "> /dev/null"), + NULL)) + die("Can't execute '%s'", tool_executable_name); + + dynstr_free(&ds_tmp); + + DBUG_VOID_RETURN; +} + + +/* + Run query using "mysql" +*/ + +static int run_query(const char *query, DYNAMIC_STRING *ds_res, + my_bool force) +{ + int ret; + File fd; + char query_file_path[FN_REFLEN]; +#ifdef WITH_WSREP + /* + Strictly speaking, WITH_WSREP on the client only means that the + client was compiled with WSREP, it doesn't mean the server was, + so the server might not have WSREP_ON variable. + + But mysql_upgrade is tightly bound to a specific server version + anyway - it was mysql_fix_privilege_tables_sql script embedded + into its binary - so even if it won't assume anything about server + wsrep-ness, it won't be any less server-dependent. + */ + const uchar sql_log_bin[]= "SET SQL_LOG_BIN=0, WSREP_ON=OFF;"; +#else + const uchar sql_log_bin[]= "SET SQL_LOG_BIN=0;"; +#endif /* WITH_WSREP */ + + DBUG_ENTER("run_query"); + DBUG_PRINT("enter", ("query: %s", query)); + if ((fd= create_temp_file(query_file_path, + opt_tmpdir[0] ? opt_tmpdir : NULL, + "sql", O_SHARE, MYF(MY_WME))) < 0) + die("Failed to create temporary file for defaults"); + + /* + Master and slave should be upgraded separately. All statements executed + by mysql_upgrade will not be binlogged. + 'SET SQL_LOG_BIN=0' is executed before any other statements. + */ + if (!opt_write_binlog) + { + if (my_write(fd, sql_log_bin, sizeof(sql_log_bin)-1, + MYF(MY_FNABP | MY_WME))) + { + my_close(fd, MYF(0)); + my_delete(query_file_path, MYF(0)); + die("Failed to write to '%s'", query_file_path); + } + } + + if (my_write(fd, (uchar*) query, strlen(query), + MYF(MY_FNABP | MY_WME))) + { + my_close(fd, MYF(0)); + my_delete(query_file_path, MYF(0)); + die("Failed to write to '%s'", query_file_path); + } + + ret= run_tool(mysql_path, + ds_res, + defaults_file, + "--database=mysql", + "--batch", /* Turns off pager etc. */ + force ? "--force": "--skip-force", + ds_res || opt_silent ? "--silent": "", + "<", + query_file_path, + "2>&1", + NULL); + + my_close(fd, MYF(0)); + my_delete(query_file_path, MYF(0)); + + DBUG_RETURN(ret); +} + + +/* + Extract the value returned from result of "show variable like ..." +*/ + +static int extract_variable_from_show(DYNAMIC_STRING* ds, char* value) +{ + char *value_start, *value_end; + size_t len; + + /* + The query returns "datadir\t<datadir>\n", skip past + the tab + */ + if ((value_start= strchr(ds->str, '\t')) == NULL) + return 1; /* Unexpected result */ + value_start++; + + /* Don't copy the ending newline */ + if ((value_end= strchr(value_start, '\n')) == NULL) + return 1; /* Unexpected result */ + + len= (size_t) MY_MIN(FN_REFLEN, value_end-value_start); + strncpy(value, value_start, len); + value[len]= '\0'; + return 0; +} + + +static int get_upgrade_info_file_name(char* name) +{ + DYNAMIC_STRING ds_datadir; + DBUG_ENTER("get_upgrade_info_file_name"); + + if (init_dynamic_string(&ds_datadir, NULL, 32, 32)) + die("Out of memory"); + + if (run_query("show variables like 'datadir'", + &ds_datadir, FALSE) || + extract_variable_from_show(&ds_datadir, name)) + { + dynstr_free(&ds_datadir); + DBUG_RETURN(1); /* Query failed */ + } + + dynstr_free(&ds_datadir); + + fn_format(name, "mysql_upgrade_info", name, "", MYF(0)); + DBUG_PRINT("exit", ("name: %s", name)); + DBUG_RETURN(0); +} + + +/* + Read the content of mysql_upgrade_info file and + compare the version number form file against + version number which mysql_upgrade was compiled for + + NOTE + This is an optimization to avoid running mysql_upgrade + when it's already been performed for the particular + version of MySQL. + + In case the MySQL server can't return the upgrade info + file it's always better to report that the upgrade hasn't + been performed. + +*/ + +static int upgrade_already_done(myf flags) +{ + FILE *in; + char upgrade_info_file[FN_REFLEN]= {0}; + + if (get_upgrade_info_file_name(upgrade_info_file)) + return 0; /* Could not get filename => not sure */ + + if (!(in= my_fopen(upgrade_info_file, O_RDONLY, flags))) + return 0; /* Could not open file => not sure */ + + bzero(upgrade_from_version, sizeof(upgrade_from_version)); + if (!fgets(upgrade_from_version, sizeof(upgrade_from_version), in)) + { + /* Preserve errno for caller */ + int save_errno= errno; + (void) my_fclose(in, flags); + errno= save_errno; + return 0; + } + + if (my_fclose(in, flags)) + return 0; + + errno= 0; + return (strncmp(upgrade_from_version, MYSQL_SERVER_VERSION, + sizeof(MYSQL_SERVER_VERSION)-1)==0); +} + + +/* + Write mysql_upgrade_info file in servers data dir indicating that + upgrade has been done for this version + + NOTE + This might very well fail but since it's just an optimization + to run mysql_upgrade only when necessary the error can be + ignored. + +*/ + +static void create_mysql_upgrade_info_file(void) +{ + FILE *out; + char upgrade_info_file[FN_REFLEN]= {0}; + + if (get_upgrade_info_file_name(upgrade_info_file)) + return; /* Could not get filename => skip */ + + if (!(out= my_fopen(upgrade_info_file, O_TRUNC | O_WRONLY, MYF(0)))) + { + fprintf(stderr, + "Could not create the upgrade info file '%s' in " + "the MariaDB Servers datadir, errno: %d\n", + upgrade_info_file, errno); + return; + } + + /* Write new version to file */ + my_fwrite(out, (uchar*) MYSQL_SERVER_VERSION, + sizeof(MYSQL_SERVER_VERSION), MY_WME); + my_fclose(out, MYF(MY_WME)); + + /* + Check if the upgrad_info_file was properly created/updated + It's not a fatal error -> just print a message if it fails + */ + if (!upgrade_already_done(MY_WME)) + fprintf(stderr, + "Upgrade file '%s' was not properly created. " + "Got error errno while checking file content: %d\n", + upgrade_info_file, errno); + return; +} + + +/* + Print connection-related arguments. +*/ + +static void print_conn_args(const char *tool_name) +{ + if (opt_verbose < 2) + return; + if (conn_args.str[0]) + verbose("Running '%s' with connection arguments: %s", tool_name, + conn_args.str); + else + verbose("Running '%s with default connection arguments", tool_name); +} + +/* + Check and upgrade(if necessary) all tables + in the server using "mysqlcheck --check-upgrade .." +*/ + +static int run_mysqlcheck_upgrade(my_bool mysql_db_only) +{ + const char *what= mysql_db_only ? "mysql database" : "tables"; + const char *arg1= mysql_db_only ? "--databases" : "--all-databases"; + const char *arg2= mysql_db_only ? "mysql" : "--skip-database=mysql"; + int retch; + if (opt_systables_only && !mysql_db_only) + { + verbose("Phase %d/%d: Checking and upgrading %s... Skipped", + ++phase, phases_total, what); + return 0; + } + verbose("Phase %d/%d: Checking and upgrading %s", ++phase, phases_total, what); + print_conn_args("mysqlcheck"); + retch= run_tool(mysqlcheck_path, + NULL, /* Send output from mysqlcheck directly to screen */ + defaults_file, + "--check-upgrade", + "--auto-repair", + !opt_silent || opt_verbose >= 1 ? "--verbose" : "", + opt_verbose >= 2 ? "--verbose" : "", + opt_verbose >= 3 ? "--verbose" : "", + opt_silent ? "--silent": "", + opt_write_binlog ? "--write-binlog" : "--skip-write-binlog", + arg1, arg2, + "2>&1", + NULL); + return retch; +} + +#define EVENTS_STRUCT_LEN 7000 + +static my_bool is_mysql() +{ + my_bool ret= TRUE; + DYNAMIC_STRING ds_events_struct; + + if (init_dynamic_string(&ds_events_struct, NULL, + EVENTS_STRUCT_LEN, EVENTS_STRUCT_LEN)) + die("Out of memory"); + + if (run_query("show create table mysql.event", + &ds_events_struct, FALSE) || + strstr(ds_events_struct.str, "IGNORE_BAD_TABLE_OPTIONS") != NULL) + ret= FALSE; + else + verbose("MariaDB upgrade detected"); + + dynstr_free(&ds_events_struct); + return(ret); +} + +static int run_mysqlcheck_views(void) +{ + const char *upgrade_views="--process-views=YES"; + if (upgrade_from_mysql) + { + /* + this has to ignore opt_systables_only, because upgrade_from_mysql + is determined by analyzing systables. if we honor opt_systables_only + here, views won't be fixed by subsequent mysql_upgrade runs + */ + upgrade_views="--process-views=UPGRADE_FROM_MYSQL"; + verbose("Phase %d/%d: Fixing views from mysql", ++phase, phases_total); + } + else if (opt_systables_only) + { + verbose("Phase %d/%d: Fixing views... Skipped", ++phase, phases_total); + return 0; + } + else + verbose("Phase %d/%d: Fixing views", ++phase, phases_total); + + print_conn_args("mysqlcheck"); + return run_tool(mysqlcheck_path, + NULL, /* Send output from mysqlcheck directly to screen */ + defaults_file, + "--all-databases", "--repair", + upgrade_views, + "--skip-process-tables", + opt_verbose ? "--verbose": "", + opt_silent ? "--silent": "", + opt_write_binlog ? "--write-binlog" : "--skip-write-binlog", + "2>&1", + NULL); +} + +static int run_mysqlcheck_fixnames(void) +{ + if (opt_systables_only) + { + verbose("Phase %d/%d: Fixing table and database names ... Skipped", + ++phase, phases_total); + return 0; + } + verbose("Phase %d/%d: Fixing table and database names", + ++phase, phases_total); + print_conn_args("mysqlcheck"); + return run_tool(mysqlcheck_path, + NULL, /* Send output from mysqlcheck directly to screen */ + defaults_file, + "--all-databases", + "--fix-db-names", + "--fix-table-names", + opt_verbose >= 1 ? "--verbose" : "", + opt_verbose >= 2 ? "--verbose" : "", + opt_verbose >= 3 ? "--verbose" : "", + opt_silent ? "--silent": "", + opt_write_binlog ? "--write-binlog" : "--skip-write-binlog", + "2>&1", + NULL); +} + + +static const char *expected_errors[]= +{ + "ERROR 1051", /* Unknown table */ + "ERROR 1060", /* Duplicate column name */ + "ERROR 1061", /* Duplicate key name */ + "ERROR 1054", /* Unknown column */ + "ERROR 1146", /* Table does not exist */ + "ERROR 1290", /* RR_OPTION_PREVENTS_STATEMENT */ + "ERROR 1347", /* 'mysql.user' is not of type 'BASE TABLE' */ + "ERROR 1348", /* Column 'Show_db_priv' is not updatable */ + "ERROR 1356", /* definer of view lack rights (UPDATE) */ + 0 +}; + + +static my_bool is_expected_error(const char* line) +{ + const char** error= expected_errors; + while (*error) + { + /* + Check if lines starting with ERROR + are in the list of expected errors + */ + if (strncmp(line, "ERROR", 5) != 0 || + strncmp(line, *error, strlen(*error)) == 0) + return 1; /* Found expected error */ + error++; + } + return 0; +} + + +static char* get_line(char* line) +{ + while (*line && *line != '\n') + line++; + if (*line) + line++; + return line; +} + + +/* Print the current line to stderr */ +static void print_line(char* line) +{ + while (*line && *line != '\n') + { + fputc(*line, stderr); + line++; + } + fputc('\n', stderr); +} + +static my_bool from_before_10_1() +{ + my_bool ret= TRUE; + DYNAMIC_STRING ds_events_struct; + + if (upgrade_from_version[0]) + { + return upgrade_from_version[1] == '.' || + strncmp(upgrade_from_version, "10.1.", 5) < 0; + } + + if (init_dynamic_string(&ds_events_struct, NULL, 2048, 2048)) + die("Out of memory"); + + if (run_query("show create table mysql.user", &ds_events_struct, FALSE) || + strstr(ds_events_struct.str, "default_role") != NULL) + ret= FALSE; + else + verbose("Upgrading from a version before MariaDB-10.1"); + + dynstr_free(&ds_events_struct); + return ret; +} + + +static void uninstall_plugins(void) +{ + if (ds_plugin_data_types.length) + { + char *plugins= ds_plugin_data_types.str; + char *next= get_line(plugins); + char buff[512]; + while(*plugins) + { + if (next[-1] == '\n') + next[-1]= 0; + verbose("uninstalling plugin for %s data type", plugins); + strxnmov(buff, sizeof(buff)-1, "UNINSTALL SONAME ", plugins,"", NULL); + run_query(buff, NULL, TRUE); + plugins= next; + next= get_line(next); + } + } +} +/** + @brief Install plugins for missing data types + @details Check for entries with "Unknown data type" in I_S.TABLES, + try to load plugins for these tables if available (MDEV-24093) + + @return Operation status + @retval TRUE - error + @retval FALSE - success +*/ +static int install_used_plugin_data_types(void) +{ + DYNAMIC_STRING ds_result; + const char *query = "SELECT table_comment FROM information_schema.tables" + " WHERE table_comment LIKE 'Unknown data type: %'"; + if (init_dynamic_string(&ds_result, "", 512, 512)) + die("Out of memory"); + run_query(query, &ds_result, TRUE); + + if (ds_result.length) + { + char *line= ds_result.str; + char *next= get_line(line); + while(*line) + { + if (next[-1] == '\n') + next[-1]= 0; + if (strstr(line, "'MYSQL_JSON'")) + { + verbose("installing plugin for MYSQL_JSON data type"); + if(!run_query("INSTALL SONAME 'type_mysql_json'", NULL, TRUE)) + { + dynstr_append(&ds_plugin_data_types, "'type_mysql_json'"); + dynstr_append(&ds_plugin_data_types, "\n"); + break; + } + else + { + fprintf(stderr, "... can't %s\n", "INSTALL SONAME 'type_mysql_json'"); + return 1; + } + } + line= next; + next= get_line(next); + } + } + dynstr_free(&ds_result); + return 0; +} +/* + Check for entries with "Unknown storage engine" in I_S.TABLES, + try to load plugins for these tables if available (MDEV-11942) +*/ +static int install_used_engines(void) +{ + char buf[512]; + DYNAMIC_STRING ds_result; + const char *query = "SELECT DISTINCT LOWER(engine) AS c1 FROM information_schema.tables" + " WHERE table_comment LIKE 'Unknown storage engine%'" + " ORDER BY c1"; + + if (opt_systables_only || !from_before_10_1()) + { + verbose("Phase %d/%d: Installing used storage engines... Skipped", ++phase, phases_total); + return 0; + } + verbose("Phase %d/%d: Installing used storage engines", ++phase, phases_total); + + if (init_dynamic_string(&ds_result, "", 512, 512)) + die("Out of memory"); + + verbose("Checking for tables with unknown storage engine"); + + run_query(query, &ds_result, TRUE); + + if (ds_result.length) + { + char *line= ds_result.str, *next=get_line(line); + do + { + if (next[-1] == '\n') + next[-1]=0; + + verbose("installing plugin for '%s' storage engine", line); + + // we simply assume soname=ha_enginename + strxnmov(buf, sizeof(buf)-1, "install soname 'ha_", line, "'", NULL); + + + if (run_query(buf, NULL, TRUE)) + fprintf(stderr, "... can't %s\n", buf); + line=next; + next=get_line(line); + } while (*line); + } + dynstr_free(&ds_result); + return 0; +} + +static int check_slave_repositories(void) +{ + DYNAMIC_STRING ds_result; + int row_count= 0; + int error= 0; + const char *query = "SELECT COUNT(*) AS c1 FROM mysql.slave_master_info"; + + if (init_dynamic_string(&ds_result, "", 512, 512)) + die("Out of memory"); + + run_query(query, &ds_result, TRUE); + + if (ds_result.length) + { + row_count= atoi((char *)ds_result.str); + if (row_count) + { + fprintf(stderr,"Slave info repository compatibility check:" + " Found data in `mysql`.`slave_master_info` table.\n"); + fprintf(stderr,"Warning: Content of `mysql`.`slave_master_info` table" + " will be ignored as MariaDB supports file based info " + "repository.\n"); + error= 1; + } + } + dynstr_free(&ds_result); + + query = "SELECT COUNT(*) AS c1 FROM mysql.slave_relay_log_info"; + + if (init_dynamic_string(&ds_result, "", 512, 512)) + die("Out of memory"); + + run_query(query, &ds_result, TRUE); + + if (ds_result.length) + { + row_count= atoi((char *)ds_result.str); + if (row_count) + { + fprintf(stderr, "Slave info repository compatibility check:" + " Found data in `mysql`.`slave_relay_log_info` table.\n"); + fprintf(stderr, "Warning: Content of `mysql`.`slave_relay_log_info` " + "table will be ignored as MariaDB supports file based " + "repository.\n"); + error= 1; + } + } + dynstr_free(&ds_result); + if (error) + { + fprintf(stderr,"Slave server may not possess the correct replication " + "metadata.\n"); + fprintf(stderr, "Execution of CHANGE MASTER as per " + "`mysql`.`slave_master_info` and `mysql`.`slave_relay_log_info` " + "table content is recommended.\n"); + } + return 0; +} + +/* + Update all system tables in MySQL Server to current + version using "mysql" to execute all the SQL commands + compiled into the mysql_fix_privilege_tables array +*/ + +static int run_sql_fix_privilege_tables(void) +{ + int found_real_errors= 0; + const char **query_ptr; + DYNAMIC_STRING ds_script; + DYNAMIC_STRING ds_result; + DBUG_ENTER("run_sql_fix_privilege_tables"); + + if (init_dynamic_string(&ds_script, "", 65536, 1024)) + die("Out of memory"); + + if (init_dynamic_string(&ds_result, "", 512, 512)) + die("Out of memory"); + + verbose("Phase %d/%d: Running 'mysql_fix_privilege_tables'", + ++phase, phases_total); + + /* + Individual queries can not be executed independently by invoking + a forked mysql client, because the script uses session variables + and prepared statements. + */ + for ( query_ptr= &mysql_fix_privilege_tables[0]; + *query_ptr != NULL; + query_ptr++ + ) + { + if (strcasecmp(*query_ptr, "flush privileges;\n")) + dynstr_append(&ds_script, *query_ptr); + } + + run_query(ds_script.str, + &ds_result, /* Collect result */ + TRUE); + + { + /* + Scan each line of the result for real errors + and ignore the expected one(s) like "Duplicate column name", + "Unknown column" and "Duplicate key name" since they just + indicate the system tables are already up to date + */ + char *line= ds_result.str; + do + { + if (!is_expected_error(line)) + { + /* Something unexpected failed, dump error line to screen */ + found_real_errors++; + print_line(line); + } + else if (strncmp(line, "WARNING", 7) == 0) + { + print_line(line); + } + } while ((line= get_line(line)) && *line); + } + + dynstr_free(&ds_result); + dynstr_free(&ds_script); + DBUG_RETURN(found_real_errors); +} + + +static void print_error(const char *error_msg, DYNAMIC_STRING *output) +{ + fprintf(stderr, "%s\n", error_msg); + fprintf(stderr, "%s", output->str); +} + + +/* Convert the specified version string into the numeric format. */ +static ulong STDCALL calc_server_version(char *some_version) +{ + uint major, minor, version; + char *point= some_version, *end_point; + major= (uint) strtoul(point, &end_point, 10); point=end_point+1; + minor= (uint) strtoul(point, &end_point, 10); point=end_point+1; + version= (uint) strtoul(point, &end_point, 10); + return (ulong) major * 10000L + (ulong)(minor * 100 + version); +} + +/** + Check if the server version matches with the server version mysql_upgrade + was compiled with. + + @return 0 match successful + 1 failed +*/ +static int check_version_match(void) +{ + DYNAMIC_STRING ds_version; + char version_str[NAME_CHAR_LEN + 1]; + + if (init_dynamic_string(&ds_version, NULL, NAME_CHAR_LEN, NAME_CHAR_LEN)) + die("Out of memory"); + + if (run_query("show variables like 'version'", + &ds_version, FALSE) || + extract_variable_from_show(&ds_version, version_str)) + { + print_error("Version check failed. Got the following error when calling " + "the 'mysql' command line client", &ds_version); + dynstr_free(&ds_version); + return 1; /* Query failed */ + } + + dynstr_free(&ds_version); + + if (calc_server_version((char *) version_str) != MYSQL_VERSION_ID) + { + fprintf(stderr, "Error: Server version (%s) does not match with the " + "version of\nthe server (%s) with which this program was built/" + "distributed. You can\nuse --skip-version-check to skip this " + "check.\n", version_str, MYSQL_SERVER_VERSION); + return 1; + } + else + return 0; +} + + +int main(int argc, char **argv) +{ + char self_name[FN_REFLEN + 1]; + + MY_INIT(argv[0]); + load_defaults_or_exit("my", load_default_groups, &argc, &argv); + defaults_argv= argv; /* Must be freed by 'free_defaults' */ + +#if defined(__WIN__) + if (GetModuleFileName(NULL, self_name, FN_REFLEN) == 0) +#endif + { + strmake_buf(self_name, argv[0]); + } + + if (init_dynamic_string(&ds_args, "", 512, 256) || + init_dynamic_string(&conn_args, "", 512, 256) || + init_dynamic_string(&ds_plugin_data_types, "", 512, 256)) + die("Out of memory"); + + if (handle_options(&argc, &argv, my_long_options, get_one_option)) + die(NULL); + if (debug_info_flag) + my_end_arg= MY_CHECK_ERROR | MY_GIVE_INFO; + if (debug_check_flag) + my_end_arg= MY_CHECK_ERROR; + + if (tty_password) + { + opt_password= get_tty_password(NullS); + /* add password to defaults file */ + add_one_option_cnf_file(&ds_args, &my_long_options[PASSWORD_OPT], opt_password); + DBUG_ASSERT(strcmp(my_long_options[PASSWORD_OPT].name, "password") == 0); + } + /* add user to defaults file */ + add_one_option_cnf_file(&ds_args, &my_long_options[USER_OPT], opt_user); + DBUG_ASSERT(strcmp(my_long_options[USER_OPT].name, "user") == 0); + + cnf_file_path= strmov(defaults_file, "--defaults-file="); + { + int fd= create_temp_file(cnf_file_path, opt_tmpdir[0] ? opt_tmpdir : NULL, + "mysql_upgrade-", 0, MYF(MY_FAE)); + if (fd < 0) + die(NULL); + my_write(fd, USTRING_WITH_LEN( "[client]\n"), MYF(MY_FAE)); + my_write(fd, (uchar*)ds_args.str, ds_args.length, MYF(MY_FAE)); + my_close(fd, MYF(0)); + } + + /* Find mysql */ + find_tool(mysql_path, IF_WIN("mysql.exe", "mysql"), self_name); + + /* Find mysqlcheck */ + find_tool(mysqlcheck_path, IF_WIN("mysqlcheck.exe", "mysqlcheck"), self_name); + + if (opt_systables_only && !opt_silent) + printf("The --upgrade-system-tables option was used, user tables won't be touched.\n"); + + /* + Read the mysql_upgrade_info file to check if mysql_upgrade + already has been run for this installation of MySQL + */ + if (!opt_force && upgrade_already_done(0)) + { + printf("This installation of MariaDB is already upgraded to %s, " + "use --force if you still need to run mysql_upgrade\n", + MYSQL_SERVER_VERSION); + goto end; + } + + if (opt_version_check && check_version_match()) + die("Upgrade failed"); + + upgrade_from_mysql= is_mysql(); + + /* + Run "mysqlcheck" and "mysql_fix_privilege_tables.sql" + */ + if (run_mysqlcheck_upgrade(TRUE) || + install_used_engines() || + install_used_plugin_data_types() || + run_mysqlcheck_views() || + run_sql_fix_privilege_tables() || + run_mysqlcheck_fixnames() || + run_mysqlcheck_upgrade(FALSE) || + check_slave_repositories()) + die("Upgrade failed" ); + + uninstall_plugins(); + verbose("Phase %d/%d: Running 'FLUSH PRIVILEGES'", ++phase, phases_total); + if (run_query("FLUSH PRIVILEGES", NULL, TRUE)) + die("Upgrade failed" ); + + verbose("OK"); + + /* Create a file indicating upgrade has been performed */ + create_mysql_upgrade_info_file(); + + DBUG_ASSERT(phase == phases_total); + +end: + free_used_memory(); + my_end(my_end_arg); + exit(0); +} |