280 lines
6.1 KiB
Markdown
280 lines
6.1 KiB
Markdown
# 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
|
|
```rust
|
|
lookup_table!(Name, "code1", "code2", "code3");
|
|
```
|
|
|
|
### Generated Code
|
|
```rust
|
|
// 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
|
|
```rust
|
|
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
|
|
```sql
|
|
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
|
|
```rust
|
|
lookup_options!(Name, "code1", "code2", "code3");
|
|
```
|
|
|
|
### Generated Code
|
|
```rust
|
|
// 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
|
|
```rust
|
|
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
|
|
```rust
|
|
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
|
|
```rust
|
|
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
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
// 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);
|
|
```
|