Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sorting

CRUDCrate supports flexible sorting with multiple format options.

Enabling Sorting

Mark fields as sortable:

#[derive(EntityToModels)]
pub struct Model {
    #[crudcrate(sortable)]
    pub name: String,

    #[crudcrate(sortable)]
    pub created_at: DateTimeUtc,

    #[crudcrate(sortable)]
    pub priority: i32,

    // Not sortable
    pub description: String,
}

Sort Syntax

JSON Array Format (React Admin)

# Sort by single field with direction
GET /items?sort=["created_at","DESC"]

# Default order is ASC when direction omitted
GET /items?sort=["name"]

REST Query Parameters

Two REST-style formats are supported:

# Using sort_by + order (preferred)
GET /items?sort_by=created_at&order=DESC

# Using sort + order (alternative)
GET /items?sort=created_at&order=DESC

# Default order is ASC
GET /items?sort_by=name

Note: The sort_by parameter takes priority over sort if both are provided.

Sort Directions

DirectionDescription
ASCAscending (A-Z, 0-9, oldest first)
DESCDescending (Z-A, 9-0, newest first)

Case-insensitive: ASC, asc, Asc all work.

Examples by Type

Strings

# A to Z
GET /items?sort=["name","ASC"]

# Z to A
GET /items?sort=["name","DESC"]

Numbers

# Lowest to highest
GET /items?sort=["priority","ASC"]

# Highest to lowest
GET /items?sort=["priority","DESC"]

Dates

# Oldest first
GET /items?sort=["created_at","ASC"]

# Newest first (common for feeds)
GET /items?sort=["created_at","DESC"]

Booleans

# false first (0), then true (1)
GET /items?sort=["is_active","ASC"]

# true first (1), then false (0)
GET /items?sort=["is_active","DESC"]

Default Sort Order

When no sort is specified, CRUDCrate uses the primary key:

// Default: ORDER BY id ASC
GET /items

Configure a different default in your handler or operations.

Multiple Column Sorting

For multi-column sorting, implement a custom handler:

use sea_orm::{Order, QueryOrder};

async fn list_items(
    Query(params): Query<FilterOptions>,
    Extension(db): Extension<DatabaseConnection>,
) -> Result<Json<Vec<ItemList>>, ApiError> {
    let query = Entity::find()
        .order_by(Column::Priority, Order::Desc)  // Primary sort
        .order_by(Column::CreatedAt, Order::Desc); // Secondary sort

    let items = query.all(&db).await?;
    Ok(Json(items.into_iter().map(Into::into).collect()))
}

Null Handling

Null values sort based on database:

DatabaseNULL Position
PostgreSQLNULLS LAST (default)
MySQLNULLS FIRST (in ASC)
SQLiteNULLS FIRST

For consistent behavior, consider using COALESCE in custom queries.

Programmatic Sorting

Use sorting in code:

use crudcrate::filtering::{parse_sorting, FilterOptions};
use sea_orm::{Order, QueryOrder};

async fn custom_list(
    Query(params): Query<FilterOptions>,
    Extension(db): Extension<DatabaseConnection>,
) -> Result<Json<Vec<Item>>, ApiError> {
    let (column, order) = parse_sorting::<Entity>(&params);

    let items = Entity::find()
        .order_by(column, order)
        .all(&db)
        .await?;

    Ok(Json(items.into_iter().map(Into::into).collect()))
}

Manual Sorting

use sea_orm::{Order, QueryOrder};

let items = Entity::find()
    .order_by(Column::Priority, Order::Desc)
    .order_by(Column::Name, Order::Asc)
    .all(&db)
    .await?;

Security

Field Validation

Only sortable fields can be used:

#[crudcrate(sortable)]
pub name: String,  // Allowed

pub secret: String,  // Not sortable - ignored

Invalid sort fields are silently ignored (falls back to default).

No SQL Injection

Sort fields are validated against entity columns:

# Attempted injection
GET /items?sort=["name; DROP TABLE items;--","ASC"]

# Result: field not found, uses default sort
# No SQL executed with injected content

Performance Tips

Index Sorted Fields

-- Index for sorted field
CREATE INDEX idx_items_created_at ON items(created_at);

-- Composite index for filter + sort
CREATE INDEX idx_items_status_created
    ON items(status, created_at DESC);

Consider Sort + Filter Combinations

The most efficient queries have indexes covering both filter and sort:

# Common query pattern
GET /items?filter={"status":"active"}&sort=["created_at","DESC"]
-- Optimal index
CREATE INDEX idx_items_status_created
    ON items(status, created_at DESC);

CRUDCrate supports sorting by columns from related entities using dot-notation syntax.

Enabling Join Sorting

Use the sortable(...) parameter inside join(...) to specify which columns from a related entity can be used for sorting:

#[derive(EntityToModels)]
pub struct Model {
    #[sea_orm(primary_key)]
    #[crudcrate(primary_key)]
    pub id: Uuid,

    #[crudcrate(filterable, sortable)]
    pub name: String,

    // Vehicles relationship with sortable columns
    #[sea_orm(ignore)]
    #[crudcrate(
        non_db_attr,
        join(one, all, depth = 1, sortable("year", "mileage"))
    )]
    pub vehicles: Vec<Vehicle>,
}

Dot-Notation Syntax

Sort using relation.column format:

# Sort customers by their vehicle's year (newest first)
GET /customers?sort=["vehicles.year","DESC"]

# Sort by vehicle mileage (lowest first)
GET /customers?sort=["vehicles.mileage","ASC"]

# REST format also supported
GET /customers?sort_by=vehicles.year&order=DESC

Combining with Filtering

# Filter by vehicle make, sort by vehicle year
GET /customers?filter={"vehicles.make":"BMW"}&sort=["vehicles.year","DESC"]

# Filter by customer name, sort by vehicle mileage
GET /customers?filter={"name":"John"}&sort=["vehicles.mileage","ASC"]

Security (Whitelist Validation)

Only columns explicitly listed in sortable(...) can be sorted:

// Only year and mileage can be sorted
#[crudcrate(join(one, all, sortable("year", "mileage")))]
pub vehicles: Vec<Vehicle>,
# Allowed - year is in sortable
GET /customers?sort=["vehicles.year","DESC"]

# Falls back to default - make is NOT in sortable
GET /customers?sort=["vehicles.make","ASC"]

Invalid sort fields silently fall back to the default sort column.

Combining Filterable and Sortable

You can use both filterable(...) and sortable(...) together inside join(...):

#[sea_orm(ignore)]
#[crudcrate(
    non_db_attr,
    join(one, all, depth = 1, filterable("make", "year", "color"), sortable("year", "mileage"))
)]
pub vehicles: Vec<Vehicle>,

Limitations

Single-level joins only: Join sorting supports direct relationships only. Nested paths like vehicles.parts.price are not supported; only single-level paths like vehicles.year.

# Supported - single level
GET /customers?sort=["vehicles.year","DESC"]

# Not supported - nested path
GET /customers?sort=["vehicles.parts.price","ASC"]

Implementation Notes

Join sorting is validated and parsed automatically. Like join filtering, full automatic query execution for join sorts requires a custom read::many::body hook. The built-in handler validates and parses join sorts but falls back to the default sort column.

Common Patterns

Newest First (Default for Lists)

#[crudcrate(sortable, exclude(create, update))]
pub created_at: DateTimeUtc,
GET /items?sort=["created_at","DESC"]

Priority Queue

#[crudcrate(sortable, filterable)]
pub priority: i32,
# High priority first, then by date
GET /items?sort=["priority","DESC"]

Alphabetical Lists

#[crudcrate(sortable, fulltext)]
pub name: String,
GET /items?sort=["name","ASC"]

Error Handling

Invalid sort parameters don’t cause errors - they fall back to defaults:

# Invalid field - falls back to default
GET /items?sort=["nonexistent","ASC"]

# Invalid format - falls back to default
GET /items?sort=not-an-array

This design prevents client errors from breaking functionality.

Next Steps