use crate::error::PgError;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct Lsn(pub u64);
impl Lsn {
pub const fn as_u64(self) -> u64 {
self.0
}
pub const fn from_u64(v: u64) -> Self {
Self(v)
}
}
impl std::str::FromStr for Lsn {
type Err = PgError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((high_str, low_str)) = s.split_once('/') else {
return Err(PgError::Protocol(format!(
"invalid LSN {s:?}: missing '/' separator"
)));
};
if low_str.contains('/') {
return Err(PgError::Protocol(format!(
"invalid LSN {s:?}: expected exactly one '/' separator, found more than one"
)));
}
if high_str.is_empty() {
return Err(PgError::Protocol(format!(
"invalid LSN {s:?}: segment before '/' is empty"
)));
}
if low_str.is_empty() {
return Err(PgError::Protocol(format!(
"invalid LSN {s:?}: segment after '/' is empty"
)));
}
let high = u32::from_str_radix(high_str, 16).map_err(|e| {
PgError::Protocol(format!(
"invalid LSN {s:?}: segment {high_str:?} is not a valid 32-bit hexadecimal value: {e}"
))
})?;
let low = u32::from_str_radix(low_str, 16).map_err(|e| {
PgError::Protocol(format!(
"invalid LSN {s:?}: segment {low_str:?} is not a valid 32-bit hexadecimal value: {e}"
))
})?;
Ok(Lsn((u64::from(high) << 32) | u64::from(low)))
}
}
impl std::fmt::Display for Lsn {
#[allow(clippy::cast_possible_truncation)]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let high = (self.0 >> 32) as u32;
let low = (self.0 & 0xFFFF_FFFF) as u32;
write!(f, "{high:X}/{low:X}")
}
}
const PG_EPOCH_OFFSET_MICROS: i64 = 946_684_800_000_000;
pub fn pg_micros_to_unix_micros(pg_micros: i64) -> i64 {
pg_micros + PG_EPOCH_OFFSET_MICROS
}
pub fn unix_micros_to_pg_micros(unix_micros: i64) -> i64 {
unix_micros - PG_EPOCH_OFFSET_MICROS
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_zero() {
assert_eq!("0/0".parse::<Lsn>().unwrap(), Lsn(0));
}
#[test]
fn parse_documented_example() {
let lsn: Lsn = "16/B374D848".parse().unwrap();
assert_eq!(lsn, Lsn((0x16_u64 << 32) | 0xB374_D848_u64));
}
#[test]
fn parse_lowercase_hex() {
let lsn: Lsn = "a/b".parse().unwrap();
assert_eq!(lsn, Lsn((0xa_u64 << 32) | 0xb_u64));
}
#[test]
fn parse_mixed_case() {
let lsn: Lsn = "aB/Cd".parse().unwrap();
assert_eq!(lsn, Lsn((0xAB_u64 << 32) | 0xCD_u64));
}
#[test]
fn parse_max_values() {
let lsn: Lsn = "FFFFFFFF/FFFFFFFF".parse().unwrap();
assert_eq!(lsn, Lsn(u64::MAX));
}
#[test]
fn parse_err_no_slash() {
let result = "16B374D848".parse::<Lsn>();
assert!(matches!(result, Err(PgError::Protocol(_))));
}
#[test]
fn parse_err_empty_string() {
let result = "".parse::<Lsn>();
assert!(matches!(result, Err(PgError::Protocol(_))));
}
#[test]
fn parse_err_two_slashes() {
let result = "1/2/3".parse::<Lsn>();
assert!(matches!(result, Err(PgError::Protocol(_))));
}
#[test]
fn parse_err_empty_high_half() {
let result = "/1".parse::<Lsn>();
assert!(matches!(result, Err(PgError::Protocol(_))));
}
#[test]
fn parse_err_empty_low_half() {
let result = "1/".parse::<Lsn>();
assert!(matches!(result, Err(PgError::Protocol(_))));
}
#[test]
fn parse_err_non_hex_high() {
let result = "G/1".parse::<Lsn>();
assert!(matches!(result, Err(PgError::Protocol(_))));
}
#[test]
fn parse_err_non_hex_low() {
let result = "1/ZZZZ".parse::<Lsn>();
assert!(matches!(result, Err(PgError::Protocol(_))));
}
#[test]
fn parse_err_high_overflows_u32() {
let result = "100000000/1".parse::<Lsn>();
assert!(matches!(result, Err(PgError::Protocol(_))));
}
#[test]
fn parse_err_low_overflows_u32() {
let result = "1/100000000".parse::<Lsn>();
assert!(matches!(result, Err(PgError::Protocol(_))));
}
#[test]
fn display_zero() {
assert_eq!(Lsn(0).to_string(), "0/0");
}
#[test]
fn display_max() {
assert_eq!(Lsn(u64::MAX).to_string(), "FFFFFFFF/FFFFFFFF");
}
#[test]
fn display_no_leading_zeros() {
assert_eq!(Lsn(0x1000).to_string(), "0/1000");
}
#[test]
fn display_documented_example() {
let lsn = Lsn((0x16_u64 << 32) | 0xB374_D848_u64);
assert_eq!(lsn.to_string(), "16/B374D848");
}
#[test]
fn round_trip_zero() {
let lsn = Lsn(0);
assert_eq!(lsn.to_string().parse::<Lsn>().unwrap(), lsn);
}
#[test]
fn round_trip_max() {
let lsn = Lsn(u64::MAX);
assert_eq!(lsn.to_string().parse::<Lsn>().unwrap(), lsn);
}
#[test]
fn round_trip_one() {
let lsn = Lsn(1);
assert_eq!(lsn.to_string().parse::<Lsn>().unwrap(), lsn);
}
#[test]
fn round_trip_mid_range() {
let lsn = Lsn(0x1234_5678_9ABC_DEF0);
assert_eq!(lsn.to_string().parse::<Lsn>().unwrap(), lsn);
}
#[test]
fn round_trip_high_bits_only() {
let lsn = Lsn(0xFFFF_FFFF_0000_0000);
assert_eq!(lsn.to_string(), "FFFFFFFF/0");
assert_eq!(lsn.to_string().parse::<Lsn>().unwrap(), lsn);
}
#[test]
fn round_trip_low_bits_only() {
let lsn = Lsn(0x0000_0000_FFFF_FFFF);
assert_eq!(lsn.to_string(), "0/FFFFFFFF");
assert_eq!(lsn.to_string().parse::<Lsn>().unwrap(), lsn);
}
#[test]
fn round_trip_realistic_wal_position() {
let lsn: Lsn = "16/B374D848".parse().unwrap();
assert_eq!(lsn.to_string().parse::<Lsn>().unwrap(), lsn);
}
#[test]
fn as_u64_from_u64_round_trip() {
let lsn = Lsn::from_u64(0x1234_5678);
assert_eq!(lsn.as_u64(), 0x1234_5678);
assert_eq!(Lsn::from_u64(lsn.as_u64()), lsn);
}
#[test]
fn pg_epoch_zero_is_known_unix_micros() {
assert_eq!(pg_micros_to_unix_micros(0), 946_684_800_000_000);
}
#[test]
fn unix_epoch_zero_is_known_pg_micros() {
assert_eq!(unix_micros_to_pg_micros(0), -946_684_800_000_000);
}
#[test]
fn epoch_pg_to_unix_round_trip() {
for pg_micros in [
0_i64,
1,
-1,
946_684_800_000_000,
1_234_567_890_123_456,
-1_234_567_890,
i64::MIN / 2,
i64::MAX / 2,
] {
let unix_micros = pg_micros_to_unix_micros(pg_micros);
assert_eq!(unix_micros_to_pg_micros(unix_micros), pg_micros);
}
}
#[test]
fn epoch_unix_to_pg_round_trip() {
for unix_micros in [0_i64, 946_684_800_000_000, -946_684_800_000_000, 1] {
let pg_micros = unix_micros_to_pg_micros(unix_micros);
assert_eq!(pg_micros_to_unix_micros(pg_micros), unix_micros);
}
}
}