From bd30bf64c3b63157bec49d3da7fb8baa11df93ca Mon Sep 17 00:00:00 2001 From: ayush-jha123 Date: Mon, 20 Apr 2026 17:16:51 +0000 Subject: [PATCH 1/2] MDEV-8235: Expose check_slave_start_position as GTID_CHECK_POS() This commit implements the GTID_CHECK_POS() SQL function, which allows validating if a given GTID position is reachable within the current set of binary logs. This is useful for external tools or orchestration layers to verify if a node is viable for replication start without actually initiating a slave connection. Features: - Returns 1 if all GTIDs in the requested state are reachable. - Returns 0 if any GTID in the requested state has been purged. - Propagates parsing errors for malformed GTID strings. - Derived from Item_bool_func to ensure native boolean handling. - Protected by HAVE_REPLICATION for safe embedded builds. Implemented via a thin wrapper over the engine-internal gtid_find_binlog_pos() logic. --- mysql-test/main/gtid_check_pos.result | 37 +++++++++++++++ mysql-test/main/gtid_check_pos.test | 44 ++++++++++++++++++ sql/item_cmpfunc.h | 23 ++++++++++ sql/item_create.cc | 23 ++++++++++ sql/item_func.cc | 16 +++++++ sql/item_func.h | 3 ++ sql/sql_repl.cc | 66 +++++++++++++++++++++++++++ sql/sql_repl.h | 1 + 8 files changed, 213 insertions(+) create mode 100644 mysql-test/main/gtid_check_pos.result create mode 100644 mysql-test/main/gtid_check_pos.test diff --git a/mysql-test/main/gtid_check_pos.result b/mysql-test/main/gtid_check_pos.result new file mode 100644 index 0000000000000..55df67a76c858 --- /dev/null +++ b/mysql-test/main/gtid_check_pos.result @@ -0,0 +1,37 @@ +# +# MDEV-8235 Expose check_slave_start_position as SQL function +# +# 1. Set up some basic GTID history +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1); +INSERT INTO t1 VALUES (2); +# Get the current GTID position manually and verify it exists +SELECT GTID_CHECK_POS('GTID_POS'); +GTID_CHECK_POS('GTID_POS') +1 +# 2. Verify invalid inputs +SELECT GTID_CHECK_POS('invalid-format'); +ERROR HY000: Could not parse GTID list +SELECT GTID_CHECK_POS(NULL); +GTID_CHECK_POS(NULL) +NULL +# 3. Check for a very old purged GTID +# (Simulate purging all logs) +FLUSH LOGS; +PURGE BINARY LOGS BEFORE NOW(); +# Depending on when it is executed, '0-1-1' might still be +# reachable if we haven't actually purged the very first file +# but purge binary logs before now() should remove everything +# except the active one. +# Since we want a deterministic test, let's check +# a completely fake server_id / seq_no that shouldn't exist +SELECT GTID_CHECK_POS('99-99-9999999'); +GTID_CHECK_POS('99-99-9999999') +1 +# 4. Check for an otherwise valid GTID (right domain and server) +# that is absent (sequence number is in the future). +SELECT GTID_CHECK_POS('ABSENT_GTID'); +GTID_CHECK_POS('ABSENT_GTID') +1 +# Cleanup +DROP TABLE t1; diff --git a/mysql-test/main/gtid_check_pos.test b/mysql-test/main/gtid_check_pos.test new file mode 100644 index 0000000000000..035e0c7c345b5 --- /dev/null +++ b/mysql-test/main/gtid_check_pos.test @@ -0,0 +1,44 @@ +--source include/have_log_bin.inc + +--echo # +--echo # MDEV-8235 Expose check_slave_start_position as SQL function +--echo # + +--echo # 1. Set up some basic GTID history +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1); +INSERT INTO t1 VALUES (2); + +--echo # Get the current GTID position manually and verify it exists +let $pos= `SELECT @@GLOBAL.gtid_binlog_pos`; +--replace_result $pos GTID_POS +eval SELECT GTID_CHECK_POS('$pos'); + +--echo # 2. Verify invalid inputs +--error ER_INCORRECT_GTID_STATE +SELECT GTID_CHECK_POS('invalid-format'); +SELECT GTID_CHECK_POS(NULL); + +--echo # 3. Check for a very old purged GTID +--echo # (Simulate purging all logs) +FLUSH LOGS; +PURGE BINARY LOGS BEFORE NOW(); + +--echo # Depending on when it is executed, '0-1-1' might still be +--echo # reachable if we haven't actually purged the very first file +--echo # but purge binary logs before now() should remove everything +--echo # except the active one. + +--echo # Since we want a deterministic test, let's check +--echo # a completely fake server_id / seq_no that shouldn't exist +SELECT GTID_CHECK_POS('99-99-9999999'); + +--echo # 4. Check for an otherwise valid GTID (right domain and server) +--echo # that is absent (sequence number is in the future). +let $server_id= `SELECT @@GLOBAL.server_id`; +let $absent_gtid= 0-$server_id-99999999; +--replace_result $absent_gtid ABSENT_GTID +eval SELECT GTID_CHECK_POS('$absent_gtid'); + +--echo # Cleanup +DROP TABLE t1; diff --git a/sql/item_cmpfunc.h b/sql/item_cmpfunc.h index 4fa07ccc8c971..224f3c8c5474c 100644 --- a/sql/item_cmpfunc.h +++ b/sql/item_cmpfunc.h @@ -2920,6 +2920,29 @@ class Item_func_null_predicate :public Item_bool_func }; +class Item_func_gtid_check_pos :public Item_bool_func +{ + String tmp_value; +public: + Item_func_gtid_check_pos(THD *thd, Item *a): Item_bool_func(thd, a) {} + LEX_CSTRING func_name_cstring() const override + { + static LEX_CSTRING name= {STRING_WITH_LEN("gtid_check_pos") }; + return name; + } + bool val_bool() override; + bool fix_length_and_dec(THD *thd) override + { + set_maybe_null(); + return FALSE; + } + +protected: + Item *shallow_copy(THD *thd) const override + { return get_item_copy(thd, this); } +}; + + class Item_func_isnull :public Item_func_null_predicate { public: diff --git a/sql/item_create.cc b/sql/item_create.cc index f2716e643668a..c522bd76e5783 100644 --- a/sql/item_create.cc +++ b/sql/item_create.cc @@ -528,6 +528,19 @@ class Create_func_connection_id : public Create_func_arg0 }; +class Create_func_gtid_check_pos : public Create_func_arg1 +{ +public: + Item *create_1_arg(THD *thd, Item *arg1) override; + + static Create_func_gtid_check_pos s_singleton; + +protected: + Create_func_gtid_check_pos() = default; + ~Create_func_gtid_check_pos() override = default; +}; + + class Create_func_database : public Create_func_arg0 { public: @@ -3576,6 +3589,15 @@ Create_func_connection_id::create_builder(THD *thd) } +Create_func_gtid_check_pos Create_func_gtid_check_pos::s_singleton; + +Item* +Create_func_gtid_check_pos::create_1_arg(THD *thd, Item *arg1) +{ + return new (thd->mem_root) Item_func_gtid_check_pos(thd, arg1); +} + + Create_func_database Create_func_database::s_singleton; Item* @@ -6355,6 +6377,7 @@ const Native_func_registry func_array[] = { { STRING_WITH_LEN("CONCAT_OPERATOR_ORACLE") }, BUILDER(Create_func_concat_operator_oracle)}, { { STRING_WITH_LEN("CONCAT_WS") }, BUILDER(Create_func_concat_ws)}, { { STRING_WITH_LEN("CONNECTION_ID") }, BUILDER(Create_func_connection_id)}, + { { STRING_WITH_LEN("GTID_CHECK_POS") }, BUILDER(Create_func_gtid_check_pos)}, { { STRING_WITH_LEN("CONV") }, BUILDER(Create_func_conv)}, { { STRING_WITH_LEN("CONVERT_TZ") }, BUILDER(Create_func_convert_tz)}, { { STRING_WITH_LEN("COS") }, BUILDER(Create_func_cos)}, diff --git a/sql/item_func.cc b/sql/item_func.cc index fe9a2976dfbdc..8abe59a790898 100644 --- a/sql/item_func.cc +++ b/sql/item_func.cc @@ -37,6 +37,7 @@ #include "sql_acl.h" // EXECUTE_ACL #include "mysqld.h" // LOCK_short_uuid_generator #include "rpl_mi.h" + #include "sql_time.h" #include #include @@ -824,6 +825,21 @@ String *Item_int_func::val_str(String *str) } +#ifndef HAVE_REPLICATION +bool Item_func_gtid_check_pos::val_bool() +{ + DBUG_ASSERT(fixed()); + String *gtid_str= args[0]->val_str(&tmp_value); + if ((null_value= args[0]->null_value)) + return 0; + + my_error(ER_NOT_SUPPORTED_YET, MYF(0), "GTID_CHECK_POS"); + null_value= 1; + return 0; +} +#endif + + bool Item_func_connection_id::fix_length_and_dec(THD *thd) { if (Item_long_func::fix_length_and_dec(thd)) diff --git a/sql/item_func.h b/sql/item_func.h index df19b49f7eef7..a2e19c06d64bc 100644 --- a/sql/item_func.h +++ b/sql/item_func.h @@ -1505,6 +1505,9 @@ class Item_func_connection_id :public Item_long_func }; + + + class Item_func_signed :public Item_int_func { public: diff --git a/sql/sql_repl.cc b/sql/sql_repl.cc index 7dbd646a17414..996e4b1ab663c 100644 --- a/sql/sql_repl.cc +++ b/sql/sql_repl.cc @@ -32,6 +32,7 @@ #include "semisync_slave.h" #include "mysys_err.h" #include "gtid_index.h" +#include "item_cmpfunc.h" enum enum_gtid_until_state { @@ -2122,6 +2123,9 @@ gtid_state_from_binlog_pos(const char *in_name, uint32 pos, String *out_str) } + + + static bool is_until_reached(binlog_send_info *info, ulong *ev_offset, Log_event_type event_type, const char **errmsg, @@ -5838,4 +5842,66 @@ int compare_log_name(const char *log_1, const char *log_2) { return res; } +bool Item_func_gtid_check_pos::val_bool() +{ + DBUG_ASSERT(fixed()); + String *gtid_str= args[0]->val_str(&tmp_value); + if ((null_value= args[0]->null_value)) + return 0; + + slave_connection_state state; + char buf[FN_REFLEN] = {0}; + const char *errormsg; + + if (!mysql_bin_log.is_open()) + { + my_error(ER_NO_BINARY_LOGGING, MYF(0)); + null_value= 1; + return 0; + } + + if (state.load(gtid_str->ptr(), gtid_str->length())) + { + /* + We purposefully do not swallow the error here. If the user passes an + ill-formed string, state.load throws ER_INCORRECT_GTID_STATE which + will correctly abort the statement and notify the user. + */ + null_value= 1; + return 0; + } + + /* + gtid_find_binlog_pos will natively iterate over the entire `state` hash, + evaluating every GTID provided in the comma-separated list. + + If ANY of the requested GTIDs are found to be purged from the binary logs, + it returns a non-null error message pinpointing the purged GTID. + + If ALL GTIDs within the requested state natively resolve (meaning they exist + in our index/binlogs, or are safely in the future), it returns a null errormsg. + + We disregard `found_in_index` and `out_start_seek` counts since we are only doing + boolean viability checking of the provided GTIDs, not actually initializing a dump thread. + */ + bool found_in_index= false; + uint32 out_start_seek= 0; + rpl_binlog_state until_binlog_state; + until_binlog_state.init(); + slave_connection_state until_gtid_state; + + errormsg= gtid_find_binlog_pos(&state, buf, &until_gtid_state, &until_binlog_state, &found_in_index, &out_start_seek); + + until_binlog_state.free(); + + if (errormsg) + { + null_value= 0; + return 0; // At least one requested GTID has been purged. + } + + null_value= 0; + return 1; // All requested GTIDs are currently viable/reachable. +} + #endif /* HAVE_REPLICATION */ diff --git a/sql/sql_repl.h b/sql/sql_repl.h index 0e5f0f27d0a6f..4ae1e411d6727 100644 --- a/sql/sql_repl.h +++ b/sql/sql_repl.h @@ -72,6 +72,7 @@ int rpl_append_gtid_state(String *dest, bool use_binlog); int rpl_load_gtid_state(slave_connection_state *state, bool use_binlog); bool rpl_gtid_pos_check(THD *thd, char *str, size_t len); bool rpl_gtid_pos_update(THD *thd, char *str, size_t len); + #else struct LOAD_FILE_IO_CACHE : public IO_CACHE { }; From ebb7195747d804ef7189a670bf4f5f678bfe2ef0 Mon Sep 17 00:00:00 2001 From: ayush-jha123 Date: Tue, 28 Apr 2026 18:42:41 +0000 Subject: [PATCH 2/2] MDEV-8235: Fix symbol visibility and embedded build for GTID_CHECK_POS --- sql/item_func.cc | 14 ++++++++++++-- sql/sql_repl.cc | 20 +++++++------------- sql/sql_repl.h | 1 + 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/sql/item_func.cc b/sql/item_func.cc index 8abe59a790898..b417ce05b363d 100644 --- a/sql/item_func.cc +++ b/sql/item_func.cc @@ -54,6 +54,7 @@ #include "sql_cte.h" #ifdef WITH_WSREP #include "mysql/service_wsrep.h" +#include "sql_repl.h" #endif /* WITH_WSREP */ #ifdef NO_EMBEDDED_ACCESS_CHECKS @@ -825,7 +826,6 @@ String *Item_int_func::val_str(String *str) } -#ifndef HAVE_REPLICATION bool Item_func_gtid_check_pos::val_bool() { DBUG_ASSERT(fixed()); @@ -833,11 +833,21 @@ bool Item_func_gtid_check_pos::val_bool() if ((null_value= args[0]->null_value)) return 0; +#ifndef HAVE_REPLICATION my_error(ER_NOT_SUPPORTED_YET, MYF(0), "GTID_CHECK_POS"); null_value= 1; return 0; -} +#else + bool is_reachable= false; + if (rpl_gtid_pos_check_reachable(gtid_str, &is_reachable)) + { + null_value= 1; + return 0; + } + null_value= 0; + return is_reachable; #endif +} bool Item_func_connection_id::fix_length_and_dec(THD *thd) diff --git a/sql/sql_repl.cc b/sql/sql_repl.cc index 996e4b1ab663c..d23530767dca0 100644 --- a/sql/sql_repl.cc +++ b/sql/sql_repl.cc @@ -5842,12 +5842,8 @@ int compare_log_name(const char *log_1, const char *log_2) { return res; } -bool Item_func_gtid_check_pos::val_bool() +bool rpl_gtid_pos_check_reachable(String *gtid_str, bool *is_reachable) { - DBUG_ASSERT(fixed()); - String *gtid_str= args[0]->val_str(&tmp_value); - if ((null_value= args[0]->null_value)) - return 0; slave_connection_state state; char buf[FN_REFLEN] = {0}; @@ -5856,8 +5852,7 @@ bool Item_func_gtid_check_pos::val_bool() if (!mysql_bin_log.is_open()) { my_error(ER_NO_BINARY_LOGGING, MYF(0)); - null_value= 1; - return 0; + return true; } if (state.load(gtid_str->ptr(), gtid_str->length())) @@ -5867,8 +5862,7 @@ bool Item_func_gtid_check_pos::val_bool() ill-formed string, state.load throws ER_INCORRECT_GTID_STATE which will correctly abort the statement and notify the user. */ - null_value= 1; - return 0; + return true; } /* @@ -5896,12 +5890,12 @@ bool Item_func_gtid_check_pos::val_bool() if (errormsg) { - null_value= 0; - return 0; // At least one requested GTID has been purged. + *is_reachable= false; + return false; // At least one requested GTID has been purged. } - null_value= 0; - return 1; // All requested GTIDs are currently viable/reachable. + *is_reachable= true; + return false; // All requested GTIDs are currently viable/reachable. } #endif /* HAVE_REPLICATION */ diff --git a/sql/sql_repl.h b/sql/sql_repl.h index 4ae1e411d6727..4f955ca352e38 100644 --- a/sql/sql_repl.h +++ b/sql/sql_repl.h @@ -72,6 +72,7 @@ int rpl_append_gtid_state(String *dest, bool use_binlog); int rpl_load_gtid_state(slave_connection_state *state, bool use_binlog); bool rpl_gtid_pos_check(THD *thd, char *str, size_t len); bool rpl_gtid_pos_update(THD *thd, char *str, size_t len); +bool rpl_gtid_pos_check_reachable(String *gtid_str, bool *is_reachable); #else