use crate::metadata::DkMeta;
mod oid {
pub const BOOL: u32 = 16;
pub const INT2: u32 = 21;
pub const INT4: u32 = 23;
pub const INT8: u32 = 20;
pub const FLOAT4: u32 = 700;
pub const FLOAT8: u32 = 701;
pub const TEXT: u32 = 25;
pub const VARCHAR: u32 = 1043;
pub const TIMESTAMP: u32 = 1114;
pub const TIMESTAMPTZ: u32 = 1184;
pub const UUID: u32 = 2950;
}
fn encode_param_msgpack(
param: &(dyn pg::types::ToSql + Sync),
type_oid: u32,
out: &mut Vec<u8>,
) -> bool {
let Some(pg_type) = pg_type_from_oid(type_oid) else {
log::warn!("encode_param_msgpack: unsupported OID {type_oid}");
return false;
};
let mut pg_buf = bytes::BytesMut::with_capacity(32);
let Ok(is_null) = param.to_sql_checked(&pg_type, &mut pg_buf) else {
return false;
};
if matches!(is_null, pg::types::IsNull::Yes) {
rmp::encode::write_nil(out).expect("write to Vec never fails");
return true;
}
match type_oid {
oid::BOOL => {
let v = pg_buf.first().is_some_and(|&b| b != 0);
rmp::encode::write_bool(out, v).expect("write to Vec never fails");
}
oid::INT2 => {
let v = i16::from_be_bytes([pg_buf[0], pg_buf[1]]);
rmp::encode::write_sint(out, i64::from(v)).expect("write to Vec never fails");
}
oid::INT4 => {
let v = i32::from_be_bytes([pg_buf[0], pg_buf[1], pg_buf[2], pg_buf[3]]);
rmp::encode::write_sint(out, i64::from(v)).expect("write to Vec never fails");
}
oid::INT8 => {
let v = i64::from_be_bytes(pg_buf[..8].try_into().expect("pg INT8 is always 8 bytes"));
rmp::encode::write_sint(out, v).expect("write to Vec never fails");
}
oid::FLOAT4 => {
let v =
f32::from_be_bytes(pg_buf[..4].try_into().expect("pg FLOAT4 is always 4 bytes"));
rmp::encode::write_f32(out, v).expect("write to Vec never fails");
}
oid::FLOAT8 => {
let v =
f64::from_be_bytes(pg_buf[..8].try_into().expect("pg FLOAT8 is always 8 bytes"));
rmp::encode::write_f64(out, v).expect("write to Vec never fails");
}
oid::TEXT | oid::VARCHAR => {
out.extend_from_slice(&pg_buf);
}
oid::TIMESTAMP | oid::TIMESTAMPTZ => {
encode_datetime(&pg_buf, out);
}
oid::UUID => {
rmp::encode::write_ext_meta(out, 16, 0x02).expect("write to Vec never fails");
out.extend_from_slice(&pg_buf[..16]);
}
_ => return false,
}
true
}
fn pg_type_from_oid(type_oid: u32) -> Option<pg::types::Type> {
Some(match type_oid {
oid::BOOL => pg::types::Type::BOOL,
oid::INT2 => pg::types::Type::INT2,
oid::INT4 => pg::types::Type::INT4,
oid::INT8 => pg::types::Type::INT8,
oid::FLOAT4 => pg::types::Type::FLOAT4,
oid::FLOAT8 => pg::types::Type::FLOAT8,
oid::TEXT => pg::types::Type::TEXT,
oid::VARCHAR => pg::types::Type::VARCHAR,
oid::TIMESTAMP => pg::types::Type::TIMESTAMP,
oid::TIMESTAMPTZ => pg::types::Type::TIMESTAMPTZ,
oid::UUID => pg::types::Type::UUID,
_ => return None,
})
}
const PG_EPOCH_DIFF_US: i64 = 946_684_800_000_000;
fn encode_datetime(pg_buf: &[u8], out: &mut Vec<u8>) {
let pg_us = i64::from_be_bytes(pg_buf[..8].try_into().expect("pg INT8 is always 8 bytes"));
let unix_us = pg_us + PG_EPOCH_DIFF_US;
let seconds = unix_us.div_euclid(1_000_000);
let nsec = i32::try_from(unix_us.rem_euclid(1_000_000) * 1_000)
.expect("nanoseconds should fit in i32");
if nsec == 0 {
rmp::encode::write_ext_meta(out, 8, 0x04).expect("write to Vec never fails");
out.extend_from_slice(&seconds.to_le_bytes());
} else {
rmp::encode::write_ext_meta(out, 16, 0x04).expect("write to Vec never fails");
out.extend_from_slice(&seconds.to_le_bytes());
out.extend_from_slice(&nsec.to_le_bytes());
out.extend_from_slice(&0i16.to_le_bytes());
out.extend_from_slice(&0i16.to_le_bytes());
}
}
pub(crate) fn compute_bucket_id(
params: &[&(dyn pg::types::ToSql + Sync)],
dk_meta: &DkMeta,
bucket_count: u64,
) -> Option<u64> {
let mut buf = Vec::with_capacity(dk_meta.keys.len() * 16);
for &(idx, type_oid) in &dk_meta.keys {
if !encode_param_msgpack(params[(idx - 1) as usize], type_oid, &mut buf) {
return None;
}
}
let hash = murmur3::murmur3_32(&mut std::io::Cursor::new(&buf), 13)
.expect("hashing in-memory buffer should not fail");
let bucket_id = u64::from(hash) % bucket_count + 1;
Some(bucket_id)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metadata::DkMeta;
#[test]
fn compute_bucket_id_maps_1based_key_to_0based_params() {
let dk = DkMeta {
tier: Some("default".to_string()),
keys: vec![(1, oid::INT8)],
};
let v: i64 = 42;
let params: Vec<&(dyn pg::types::ToSql + Sync)> = vec![&v];
assert!(
compute_bucket_id(¶ms, &dk, 3000).is_some(),
"must not panic: 1-based idx 1 must map to params[0], not params[1]"
);
}
}