diff --git a/mysql-test/main/backup_server_restore.result b/mysql-test/main/backup_server_restore.result new file mode 100644 index 0000000000000..83bf230bc8619 --- /dev/null +++ b/mysql-test/main/backup_server_restore.result @@ -0,0 +1,41 @@ +Prepare database +CREATE TABLE tinno (i INTEGER) ENGINE=InnoDB; +INSERT INTO tinno VALUES (1), (2), (3), (4); +CREATE TABLE tariatr (i INTEGER) ENGINE=Aria TRANSACTIONAL=1; +INSERT INTO tariatr VALUES (2), (3), (5), (7); +CREATE TABLE tariant (i INTEGER) ENGINE=Aria TRANSACTIONAL=0; +INSERT INTO tariant VALUES (1), (1), (2), (3), (5); +Back up the database +BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; +Restore the database +# restart: --datadir=MYSQLTEST_VARDIR/some_directory +Check contents after restore +SELECT * FROM tinno; +i +1 +2 +3 +4 +SELECT * FROM tariatr; +i +2 +3 +5 +7 +SELECT * FROM tariant; +i +1 +1 +2 +3 +5 +Warnings: +Error 145 Got error '145 "Table was marked as crashed and should be repaired"' for './test/tariant' +Warning 1034 1 client is using or hasn't closed the table properly +Note 1034 Table is fixed +Restart database in original data directory +# restart +Clean up +DROP TABLE tinno; +DROP TABLE tariatr; +DROP TABLE tariant; diff --git a/mysql-test/main/backup_server_restore.test b/mysql-test/main/backup_server_restore.test new file mode 100644 index 0000000000000..ef1f5af27a68b --- /dev/null +++ b/mysql-test/main/backup_server_restore.test @@ -0,0 +1,38 @@ +--source include/not_windows.inc +--source include/have_innodb.inc + +--echo Prepare database +CREATE TABLE tinno (i INTEGER) ENGINE=InnoDB; +INSERT INTO tinno VALUES (1), (2), (3), (4); +CREATE TABLE tariatr (i INTEGER) ENGINE=Aria TRANSACTIONAL=1; +INSERT INTO tariatr VALUES (2), (3), (5), (7); +CREATE TABLE tariant (i INTEGER) ENGINE=Aria TRANSACTIONAL=0; +INSERT INTO tariant VALUES (1), (1), (2), (3), (5); + +--echo Back up the database +evalp BACKUP SERVER TO '$MYSQLTEST_VARDIR/some_directory'; + +--echo Restore the database +--disable_query_log +call mtr.add_suppression("InnoDB: Did not find any checkpoint after LSN="); +call mtr.add_suppression("InnoDB: Renaming ib_[0-9]+.log to ib_logfile0"); +call mtr.add_suppression("mariadbd: Got error '145 \"Table was marked as crashed and should be repaired\"' for "); +call mtr.add_suppression("Checking table: "); +--enable_query_log +--let $restart_parameters=--datadir=$MYSQLTEST_VARDIR/some_directory +--source include/restart_mysqld.inc + +--echo Check contents after restore +SELECT * FROM tinno; +SELECT * FROM tariatr; +SELECT * FROM tariant; + +--echo Restart database in original data directory +--let $restart_parameters= +--source include/restart_mysqld.inc + +--echo Clean up +DROP TABLE tinno; +DROP TABLE tariatr; +DROP TABLE tariant; +--rmdir $MYSQLTEST_VARDIR/some_directory diff --git a/sql/sql_backup.cc b/sql/sql_backup.cc index 92f52ddf5c8ab..ba9f3ff55c592 100644 --- a/sql/sql_backup.cc +++ b/sql/sql_backup.cc @@ -21,9 +21,7 @@ #include "sql_backup_interface.h" #include "sql_parse.h" -#ifdef _WIN32 -#elif defined __APPLE__ -#else +#if !defined __APPLE__ && !defined _WIN32 using copying_step= ssize_t(int,int,size_t,off_t*); template static ssize_t copy(int in_fd, int out_fd, off_t c) noexcept diff --git a/sql/sql_backup_interface.h b/sql/sql_backup_interface.h index b7d0470598585..e9ed64d45b6d8 100644 --- a/sql/sql_backup_interface.h +++ b/sql/sql_backup_interface.h @@ -13,19 +13,34 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */ -#ifdef _WIN32 -/* You have to use CopyFileEx() and friends manually */ -#elif defined __APPLE__ +#ifdef __APPLE__ # include # include # include -# define copy_file(src, dst, off) \ - fcopyfile(src, dst, nullptr, COPYFILE_ALL | COPYFILE_CLONE) -# define copy_entire_file(src, dst) copy_file(src, dst,) -#else -# ifdef __cplusplus +#endif + +#ifdef __cplusplus extern "C" -# endif +{ +#endif + +#ifdef _WIN32 +/* You have to use CopyFileEx() and friends manually */ +#elif defined __APPLE__ + +inline +int copy_entire_file(int src, int dst) +{ + return fcopyfile(src, dst, nullptr, COPYFILE_ALL | COPYFILE_CLONE); +} + +inline +int copy_file(int src, int dst, off_t) +{ + return fcopyfile(src, dst, nullptr, COPYFILE_ALL | COPYFILE_CLONE); +} + +#else /** Copy a file. @param src source file descriptor @param dst target to append src to @@ -34,14 +49,15 @@ extern "C" @retval 0 on success */ int copy_file(int src, int dst, off_t size); -# ifdef __cplusplus -extern "C" -# endif + /** Copy an entire file. @param src source file descriptor @param dst target to append src to -@param size amount of data to be copied @return error code (negative) @retval 0 on success */ int copy_entire_file(int src, int dst); #endif + +#ifdef __cplusplus +} +#endif diff --git a/storage/innobase/handler/backup_innodb.cc b/storage/innobase/handler/backup_innodb.cc index d8f1c91331c67..0ef833491c472 100644 --- a/storage/innobase/handler/backup_innodb.cc +++ b/storage/innobase/handler/backup_innodb.cc @@ -20,6 +20,12 @@ #include "trx0trx.h" #include +#ifdef __APPLE__ +# include +# include +# include +#endif + /** Associate a transaction with the current session @param thd session @return InnoDB transaction */ diff --git a/storage/maria/CMakeLists.txt b/storage/maria/CMakeLists.txt index 9bdd729840077..7b4270ed4366d 100644 --- a/storage/maria/CMakeLists.txt +++ b/storage/maria/CMakeLists.txt @@ -45,6 +45,7 @@ SET(ARIA_SOURCES ma_init.c ma_open.c ma_extra.c ma_info.c ma_rkey.c ha_maria.h maria_def.h ma_recovery_util.c ma_servicethread.c ma_norec.c ma_crypt.c ma_backup.c + ma_backup.cc ma_backup.h ) IF(APPLE) diff --git a/storage/maria/ha_maria.cc b/storage/maria/ha_maria.cc index d28eca0e15ace..1ae6ab89e431a 100644 --- a/storage/maria/ha_maria.cc +++ b/storage/maria/ha_maria.cc @@ -23,6 +23,7 @@ #include #include #include "ha_maria.h" +#include "ma_backup.h" #include "trnman_public.h" #include "trnman.h" @@ -3941,6 +3942,9 @@ static int ha_maria_init(void *p) maria_hton->prepare_for_backup= maria_prepare_for_backup; maria_hton->end_backup= maria_end_backup; maria_hton->update_optimizer_costs= aria_update_optimizer_costs; + maria_hton->backup_start= aria_backup_start; + maria_hton->backup_step= aria_backup_step; + maria_hton->backup_end= aria_backup_end; /* TODO: decide if we support Maria being used for log tables */ maria_hton->flags= (HTON_CAN_RECREATE | HTON_SUPPORT_LOG_TABLES | diff --git a/storage/maria/ma_backup.cc b/storage/maria/ma_backup.cc new file mode 100644 index 0000000000000..a3a68afd7336e --- /dev/null +++ b/storage/maria/ma_backup.cc @@ -0,0 +1,372 @@ +/* Copyright (c) 2026, MariaDB plc + + 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 "maria_def.h" +#include "ma_backup.h" +#include +#include +#include +#include +#include + +#ifdef __APPLE__ +#include +#include +#include +#endif + +/* + Implementation of functions declatred in ma_backup.h: + BACKUP SERVER support for Aria engine +*/ + +using namespace std::string_literals; +namespace +{ + class Source_dir + { + public: + Source_dir(const char* path, myf flags) noexcept + { + dir_info= my_dir(path, flags); + if (!dir_info) + { + my_error(ER_CANT_READ_DIR, MYF(0), path, my_errno); + } + } + ~Source_dir() noexcept + { + my_dirend(dir_info); + } + bool is_error() const noexcept + { + return !dir_info; + } + template + int for_each(Fn fn) const noexcept + { + for (size_t i= 0; i < dir_info->number_of_files; i++) + { + if (fn(dir_info->dir_entry[i]) != 0) + return 1; + } + return 0; + } + + private: + MY_DIR *dir_info {nullptr}; + }; + + + /** Backup state; protected by log_sys.latch */ + class Aria_backup + { + public: + explicit Aria_backup(THD *thd, backup_target tgt) noexcept + : target(tgt) +#ifndef _WIN32 + , datadir_fd(open(maria_data_root, O_DIRECTORY)) + { + if (datadir_fd < 0) + { + my_error(ER_CANT_READ_DIR, MYF(0), maria_data_root, errno); + return; + } +#else + { +#endif // _WIN32 + translog_disable_purge(); + } + + bool is_initialized() const noexcept + { +#ifndef _WIN32 + return datadir_fd >= 0; +#else + return true; +#endif // _WIN32 + } + + ~Aria_backup() noexcept + { +#ifndef _WIN32 + if (datadir_fd >= 0) + close(datadir_fd); +#endif // _WIN32 + } + + int end(THD *thd, bool abort) noexcept + { + int ret_val = 0; + if (!abort) { + if (int err= perform_backup() != 0) + { + ret_val= err; + }; + } + translog_enable_purge(); + return ret_val; + } + private: + backup_target target; +#ifndef _WIN32 + const int datadir_fd; +#endif + static const LEX_CSTRING data_exts[]; + static const LEX_CSTRING log_file_prefix; + using dir_name = std::string; + using dir_contents = std::vector; + using database_dir = std::pair; + std::vector database_dirs; + std::vector log_files; + bool have_control_file = false; + + int perform_backup() noexcept + { + if (scan_datadir()) + return 1; + if (copy_databases()) + return 1; + if (copy_control_file()) + return 1; + if(translog_flush(translog_get_horizon())) + return 1; + if (copy_logs()) + return 1; + return 0; + } + + int scan_datadir() noexcept + { + const char* base_dir = maria_data_root; + Source_dir datadir(base_dir, MYF(MY_WANT_STAT)); + if (datadir.is_error()) + return 1; + datadir.for_each([this](const fileinfo &fi) + { + if (fi.mystat->st_mode & S_IFDIR) + { + if (scan_database_dir(fi.name) != 0) + return 1; + } else if (begins_with(fi.name, log_file_prefix)) + log_files.emplace_back(fi.name); + else if (strcmp(fi.name, "aria_log_control") == 0) + have_control_file = true; + return 0; + }); + return 0; + } + + int scan_database_dir(const char* dir_name) noexcept + { + const char* base_dir = maria_data_root; + std::string dir_path = std::string(base_dir) + "/" + dir_name; + Source_dir db_dir(dir_path.c_str(), MYF(0)); + if (db_dir.is_error()) + return 1; + std::vector files_to_backup; + db_dir.for_each([&files_to_backup](const fileinfo &fi) + { + if (is_db_file(fi.name)) + files_to_backup.emplace_back(fi.name); + return 0; + }); + if (!files_to_backup.empty()) + database_dirs.emplace_back(dir_name, std::move(files_to_backup)); + return 0; + } + + int copy_databases() noexcept + { + for (const database_dir& dir : database_dirs) + { + const char* dir_name = dir.first.c_str(); + if (ensure_target_subdir(dir_name) != 0) + { + my_error(ER_CANT_CREATE_FILE, MYF(0), dir_name, errno); + return 1; + } + if (copy_database(dir) != 0) + return 1; + } + return 0; + } + + /* + Create directory in the target directory if it does not exist. + Return 0 on success, non-0 on failure. Set errno in case of failure + */ + int ensure_target_subdir(const char* name) noexcept + { +#ifdef _WIN32 + std::string dir_path= targetPath() + "/" + name; + if (!CreateDirectory(dir_path.c_str(), nullptr)) + { + DWORD err = GetLastError(); + if (err != ERROR_ALREADY_EXISTS) + { + my_osmaperr(err); + return 1; + } + } +#else + if (mkdirat(target.fd, name, 0777) != 0) + return (errno != EEXIST); +#endif + return 0; + } + + int copy_database(const database_dir& dir) noexcept + { + for (const std::string& file : dir.second) + { + std::string file_path= dir.first + "/" + file; + if (copy_file(file_path) != 0) + return 1; + } + return 0; + } + + int copy_control_file() noexcept + { + if (!have_control_file) + return 0; + return copy_file("aria_log_control"); + } + + int copy_logs() noexcept + { + for (const std::string& file : log_files) + { + if (copy_file(file) != 0) + return 1; + } + return 0; + } + + int copy_file(const std::string &path) const noexcept + { + return copy_file(path.c_str()); + } + + int copy_file(const char*path) const noexcept + { +#ifndef _WIN32 + int ret_val = 0; + int src_fd = openat(datadir_fd, path, O_RDONLY); + if (src_fd < 0) + { + my_error(ER_CANT_OPEN_FILE, MYF(0), path, errno); + return 1; + } + int tgt_fd = openat(target.fd, path, + O_CREAT | O_EXCL | O_WRONLY, 0777); + if (tgt_fd < 0) + { + my_error(ER_CANT_CREATE_FILE, MYF(0), path, errno); + ret_val = 1; + goto finish; + } + if (copy_entire_file(src_fd, tgt_fd) != 0) + { + my_error(ER_ERROR_ON_WRITE, MYF(0), path, errno); + ret_val = 1; + } + close(tgt_fd); + finish: + close(src_fd); + return ret_val; +#else + std::string src_path= std::string(maria_data_root) + "/" + path; + std::string dest_path= targetPath() + "/" + path; + if(!CopyFileExA(src_path.c_str(), dest_path.c_str(), nullptr, nullptr, nullptr, + COPY_FILE_NO_BUFFERING)) + { + my_osmaperr(GetLastError()); + my_error(ER_CANT_CREATE_FILE, MYF(0), dest_path.c_str(), errno); + return 1; + } + return 0; +#endif + } + + + static bool is_db_file(const char* file_name) noexcept; + + static bool ends_with(const char* str, const LEX_CSTRING& suffix) noexcept + { + size_t str_len = strlen(str); + size_t suffix_len = suffix.length; + if (str_len < suffix_len) + return false; + return memcmp(str + str_len - suffix_len, + suffix.str, + suffix_len) == 0; + } + + static bool begins_with(const char* str, const LEX_CSTRING& prefix) noexcept + { + return strncmp(str, prefix.str, prefix.length) == 0; + } + +#ifdef _WIN32 + /** @return the target directory path */ + std::string targetPath() const + { + return std::string(target.path); + } +#endif + }; + + /* TODO: .frm failes are not Aria-specific, .MYD and .MYI are MyISAM files; + they are copied here as a stop-gap */ + const LEX_CSTRING Aria_backup::data_exts[] {{C_STRING_WITH_LEN(".MAD")}, + {C_STRING_WITH_LEN(".MAI")}, + {C_STRING_WITH_LEN(".MYD")}, + {C_STRING_WITH_LEN(".MYI")}, + {C_STRING_WITH_LEN(".frm")}}; + const LEX_CSTRING Aria_backup::log_file_prefix {C_STRING_WITH_LEN("aria_log.")}; + + bool Aria_backup::is_db_file(const char* file_name) noexcept + { + for (const LEX_CSTRING& ext : data_exts) + { + if (ends_with(file_name, ext)) + return true; + } + /* As a stop-gap db/opt files are also copied here, this should be done in SQL layer. */ + return !strcmp(file_name, "db.opt"); + } + + std::unique_ptr aria_backup; +} + +int aria_backup_start(THD *thd, backup_target target) noexcept +{ + aria_backup= std::make_unique(thd, target); + return !aria_backup->is_initialized(); +} + +int aria_backup_step(THD *thd) noexcept +{ + return 0; +} + +int aria_backup_end(THD *thd, bool abort) noexcept +{ + int ret_val= aria_backup->end(thd, abort); + aria_backup.reset(); + return ret_val; +} diff --git a/storage/maria/ma_backup.h b/storage/maria/ma_backup.h new file mode 100644 index 0000000000000..3bec606648dac --- /dev/null +++ b/storage/maria/ma_backup.h @@ -0,0 +1,47 @@ +/* Copyright (c) 2026, MariaDB plc + + 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 */ + +#pragma once + +/* BACKUP SERVER support for Aria engine. */ + +#include +#include + +/** + Start of BACKUP SERVER: collect all files to be backed up + @param thd current session + @param target target directory + @return error code + @retval 0 on success +*/ +int aria_backup_start(THD *thd, backup_target target) noexcept; + +/** + Process a file that was collected in backup_start(). + @param thd current session + @return number of files remaining, or negative on error + @retval 0 on completion +*/ +int aria_backup_step(THD *thd) noexcept; + +/** + Finish copying and determine the logical time of the backup snapshot. + @param thd current session + @param abort whether BACKUP SERVER was aborted + @return error code + @retval 0 on success +*/ +int aria_backup_end(THD *thd, bool abort) noexcept;