Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions mysql-test/main/gtid_check_pos.result
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#
# 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);
# The current GTID position is reachable
SELECT GTID_CHECK_POS('GTID_POS');
GTID_CHECK_POS('GTID_POS')
1
# 2. Invalid input raises an error; NULL input returns NULL
SELECT GTID_CHECK_POS('invalid-format');
ERROR HY000: Could not parse GTID list
SELECT GTID_CHECK_POS(NULL);
GTID_CHECK_POS(NULL)
NULL
# 3. Empty string (empty GTID list) returns 1 (trivially reachable)
SELECT GTID_CHECK_POS('');
GTID_CHECK_POS('')
1
# 4. Rotate to a new binlog and purge the old one
FLUSH LOGS;
# Write a new GTID into the new (post-purge) active binlog
INSERT INTO t1 VALUES (3);
# The new position (in the current active log) is reachable
SELECT GTID_CHECK_POS('NEW_GTID_POS');
GTID_CHECK_POS('NEW_GTID_POS')
1
Comment thread
gkodinov marked this conversation as resolved.
# The old position (now in a purged log) returns 0
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one fails on buildbot:

main.gtid_check_pos                      w7 [ fail ]
        Test ended at 2026-06-03 19:55:16

CURRENT_TEST: main.gtid_check_pos
--- C:/a/server/server/mysql-test/main/gtid_check_pos.result	2026-06-03 19:12:08.051490800 +0000
+++ C:\a\server\server\mysql-test\main\gtid_check_pos.reject	2026-06-03 19:55:13.752902600 +0000
@@ -30,7 +30,7 @@
 # The old position (now in a purged log) returns 0
 SELECT GTID_CHECK_POS('OLD_GTID_POS');
 GTID_CHECK_POS('OLD_GTID_POS')
-0
+1

SELECT GTID_CHECK_POS('OLD_GTID_POS');
GTID_CHECK_POS('OLD_GTID_POS')
0
# A completely fake domain/server/seqno (never existed) returns 0
SELECT GTID_CHECK_POS('99-99-9999999');
GTID_CHECK_POS('99-99-9999999')
0
# 5. A future sequence number in a known domain returns 0
SELECT GTID_CHECK_POS('ABSENT_GTID');
GTID_CHECK_POS('ABSENT_GTID')
0
# 6. Mixed list: reachable pos combined with an unknown domain returns 0
SELECT GTID_CHECK_POS('NEW_GTID_POS,99-99-9999999');
GTID_CHECK_POS('NEW_GTID_POS,99-99-9999999')
0
# 7. List with only the current reachable position returns 1
SELECT GTID_CHECK_POS('NEW_GTID_POS');
GTID_CHECK_POS('NEW_GTID_POS')
1
Comment thread
gkodinov marked this conversation as resolved.
# Cleanup
DROP TABLE t1;
62 changes: 62 additions & 0 deletions mysql-test/main/gtid_check_pos.test
Comment thread
gkodinov marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
--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 # The current GTID position is reachable
let $pos= `SELECT @@GLOBAL.gtid_binlog_pos`;
--replace_result $pos GTID_POS
eval SELECT GTID_CHECK_POS('$pos');
Comment thread
gkodinov marked this conversation as resolved.

--echo # 2. Invalid input raises an error; NULL input returns NULL
--error ER_INCORRECT_GTID_STATE
SELECT GTID_CHECK_POS('invalid-format');
SELECT GTID_CHECK_POS(NULL);

--echo # 3. Empty string (empty GTID list) returns 1 (trivially reachable)
SELECT GTID_CHECK_POS('');

--echo # 4. Rotate to a new binlog and purge the old one
FLUSH LOGS;
let $current_log= query_get_value(SHOW MASTER STATUS, File, 1);
--disable_query_log
eval PURGE BINARY LOGS TO '$current_log';
--enable_query_log

--echo # Write a new GTID into the new (post-purge) active binlog
INSERT INTO t1 VALUES (3);

--echo # The new position (in the current active log) is reachable
let $new_pos= `SELECT @@GLOBAL.gtid_binlog_pos`;
--replace_result $new_pos NEW_GTID_POS
eval SELECT GTID_CHECK_POS('$new_pos');

--echo # The old position (now in a purged log) returns 0
--replace_result $pos OLD_GTID_POS
eval SELECT GTID_CHECK_POS('$pos');

--echo # A completely fake domain/server/seqno (never existed) returns 0
SELECT GTID_CHECK_POS('99-99-9999999');

--echo # 5. A future sequence number in a known domain returns 0
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 # 6. Mixed list: reachable pos combined with an unknown domain returns 0
--replace_result $new_pos NEW_GTID_POS
eval SELECT GTID_CHECK_POS('$new_pos,99-99-9999999');

--echo # 7. List with only the current reachable position returns 1
--replace_result $new_pos NEW_GTID_POS
eval SELECT GTID_CHECK_POS('$new_pos');

--echo # Cleanup
DROP TABLE t1;
23 changes: 23 additions & 0 deletions sql/item_cmpfunc.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item_func_gtid_check_pos>(thd, this); }
};


class Item_func_isnull :public Item_func_null_predicate
{
public:
Expand Down
23 changes: 23 additions & 0 deletions sql/item_create.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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*
Expand Down Expand Up @@ -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)},
Expand Down
40 changes: 40 additions & 0 deletions sql/item_func.cc
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#include "sql_acl.h" // EXECUTE_ACL
#include "mysqld.h" // LOCK_short_uuid_generator
#include "rpl_mi.h"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no white-space only changes please.

#include "sql_time.h"
#include <m_ctype.h>
#include <hash.h>
Expand All @@ -51,6 +52,7 @@
#include "debug_sync.h"
#include "sql_base.h"
#include "sql_cte.h"
#include "sql_repl.h"
#ifdef WITH_WSREP
#include "mysql/service_wsrep.h"
#endif /* WITH_WSREP */
Expand Down Expand Up @@ -824,6 +826,44 @@ String *Item_int_func::val_str(String *str)
}


/**
@brief Expose replication start position validation as SQL function GTID_CHECK_POS().

This function accepts a string representation of one or more GTID lists
and verifies if they exist and are reachable (viable) within the current
set of binary logs.

@param gtid_str A string representation of GTIDs (e.g. '0-1-1,0-1-2').
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will make doxygen return an error if one day it was run on these files. This is a method documentation block. And yet, you're talking about the SQL function itself. I'd move this particular block towards the class definition (?), reformat it a bit so that there's no @return or @param directives and then add a real one here explaining the return value and what the function does in specific.


@return
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw, use @RetVal if you want to do good doxygen. Or don't do doxygen.

- 1 if all requested GTIDs are reachable/viable.
- 0 if any requested GTID has been purged or is absent (in the future).
- NULL if the input argument is NULL or if check fails.
*/
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;

#ifdef HAVE_REPLICATION
bool is_reachable= false;
if (rpl_gtid_pos_check_reachable(gtid_str, &is_reachable))
Comment thread
gkodinov marked this conversation as resolved.
{
null_value= 1;
return 0;
}
null_value= 0;
return is_reachable;
#else
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))
Expand Down
3 changes: 3 additions & 0 deletions sql/item_func.h
Original file line number Diff line number Diff line change
Expand Up @@ -1505,6 +1505,9 @@ class Item_func_connection_id :public Item_long_func
};





class Item_func_signed :public Item_int_func
{
public:
Expand Down
110 changes: 110 additions & 0 deletions sql/sql_repl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2122,6 +2122,7 @@ 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,
Expand Down Expand Up @@ -5838,4 +5839,113 @@ int compare_log_name(const char *log_1, const char *log_2) {
return res;
}

/**
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's what good doxygen looks like! Nice! I'd consider doing some @sa too towards the bigger picture/subsystem. But this is optional.

@brief Helper function to validate if all GTIDs in a given string are reachable/viable.

This checks the requested GTID state against the current binlog state. It evaluates:
1. If binary logging is enabled.
2. If the GTID string can be parsed correctly.
3. If each GTID is present in the master's binlog state and has not been written in the future.
4. If all requested GTIDs have not been purged from the binary log files.

@param gtid_str The input string containing GTIDs.
@param is_reachable [OUT] Set to true if all requested GTIDs exist and are not purged.

@retval true An error occurred (binary logging disabled or malformed string).
@retval false Success. The reachability state is stored in `is_reachable`.
*/
bool rpl_gtid_pos_check_reachable(String *gtid_str, bool *is_reachable)
{

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));
return true;
}

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.
*/
return true;
}

/*
Verify that each requested GTID actually exists in the binlog state
and is not in the future.
*/
for (uint32 i= 0; i < state.hash.records; ++i)
{
slave_connection_state::entry *entry=
(slave_connection_state::entry *)my_hash_element(&state.hash, i);
rpl_gtid *slave_gtid= &entry->gtid;
rpl_gtid master_gtid;
rpl_gtid master_replication_gtid;

bool in_binlog= mysql_bin_log.find_in_binlog_state(slave_gtid->domain_id,
slave_gtid->server_id,
&master_gtid);
if (in_binlog)
{
if (slave_gtid->seq_no > master_gtid.seq_no)
{
*is_reachable= false;
return false;
}
}
else
{
bool start_at_own_slave_pos=
rpl_global_gtid_slave_state->domain_to_gtid(slave_gtid->domain_id,
&master_replication_gtid) &&
slave_gtid->server_id == master_replication_gtid.server_id &&
slave_gtid->seq_no == master_replication_gtid.seq_no;
if (!start_at_own_slave_pos)
{
*is_reachable= false;
return false;
}
}
}

/*
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)
{
*is_reachable= false;
return false; // At least one requested GTID has been purged.
}

*is_reachable= true;
return false; // All requested GTIDs are currently viable/reachable.
}

#endif /* HAVE_REPLICATION */
2 changes: 2 additions & 0 deletions sql/sql_repl.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ 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

struct LOAD_FILE_IO_CACHE : public IO_CACHE { };
Expand Down
Loading