Skip to main content

Overview

The ordering system allows API clients to specify sort orders for query results. It supports multiple fields with explicit directions and validates ordering terms against schemas to ensure only orderable fields are used.

Ordering Syntax

Basic Ordering

Order by a single field:
use bomboni_request::ordering::Ordering;

// Ascending order (default)
let ordering = Ordering::parse("name").unwrap();

// Descending order
let ordering = Ordering::parse("age desc").unwrap();

// Explicit ascending
let ordering = Ordering::parse("created_at asc").unwrap();

Multi-Field Ordering

Order by multiple fields with comma separation:
// Order by name (ascending), then age (descending)
let ordering = Ordering::parse("name asc, age desc").unwrap();

// Mixed with defaults
let ordering = Ordering::parse("priority desc, created_at").unwrap();

Nested Fields

Use dot notation for nested object fields:
let ordering = Ordering::parse("user.displayName desc, task.userId asc").unwrap();

Ordering Directions

use bomboni_request::ordering::{OrderingDirection, OrderingTerm};

let term = OrderingTerm {
    name: "age".into(),
    direction: OrderingDirection::Ascending,
};
// Results ordered from smallest to largest

Parsing and Validation

Parse from String

use bomboni_request::ordering::Ordering;

let ordering = Ordering::parse("displayName desc, age asc").unwrap();
assert_eq!(ordering.to_string(), "displayName desc, age asc");

Schema Validation

Only fields marked as ordered in the schema can be used for ordering:
use bomboni_request::schema::{Schema, FieldMemberSchema, ValueType};
use bomboni_macros::btree_map_into;

let schema = Schema {
    members: btree_map_into! {
        "id" => FieldMemberSchema::new_ordered(ValueType::String),
        "age" => FieldMemberSchema::new_ordered(ValueType::Integer),
        "name" => FieldMemberSchema::new(ValueType::String), // Not orderable
    },
};

// Valid - both fields are orderable
let ordering = Ordering::parse("id asc, age desc").unwrap();
ordering.validate(&schema).unwrap();

// Invalid - name is not marked as orderable
let invalid_ordering = Ordering::parse("name asc").unwrap();
assert!(invalid_ordering.validate(&schema).is_err());

Ordering Evaluation

Compare Items

Use ordering to compare two items:
use bomboni_request::ordering::Ordering;
use bomboni_request::testing::schema::UserItem;
use std::cmp::Ordering as CmpOrdering;

let ordering = Ordering::parse("displayName desc, age asc").unwrap();

let a = UserItem {
    id: "1".into(),
    display_name: "Alice".into(),
    age: 30,
};

let b = UserItem {
    id: "2".into(),
    display_name: "Bob".into(),
    age: 25,
};

let comparison = ordering.evaluate(&a, &b).unwrap();
assert_eq!(comparison, CmpOrdering::Greater); // Alice > Bob (desc order)

Sort Collections

use bomboni_request::ordering::Ordering;
use bomboni_request::testing::schema::UserItem;

let mut users = vec![
    UserItem { id: "3".into(), display_name: "Charlie".into(), age: 25 },
    UserItem { id: "1".into(), display_name: "Alice".into(), age: 30 },
    UserItem { id: "2".into(), display_name: "Bob".into(), age: 25 },
];

let ordering = Ordering::parse("age desc, displayName asc").unwrap();

users.sort_by(|a, b| ordering.evaluate(a, b).unwrap());

// Results:
// 1. Alice (age: 30)
// 2. Bob (age: 25, alphabetically first)
// 3. Charlie (age: 25, alphabetically second)

Ordering Terms

Access individual ordering terms:
use bomboni_request::ordering::Ordering;

let ordering = Ordering::parse("priority desc, created_at asc").unwrap();

for term in ordering.iter() {
    println!("Field: {}, Direction: {}", term.name, term.direction);
}
// Output:
// Field: priority, Direction: desc
// Field: created_at, Direction: asc

Integration with Queries

List Queries

Ordering is automatically integrated with list queries:
use bomboni_request::query::list::{ListQueryBuilder, ListQueryConfig};
use bomboni_request::query::page_token::plain::PlainPageTokenBuilder;
use bomboni_request::ordering::{OrderingTerm, OrderingDirection};
use bomboni_request::testing::schema::UserItem;
use std::collections::BTreeMap;

let builder = ListQueryBuilder::new(
    UserItem::get_schema(),
    BTreeMap::new(),
    ListQueryConfig {
        max_page_size: Some(100),
        default_page_size: 20,
        primary_ordering_term: Some(OrderingTerm {
            name: "id".into(),
            direction: OrderingDirection::Ascending,
        }),
        max_filter_length: Some(1000),
        max_ordering_length: Some(100),
    },
    PlainPageTokenBuilder {},
);

let query = builder.build(
    Some(50),
    None,
    None,
    Some("displayName desc, age asc") // Ordering parameter
).unwrap();

assert_eq!(query.ordering.to_string(), "id asc, displayName desc, age asc");
// Note: primary_ordering_term (id asc) is prepended automatically

Primary Ordering Terms

The primary_ordering_term is automatically prepended to ordering specifications if not already present. This ensures consistent pagination behavior.
let config = ListQueryConfig {
    primary_ordering_term: Some(OrderingTerm {
        name: "id".into(),
        direction: OrderingDirection::Ascending,
    }),
    // ... other config
};

// Client requests: "age desc"
// Actual ordering used: "id asc, age desc"

SQL Generation

Convert ordering to SQL ORDER BY clauses:
use bomboni_request::sql::{SqlDialect, ordering::SqlOrderingBuilder};
use bomboni_request::ordering::Ordering;
use bomboni_request::testing::schema::UserItem;

let ordering = Ordering::parse("displayName desc, age asc").unwrap();
let schema = UserItem::get_schema();

let sql = SqlOrderingBuilder::new(SqlDialect::Postgres, &schema)
    .build(&ordering)
    .unwrap();

assert_eq!(sql, r#""displayName" DESC, "age" ASC"#);

Error Handling

Parse Errors

use bomboni_request::ordering::Ordering;

// Duplicate fields are not allowed
let result = Ordering::parse("name asc, name desc");
assert!(result.is_err()); // DuplicateField error

// Invalid direction
let result = Ordering::parse("name invalid");
assert!(result.is_err()); // InvalidDirection error

// Too many parts
let result = Ordering::parse("name asc extra");
assert!(result.is_err()); // InvalidTermFormat error

Validation Errors

// Unknown field
let ordering = Ordering::parse("unknown_field asc").unwrap();
let result = ordering.validate(&schema);
assert!(result.is_err()); // UnknownMember error

// Unorderable field
let ordering = Ordering::parse("description asc").unwrap();
let result = ordering.validate(&schema);
assert!(result.is_err()); // UnorderedField error

Length Limits

Configure maximum ordering specification length:
use bomboni_request::query::list::ListQueryConfig;

let config = ListQueryConfig {
    max_ordering_length: Some(100), // Maximum 100 characters
    // ... other config
};

// Ordering strings exceeding this limit will be rejected

Best Practices

1

Mark Appropriate Fields as Orderable

Only mark indexed fields as orderable in your schema to ensure query performance
2

Always Include Primary Ordering

Use primary_ordering_term to ensure deterministic pagination with stable sort orders
3

Validate Before Use

Always validate ordering specifications against schemas before evaluation or SQL generation
4

Set Length Limits

Configure max_ordering_length to prevent excessively complex ordering specifications
Without a primary ordering term that guarantees unique sort order, pagination behavior may be unpredictable when multiple items have the same values for all ordering fields.

Complete Example

use bomboni_request::ordering::{Ordering, OrderingDirection};
use bomboni_request::testing::schema::UserItem;

// Parse ordering specification
let ordering = Ordering::parse("displayName desc, age asc").unwrap();
assert_eq!(ordering.to_string(), "displayName desc, age asc");

// Compare items
let alice = UserItem {
    id: "1".into(),
    display_name: "Alice".into(),
    age: 30,
};

let bob = UserItem {
    id: "2".into(), 
    display_name: "Bob".into(),
    age: 25,
};

let comparison = ordering.evaluate(&alice, &bob).unwrap();
assert_eq!(comparison, std::cmp::Ordering::Greater); // Alice > Bob by displayName desc

// Sort a collection
let mut users = vec![bob.clone(), alice.clone()];
users.sort_by(|a, b| ordering.evaluate(a, b).unwrap());
assert_eq!(users[0].display_name, "Bob"); // Bob comes first (desc order)

Pagination

Use ordering with list queries

SQL Generation

Convert ordering to SQL ORDER BY clauses

Build docs developers (and LLMs) love