// Copyright 2026 Noverberrain.
// SPDX-License-Identifier: Apache-2.0
///|
pub(all) enum TransactionAction {
Begin
Commit
Rollback
Xid(UInt64)
} derive(Debug, Eq)
///|
pub extend TransactionAction with Eq::{equal, not_equal}
///|
pub extend TransactionAction with @debug.Debug::{to_repr}
///|
pub struct TransactionBoundary {
header : EventHeader
action : TransactionAction
} derive(Debug, Eq)
///|
pub extend TransactionBoundary with Eq::{equal, not_equal}
///|
pub extend TransactionBoundary with @debug.Debug::{to_repr}
///|
/// Parse checksum-verified QUERY (2) or XID (16) transaction control.
/// QUERY requires a 13-byte post-header, zero error_code, bounded status/db
/// fields and the terminating db NUL. Status variable contents are not decoded.
/// Only BEGIN, COMMIT and ROLLBACK are accepted, case-insensitively, with ASCII
/// whitespace and at most one final semicolon. No general SQL interpretation.
pub fn TransactionBoundary::parse(
format : FormatDescription,
data : BytesView,
file_offset~ : UInt64,
) -> TransactionBoundary raise DecodeError {
let header = EventHeader::parse(data, file_offset~)
guard header.event_type == 2 || header.event_type == 16 else {
raise UnsupportedTransactionEvent(
file_offset~,
event_type=header.event_type,
)
}
let body = format.payload(data, file_offset~)
let action = if header.event_type == 16 {
guard format.post_header_lengths.get(15) is Some(0) && body.length() == 8 else {
raise InvalidTransactionEvent(
file_offset~,
reason="XID requires zero post-header and exactly eight body bytes",
)
}
let mut xid = 0UL
for i in 0..<8 {
xid = xid | (body[i].to_uint().to_uint64() << (8 * i))
}
Xid(xid)
} else {
guard format.post_header_lengths.get(1) is Some(13) && body.length() >= 13 else {
raise InvalidTransactionEvent(
file_offset~,
reason="QUERY requires a complete thirteen-byte post-header",
)
}
guard read_u16_le(body, 9) == 0 else {
raise InvalidTransactionEvent(
file_offset~,
reason="failed QUERY cannot establish a transaction boundary",
)
}
let status_length = read_u16_le(body, 11).to_uint().reinterpret_as_int()
let database_length = body[8].to_int()
let query_start = 13 + status_length + database_length + 1
guard query_start <= body.length() && body[query_start - 1] == 0 else {
raise InvalidTransactionEvent(
file_offset~,
reason="truncated status/database or missing database terminator",
)
}
let database_start = 13 + status_length
for i in database_start..<(query_start - 1) {
guard body[i] != 0 else {
raise InvalidTransactionEvent(
file_offset~,
reason="NUL inside QUERY database name",
)
}
}
let mut start = query_start
let mut end = body.length()
while start < end && transaction_space(body[start]) {
start += 1
}
while end > start && transaction_space(body[end - 1]) {
end -= 1
}
if end > start && body[end - 1] == 59 {
end -= 1
while end > start && transaction_space(body[end - 1]) {
end -= 1
}
}
guard end - start == 5 || end - start == 6 || end - start == 8 else {
raise UnsupportedTransactionStatement(file_offset~)
}
let statement = Bytes::makei(end - start, i => {
let byte = body[start + i].to_int()
(if byte >= 97 && byte <= 122 { byte - 32 } else { byte }).to_byte()
})
if statement == b"BEGIN" {
Begin
} else if statement == b"COMMIT" {
Commit
} else if statement == b"ROLLBACK" {
Rollback
} else {
raise UnsupportedTransactionStatement(file_offset~)
}
}
{ header, action, }
}
///|
fn transaction_space(byte : Byte) -> Bool {
byte == 32 || (byte >= 9 && byte <= 13)
}