use core::cmp::Ordering;
crate::wire_enum! {
pub enum TypeCode: u8 {
U8 = 1,
I8 = 2,
U16 = 3,
I16 = 4,
U32 = 5,
I32 = 6,
F32 = 7,
U64 = 8,
I64 = 9,
F64 = 10,
String = 11,
U128 = 12,
UUID = 13,
Blob = 14,
I128 = 15,
Date = 16,
Timestamp = 17,
Decimal = 18,
}
}
impl TypeCode {
pub const fn wire_name(self) -> &'static str {
match self {
TypeCode::U8 => "U8",
TypeCode::I8 => "I8",
TypeCode::U16 => "U16",
TypeCode::I16 => "I16",
TypeCode::U32 => "U32",
TypeCode::I32 => "I32",
TypeCode::F32 => "F32",
TypeCode::U64 => "U64",
TypeCode::I64 => "I64",
TypeCode::F64 => "F64",
TypeCode::String => "STRING",
TypeCode::U128 => "U128",
TypeCode::UUID => "UUID",
TypeCode::Blob => "BLOB",
TypeCode::I128 => "I128",
TypeCode::Date => "DATE",
TypeCode::Timestamp => "TIMESTAMP",
TypeCode::Decimal => "DECIMAL",
}
}
#[inline(always)]
pub const fn is_temporal(self) -> bool {
matches!(self, TypeCode::Date | TypeCode::Timestamp)
}
#[inline(always)]
pub const fn storage_type(self) -> TypeCode {
match self {
TypeCode::Date => TypeCode::I32,
TypeCode::Timestamp | TypeCode::Decimal => TypeCode::I64,
t => t,
}
}
#[inline(always)]
pub const fn is_float(self) -> bool {
matches!(self, TypeCode::F32 | TypeCode::F64)
}
#[inline]
pub const fn register_image(self) -> TypeCode {
match self {
TypeCode::F32 | TypeCode::F64 => TypeCode::F64,
TypeCode::U64 | TypeCode::String | TypeCode::Date | TypeCode::Timestamp | TypeCode::Decimal => self,
_ => TypeCode::I64,
}
}
#[inline(always)]
pub const fn is_wide_int(self) -> bool {
matches!(self, TypeCode::U128 | TypeCode::UUID | TypeCode::I128)
}
#[inline(always)]
pub const fn is_german_string(self) -> bool {
matches!(self, TypeCode::String | TypeCode::Blob)
}
#[inline(always)]
pub const fn is_signed_int(self) -> bool {
matches!(
self.storage_type(),
TypeCode::I8 | TypeCode::I16 | TypeCode::I32 | TypeCode::I64 | TypeCode::I128
)
}
#[inline(always)]
pub const fn is_fixed_int(self) -> bool {
FixedInt::from_type_code(self).is_some()
}
#[inline(always)]
const fn is_int(self) -> bool {
self.is_fixed_int() || matches!(self, TypeCode::U128 | TypeCode::I128)
}
#[inline(always)]
pub const fn is_pk_eligible(self) -> bool {
self.is_fixed_int() || self.is_wide_int()
}
#[inline(always)]
pub const fn wire_stride(self) -> usize {
match self {
TypeCode::U8 | TypeCode::I8 => 1,
TypeCode::U16 | TypeCode::I16 => 2,
TypeCode::F32 | TypeCode::U32 | TypeCode::I32 | TypeCode::Date => 4,
TypeCode::F64 | TypeCode::U64 | TypeCode::I64 | TypeCode::Timestamp | TypeCode::Decimal => 8,
TypeCode::U128 | TypeCode::UUID | TypeCode::String | TypeCode::Blob | TypeCode::I128 => 16,
}
}
pub const fn int_domain_fits(self, target: TypeCode) -> bool {
if !self.is_int() || !target.is_int() {
return false;
}
if self.is_signed_int() == target.is_signed_int() {
target.wire_stride() >= self.wire_stride()
} else {
target.is_signed_int() && target.wire_stride() > self.wire_stride()
}
}
#[inline]
pub const fn is_widening_promotion(self, target: TypeCode) -> bool {
target.is_fixed_int() && self.int_domain_fits(target)
}
#[inline]
pub const fn reindex_output_type(self) -> TypeCode {
if self.is_fixed_int() || matches!(self, TypeCode::I128) {
self
} else {
TypeCode::U128
}
}
pub const fn packs_at(self, slot: TypeCode) -> bool {
slot.as_wire() == self.reindex_output_type().as_wire()
|| matches!(self.join_key_common_type(slot), Ok(t) if t.as_wire() == slot.as_wire())
}
pub const fn join_key_common_type(self, other: TypeCode) -> Result<TypeCode, JoinKeyRule> {
let (l, r) = (self, other);
if l.is_float() || r.is_float() {
return Err(JoinKeyRule::Float);
}
if l.is_german_string() != r.is_german_string() {
return Err(JoinKeyRule::StringWithNative);
}
if l.as_wire() == r.as_wire() {
return Ok(l.reindex_output_type());
}
if l.is_german_string() {
return Ok(TypeCode::U128);
}
if l.is_temporal() && r.is_temporal() {
return Err(JoinKeyRule::UnitMismatch);
}
if l.is_signed_int() && r.is_signed_int() {
let (l, r) = (l.storage_type(), r.storage_type());
return Ok(if l.wire_stride() >= r.wire_stride() { l } else { r });
}
if !l.is_signed_int() && !r.is_signed_int() {
let wider = if l.wire_stride() >= r.wire_stride() { l } else { r };
return Ok(if wider.wire_stride() == 16 {
TypeCode::U128
} else {
wider
});
}
let (s, u) = if l.is_signed_int() { (l, r) } else { (r, l) };
let uw = u.wire_stride() * 2;
let common_w = if uw > s.wire_stride() { uw } else { s.wire_stride() };
match common_w {
2 => Ok(TypeCode::I16),
4 => Ok(TypeCode::I32),
8 => Ok(TypeCode::I64),
16 => Ok(TypeCode::I128),
_ => Err(JoinKeyRule::NoSigned256),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JoinKeyRule {
Float,
UnitMismatch,
StringWithNative,
NoSigned256,
}
impl core::fmt::Display for TypeCode {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.wire_name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ColType {
pub tc: TypeCode,
pub scale: u8,
}
impl ColType {
pub const fn of(tc: TypeCode) -> Self {
ColType { tc, scale: 0 }
}
pub const fn decimal(scale: u8) -> Self {
ColType { tc: TypeCode::Decimal, scale }
}
pub const fn is_decimal(self) -> bool {
matches!(self.tc, TypeCode::Decimal)
}
pub const fn is_admissible(self) -> bool {
self.scale <= crate::decimal::MAX_DECIMAL_SCALE && (self.scale == 0 || self.is_decimal())
}
pub fn from_wire(code: u8, scale: u8) -> Option<ColType> {
TypeCode::from_wire(code)
.map(|tc| ColType { tc, scale })
.filter(|t| t.is_admissible())
}
pub const fn decimal_domains_match(self, other: Self) -> bool {
if !self.is_decimal() && !other.is_decimal() {
return true;
}
self.tc.as_wire() == other.tc.as_wire() && self.scale == other.scale
}
pub fn register_image(self) -> Self {
ColType {
tc: self.tc.register_image(),
scale: self.scale,
}
}
}
impl From<TypeCode> for ColType {
fn from(tc: TypeCode) -> Self {
ColType::of(tc)
}
}
impl core::fmt::Display for ColType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self.tc {
TypeCode::Decimal => write!(f, "DECIMAL({}, {})", crate::decimal::MAX_DECIMAL_SCALE, self.scale),
tc => write!(f, "{tc}"),
}
}
}
#[inline(always)]
pub fn cmp_col_window(a: &[u8], a_blob: &[u8], b: &[u8], b_blob: &[u8], tc: TypeCode) -> Ordering {
debug_assert!(a.len() == b.len(), "cmp_col_window: windows must be equal length");
#[inline(always)]
fn arr<const N: usize>(s: &[u8]) -> [u8; N] {
s.try_into().unwrap()
}
use TypeCode as T;
match tc {
T::U8 => a[0].cmp(&b[0]),
T::I8 => (a[0] as i8).cmp(&(b[0] as i8)),
T::U16 => u16::from_le_bytes(arr(a)).cmp(&u16::from_le_bytes(arr(b))),
T::I16 => i16::from_le_bytes(arr(a)).cmp(&i16::from_le_bytes(arr(b))),
T::U32 => u32::from_le_bytes(arr(a)).cmp(&u32::from_le_bytes(arr(b))),
T::I32 | T::Date => i32::from_le_bytes(arr(a)).cmp(&i32::from_le_bytes(arr(b))),
T::U64 => u64::from_le_bytes(arr(a)).cmp(&u64::from_le_bytes(arr(b))),
T::I64 | T::Timestamp | T::Decimal => i64::from_le_bytes(arr(a)).cmp(&i64::from_le_bytes(arr(b))),
T::U128 | T::UUID => u128::from_le_bytes(arr(a)).cmp(&u128::from_le_bytes(arr(b))),
T::I128 => i128::from_le_bytes(arr(a)).cmp(&i128::from_le_bytes(arr(b))),
T::F32 => f32::from_le_bytes(arr(a)).total_cmp(&f32::from_le_bytes(arr(b))),
T::F64 => f64::from_le_bytes(arr(a)).total_cmp(&f64::from_le_bytes(arr(b))),
T::String | T::Blob => crate::compare_german_strings(&arr(a), a_blob, &arr(b), b_blob),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PkRule {
NotPacked,
Empty,
TooManyColumns { count: usize, max: usize },
IndexOutOfRange { col: u32 },
Duplicate { col: u32 },
NotEligible { col: u32, type_code: TypeCode },
Nullable { col: u32 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PkListRole {
PrimaryKey,
ColumnList,
}
impl PkListRole {
fn noun(self) -> &'static str {
match self {
PkListRole::PrimaryKey => "primary key",
PkListRole::ColumnList => "column list",
}
}
}
impl PkRule {
pub fn for_role(&self, role: PkListRole) -> String {
self.named(role, |_| None)
}
pub fn named<'a>(&self, role: PkListRole, name: impl Fn(u32) -> Option<&'a str>) -> String {
let what = role.noun();
let col = |c: u32| match name(c) {
Some(n) => format!("'{n}'"),
None => c.to_string(),
};
match *self {
PkRule::NotPacked => format!("{what} word carries no packed-list flag"),
PkRule::Empty => format!("{what} must name at least one column"),
PkRule::TooManyColumns { count, max } => {
format!("{what} column count {count} out of range 1..={max}")
}
PkRule::IndexOutOfRange { col } => format!("{what} index {col} out of bounds"),
PkRule::Duplicate { col: c } => format!("{what} names column {} twice", col(c)),
PkRule::NotEligible { col: c, type_code } => format!(
"{what} column {} has type_code {type_code}; only fixed-width integer, \
U128, UUID, and I128 columns can be key columns \
(String, Blob, and float columns cannot)",
col(c)
),
PkRule::Nullable { col: c } => format!("{what} column {} must not be nullable", col(c)),
}
}
}
pub(crate) fn validate_pk_indices(pk_cols: &[u32], ncols: usize, max_pk: usize) -> Result<(), PkRule> {
if pk_cols.is_empty() {
return Err(PkRule::Empty);
}
if pk_cols.len() > max_pk {
return Err(PkRule::TooManyColumns { count: pk_cols.len(), max: max_pk });
}
for (j, &c) in pk_cols.iter().enumerate() {
if c as usize >= ncols {
return Err(PkRule::IndexOutOfRange { col: c });
}
if pk_cols[..j].contains(&c) {
return Err(PkRule::Duplicate { col: c });
}
}
Ok(())
}
pub fn validate_pk_tuple(
pk_cols: &[u32],
ncols: usize,
max_pk: usize,
col: impl Fn(u32) -> (TypeCode, bool),
) -> Result<(), PkRule> {
debug_assert!(max_pk <= crate::MAX_PK_COLUMNS);
validate_pk_indices(pk_cols, ncols, max_pk)?;
for &c in pk_cols {
let (type_code, nullable) = col(c);
if !type_code.is_pk_eligible() {
return Err(PkRule::NotEligible { col: c, type_code });
}
if nullable {
return Err(PkRule::Nullable { col: c });
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FixedInt {
U8,
I8,
U16,
I16,
U32,
I32,
U64,
I64,
}
impl FixedInt {
#[inline(always)]
pub const fn from_type_code(tc: TypeCode) -> Option<Self> {
match tc {
TypeCode::U8 => Some(Self::U8),
TypeCode::I8 => Some(Self::I8),
TypeCode::U16 => Some(Self::U16),
TypeCode::I16 => Some(Self::I16),
TypeCode::U32 => Some(Self::U32),
TypeCode::I32 => Some(Self::I32),
TypeCode::U64 => Some(Self::U64),
TypeCode::I64 => Some(Self::I64),
TypeCode::Date => Some(Self::I32),
TypeCode::Timestamp | TypeCode::Decimal => Some(Self::I64),
TypeCode::F32
| TypeCode::F64
| TypeCode::U128
| TypeCode::UUID
| TypeCode::String
| TypeCode::Blob
| TypeCode::I128 => None,
}
}
pub const fn exact(tc: TypeCode) -> Option<Self> {
match Self::from_type_code(tc) {
Some(fi) if fi.type_code().as_wire() == tc.as_wire() => Some(fi),
_ => None,
}
}
pub const fn type_code(self) -> TypeCode {
match self {
Self::U8 => TypeCode::U8,
Self::I8 => TypeCode::I8,
Self::U16 => TypeCode::U16,
Self::I16 => TypeCode::I16,
Self::U32 => TypeCode::U32,
Self::I32 => TypeCode::I32,
Self::U64 => TypeCode::U64,
Self::I64 => TypeCode::I64,
}
}
#[inline(always)]
pub const fn width(self) -> usize {
match self {
Self::U8 | Self::I8 => 1,
Self::U16 | Self::I16 => 2,
Self::U32 | Self::I32 => 4,
Self::U64 | Self::I64 => 8,
}
}
pub const fn range(self) -> (i128, i128) {
match self {
Self::U8 => (0, u8::MAX as i128),
Self::I8 => (i8::MIN as i128, i8::MAX as i128),
Self::U16 => (0, u16::MAX as i128),
Self::I16 => (i16::MIN as i128, i16::MAX as i128),
Self::U32 => (0, u32::MAX as i128),
Self::I32 => (i32::MIN as i128, i32::MAX as i128),
Self::U64 => (0, u64::MAX as i128),
Self::I64 => (i64::MIN as i128, i64::MAX as i128),
}
}
pub const fn pack(self, v: i128) -> u128 {
debug_assert!(self.range().0 <= v && v <= self.range().1);
(v as u128) & crate::image_mask(self.width())
}
pub const fn unpack(self, v: u128) -> i64 {
let shift = 128 - 8 * self.width() as u32;
if self.is_signed() {
((v << shift) as i128 >> shift) as i64
} else {
((v << shift) >> shift) as i64
}
}
#[inline(always)]
pub const fn is_signed(self) -> bool {
matches!(self, Self::I8 | Self::I16 | Self::I32 | Self::I64)
}
#[inline(always)]
pub fn decode_le_i64(self, b: &[u8]) -> i64 {
debug_assert!(b.len() >= self.width());
match self {
Self::U8 => b[0] as i64,
Self::I8 => b[0] as i8 as i64,
Self::U16 => u16::from_le_bytes(b[..2].try_into().unwrap()) as i64,
Self::I16 => i16::from_le_bytes(b[..2].try_into().unwrap()) as i64,
Self::U32 => u32::from_le_bytes(b[..4].try_into().unwrap()) as i64,
Self::I32 => i32::from_le_bytes(b[..4].try_into().unwrap()) as i64,
Self::U64 => u64::from_le_bytes(b[..8].try_into().unwrap()) as i64,
Self::I64 => i64::from_le_bytes(b[..8].try_into().unwrap()),
}
}
}
pub fn narrow_f32(v: f64) -> Option<f32> {
let f = v as f32;
(f.is_finite() || !v.is_finite()).then_some(f)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScalarKind {
Int(FixedInt),
F32,
F64,
}
impl ScalarKind {
pub const fn from_type_code(tc: TypeCode) -> Option<Self> {
match (tc, FixedInt::from_type_code(tc)) {
(_, Some(fi)) => Some(Self::Int(fi)),
(TypeCode::F32, None) => Some(Self::F32),
(TypeCode::F64, None) => Some(Self::F64),
_ => None,
}
}
}
const _: () = {
let mut i = 0;
while i < TypeCode::ALL.len() {
let tc = TypeCode::ALL[i];
let kind = ScalarKind::from_type_code(tc);
assert!(
kind.is_some() != (tc.is_wide_int() || tc.is_german_string()),
"a type is wide iff it has no scalar image"
);
assert!(
tc.wire_stride() == tc.storage_type().wire_stride(),
"a type must have its storage type's width"
);
if let Some(fi) = FixedInt::from_type_code(tc) {
assert!(
fi.type_code().as_wire() == tc.storage_type().as_wire(),
"FixedInt::type_code must invert from_type_code up to the storage type"
);
assert!(
fi.is_signed() == tc.is_signed_int(),
"FixedInt::is_signed must match the type's sign"
);
assert!(
fi.width() == tc.wire_stride(),
"FixedInt::width must be the type's wire stride"
);
}
i += 1;
}
};
#[cfg(test)]
#[path = "tests/types.rs"]
mod tests;