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

7.5 KiB

sqlx-record Entity Skill

Detailed guidance for #[derive(Entity)] macro.

Triggers

  • "derive entity", "entity macro"
  • "generate crud", "crud methods"
  • "primary key", "version field"
  • "table name", "rename field"

Struct Attributes

#[table_name]

#[derive(Entity, FromRow)]
#[table_name = "users"]  // or #[table_name("users")]
struct User { ... }
  • Optional: defaults to snake_case of struct name
  • User -> users, OrderItem -> order_items

Field Attributes

#[primary_key]

#[primary_key]
id: Uuid,
  • Required on one field
  • Generates get_by_{pk}, update_by_{pk} methods
  • Supports: Uuid, String, i32, i64, etc.

#[rename]

#[rename("user_name")]  // or #[rename = "user_name"]
name: String,
  • Maps struct field to different database column
  • Use when DB column doesn't match Rust naming

#[version]

#[version]
version: u32,
  • Auto-increments on every update
  • Wraps on overflow (u32::MAX -> 0)
  • Generates get_version(), get_versions() methods
  • Supports: u32, u64, i32, i64

#[field_type]

#[field_type("BIGINT")]  // or #[field_type = "BIGINT"]
large_count: i64,
  • SQLx type hint for compile-time validation
  • Adds type annotation in SELECT: field as "field: TYPE"

#[soft_delete]

#[soft_delete]
is_deleted: bool,
  • Enables soft delete functionality
  • Generates delete(), restore(), hard_delete() methods
  • Field must be bool type

#[created_at]

#[created_at]
created_at: i64,
  • Auto-set to current timestamp (milliseconds) on insert
  • Field must be i64 type
  • Excluded from UpdateForm

#[updated_at]

#[updated_at]
updated_at: i64,
  • Auto-set to current timestamp (milliseconds) on every update
  • Field must be i64 type
  • Excluded from UpdateForm

Generated Methods

Insert

pub async fn insert<E>(&self, executor: E) -> Result<PkType, sqlx::Error>

// Batch insert
pub async fn insert_many(executor, entities: &[Self]) -> Result<Vec<PkType>, Error>

// Insert or update on conflict
pub async fn upsert(&self, executor) -> Result<PkType, Error>
pub async fn insert_or_update(&self, executor) -> Result<PkType, Error>  // alias

Get Methods

// By single primary key
pub async fn get_by_id(executor, id: &Uuid) -> Result<Option<Self>, Error>

// By multiple primary keys
pub async fn get_by_ids(executor, ids: &[Uuid]) -> Result<Vec<Self>, Error>

// Generic primary key access
pub async fn get_by_primary_key(executor, pk: &PkType) -> Result<Option<Self>, Error>

Find Methods

// Basic find
pub async fn find(executor, filters: Vec<Filter>, index: Option<&str>) -> Result<Vec<Self>, Error>

// Find first match
pub async fn find_one(executor, filters: Vec<Filter>, index: Option<&str>) -> Result<Option<Self>, Error>

// With ordering
pub async fn find_ordered(
    executor,
    filters: Vec<Filter>,
    index: Option<&str>,
    order_by: Vec<(&str, bool)>  // (field, is_ascending)
) -> Result<Vec<Self>, Error>

// With ordering and pagination
pub async fn find_ordered_with_limit(
    executor,
    filters: Vec<Filter>,
    index: Option<&str>,
    order_by: Vec<(&str, bool)>,
    offset_limit: Option<(u32, u32)>  // (offset, limit)
) -> Result<Vec<Self>, Error>

// Count matching
pub async fn count(executor, filters: Vec<Filter>, index: Option<&str>) -> Result<u64, Error>

// Paginated results
pub async fn paginate(
    executor,
    filters: Vec<Filter>,
    index: Option<&str>,
    order_by: Vec<(&str, bool)>,
    page_request: PageRequest
) -> Result<Page<Self>, Error>

// Select specific columns only
pub async fn find_partial(
    executor,
    select_fields: &[&str],
    filters: Vec<Filter>,
    index: Option<&str>
) -> Result<Vec<Row>, Error>

Update Methods

// Update instance
pub async fn update(&self, executor, form: UpdateForm) -> Result<(), Error>

// Update by primary key
pub async fn update_by_id(executor, id: &Uuid, form: UpdateForm) -> Result<(), Error>

// Update multiple
pub async fn update_by_ids(executor, ids: &[Uuid], form: UpdateForm) -> Result<(), Error>

// Create update form
pub fn update_form() -> UpdateForm

Diff Methods

// Compare form with model
pub fn model_diff(form: &UpdateForm, model: &Self) -> serde_json::Value

// Compare form with database
pub async fn db_diff(form: &UpdateForm, pk: &PkType, executor) -> Result<Value, Error>

// Modify form to only include changes
pub fn diff_modify(form: &mut UpdateForm, model: &Self) -> serde_json::Value

// Convert entity to update form
pub fn to_update_form(&self) -> UpdateForm

// Get initial state as JSON
pub fn initial_diff(&self) -> serde_json::Value

Version Methods (if #[version] exists)

pub async fn get_version(executor, pk: &PkType) -> Result<Option<VersionType>, Error>
pub async fn get_versions(executor, pks: &[PkType]) -> Result<HashMap<PkType, VersionType>, Error>

Soft Delete Methods (if #[soft_delete] exists)

// Soft delete - sets field to true
pub async fn delete(&self, executor) -> Result<(), Error>
pub async fn delete_by_id(executor, id: &Uuid) -> Result<(), Error>

// Hard delete - permanently removes row
pub async fn hard_delete(&self, executor) -> Result<(), Error>
pub async fn hard_delete_by_id(executor, id: &Uuid) -> Result<(), Error>

// Restore - sets field to false
pub async fn restore(&self, executor) -> Result<(), Error>
pub async fn restore_by_id(executor, id: &Uuid) -> Result<(), Error>

// Get field name
pub const fn soft_delete_field() -> &'static str

Metadata Methods

pub const fn table_name() -> &'static str
pub fn entity_key(pk: &PkType) -> String  // "/entities/{table}/{pk}"
pub fn entity_changes_table_name() -> String  // "entity_changes_{table}"
pub const fn primary_key_field() -> &'static str
pub const fn primary_key_db_field() -> &'static str
pub fn primary_key(&self) -> &PkType
pub fn select_fields() -> Vec<&'static str>

UpdateForm

Generated struct {Entity}UpdateForm with all non-PK fields as Option.

// Builder pattern
let form = User::update_form()
    .with_name("Alice")
    .with_email("alice@example.com");

// Setter pattern
let mut form = User::update_form();
form.set_name("Alice");

// Execute
User::update_by_id(&pool, &id, form).await?;

Only set fields are updated - others remain unchanged.

Complete Example

use sqlx_record::prelude::*;
use sqlx::FromRow;
use uuid::Uuid;

#[derive(Entity, FromRow, Debug, Clone)]
#[table_name = "products"]
pub struct Product {
    #[primary_key]
    pub id: Uuid,

    #[rename("product_name")]
    pub name: String,

    pub price_cents: i64,

    pub category_id: Uuid,

    pub is_active: bool,

    #[version]
    pub version: u32,

    #[field_type("TEXT")]
    pub description: Option<String>,
}

// Usage
async fn example(pool: &Pool) -> Result<(), Error> {
    // Create
    let product = Product {
        id: new_uuid(),
        name: "Widget".into(),
        price_cents: 999,
        category_id: category_id,
        is_active: true,
        version: 0,
        description: Some("A great widget".into()),
    };
    product.insert(pool).await?;

    // Read
    let product = Product::get_by_id(pool, &product.id).await?.unwrap();

    // Update
    Product::update_by_id(pool, &product.id,
        Product::update_form()
            .with_price_cents(1299)
            .with_is_active(false)
    ).await?;

    // Find active products in category
    let products = Product::find(pool,
        filters![("is_active", true), ("category_id", category_id)],
        None
    ).await?;

    Ok(())
}