// Copyright 2026 Noverberrain.
// SPDX-License-Identifier: Apache-2.0
///|
/// One GTID interval [start, end_exclusive). Values fit MySQL's positive GNO
/// range; end_exclusive may equal INT64_MAX, which is not itself a valid GNO.
pub struct GtidInterval {
start : UInt64
end_exclusive : UInt64
} derive(Debug, Eq)
///|
pub extend GtidInterval with Eq::{equal, not_equal}
///|
pub extend GtidInterval with @debug.Debug::{to_repr}
///|
/// A 16-byte SID and its ordered, nonoverlapping, nonadjacent intervals.
/// The SID is retained verbatim, without UUID version/variant restrictions.
pub struct GtidSidIntervals {
sid : Bytes
intervals : ReadOnlyArray[GtidInterval]
} derive(Debug, Eq)
///|
pub extend GtidSidIntervals with Eq::{equal, not_equal}
///|
pub extend GtidSidIntervals with @debug.Debug::{to_repr}
///|
/// Historical GTIDs declared by PREVIOUS_GTIDS_EVENT (35).
/// SID entries retain wire order, including repeated SIDs and empty entries;
/// this is a decoded representation, not a normalized GTID set.
pub struct PreviousGtidsEvent {
header : EventHeader
sids : ReadOnlyArray[GtidSidIntervals]
} derive(Debug, Eq)
///|
pub extend PreviousGtidsEvent with Eq::{equal, not_equal}
///|
pub extend PreviousGtidsEvent with @debug.Debug::{to_repr}
///|
priv struct GtidSetReader {
data : BytesView
file_offset : UInt64
mut position : Int32
}
///|
fn GtidSetReader::invalid(self : GtidSetReader, reason : String) -> DecodeError {
InvalidPreviousGtidsEvent(
file_offset=self.file_offset,
body_offset=self.position,
reason~,
)
}
///|
fn GtidSetReader::remaining(self : GtidSetReader) -> Int32 {
self.data.length() - self.position
}
///|
fn GtidSetReader::uint64(self : GtidSetReader) -> UInt64 raise DecodeError {
guard self.remaining() >= 8 else {
raise self.invalid("truncated eight-byte integer")
}
let mut value = 0UL
for i in 0..<8 {
value = value |
(self.data[self.position + i].to_uint().to_uint64() << (8 * i))
}
self.position += 8
value
}
///|
/// Decode a complete MySQL 8.0 event using its current FDE, including CRC/OFF.
/// Requires a zero-length post-header and exact consumption of the event body.
/// Counts are checked against remaining bytes and quotas before narrowing or
/// allocating entries. max_intervals bounds the sum across all SID entries.
/// Zero quotas allow only the corresponding empty collection; negative quotas
/// are invalid. Trailing input outside the declared event is ignored.
/// This does not validate transaction history, advance replication state, or
/// add support for GTID-tagged transactions to TransactionDecoder/the CLI.
pub fn PreviousGtidsEvent::parse(
format : FormatDescription,
data : BytesView,
file_offset~ : UInt64,
max_event_size~ : UInt32 = 16_777_216,
max_sids~ : Int32 = 1024,
max_intervals~ : Int32 = 65536,
) -> PreviousGtidsEvent raise DecodeError {
guard max_sids >= 0 && max_intervals >= 0 else {
raise InvalidGtidSetLimits(max_sids~, max_intervals~)
}
let header = EventHeader::parse(data, file_offset~, max_event_size~)
guard header.event_type == 35 else {
raise UnexpectedEventType(
file_offset~,
expected=35,
actual=header.event_type,
)
}
let body = format.payload(data, file_offset~, max_event_size~)
let reader = GtidSetReader::{ data: body, file_offset, position: 0, }
guard format.post_header_lengths.get(34) is Some(0) else {
raise reader.invalid("PREVIOUS_GTIDS requires a zero-length post-header")
}
let count = reader.uint64()
guard count <= max_sids.reinterpret_as_uint().to_uint64() else {
raise GtidSetLimitExceeded(
file_offset~,
reason="SID count exceeds max_sids",
)
}
guard count <= (reader.remaining() / 24).reinterpret_as_uint().to_uint64() else {
raise reader.invalid("SID count exceeds remaining body")
}
let sids = []
let mut total_intervals = 0
for _ in 0..= 24 else {
raise reader.invalid("truncated SID entry")
}
let sid = body[reader.position:reader.position + 16].to_owned()
reader.position += 16
let interval_count = reader.uint64()
guard interval_count <=
(max_intervals - total_intervals).reinterpret_as_uint().to_uint64() else {
raise GtidSetLimitExceeded(
file_offset~,
reason="total interval count exceeds max_intervals",
)
}
guard interval_count <=
(reader.remaining() / 16).reinterpret_as_uint().to_uint64() else {
raise reader.invalid("interval count exceeds remaining body")
}
total_intervals += interval_count.to_int()
let intervals = []
let mut last_end = 0UL
for _ in 0.. last_end &&
end_exclusive > start &&
end_exclusive <= 0x7fffffffffffffffUL else {
raise reader.invalid(
"invalid, unordered, overlapping or adjacent interval",
)
}
intervals.push(GtidInterval::{ start, end_exclusive, })
last_end = end_exclusive
}
sids.push(GtidSidIntervals::{
sid,
intervals: ReadOnlyArray::from_array(intervals),
})
}
guard reader.remaining() == 0 else {
raise reader.invalid("trailing bytes inside PREVIOUS_GTIDS body")
}
{ header, sids: ReadOnlyArray::from_array(sids), }
}