donadb-rel 0.1.2

DonaDB Relational — typed schemas, secondary indexes, and relational scans on top of DonaDB. Built for the TruthLinked blockchain.
Documentation
// schema.rs — typed field definitions, Schema, Record, FieldValue.
//
// A Schema is a named ordered list of Fields.
// A Record is an ordered list of FieldValues matching a Schema.
// Records are encoded to Bytes by codec.rs and stored as DonaDB values.
// DonaDB stores opaque bytes — schemas live only in the application process.
//
// FieldType enum covers all types needed for blockchain state:
//   U8, U16, U32, U64, U128   — numeric types
//   I64                        — signed (for deltas / price movements)
//   Bool                       — flags
//   Bytes(len)                 — fixed-length byte arrays (addresses, hashes)
//   VarBytes                   — variable-length blobs
//   Address                    — alias for Bytes(32), common enough to name
//   Hash                       — alias for Bytes(32)
//
// Schemas are registered in RelTable at construction time.
// The schema name maps to a DonaDB family (or a namespaced sub-key prefix).

use serde::{Deserialize, Serialize};

/// Type of a single field in a schema.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FieldType {
    U8,
    U16,
    U32,
    U64,
    U128,
    I64,
    Bool,
    /// Fixed-length byte array.
    Bytes(usize),
    /// Variable-length byte array (prefixed with 4-byte length on disk).
    VarBytes,
    /// 32-byte account/contract address.
    Address,
    /// 32-byte hash (block hash, tx hash, etc).
    Hash,
}

impl FieldType {
    /// Fixed encoded size in bytes. None if variable-length.
    pub fn fixed_size(&self) -> Option<usize> {
        match self {
            FieldType::U8 => Some(1),
            FieldType::U16 => Some(2),
            FieldType::U32 => Some(4),
            FieldType::U64 => Some(8),
            FieldType::U128 => Some(16),
            FieldType::I64 => Some(8),
            FieldType::Bool => Some(1),
            FieldType::Bytes(n) => Some(*n),
            FieldType::Address => Some(32),
            FieldType::Hash => Some(32),
            FieldType::VarBytes => None,
        }
    }

    pub fn type_name(&self) -> &'static str {
        match self {
            FieldType::U8 => "u8",
            FieldType::U16 => "u16",
            FieldType::U32 => "u32",
            FieldType::U64 => "u64",
            FieldType::U128 => "u128",
            FieldType::I64 => "i64",
            FieldType::Bool => "bool",
            FieldType::Bytes(_) => "bytes",
            FieldType::VarBytes => "varbytes",
            FieldType::Address => "address",
            FieldType::Hash => "hash",
        }
    }
}

/// A named field with its type and whether it is the primary key component.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Field {
    pub name: String,
    pub field_type: FieldType,
    /// If true, this field is part of the primary key.
    /// Composite primary keys: all fields with is_key=true, in declaration order.
    pub is_key: bool,
    /// If true, a secondary index is automatically maintained for this field.
    pub indexed: bool,
}

impl Field {
    pub fn key(name: impl Into<String>, field_type: FieldType) -> Self {
        Self {
            name: name.into(),
            field_type,
            is_key: true,
            indexed: false,
        }
    }

    pub fn value(name: impl Into<String>, field_type: FieldType) -> Self {
        Self {
            name: name.into(),
            field_type,
            is_key: false,
            indexed: false,
        }
    }

    pub fn indexed_value(name: impl Into<String>, field_type: FieldType) -> Self {
        Self {
            name: name.into(),
            field_type,
            is_key: false,
            indexed: true,
        }
    }
}

/// A typed value for one field in a record.
/// Raw byte fields are stored as Vec<u8> which implements serde natively.
/// Construct from a Bytes value using FieldValue::Bytes(b.to_vec()).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FieldValue {
    U8(u8),
    U16(u16),
    U32(u32),
    U64(u64),
    U128(u128),
    I64(i64),
    Bool(bool),
    /// Raw bytes. Use `.as_slice()` or convert to `bytes::Bytes` as needed.
    Bytes(Vec<u8>),
    Null,
}

impl FieldValue {
    pub fn type_name(&self) -> &'static str {
        match self {
            FieldValue::U8(_) => "u8",
            FieldValue::U16(_) => "u16",
            FieldValue::U32(_) => "u32",
            FieldValue::U64(_) => "u64",
            FieldValue::U128(_) => "u128",
            FieldValue::I64(_) => "i64",
            FieldValue::Bool(_) => "bool",
            FieldValue::Bytes(_) => "bytes",
            FieldValue::Null => "null",
        }
    }

    pub fn as_u64(&self) -> Option<u64> {
        match self {
            FieldValue::U8(v) => Some(*v as u64),
            FieldValue::U16(v) => Some(*v as u64),
            FieldValue::U32(v) => Some(*v as u64),
            FieldValue::U64(v) => Some(*v),
            _ => None,
        }
    }

    pub fn as_u128(&self) -> Option<u128> {
        match self {
            FieldValue::U128(v) => Some(*v),
            other => other.as_u64().map(|v| v as u128),
        }
    }

    pub fn as_bytes(&self) -> Option<&[u8]> {
        match self {
            FieldValue::Bytes(v) => Some(v.as_slice()),
            _ => None,
        }
    }
}

// ── Schema ───────────────────────────────────────────────────────────────────

/// A named, ordered list of Fields describing one entity type.
/// Schemas are immutable after registration — changing a schema requires migration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Schema {
    pub name: String,
    pub fields: Vec<Field>,
}

impl Schema {
    pub fn new(name: impl Into<String>, fields: Vec<Field>) -> Self {
        Self {
            name: name.into(),
            fields,
        }
    }

    pub fn field(&self, name: &str) -> Option<(usize, &Field)> {
        self.fields.iter().enumerate().find(|(_, f)| f.name == name)
    }

    pub fn key_fields(&self) -> Vec<(usize, &Field)> {
        self.fields
            .iter()
            .enumerate()
            .filter(|(_, f)| f.is_key)
            .collect()
    }

    pub fn indexed_fields(&self) -> Vec<(usize, &Field)> {
        self.fields
            .iter()
            .enumerate()
            .filter(|(_, f)| f.indexed)
            .collect()
    }

    pub fn value_fields(&self) -> Vec<(usize, &Field)> {
        self.fields
            .iter()
            .enumerate()
            .filter(|(_, f)| !f.is_key)
            .collect()
    }
}

// ── Record ───────────────────────────────────────────────────────────────────

/// An ordered list of FieldValues matching the fields of a Schema.
/// Field order must match Schema field order exactly.
#[derive(Debug, Clone, PartialEq)]
pub struct Record {
    pub values: Vec<FieldValue>,
}

impl Record {
    pub fn new(values: Vec<FieldValue>) -> Self {
        Self { values }
    }

    pub fn get(&self, idx: usize) -> Option<&FieldValue> {
        self.values.get(idx)
    }

    pub fn get_by_name<'a>(&'a self, schema: &Schema, name: &str) -> Option<&'a FieldValue> {
        let (idx, _) = schema.field(name)?;
        self.values.get(idx)
    }

    /// Validate that this record's values match the schema field types.
    pub fn validate(&self, schema: &Schema) -> Result<(), crate::error::RelError> {
        if self.values.len() != schema.fields.len() {
            return Err(crate::error::RelError::Schema(format!(
                "record has {} values, schema '{}' expects {}",
                self.values.len(),
                schema.name,
                schema.fields.len()
            )));
        }
        for (field, value) in schema.fields.iter().zip(self.values.iter()) {
            let ok = match (&field.field_type, value) {
                (FieldType::U8, FieldValue::U8(_)) => true,
                (FieldType::U16, FieldValue::U16(_)) => true,
                (FieldType::U32, FieldValue::U32(_)) => true,
                (FieldType::U64, FieldValue::U64(_)) => true,
                (FieldType::U128, FieldValue::U128(_)) => true,
                (FieldType::I64, FieldValue::I64(_)) => true,
                (FieldType::Bool, FieldValue::Bool(_)) => true,
                (FieldType::Bytes(n), FieldValue::Bytes(b)) => b.len() == *n,
                (FieldType::VarBytes, FieldValue::Bytes(_)) => true,
                (FieldType::Address, FieldValue::Bytes(b)) => b.len() == 32,
                (FieldType::Hash, FieldValue::Bytes(b)) => b.len() == 32,
                (_, FieldValue::Null) => true, // null is always acceptable
                _ => false,
            };
            if !ok {
                return Err(crate::error::RelError::TypeMismatch {
                    field: field.name.clone(),
                    expected: field.field_type.type_name().to_string(),
                    got: value.type_name().to_string(),
                });
            }
        }
        Ok(())
    }
}