tempest-engine 0.0.2

Relational database engine for TempestDB
Documentation
use std::borrow::Cow;

use bytes::{Buf, BufMut, Bytes, BytesMut};
use derive_more::Display;
use serde::{Deserialize, Serialize};
use tempest_core::encoding::{
    BufGetLexicalExt, BufGetRawExt, BufPutLexicalExt, BufPutRawExt, LexicalDecodeError,
    RawDecodeError,
};

/// A primitive or enum type tag. The `u32` inside `Enum` is the raw `TypeId`.
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum TempestType {
    #[display("Int64")]
    Int64,
    #[display("Bool")]
    Bool,
    #[display("String")]
    String,
    /// Enum type identified by its catalog `TypeId` (stored as raw `u32`).
    #[display("Enum({_0})")]
    Enum(u32),
}

impl TempestType {
    pub fn name(&self) -> &'static str {
        match self {
            Self::Int64 => "Int64",
            Self::Bool => "Bool",
            Self::String => "String",
            Self::Enum(_) => "Enum",
        }
    }
}

impl std::str::FromStr for TempestType {
    type Err = ();
    fn from_str(s: &str) -> Result<Self, ()> {
        match s {
            "Int64" => Ok(Self::Int64),
            "Bool" => Ok(Self::Bool),
            "String" => Ok(Self::String),
            _ => Err(()),
        }
    }
}

#[derive(derive_more::Debug, Display, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub enum TempestValue<'a> {
    Int64(i64) = 0,
    Bool(bool) = 1,
    String(Cow<'a, str>) = 2,
    #[display("Enum({type_id}:{variant_id})")]
    Enum { type_id: u32, variant_id: u32, fields: Vec<TempestValue<'a>> } = 3,
}

impl<'a> TempestValue<'a> {
    pub fn ty(&self) -> TempestType {
        match self {
            TempestValue::Int64(_) => TempestType::Int64,
            TempestValue::Bool(_) => TempestType::Bool,
            TempestValue::String(_) => TempestType::String,
            TempestValue::Enum { type_id, .. } => TempestType::Enum(*type_id),
        }
    }

    pub fn encode(&self, buf: &mut BytesMut) {
        match self {
            &TempestValue::Int64(i) => buf.put_i64_raw(i),
            &TempestValue::Bool(b) => buf.put_bool_raw(b),
            TempestValue::String(s) => buf.put_str_raw(s),
            TempestValue::Enum { variant_id, fields, .. } => {
                buf.put_u32(*variant_id);
                for field in fields {
                    field.encode(buf);
                }
            }
        }
    }

    pub fn decode(
        buf: &mut Bytes,
        ty: TempestType,
    ) -> Result<TempestValue<'static>, RawDecodeError> {
        match ty {
            TempestType::Int64 => buf.get_i64_raw().map(TempestValue::Int64),
            TempestType::Bool => buf.get_bool_raw().map(TempestValue::Bool),
            TempestType::String => buf
                .get_str_raw()
                .map(|s| TempestValue::String(Cow::Owned(s))),
            TempestType::Enum(type_id) => {
                let variant_id = buf.get_u32();
                Ok(TempestValue::Enum { type_id, variant_id, fields: vec![] })
            }
        }
    }

    pub fn encode_lexical(&self, buf: &mut BytesMut) {
        match self {
            &TempestValue::Int64(i) => buf.put_i64_lexical(i),
            &TempestValue::Bool(b) => buf.put_bool_lexical(b),
            TempestValue::String(s) => buf.put_str_lexical(s),
            TempestValue::Enum { .. } => panic!("enum values cannot be used as primary key"),
        }
    }

    pub fn decode_lexical(
        buf: &mut Bytes,
        ty: TempestType,
    ) -> Result<TempestValue<'static>, LexicalDecodeError> {
        match ty {
            TempestType::Int64 => buf.get_i64_lexical().map(TempestValue::Int64),
            TempestType::Bool => buf.get_bool_lexical().map(TempestValue::Bool),
            TempestType::String => buf
                .get_str_lexical()
                .map(|s| TempestValue::String(Cow::Owned(s))),
            TempestType::Enum(_) => panic!("enum values cannot be used as primary key"),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::borrow::Cow;

    use bytes::{Bytes, BytesMut};

    use super::*;

    // -- helpers --

    fn roundtrip(val: TempestValue<'_>) -> TempestValue<'static> {
        let ty = val.ty();
        let mut buf = BytesMut::new();
        val.encode(&mut buf);
        TempestValue::decode(&mut buf.freeze(), ty).unwrap()
    }

    fn roundtrip_lexical(val: TempestValue<'_>) -> TempestValue<'static> {
        let ty = val.ty();
        let mut buf = BytesMut::new();
        val.encode_lexical(&mut buf);
        TempestValue::decode_lexical(&mut buf.freeze(), ty).unwrap()
    }

    fn encode_lexical_bytes(val: TempestValue<'_>) -> Bytes {
        let mut buf = BytesMut::new();
        val.encode_lexical(&mut buf);
        buf.freeze()
    }

    // -- raw roundtrip --

    #[test]
    fn test_int64_roundtrip() {
        for val in [0i64, 1, -1, i64::MIN, i64::MAX, 42, -1000] {
            let result = roundtrip(TempestValue::Int64(val));
            assert_eq!(result, TempestValue::Int64(val));
        }
    }

    #[test]
    fn test_bool_roundtrip() {
        for val in [true, false] {
            let result = roundtrip(TempestValue::Bool(val));
            assert_eq!(result, TempestValue::Bool(val));
        }
    }

    #[test]
    fn test_string_roundtrip() {
        for val in ["", "hello", "tempest", "unicode: ??", "hel\x00lo"] {
            let result = roundtrip(TempestValue::String(Cow::Borrowed(val)));
            assert_eq!(result, TempestValue::String(Cow::Borrowed(val)));
        }
    }

    #[test]
    fn test_enum_roundtrip() {
        let val = TempestValue::Enum { type_id: 5, variant_id: 2, fields: vec![] };
        let result = roundtrip(val.clone());
        assert_eq!(result, val);
    }

    #[test]
    fn test_decode_returns_owned_string() {
        let result = roundtrip(TempestValue::String(Cow::Borrowed("hello")));
        if let TempestValue::String(cow) = result {
            assert!(
                matches!(cow, Cow::Owned(_)),
                "decoded string should be Cow::Owned"
            );
        } else {
            panic!("expected String variant");
        }
    }

    // -- lexical roundtrip --

    #[test]
    fn test_int64_lexical_roundtrip() {
        for val in [0i64, 1, -1, i64::MIN, i64::MAX, 42, -1000] {
            let result = roundtrip_lexical(TempestValue::Int64(val));
            assert_eq!(result, TempestValue::Int64(val));
        }
    }

    #[test]
    fn test_bool_lexical_roundtrip() {
        for val in [true, false] {
            let result = roundtrip_lexical(TempestValue::Bool(val));
            assert_eq!(result, TempestValue::Bool(val));
        }
    }

    #[test]
    fn test_string_lexical_roundtrip() {
        for val in ["", "hello", "tempest", "hel\x00lo", "\x00\x00\x00"] {
            let result = roundtrip_lexical(TempestValue::String(Cow::Borrowed(val)));
            assert_eq!(result, TempestValue::String(Cow::Borrowed(val)));
        }
    }

    // -- lexical ordering --

    #[test]
    fn test_int64_lexical_ordering() {
        let cases = [i64::MIN, -1000, -1, 0, 1, 1000, i64::MAX];
        for pair in cases.windows(2) {
            let (a, b) = (pair[0], pair[1]);
            assert!(
                encode_lexical_bytes(TempestValue::Int64(a))
                    < encode_lexical_bytes(TempestValue::Int64(b)),
                "{} should encode less than {}",
                a,
                b
            );
        }
    }

    #[test]
    fn test_bool_lexical_ordering() {
        assert!(
            encode_lexical_bytes(TempestValue::Bool(false))
                < encode_lexical_bytes(TempestValue::Bool(true))
        );
    }

    #[test]
    fn test_string_lexical_ordering() {
        let cases = ["", "a", "aa", "ab", "b", "z"];
        for pair in cases.windows(2) {
            let (a, b) = (pair[0], pair[1]);
            assert!(
                encode_lexical_bytes(TempestValue::String(Cow::Borrowed(a)))
                    < encode_lexical_bytes(TempestValue::String(Cow::Borrowed(b))),
                "{:?} should encode less than {:?}",
                a,
                b
            );
        }
    }

    // -- ty() --

    #[test]
    fn test_ty_returns_correct_discriminant() {
        assert_eq!(TempestValue::Int64(Default::default()).ty(), TempestType::Int64);
        assert_eq!(TempestValue::Bool(Default::default()).ty(), TempestType::Bool);
        assert_eq!(TempestValue::String(Default::default()).ty(), TempestType::String);
        assert_eq!(
            TempestValue::Enum { type_id: 3, variant_id: 0, fields: vec![] }.ty(),
            TempestType::Enum(3)
        );
    }

    // -- raw vs lexical differ for i64 --

    #[test]
    fn test_raw_and_lexical_differ_for_negative_i64() {
        let val = TempestValue::Int64(-1);
        let mut raw_buf = BytesMut::new();
        let mut lex_buf = BytesMut::new();
        val.encode(&mut raw_buf);
        val.encode_lexical(&mut lex_buf);
        assert_ne!(
            raw_buf, lex_buf,
            "raw and lexical encodings of -1 must differ"
        );
    }

    // -- cursor advancement --

    #[test]
    fn test_decode_advances_cursor() {
        let mut buf = BytesMut::new();
        TempestValue::Int64(42).encode(&mut buf);
        TempestValue::Bool(true).encode(&mut buf);
        TempestValue::String(Cow::Borrowed("hi")).encode(&mut buf);

        let mut bytes = buf.freeze();
        assert_eq!(
            TempestValue::decode(&mut bytes, TempestType::Int64).unwrap(),
            TempestValue::Int64(42)
        );
        assert_eq!(
            TempestValue::decode(&mut bytes, TempestType::Bool).unwrap(),
            TempestValue::Bool(true)
        );
        assert_eq!(
            TempestValue::decode(&mut bytes, TempestType::String).unwrap(),
            TempestValue::String(Cow::Borrowed("hi"))
        );
        assert!(bytes.is_empty());
    }

    #[test]
    fn test_decode_lexical_advances_cursor() {
        let mut buf = BytesMut::new();
        TempestValue::Int64(-99).encode_lexical(&mut buf);
        TempestValue::String(Cow::Borrowed("foo")).encode_lexical(&mut buf);
        TempestValue::Bool(false).encode_lexical(&mut buf);

        let mut bytes = buf.freeze();
        assert_eq!(
            TempestValue::decode_lexical(&mut bytes, TempestType::Int64).unwrap(),
            TempestValue::Int64(-99)
        );
        assert_eq!(
            TempestValue::decode_lexical(&mut bytes, TempestType::String).unwrap(),
            TempestValue::String(Cow::Borrowed("foo"))
        );
        assert_eq!(
            TempestValue::decode_lexical(&mut bytes, TempestType::Bool).unwrap(),
            TempestValue::Bool(false)
        );
        assert!(bytes.is_empty());
    }
}