use rupg_common::{Error, Result, SqlState};
use rupg_pgcatalog::builtin;
use rupg_types::{
Array, JsonItem, JsonToken as Token, JsonTree, NetFamily, Numeric, NumericSign, RANGE_EMPTY,
RANGE_LB_INF, RANGE_UB_INF, Range, TypeInfo, Value, format_type, numeric_in, oid,
};
use crate::{Kernel, bad_value, not_yet, type_error};
macro_rules! fixed {
($src:expr; $($plain:literal, $seeded:literal => $ty:expr;)*) => {
match $src {
$(
$plain => |_, args| plain($ty, args),
$seeded => |_, args| seeded($ty, args),
)*
_ => return None,
}
};
}
pub(crate) fn by_src(src: &str) -> Option<Kernel> {
match src {
"hash_array" | "hash_range" | "hash_multirange" | "hash_record" => {
return Some(|call, args| plain(call.args.first().copied().unwrap_or(0), args));
}
"hash_array_extended"
| "hash_range_extended"
| "hash_multirange_extended"
| "hash_record_extended" => {
return Some(|call, args| seeded(call.args.first().copied().unwrap_or(0), args));
}
_ => {}
}
Some(fixed! { src;
"hashbool", "hashboolextended" => oid::BOOL;
"hashchar", "hashcharextended" => oid::CHAR;
"hashint2", "hashint2extended" => oid::INT2;
"hashint4", "hashint4extended" => oid::INT4;
"hashint8", "hashint8extended" => oid::INT8;
"hashoid", "hashoidextended" => oid::OID;
"hashenum", "hashenumextended" => oid::OID;
"hashoid8", "hashoid8extended" => oid::OID8;
"hashxid", "hashxidextended" => oid::XID;
"hashxid8", "hashxid8extended" => oid::XID8;
"hashcid", "hashcidextended" => oid::CID;
"hashtid", "hashtidextended" => oid::TID;
"hashfloat4", "hashfloat4extended" => oid::FLOAT4;
"hashfloat8", "hashfloat8extended" => oid::FLOAT8;
"hash_numeric", "hash_numeric_extended" => oid::NUMERIC;
"hashoidvector", "hashoidvectorextended" => oid::OIDVECTOR;
"hashname", "hashnameextended" => oid::NAME;
"hashtext", "hashtextextended" => oid::TEXT;
"hashbpchar", "hashbpcharextended" => oid::BPCHAR;
"hashbytea", "hashbyteaextended" => oid::BYTEA;
"hashdate", "hashdateextended" => oid::DATE;
"time_hash", "time_hash_extended" => oid::TIME;
"timetz_hash", "timetz_hash_extended" => oid::TIMETZ;
"timestamp_hash", "timestamp_hash_extended" => oid::TIMESTAMP;
"timestamptz_hash", "timestamptz_hash_extended" => oid::TIMESTAMPTZ;
"interval_hash", "interval_hash_extended" => oid::INTERVAL;
"uuid_hash", "uuid_hash_extended" => oid::UUID;
"hashmacaddr", "hashmacaddrextended" => oid::MACADDR;
"hashmacaddr8", "hashmacaddr8extended" => oid::MACADDR8;
"hashinet", "hashinetextended" => oid::INET;
"pg_lsn_hash", "pg_lsn_hash_extended" => oid::PG_LSN;
"jsonb_hash", "jsonb_hash_extended" => oid::JSONB;
})
}
fn plain(ty: u32, args: &[Value]) -> Result<Value> {
let value = args.first().ok_or_else(bad_value)?;
let hash = hash(ty, value, Seed::Plain)?;
#[allow(clippy::cast_possible_truncation)]
Ok(Value::Int4((hash as u32).cast_signed()))
}
fn seeded(ty: u32, args: &[Value]) -> Result<Value> {
let (Some(value), Some(Value::Int8(seed))) = (args.first(), args.get(1)) else {
return Err(bad_value());
};
Ok(Value::Int8(hash(ty, value, Seed::Extended(seed.cast_unsigned()))?.cast_signed()))
}
#[derive(Clone, Copy)]
enum Seed {
Plain,
Extended(u64),
}
impl Seed {
fn bytes(self, data: &[u8]) -> u64 {
match self {
Seed::Plain => u64::from(hash_bytes(data, 0).1),
Seed::Extended(seed) => {
let (b, c) = hash_bytes(data, seed);
u64::from(b) << 32 | u64::from(c)
}
}
}
fn word(self, word: u32) -> u64 {
match self {
Seed::Plain => u64::from(hash_word(word, 0).1),
Seed::Extended(seed) => {
let (b, c) = hash_word(word, seed);
u64::from(b) << 32 | u64::from(c)
}
}
}
fn int8(self, value: i64) -> u64 {
#[allow(clippy::cast_possible_truncation)]
let (low, high) = (value as u32, (value >> 32) as u32);
self.word(low ^ if value >= 0 { high } else { !high })
}
fn zero(self) -> u64 {
match self {
Seed::Plain => 0,
Seed::Extended(seed) => seed,
}
}
fn combine(self, result: u64, element: u64) -> u64 {
let next = (result << 5).wrapping_sub(result).wrapping_add(element);
match self {
Seed::Plain => next & u64::from(u32::MAX),
Seed::Extended(_) => next,
}
}
}
pub fn partition_hash(values: &[(u32, Value)]) -> Result<u64> {
let mut row = 0u64;
for (ty, value) in values {
if value.is_null() {
continue;
}
row = hash_combine64(row, extended_hash(*ty, value, HASH_PARTITION_SEED)?);
}
Ok(row)
}
pub const HASH_PARTITION_SEED: u64 = 0x7A5B_2236_7996_DCFD;
pub fn hash_combine64(row: u64, one: u64) -> u64 {
row ^ one.wrapping_add(0x49a0_f4dd_15e5_a8e3).wrapping_add(row << 54).wrapping_add(row >> 7)
}
pub fn extended_hash(ty: u32, value: &Value, seed: u64) -> Result<u64> {
hash(crate::io::base_type(ty), value, Seed::Extended(seed))
}
fn hash(ty: u32, value: &Value, seed: Seed) -> Result<u64> {
Ok(match (ty, value) {
(oid::BOOL, Value::Bool(v)) => seed.word(u32::from(*v)),
(oid::CHAR, Value::Char(v)) => seed.word(i32::from(v.cast_signed()).cast_unsigned()),
(oid::INT2, Value::Int2(v)) => seed.word(i32::from(*v).cast_unsigned()),
(oid::INT4 | oid::DATE, Value::Int4(v) | Value::Date(v)) => seed.word(v.cast_unsigned()),
(oid::INT8 | oid::OID8 | oid::XID8 | oid::PG_LSN, Value::Int8(v))
| (oid::TIME, Value::Time(v))
| (oid::TIMESTAMP, Value::Timestamp(v))
| (oid::TIMESTAMPTZ, Value::TimestampTz(v)) => seed.int8(*v),
(_, Value::Oid(v)) if is_oid(ty) => seed.word(*v),
(oid::TID, Value::Int8(v)) => {
let (block, offset) = crate::io::tid_parts(*v)?;
let mut data = [0; 6];
#[allow(clippy::cast_possible_truncation)]
{
data[..2].copy_from_slice(&((block >> 16) as u16).to_le_bytes());
data[2..4].copy_from_slice(&(block as u16).to_le_bytes());
}
data[4..].copy_from_slice(&offset.to_le_bytes());
seed.bytes(&data)
}
(oid::FLOAT4, Value::Float4(v)) => float(f64::from(*v), seed),
(oid::FLOAT8, Value::Float8(v)) => float(*v, seed),
(oid::NUMERIC, Value::Numeric(v)) => numeric(v, seed),
(oid::OIDVECTOR, Value::Array(array)) => {
let mut data = Vec::with_capacity(array.values.len() * 4);
for value in &array.values {
let Some(Value::Oid(v)) = value else { return Err(bad_value()) };
data.extend_from_slice(&v.to_le_bytes());
}
seed.bytes(&data)
}
(oid::TEXT | oid::VARCHAR | oid::NAME, Value::Text(v)) => seed.bytes(v.as_bytes()),
(oid::BPCHAR, Value::Text(v)) => seed.bytes(v.trim_end_matches(' ').as_bytes()),
(oid::BYTEA, Value::Bytea(v)) => seed.bytes(v),
(oid::TIMETZ, Value::TimeTz(time, zone)) => {
seed.int8(*time) ^ seed.word(zone.cast_unsigned())
}
(oid::INTERVAL, Value::Interval(v)) => {
#[allow(clippy::cast_possible_truncation)]
seed.int8(crate::compare::span(v) as i64)
}
(oid::UUID, Value::Uuid(v)) => seed.bytes(v),
(oid::MACADDR, Value::Int8(v)) => seed.bytes(&v.to_be_bytes()[2..]),
(oid::MACADDR8, Value::Int8(v)) => seed.bytes(&v.to_be_bytes()),
(oid::INET | oid::CIDR, Value::Inet(v)) => {
let (family, size) = match v.family {
NetFamily::V4 => (2, 4),
NetFamily::V6 => (3, 16),
};
let mut data = vec![family, v.bits];
data.extend_from_slice(&v.addr[..size]);
seed.bytes(&data)
}
(_, Value::Array(array)) => self::array(ty, array, seed)?,
(_, Value::Range(range)) => {
let subtype = builtin::range_subtype(ty).ok_or_else(|| no_hash(ty))?;
self::range(subtype, range, seed)?
}
(_, Value::Multirange(ranges)) => {
let subtype = builtin::multirange_range(ty)
.and_then(builtin::range_subtype)
.ok_or_else(|| no_hash(ty))?;
let mut result = 1;
for range in ranges {
result = seed.combine(result, self::range(subtype, range, seed)?);
}
result
}
(_, Value::Record(record)) => {
let mut result = 0;
for (&ty, value) in record.types.iter().zip(&record.values) {
let element = if value.is_null() {
0
} else {
hash(ty, value, seed).map_err(|err| {
if err.message() == no_hash(ty).message() {
no_hash_for(ty, seed)
} else {
err
}
})?
};
result = seed.combine(result, element);
}
result
}
(oid::JSONB, _) => jsonb(value.as_str().ok_or_else(bad_value)?, seed)?,
(oid::ACLITEM, _) => return Err(not_yet("the hash of aclitem")),
_ if has_hash(ty) => return Err(bad_value()),
_ => return Err(no_hash(ty)),
})
}
fn is_oid(ty: u32) -> bool {
matches!(
ty,
oid::OID
| oid::XID
| oid::CID
| oid::REGPROC
| oid::REGPROCEDURE
| oid::REGOPER
| oid::REGOPERATOR
| oid::REGCLASS
| oid::REGTYPE
| oid::REGCONFIG
| oid::REGDICTIONARY
| oid::REGNAMESPACE
| oid::REGROLE
| oid::REGCOLLATION
| oid::REGDATABASE
)
}
fn has_hash(ty: u32) -> bool {
matches!(
ty,
oid::JSONB
| oid::ACLITEM
| oid::BOOL
| oid::CHAR
| oid::INT2
| oid::INT4
| oid::INT8
| oid::OID8
| oid::XID8
| oid::PG_LSN
| oid::DATE
| oid::TIME
| oid::TIMESTAMP
| oid::TIMESTAMPTZ
| oid::TID
| oid::FLOAT4
| oid::FLOAT8
| oid::NUMERIC
| oid::OIDVECTOR
| oid::TEXT
| oid::VARCHAR
| oid::NAME
| oid::BPCHAR
| oid::BYTEA
| oid::TIMETZ
| oid::INTERVAL
| oid::UUID
| oid::MACADDR
| oid::MACADDR8
| oid::INET
| oid::CIDR
) || is_oid(ty)
}
fn no_hash(ty: u32) -> Error {
Error::new(
SqlState::UNDEFINED_FUNCTION,
format!("could not identify a hash function for type {}", format_type(ty)),
)
}
fn no_hash_for(ty: u32, seed: Seed) -> Error {
match seed {
Seed::Plain => no_hash(ty),
Seed::Extended(_) => Error::new(
SqlState::UNDEFINED_FUNCTION,
format!("could not identify an extended hash function for type {}", format_type(ty)),
),
}
}
pub(crate) fn uint32_extended(word: u32, seed: u64) -> u64 {
Seed::Extended(seed).word(word)
}
const JB_FARRAY: u32 = 0x4000_0000;
const JB_FOBJECT: u32 = 0x2000_0000;
fn jsonb(text: &str, seed: Seed) -> Result<u64> {
let tree = JsonTree::parse(text).map_err(type_error)?;
if matches!(tree.item(0), JsonItem::Array(v) if v.is_empty())
|| matches!(tree.item(0), JsonItem::Object(v) if v.is_empty())
{
return Ok(seed.zero());
}
let mut result = 0;
for token in tree.tokens() {
let element = match token {
Token::Begin { node, raw, .. } => {
let flag = if raw || matches!(tree.item(node), JsonItem::Array(_)) {
JB_FARRAY
} else {
JB_FOBJECT
};
result ^= match seed {
Seed::Plain => u64::from(flag),
Seed::Extended(_) => u64::from(flag) << 32 | u64::from(flag),
};
continue;
}
Token::End { .. } => continue,
Token::Key(key, _) => seed.bytes(key.as_bytes()),
Token::Scalar { node, .. } => match tree.item(node) {
JsonItem::Null => seed.zero().wrapping_add(1),
JsonItem::Bool(v) => match seed {
Seed::Extended(value) if value != 0 => seed.word(u32::from(*v)),
_ => {
if *v {
2
} else {
4
}
}
},
JsonItem::String(v) => seed.bytes(v.as_bytes()),
JsonItem::Number => {
numeric(&numeric_in(tree.text(node), -1).map_err(type_error)?, seed)
}
JsonItem::Array(_) | JsonItem::Object(_) => return Err(bad_value()),
},
};
result = match seed {
Seed::Plain =>
{
#[allow(clippy::cast_possible_truncation)]
u64::from((result as u32).rotate_left(1))
}
Seed::Extended(_) => {
(result << 1) & 0xffff_fffe_ffff_fffe | (result >> 31) & 0x0000_0001_0000_0001
}
} ^ element;
}
Ok(result)
}
fn float(value: f64, seed: Seed) -> u64 {
if value == 0.0 {
return seed.zero();
}
let value = if value.is_nan() { f64::NAN } else { value };
seed.bytes(&value.to_le_bytes())
}
pub(crate) fn float8_hash(value: f64) -> u32 {
#[allow(clippy::cast_possible_truncation)]
let hash = float(value, Seed::Plain) as u32;
hash
}
pub(crate) fn sample_hash(row: u64, seed: u32) -> u32 {
let mut data = [0; 12];
data[..8].copy_from_slice(&row.to_le_bytes());
data[8..].copy_from_slice(&seed.to_le_bytes());
hash_bytes(&data, 0).1
}
fn numeric(value: &Numeric, seed: Seed) -> u64 {
if matches!(
value.sign(),
NumericSign::NaN | NumericSign::Infinity | NumericSign::NegativeInfinity
) {
return seed.zero();
}
let digits = value.digits();
let Some(first) = digits.iter().position(|&d| d != 0) else {
return match seed {
Seed::Plain => u64::from(u32::MAX),
Seed::Extended(seed) => seed.wrapping_sub(1),
};
};
let last = digits.iter().rposition(|&d| d != 0).unwrap_or(first);
let data: Vec<u8> = digits[first..=last].iter().flat_map(|d| d.to_le_bytes()).collect();
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let weight = i64::from(value.weight()) - first as i64;
let weight = weight.cast_unsigned();
match seed {
Seed::Plain => (seed.bytes(&data) ^ weight) & u64::from(u32::MAX),
Seed::Extended(_) => seed.bytes(&data) ^ weight,
}
}
fn array(ty: u32, array: &Array<Value>, seed: Seed) -> Result<u64> {
let element = TypeInfo::get(ty).filter(|info| info.is_array()).ok_or_else(|| no_hash(ty))?.elem;
if element != oid::RECORD && !has_hash(element) && !is_container(element) {
return Err(no_hash_for(element, seed));
}
let mut result = 1;
for value in &array.values {
let hash = match value {
None => 0,
Some(value) => hash(element, value, seed)?,
};
result = seed.combine(result, hash);
}
Ok(result)
}
fn is_container(ty: u32) -> bool {
builtin::range_subtype(ty).is_some()
|| builtin::multirange_range(ty).is_some()
|| TypeInfo::get(ty).is_some_and(TypeInfo::is_array)
}
fn range(subtype: u32, range: &Range, seed: Seed) -> Result<u64> {
let bound = |value: &Value, missing: u8| -> Result<u64> {
if range.flags & (RANGE_EMPTY | missing) != 0 { Ok(0) } else { hash(subtype, value, seed) }
};
let lower = bound(&range.lower, RANGE_LB_INF)?;
let upper = bound(&range.upper, RANGE_UB_INF)?;
let result = seed.word(u32::from(range.flags)) ^ lower;
let result = match seed {
Seed::Plain =>
{
#[allow(clippy::cast_possible_truncation)]
u64::from((result as u32).rotate_left(1))
}
Seed::Extended(_) => {
(result << 1) & 0xffff_fffe_ffff_fffe | (result >> 31) & 0x0000_0001_0000_0001
}
};
Ok(result ^ upper)
}
fn mix(a: &mut u32, b: &mut u32, c: &mut u32) {
*a = a.wrapping_sub(*c) ^ c.rotate_left(4);
*c = c.wrapping_add(*b);
*b = b.wrapping_sub(*a) ^ a.rotate_left(6);
*a = a.wrapping_add(*c);
*c = c.wrapping_sub(*b) ^ b.rotate_left(8);
*b = b.wrapping_add(*a);
*a = a.wrapping_sub(*c) ^ c.rotate_left(16);
*c = c.wrapping_add(*b);
*b = b.wrapping_sub(*a) ^ a.rotate_left(19);
*a = a.wrapping_add(*c);
*c = c.wrapping_sub(*b) ^ b.rotate_left(4);
*b = b.wrapping_add(*a);
}
fn finish(a: &mut u32, b: &mut u32, c: &mut u32) {
*c = (*c ^ *b).wrapping_sub(b.rotate_left(14));
*a = (*a ^ *c).wrapping_sub(c.rotate_left(11));
*b = (*b ^ *a).wrapping_sub(a.rotate_left(25));
*c = (*c ^ *b).wrapping_sub(b.rotate_left(16));
*a = (*a ^ *c).wrapping_sub(c.rotate_left(4));
*b = (*b ^ *a).wrapping_sub(a.rotate_left(14));
*c = (*c ^ *b).wrapping_sub(b.rotate_left(24));
}
fn start(len: u32, seed: u64) -> (u32, u32, u32) {
let init = 0x9e37_79b9_u32.wrapping_add(len).wrapping_add(3_923_095);
let (mut a, mut b, mut c) = (init, init, init);
if seed != 0 {
#[allow(clippy::cast_possible_truncation)]
{
a = a.wrapping_add((seed >> 32) as u32);
b = b.wrapping_add(seed as u32);
}
mix(&mut a, &mut b, &mut c);
}
(a, b, c)
}
fn hash_bytes(data: &[u8], seed: u64) -> (u32, u32) {
#[allow(clippy::cast_possible_truncation)]
let (mut a, mut b, mut c) = start(data.len() as u32, seed);
let word = |bytes: &[u8]| u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let (chunks, tail) = data.as_chunks::<12>();
for chunk in chunks {
a = a.wrapping_add(word(&chunk[..4]));
b = b.wrapping_add(word(&chunk[4..8]));
c = c.wrapping_add(word(&chunk[8..]));
mix(&mut a, &mut b, &mut c);
}
let mut rest = [0; 12];
rest[..tail.len()].copy_from_slice(tail);
a = a.wrapping_add(word(&rest[..4]));
b = b.wrapping_add(word(&rest[4..8]));
c = c.wrapping_add(word(&rest[8..]) << 8);
finish(&mut a, &mut b, &mut c);
(b, c)
}
fn hash_word(word: u32, seed: u64) -> (u32, u32) {
let (mut a, mut b, mut c) = start(4, seed);
a = a.wrapping_add(word);
finish(&mut a, &mut b, &mut c);
(b, c)
}