WL#6591: Restore backup on existing GTID-aware server - make GTID_PURGED settable always

Affects: Server-8.0   —   Status: Complete   —   Priority: Medium

The goal of this worklog is to extend the possible use cases where it is 
allowed to restore a backup on a server that has GTIDs enabled. In particular, 
we make it possible to restore backups on an existing server without losing 
existing GTID information and binary logs.

In general, the server is always in a particular state. The state was created by 
executing a sequence of transactions. The variable GTID_EXECUTED contains the 
GTIDs of the transactions that created the current state. Some of these 
transactions may exist in a binary log, whereas other transactions may not exist 
- e.g. because the binary log was purged, or because the server state was 
created by restoring a backup. The variable GTID_PURGED contains the subset of 
GTID_EXECUTED that does *not* exist in the binary log. The latter
is subset of the former:

   GTID_PURGED \subset= GTID_EXECUTED                             (*)

To give some example, say the executed and the purged sets are as the following

  GTID_EXECUTED : = \union
                    A:1-10,A:21-30,
                    B:9-100,
		    C:2-1000

  GTID_PURGED := A:21-30,B:9-100,C:2-2

where A,B,C are uuid:s of the source servers; notice (*) is satisfied.
This combination translates in that the server's binlog does not
contain the 2nd range from A, misses out the whole B, and contains
all but the very first known transaction from C which is C:2.

When a backup is restored on a server, the state changes and thus we need to add 
some GTIDs to GTID_EXECUTED. The GTIDs that we need to add are precisely those 
that existed in GTID_EXECUTED when the backup was taken\footnote{
notice, the backup is here the full backup, not a partial one}.
Unless the binary logs are restored too, the same GTIDs need to be added to
GTID_PURGED.

Currently, it is possible to add GTIDs to GTID_PURGED only when GTID_EXECUTED is 
empty, i.e., when restoring a backup on an empty server. In this worklog, we 
will make it possible to add GTIDs to GTID_PURGED at any time, even when 
GTID_EXECUTED is nonempty.

Notice that GTID_PURGED initialization by restarted server does not
have to require any refinement. Purging gtids "in the middle", through backup
restore, has been already possible. When the server restarts the purged set is
initialized through provisioned mysql.gtid_executed.

== Functional Requirements ==

F1. It shall be possible to assign to GTID_PURGED in run time,
    regardless of whether GTID_EXECUTED is empty or not,
    through the following statement:

      SET GTID_PURGED = "<gtid_set>"

F2. It shall be possible to append a certain gtid set via a "flavored"
    syntax of the SET assignment operator:

      SET GTID_PURGED = "+<gtid_set>"

    where the '+' character must be first character of the rhs string.
    The increment-like syntax is akin to that of DEBUG system variable.

F3. The string "<gtid_set>" shall be a string of the form described at
    http://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html
    If it does not have this form, an error shall be generated.

F4. "<gtid_set>" the rhs of the assignment operator is a set of GTIDs.
    In case of the append assignment this set must be disjoint with
    the current values of @@GLOBAL.GTID_EXECUTED and @@GLOBAL.GTID_OWNED.

    In case of the plain assignment the being assigned set must be
    a superset of the current GTID_PURGED's value, disjoint with GTID_EXECUTED
    that are not purged, and disjoint with @@GLOBAL.GTID_OWNED.

    When the conditions do not hold an error shall be generated.

F5. The result of the assignment operator and the function work shall be:
    - GTID_PURGED becomes the union of GTID_PURGED and "<gtid_set>".
    - GTID_EXECUTED becomes the union of GTID_EXECUTED and "<gtid_set>".

F6. Prerequisites of the WL must remain:
    - The binary log is *not* rotated.
      The GTIDs of "<gtid_set>" are stored in
      'mysql.gtid_executed' table which is the stable memory for the purge set
      initialization.

== Non Functional Requirements ==

N1. The old dump, described by the old plainly assinged GTID_PURGED
    should be restorable on the new server in conditions
    of the old server that is when the current value GTID_EXECUTED is empty.

N2. The new dump should be restorable on the old server
    in conditions of the old server.

Footnotes:

$. Previous_gtids_log_event might've been considered as the purged
   info placeholder which would little bit complicate the purged set
   initialization requiring two logs to process rather than one, but
   on the other hand require only the binlog.

IS-1: The new increment-like assignment

  SET GTID_PURGED = "+<gtid_set>"

normally should be generated by a backup program.
It's semantics is to append the disjoint rhs set to the current values
of GTID_PURGED and GTID_EXECUTED.
See F4 for constraints.

IS-2: The former syntax of

  SET GTID_PURGED = "<gtid_set>"

is made also to cover a new use case when GTID_PURGED is not empty,
that is to override the current value.
See F4 for constraints.

The core change in code is that the condition to allow setting GTID_PURGED is 
changed. Before it was:

  GTID_EXECUTED \not= \empty and GTID_OWNED \not= \empty

We need to change it to 

  GTID_OWNED \cap gtid_set => \empty

combined with one of the following two:

  GTID_EXECUTED \cap  gtid_set => empty,

  gtid_set \superset GTID_PURGED => true and (GTID_EXECUTED - GTID_PURGED) \cap
gtid_set => empty

where the first deals with IS-1 syntax, and the 2nd covers the overriding
semantics of IS-2 that is applies when GTID_EXECUTED \not= \empty.

In addition we need to change the error message generated when the condition is 
violated accordingly, and turn mysqldump to output the append semantics assignment
in response to --set-gtid-purged = AUTO.
diff --git a/sql/rpl_gtid_state.cc b/sql/rpl_gtid_state.cc
index 7829df2..17a965d 100644
--- a/sql/rpl_gtid_state.cc
+++ b/sql/rpl_gtid_state.cc
@@ -686,21 +686,22 @@ enum_return_status Gtid_state::add_lost_gtids(const 
Gtid_set *gtid_set)
 
   gtid_set->dbug_print("add_lost_gtids");
 
-  if (!executed_gtids.is_empty())
+  if (executed_gtids.is_intersection_nonempty(gtid_set))
   {
-    BINLOG_ERROR((ER(ER_CANT_SET_GTID_PURGED_WHEN_GTID_EXECUTED_IS_NOT_EMPTY)),
-                 (ER_CANT_SET_GTID_PURGED_WHEN_GTID_EXECUTED_IS_NOT_EMPTY,
-                 MYF(0)));
+    my_error(ER_CANT_SET_GTID_PURGED_WHEN_GTID_EXECUTED_IS_NOT_EMPTY, MYF(0));
     RETURN_REPORTED_ERROR;
   }
-  if (!owned_gtids.is_empty())
+  Gtid_set owned_gtid_set(global_sid_map);
+  // Reduce risk for memory allocation in owned_gtids.get_gtids
+  Gtid_set::Interval interval_list[64];
+  owned_gtid_set.add_interval_memory(64, interval_list);
+  owned_gtids.get_gtids(owned_gtid_set);
+  if (owned_gtid_set.is_intersection_nonempty(gtid_set))
   {
-    BINLOG_ERROR((ER(ER_CANT_SET_GTID_PURGED_WHEN_OWNED_GTIDS_IS_NOT_EMPTY)),
-                 (ER_CANT_SET_GTID_PURGED_WHEN_OWNED_GTIDS_IS_NOT_EMPTY,
-                 MYF(0)));
+    my_error(ER_CANT_SET_GTID_PURGED_WHEN_OWNED_GTIDS_IS_NOT_EMPTY, MYF(0));
     RETURN_REPORTED_ERROR;
   }
-  DBUG_ASSERT(lost_gtids.is_empty());
+  DBUG_ASSERT(!lost_gtids.is_intersection_nonempty(gtid_set));
 
   if (save(gtid_set))
     RETURN_REPORTED_ERROR;
diff --git a/sql/share/errmsg-utf8.txt b/sql/share/errmsg-utf8.txt
index 1964a5b..d08cad4 100755
--- a/sql/share/errmsg-utf8.txt
+++ b/sql/share/errmsg-utf8.txt
@@ -6901,10 +6901,10 @@ ER_CANT_SET_GTID_PURGED_WHEN_GTID_MODE_IS_OFF
   eng "@@GLOBAL.GTID_PURGED can only be set when @@GLOBAL.GTID_MODE = ON."
 
 ER_CANT_SET_GTID_PURGED_WHEN_GTID_EXECUTED_IS_NOT_EMPTY
-  eng "@@GLOBAL.GTID_PURGED can only be set when @@GLOBAL.GTID_EXECUTED is 
empty."
+  eng "@@GLOBAL.GTID_PURGED can only be set to a value that does not intersect 
with @@GLOBAL.GTID_EXECUTED. Suggest to use GTID_SUBTRACT(gtid_set, 
@@GLOBAL.GTID_EXECUTED)."
 
 ER_CANT_SET_GTID_PURGED_WHEN_OWNED_GTIDS_IS_NOT_EMPTY
-  eng "@@GLOBAL.GTID_PURGED can only be set when there are no ongoing 
transactions (not even in other clients)."
+  eng "@@GLOBAL.GTID_PURGED can only be set to a value that does not intersect 
with @@GLOBAL.GTID_PURGED."
 
 ER_GTID_PURGED_WAS_CHANGED
   eng "@@GLOBAL.GTID_PURGED was changed from '%s' to '%s'."