Data Types
Data Types
Overview
ic-dbms provides a rich set of data types for defining table schemas. Each type maps to both Rust types and Candid types for seamless integration with the Internet Computer ecosystem.
Type categories:
| Category | Types |
|---|---|
| Integers | Uint8, Uint16, Uint32, Uint64, Int8, Int16, Int32, Int64 |
| Decimal | Decimal |
| Text | Text |
| Boolean | Boolean |
| Date/Time | Date, DateTime |
| Binary | Blob |
| Identifiers | Principal, Uuid |
| Semi-structured | Json |
| Wrapper | Nullable<T> |
Integer Types
Unsigned Integers
Uint8 - 8-bit unsigned integer (0 to 255)
use ic_dbms_api::prelude::Uint8;
#[derive(Table, ...)]
#[table = "settings"]
pub struct Setting {
#[primary_key]
pub id: Uint32,
pub priority: Uint8, // 0-255
}
// Usage
let setting = SettingInsertRequest {
id: 1.into(),
priority: 10.into(), // or Uint8::from(10)
};
Uint16 - 16-bit unsigned integer (0 to 65,535)
use ic_dbms_api::prelude::Uint16;
pub struct Product {
pub stock_count: Uint16, // 0-65,535
}
let count: Uint16 = 1000.into();
Uint32 - 32-bit unsigned integer (0 to 4,294,967,295)
use ic_dbms_api::prelude::Uint32;
pub struct User {
#[primary_key]
pub id: Uint32, // Common for primary keys
}
let id: Uint32 = 12345.into();
Uint64 - 64-bit unsigned integer (0 to 18,446,744,073,709,551,615)
use ic_dbms_api::prelude::Uint64;
pub struct Transaction {
pub amount_e8s: Uint64, // For large numbers like token amounts
}
let amount: Uint64 = 1_000_000_000u64.into();
Signed Integers
Int8 - 8-bit signed integer (-128 to 127)
use ic_dbms_api::prelude::Int8;
pub struct Temperature {
pub celsius: Int8, // -128 to 127
}
let temp: Int8 = (-10).into();
Int16 - 16-bit signed integer (-32,768 to 32,767)
use ic_dbms_api::prelude::Int16;
pub struct Altitude {
pub meters: Int16, // Can be negative (below sea level)
}
let altitude: Int16 = (-100).into();
Int32 - 32-bit signed integer (-2,147,483,648 to 2,147,483,647)
use ic_dbms_api::prelude::Int32;
pub struct Account {
pub balance_cents: Int32, // Can be negative (debt)
}
let balance: Int32 = (-5000).into();
Int64 - 64-bit signed integer
use ic_dbms_api::prelude::Int64;
pub struct Statistics {
pub total_change: Int64, // Large signed values
}
let change: Int64 = (-1_000_000_000i64).into();
Decimal
Decimal - Arbitrary-precision decimal number
use ic_dbms_api::prelude::Decimal;
pub struct Product {
pub price: Decimal, // $19.99
pub weight_kg: Decimal, // 2.5
}
// From f64
let price: Decimal = 19.99.into();
// From string (more precise)
let price: Decimal = "19.99".parse().unwrap();
// With sanitizer for rounding
#[derive(Table, ...)]
#[table = "products"]
pub struct Product {
#[primary_key]
pub id: Uint32,
#[sanitizer(RoundToScaleSanitizer(2))] // Round to 2 decimal places
pub price: Decimal,
}
Note: Use RoundToScaleSanitizer to ensure consistent decimal precision.
Text
Text - UTF-8 string
use ic_dbms_api::prelude::Text;
pub struct User {
pub name: Text,
pub email: Text,
pub bio: Text,
}
// From &str
let name: Text = "Alice".into();
// From String
let email: Text = String::from("alice@example.com").into();
// Access the string
let text: Text = "Hello".into();
assert_eq!(text.as_str(), "Hello");
With validation:
#[derive(Table, ...)]
#[table = "users"]
pub struct User {
#[primary_key]
pub id: Uint32,
#[validate(MaxStrlenValidator(100))]
pub name: Text,
#[validate(EmailValidator)]
pub email: Text,
}
With sanitization:
#[derive(Table, ...)]
#[table = "users"]
pub struct User {
#[primary_key]
pub id: Uint32,
#[sanitizer(TrimSanitizer)]
#[sanitizer(CollapseWhitespaceSanitizer)]
pub name: Text,
#[sanitizer(LowerCaseSanitizer)]
pub email: Text,
}
Boolean
Boolean - True or false value
use ic_dbms_api::prelude::Boolean;
pub struct User {
pub is_active: Boolean,
pub email_verified: Boolean,
}
let active: Boolean = true.into();
let verified: Boolean = false.into();
// Convert back
let value: bool = active.into();
Filtering by boolean:
// Find active users
let filter = Filter::eq("is_active", Value::Boolean(true));
// Find unverified users
let filter = Filter::eq("email_verified", Value::Boolean(false));
Date and Time
Date
Date - Calendar date (year, month, day)
use ic_dbms_api::prelude::Date;
pub struct Event {
pub event_date: Date,
}
// Create from components
let date = Date::new(2024, 6, 15); // June 15, 2024
// From chrono NaiveDate (if using chrono)
use chrono::NaiveDate;
let naive = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
let date: Date = naive.into();
DateTime
DateTime - Date and time with timezone
use ic_dbms_api::prelude::DateTime;
pub struct User {
pub created_at: DateTime,
pub last_login: DateTime,
}
// Current time
let now = DateTime::now();
// From chrono DateTime<Utc>
use chrono::{DateTime as ChronoDateTime, Utc};
let chrono_dt: ChronoDateTime<Utc> = Utc::now();
let dt: DateTime = chrono_dt.into();
With sanitization:
#[derive(Table, ...)]
#[table = "events"]
pub struct Event {
#[primary_key]
pub id: Uint32,
#[sanitizer(UtcSanitizer)] // Convert to UTC
pub scheduled_at: DateTime,
}
Binary Data
Blob
Blob - Binary large object (byte array)
use ic_dbms_api::prelude::Blob;
pub struct Document {
pub content: Blob, // File content
pub thumbnail: Blob, // Image data
}
// From Vec<u8>
let data: Vec<u8> = vec![0x89, 0x50, 0x4E, 0x47]; // PNG header
let blob: Blob = data.into();
// From slice
let blob: Blob = Blob::from(&[1, 2, 3, 4][..]);
// Access bytes
let bytes: &[u8] = blob.as_slice();
Note: Be mindful of storage costs when storing large blobs. Consider storing only references (hashes, URLs) for very large files.
Identifiers
Principal
Principal - Internet Computer principal identifier
use ic_dbms_api::prelude::Principal;
pub struct User {
pub owner: Principal, // IC principal who owns this record
}
// From text
let principal = Principal::from_text("aaaaa-aa").unwrap();
// Anonymous principal
let anon = Principal::anonymous();
// Caller principal (in canister)
let caller = ic_cdk::caller();
Use cases:
- Storing user identities
- Recording ownership
- Access control references
Uuid
Uuid - Universally unique identifier (128-bit)
use ic_dbms_api::prelude::Uuid;
pub struct Order {
#[primary_key]
pub id: Uuid, // UUID as primary key
}
// Generate new UUID
let id = Uuid::new_v4();
// From string
let id: Uuid = "550e8400-e29b-41d4-a716-446655440000".parse().unwrap();
// From bytes
let bytes: [u8; 16] = [/* 16 bytes */];
let id = Uuid::from_bytes(bytes);
Benefits over sequential IDs:
- Globally unique without coordination
- No sequential guessing
- Safe for distributed systems
Semi-Structured Data
Json
Json - JSON object or array
use ic_dbms_api::prelude::Json;
use std::str::FromStr;
pub struct User {
pub metadata: Json, // Flexible schema
pub preferences: Json, // User settings
}
// From string
let json = Json::from_str(r#"{"theme": "dark", "language": "en"}"#).unwrap();
// From serde_json::Value
use serde_json::json;
let json: Json = json!({
"notifications": true,
"timezone": "UTC"
}).into();
Querying JSON:
// Check if JSON contains pattern
let filter = Filter::json("metadata", JsonFilter::contains(
Json::from_str(r#"{"active": true}"#).unwrap()
));
// Extract and compare
let filter = Filter::json("preferences",
JsonFilter::extract_eq("theme", Value::Text("dark".into()))
);
// Check path exists
let filter = Filter::json("metadata", JsonFilter::has_key("email"));
See the JSON Reference for comprehensive JSON documentation.
Nullable
Nullable<T> - Optional value wrapper
use ic_dbms_api::prelude::{Nullable, Text, Uint32};
pub struct User {
#[primary_key]
pub id: Uint32,
pub name: Text,
pub phone: Nullable<Text>, // Optional phone number
pub age: Nullable<Uint32>, // Optional age
pub bio: Nullable<Text>, // Optional biography
}
// Insert with value
let user = UserInsertRequest {
id: 1.into(),
name: "Alice".into(),
phone: Nullable::Value("555-1234".into()),
age: Nullable::Null,
bio: Nullable::Null,
};
// Check if null
let phone = user.phone;
match phone {
Nullable::Value(p) => println!("Phone: {}", p.as_str()),
Nullable::Null => println!("No phone number"),
}
Filtering nullable fields:
// Find users with phone numbers
let filter = Filter::not_null("phone");
// Find users without phone numbers
let filter = Filter::is_null("phone");
// Find users with specific phone
let filter = Filter::eq("phone", Value::Text("555-1234".into()));
Nullable foreign keys:
pub struct Employee {
#[primary_key]
pub id: Uint32,
pub name: Text,
#[foreign_key(entity = "Employee", table = "employees", column = "id")]
pub manager_id: Nullable<Uint32>, // Top-level employees have no manager
}
Custom Types
Beyond the built-in types listed above, ic-dbms supports user-defined custom data types. Custom types let you store enums, structs, and newtypes in your tables by implementing the CustomDataType trait.
use ic_dbms_api::prelude::*;
#[derive(Debug, Table, CandidType, Deserialize, Clone, PartialEq, Eq)]
#[table = "tasks"]
pub struct Task {
#[primary_key]
pub id: Uint32,
#[custom_type]
pub priority: Priority, // User-defined custom type
}
See the Custom Data Types Guide for step-by-step instructions on defining and using custom types.
Type Conversion Reference
| ic-dbms Type | Rust Type | Candid Type |
|---|---|---|
Uint8 |
u8 |
nat8 |
Uint16 |
u16 |
nat16 |
Uint32 |
u32 |
nat32 |
Uint64 |
u64 |
nat64 |
Int8 |
i8 |
int8 |
Int16 |
i16 |
int16 |
Int32 |
i32 |
int32 |
Int64 |
i64 |
int64 |
Decimal |
rust_decimal::Decimal |
text (serialized) |
Text |
String |
text |
Boolean |
bool |
bool |
Date |
chrono::NaiveDate |
record { year; month; day } |
DateTime |
chrono::DateTime<Utc> |
int64 (timestamp) |
Blob |
Vec<u8> |
blob |
Principal |
candid::Principal |
principal |
Uuid |
uuid::Uuid |
text |
Json |
serde_json::Value |
text (serialized) |
Nullable<T> |
Option<T> |
opt T |
Conversion examples:
// Rust primitive to ic-dbms type
let uint: Uint32 = 42u32.into();
let text: Text = "hello".into();
let boolean: Boolean = true.into();
// ic-dbms type to Rust primitive
let num: u32 = uint.into();
let s: String = text.into();
let b: bool = boolean.into();