use crate::{Surrogate, Value};
pub const DEFAULT_IDENTITY_COLUMN: &str = "id";
pub const ROWID_COLUMN: &str = "_rowid";
pub const HEADLESS_SENTINEL_PREFIX: &str = "__local_";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StorageKey(Surrogate);
impl StorageKey {
pub fn for_surrogate(surrogate: Surrogate) -> Self {
Self(surrogate)
}
pub fn parse(key: &str) -> Option<Self> {
if key.len() != 8
|| !key
.bytes()
.all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
{
return None;
}
let raw = u32::from_str_radix(key, 16).ok()?;
Some(Self(Surrogate::new(raw)))
}
pub fn surrogate(&self) -> Surrogate {
self.0
}
pub fn to_identity(&self) -> RowIdentity {
RowIdentity::for_surrogate(self.0)
}
}
impl std::fmt::Display for StorageKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:08x}", self.0.as_u32())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RowIdentity(String);
impl RowIdentity {
pub fn for_surrogate(surrogate: Surrogate) -> Self {
Self(surrogate.as_u32().to_string())
}
pub fn of_stored_row(
body: &[u8],
declared_primary_key: Option<&str>,
key: StorageKey,
) -> RowIdentity {
match crate::value_from_msgpack(body) {
Ok(row) => Self::of_row_value(&row, declared_primary_key, key),
Err(_) => key.to_identity(),
}
}
pub fn of_row_value(
row: &Value,
declared_primary_key: Option<&str>,
key: StorageKey,
) -> RowIdentity {
let column = declared_primary_key.unwrap_or(DEFAULT_IDENTITY_COLUMN);
let Value::Object(obj) = row else {
return key.to_identity();
};
obj.get(column)
.and_then(value_to_pk_string)
.or_else(|| obj.get(ROWID_COLUMN).and_then(value_to_pk_string))
.map(RowIdentity::from_user_key)
.unwrap_or_else(|| key.to_identity())
}
pub fn from_user_key(key: impl Into<String>) -> Self {
Self(key.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl std::fmt::Display for RowIdentity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
pub fn declared_key(primary_key: Option<&str>) -> Option<&str> {
primary_key.filter(|key| {
!key.eq_ignore_ascii_case(DEFAULT_IDENTITY_COLUMN)
&& !key.eq_ignore_ascii_case(ROWID_COLUMN)
})
}
pub fn extract_pk_value(body: &[u8], field: &str) -> Option<String> {
let Value::Object(obj) = crate::value_from_msgpack(body).ok()? else {
return None;
};
value_to_pk_string(obj.get(field)?)
}
pub fn value_to_pk_string(v: &Value) -> Option<String> {
match v {
Value::String(s) => Some(s.clone()),
Value::Integer(n) => Some(n.to_string()),
Value::Float(f) => Some(f.to_string()),
Value::Bool(b) => Some(b.to_string()),
Value::Decimal(d) => Some(d.to_string()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn body(fields: &[(&str, Value)]) -> Vec<u8> {
let mut obj = std::collections::HashMap::new();
for (name, value) in fields {
obj.insert((*name).to_string(), value.clone());
}
crate::value_to_msgpack(&Value::Object(obj)).expect("encode msgpack")
}
#[test]
fn of_stored_row_uses_declared_primary_key() {
let key = StorageKey::for_surrogate(Surrogate::new(9));
let body = body(&[
("id", Value::String("ignored".into())),
("sku", Value::Integer(42)),
]);
assert_eq!(
RowIdentity::of_stored_row(&body, Some("sku"), key).as_str(),
"42"
);
}
#[test]
fn of_stored_row_falls_back_to_id_column() {
let key = StorageKey::for_surrogate(Surrogate::new(9));
let body = body(&[("id", Value::String("user-1".into()))]);
assert_eq!(
RowIdentity::of_stored_row(&body, None, key).as_str(),
"user-1"
);
}
#[test]
fn of_stored_row_without_identity_column_is_decimal_surrogate() {
let key = StorageKey::for_surrogate(Surrogate::new(9));
let body = body(&[("name", Value::String("x".into()))]);
assert_eq!(RowIdentity::of_stored_row(&body, None, key).as_str(), "9");
assert_eq!(
RowIdentity::of_stored_row(&body, Some("sku"), key).as_str(),
"9"
);
}
#[test]
fn value_to_pk_string_rejects_non_scalars() {
assert_eq!(value_to_pk_string(&Value::Array(Vec::new())), None);
assert_eq!(value_to_pk_string(&Value::Null), None);
assert_eq!(
value_to_pk_string(&Value::Bool(true)).as_deref(),
Some("true")
);
}
#[test]
fn formats_zero_padded_lowercase() {
assert_eq!(
StorageKey::for_surrogate(Surrogate::new(0)).to_string(),
"00000000"
);
assert_eq!(
StorageKey::for_surrogate(Surrogate::new(42)).to_string(),
"0000002a"
);
assert_eq!(
StorageKey::for_surrogate(Surrogate::new(0xDEAD_BEEF)).to_string(),
"deadbeef"
);
}
#[test]
fn lex_order_matches_numeric() {
let a = StorageKey::for_surrogate(Surrogate::new(0x10)).to_string();
let b = StorageKey::for_surrogate(Surrogate::new(0x100)).to_string();
assert!(a < b);
}
#[test]
fn parse_rejects_wrong_shape() {
assert!(StorageKey::parse("").is_none());
assert!(StorageKey::parse("abc").is_none());
assert!(StorageKey::parse("DEADBEEF").is_none());
assert!(StorageKey::parse("zzzzzzzz").is_none());
assert!(StorageKey::parse("123456789").is_none());
}
#[test]
fn parse_roundtrips_surrogate() {
let key = StorageKey::for_surrogate(Surrogate::new(0x2a));
let parsed = StorageKey::parse(&key.to_string()).expect("valid storage key");
assert_eq!(parsed.surrogate(), Surrogate::new(0x2a));
}
#[test]
fn to_identity_is_decimal() {
let key = StorageKey::for_surrogate(Surrogate::new(42));
assert_eq!(key.to_identity().as_str(), "42");
}
#[test]
fn parse_none_yields_user_key_identity() {
assert_eq!(
StorageKey::parse("user-declared-id")
.map(|key| key.to_identity())
.unwrap_or_else(|| RowIdentity::from_user_key("user-declared-id"))
.as_str(),
"user-declared-id"
);
}
#[test]
fn of_stored_row_keeps_the_rowid_identity_under_a_new_surrogate() {
let copied = body(&[
("_rowid", Value::Integer(123)),
("name", Value::String("a".into())),
]);
let new_key = StorageKey::for_surrogate(Surrogate::new(456));
assert_eq!(
RowIdentity::of_stored_row(&copied, None, new_key).as_str(),
"123"
);
let declared = body(&[("sku", Value::Integer(9)), ("_rowid", Value::Integer(123))]);
assert_eq!(
RowIdentity::of_stored_row(&declared, Some("sku"), new_key).as_str(),
"9",
"a declared key wins over _rowid"
);
}
}