sqlx-record/.claude/skills/sqlx-lookup.md

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);