6.1 KiB
6.1 KiB
sqlx-record Lookup Tables Skill
Guide to lookup_table! and lookup_options! macros.
Triggers
- "lookup table", "lookup options"
- "code enum", "status enum"
- "type-safe codes", "constants"
lookup_table! Macro
Creates a database-backed lookup entity with type-safe code enum.
Syntax
lookup_table!(Name, "code1", "code2", "code3");
Generated Code
// Entity struct with CRUD via #[derive(Entity)]
#[derive(Entity, FromRow)]
pub struct Name {
#[primary_key]
pub code: String,
pub name: String,
pub description: String,
pub is_active: bool,
}
// Type-safe enum
pub enum NameCode {
Code1,
Code2,
Code3,
}
// String constants
impl Name {
pub const CODE1: &'static str = "code1";
pub const CODE2: &'static str = "code2";
pub const CODE3: &'static str = "code3";
}
// Display impl (enum -> string)
impl Display for NameCode { ... }
// TryFrom impl (string -> enum)
impl TryFrom<&str> for NameCode { ... }
Example
lookup_table!(OrderStatus,
"pending",
"processing",
"shipped",
"delivered",
"cancelled"
);
// Usage
let status = OrderStatus::PENDING; // "pending"
let code = OrderStatusCode::Pending;
println!("{}", code); // "pending"
// Parse from string
let code = OrderStatusCode::try_from("shipped")?;
// Query the lookup table
let statuses = OrderStatus::find(&pool, filters![("is_active", true)], None).await?;
// Get specific status
let status = OrderStatus::get_by_code(&pool, OrderStatus::PENDING).await?;
Database Schema
CREATE TABLE order_status (
code VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
INSERT INTO order_status VALUES
('pending', 'Pending', 'Order awaiting processing', TRUE),
('processing', 'Processing', 'Order being prepared', TRUE),
('shipped', 'Shipped', 'Order in transit', TRUE),
('delivered', 'Delivered', 'Order completed', TRUE),
('cancelled', 'Cancelled', 'Order cancelled', TRUE);
lookup_options! Macro
Creates code enum without database entity (for embedded options).
Syntax
lookup_options!(Name, "code1", "code2", "code3");
Generated Code
// Type-safe enum
pub enum NameCode {
Code1,
Code2,
Code3,
}
// Unit struct for constants
pub struct Name;
impl Name {
pub const CODE1: &'static str = "code1";
pub const CODE2: &'static str = "code2";
pub const CODE3: &'static str = "code3";
}
// Display and TryFrom implementations
Example
lookup_options!(PaymentMethod,
"credit-card",
"debit-card",
"paypal",
"bank-transfer",
"crypto"
);
// Usage
let method = PaymentMethod::CREDIT_CARD; // "credit-card"
// In entity
#[derive(Entity)]
struct Order {
#[primary_key]
id: Uuid,
payment_method: String, // Stores the code string
}
// Query with constant
let orders = Order::find(&pool,
filters![("payment_method", PaymentMethod::PAYPAL)],
None
).await?;
// Type-safe matching
match PaymentMethodCode::try_from(order.payment_method.as_str())? {
PaymentMethodCode::CreditCard => process_credit_card(),
PaymentMethodCode::DebitCard => process_debit_card(),
PaymentMethodCode::Paypal => process_paypal(),
PaymentMethodCode::BankTransfer => process_bank_transfer(),
PaymentMethodCode::Crypto => process_crypto(),
}
Code Naming Conventions
Codes are converted to enum variants using camelCase:
| Code String | Enum Variant | Constant |
|---|---|---|
"active" |
Active |
ACTIVE |
"pro-rata" |
ProRata |
PRO_RATA |
"full-24-hours" |
Full24Hours |
FULL_24_HOURS |
"activity_added" |
ActivityAdded |
ACTIVITY_ADDED |
"no-refunds" |
NoRefunds |
NO_REFUNDS |
When to Use Each
Use lookup_table! when:
- Lookup values are stored in database
- Values may change at runtime
- Need to query/manage lookup values
- Want audit trail on lookup changes
Use lookup_options! when:
- Codes are compile-time constants
- No database table needed
- Codes are embedded in other entities
- Values won't change without code deployment
Real-World Examples
Status Workflows
lookup_table!(TaskStatus,
"todo",
"in-progress",
"review",
"done",
"archived"
);
impl Task {
pub fn can_transition(&self, to: TaskStatusCode) -> bool {
match (TaskStatusCode::try_from(self.status.as_str()), to) {
(Ok(TaskStatusCode::Todo), TaskStatusCode::InProgress) => true,
(Ok(TaskStatusCode::InProgress), TaskStatusCode::Review) => true,
(Ok(TaskStatusCode::Review), TaskStatusCode::Done) => true,
(Ok(TaskStatusCode::Review), TaskStatusCode::InProgress) => true,
(Ok(_), TaskStatusCode::Archived) => true,
_ => false,
}
}
}
Configuration Options
lookup_options!(LogLevel, "debug", "info", "warn", "error");
lookup_options!(Theme, "light", "dark", "system");
lookup_options!(Language, "en", "es", "fr", "de", "ja");
#[derive(Entity)]
struct UserPreferences {
#[primary_key]
user_id: Uuid,
log_level: String,
theme: String,
language: String,
}
// Usage
let prefs = UserPreferences {
user_id: user_id,
log_level: LogLevel::INFO.into(),
theme: Theme::DARK.into(),
language: Language::EN.into(),
};
Domain-Specific Types
lookup_options!(FastingPattern,
"time-restricted",
"omad",
"alternate-day",
"five-two",
"extended",
"custom"
);
lookup_options!(ProgramType,
"self-paced",
"live",
"challenge",
"maintenance"
);
lookup_options!(SubscriptionTier,
"free",
"basic",
"premium",
"unlimited"
);
Error Handling
// TryFrom returns Result
match UserStatusCode::try_from(unknown_string) {
Ok(code) => handle_status(code),
Err(msg) => {
// msg: "Unknown userstatus code: invalid_value"
log::warn!("{}", msg);
handle_unknown_status()
}
}
// Or use unwrap_or for default
let code = UserStatusCode::try_from(status_str)
.unwrap_or(UserStatusCode::Pending);