#query-builder #search-query #sorting #sqlx #filtering #pagination #date-range

sqlx-paginated

A flexible, type-safe SQLx query builder for dynamic web APIs, offering seamless pagination, searching, filtering, and sorting

7 releases

new 0.2.28 Dec 21, 2024
0.2.27 Dec 12, 2024
0.2.25 Nov 26, 2024

#55 in Date and time

Download history 374/week @ 2024-11-08 67/week @ 2024-11-15 133/week @ 2024-11-22 28/week @ 2024-11-29 142/week @ 2024-12-06 104/week @ 2024-12-13

410 downloads per month

MIT license

110KB
2K SLoC

Paginated queries for SQLx

Rust crates.io docs License: MIT

A flexible, type-safe SQLx query builder for dynamic web APIs, offering seamless pagination, searching, filtering, and sorting.

Table of Contents

Features

Core Capabilities

  • πŸ” Full-text search with column specification
  • πŸ“‘ Smart pagination with customizable page size
  • πŸ”„ Dynamic sorting on any column
  • 🎯 Flexible filtering system
  • πŸ“… Date range filtering
  • πŸ”’ Type-safe operations
  • ⚑ High performance
  • πŸ›‘οΈ SQL injection protection

Technical Features

  • Builder patterns for query parameters and query construction
  • Graceful error handling
  • Logging with tracing (if enabled)
  • Macro and function support

Query Features

  • Case-insensitive search
  • Multiple column search
  • Complex filtering conditions
  • Date-based filtering
  • Dynamic sort direction
  • Customizable page size
  • Result count optimization (opt-out of total records lookup ahead)

Database Support

Current vs Planned Support

Database Status Version Features Notes
PostgreSQL βœ… Supported 12+ All features supported Ready
SQLite 🚧 Planned 3.35+ Basic features planned Development starting in Q1 2025
MySQL 🚧 Planned 8.0+ Core features planned On roadmap

⚠️ Note: This documentation covers PostgreSQL features only, as it's currently the only supported database.

Market Analysis

Ecosystem Gaps

  1. Query builders

    • Diesel: Full ORM, can be heavyweight
    • SeaQuery: Generic and can be verbose
    • sqlbuilder: Basic SQL building without pagination or security
  2. Missing features in existing solutions

    • Easy integration with web frameworks
    • Automatic type casting
    • Typesafe search/filter/sort/pagination capabilities

Unique Selling Points

  1. Quick Web Framework Integration with minimal footprint

Actix Web handler example

use sqlx_paginated::{paginated_query_as, FlatQueryParams};
use actix_web::{web, Responder, HttpResponse};

async fn list_users(web::Query(params): web::Query<FlatQueryParams>) -> impl Responder {
    let paginated_users = paginated_query_as!(User, "SELECT * FROM users")
        .with_params(params)
        .fetch_paginated(&pool)
        .await
        .unwrap();
    
    HttpResponse::Ok().json(json!(paginated_users))
}
  1. Type Safety & Ergonomics for parameter configuration
let params = QueryParamsBuilder::<User>::new()
    .with_pagination(1, 10)
    .with_sort("created_at", QuerySortDirection::Descending)
    .with_search("john", vec!["name", "email"])
    .build();
  1. Advanced Builder Patterns
  • Optional fluent API for query parameters (QueryParams) which allow defining search, search location, date filtering, ordering, and custom filtering.
  • Fluent API for the entire supported feature set, more here: advanced example
    paginated_query_as!(UserExample, "SELECT * FROM users")
        .with_params(initial_params)
        .with_query_builder(|params| {
            // Can override the default query builder (build_query_with_safe_defaults) with a complete custom one:
            QueryBuilder::<UserExample, Postgres>::new()
                .with_search(params) // Add or remove search feature from the query;
                .with_filters(params) // Add or remove custom filters from the query;
                .with_date_range(params) // Add or remove data range;
                .with_raw_condition("") // Add raw condition, no checks.
                .disable_protection() // This removes all column safety checks.
                .with_combined_conditions(|builder| {
                   // ...
                .build()
        })
        .disable_totals_count() // Disables the calculation of total record count
        .fetch_paginated(&pool)
        .await
        .unwrap()

Target Audience

  1. Primary users

    • Rust web developers
    • Teams needing secure query building
    • Projects requiring pagination APIs
    • SQLx users wanting higher-level abstractions
  2. Use cases

    • REST APIs with pagination
    • Admin panels
    • Data exploration interfaces

Installation

Add to Cargo.toml:

[dependencies]
sqlx_paginated = { version = "0.2.28", features = ["postgres"] }

Quick Start

Basic Usage

#[derive(sqlx::FromRow, serde::Serialize)]
struct User {
    id: i64,
    first_name: String,
    last_name: String,
    email: String,
    confirmed: bool,
    created_at: Option<DateTime<Utc>>,
}

/// Macro usage example
async fn get_users(pool: &PgPool) -> Result<PaginatedResponse<User>, sqlx::Error> {
    let params = QueryParamsBuilder::<User>::new()
        .with_pagination(1, 10)
        .with_sort("created_at", QuerySortDirection::Descending)
        .with_search("replace with dynamic value", vec!["first_name", "last_name", "email"])
        .build();
    let paginated_response = paginated_query_as!(User, "SELECT * FROM users")
        // Alternative function call example (if macros cannot be applied to your use case):
        // paginated_query_as::<User>("SELECT * FROM users")
        .with_params(params)
        .fetch_paginated(pool)
        .await?;

    Ok(paginated_response)
}

Response Example

{
  "records": [
    {
      "id": "409e3900-c190-4dad-882d-ec2d40245329",
      "first_name": "John",
      "last_name": "Smith",
      "email": "john@example.com",
      "confirmed": true,
      "created_at": "2024-01-01T00:00:00Z"
    }
  ],
  "page": 1,
  "page_size": 10,
  "total_pages": 1
}

API Reference

Pagination Parameters

Parameter Type Default Min Max Description
page integer 1 1 n/a Current page number
page_size integer 10 10 50 Number of records per page

Example:

GET /v1/internal/users?page=2&page_size=20

Sort Parameters

Parameter Type Default Allowed Values Description
sort_column string created_at Any valid table column Column name to sort by
sort_direction string descending ascending, descending Sort direction

Example:

GET /v1/internal/users?sort_column=last_name&sort_direction=ascending

Search Parameters

Parameter Type Default Max Length Description
search string null 100 Search term to filter results
search_columns string name,description n/a Comma-separated list of columns

Example:

GET /v1/internal/users?search=john&search_columns=first_name,last_name,email

Date Range Parameters

Parameter Type Default Format Description
date_column string created_at Column name Column to filter on
date_after datetime null ISO 8601 Start of date range
date_before datetime null ISO 8601 End of date range

Example:

GET /v1/internal/users?date_column=created_at&date_after=2024-01-01T00:00:00Z

Filtering Parameters

Parameter Type Default Max Length Description
* string,boolean,datetime null 100 Any valid table column for given struct

Example:

GET /v1/internal/users?confirmed=true

Query Examples

  • Given the following struct, we can then perform search and filtering against its own fields.
  • We should also receive a paginated response back with the matching records.
#[derive(Serialize, Deserialize, FromRow, Default)]
pub struct User {
    pub id: Option<Uuid>,
    pub first_name: String,
    pub last_name: String,
    pub confirmed: Option<bool>,
    pub created_at: Option<DateTime<Utc>>,
    pub updated_at: Option<DateTime<Utc>>,
}
  1. Combined search, sort, date range, pagination and custom filter

  • Notice the confirmed=true filter.

Request:

GET /v1/internal/users
    ?search=john
    &search_columns=first_name,last_name,email
    &sort_column=created_at
    &sort_direction=descending
    &date_before=2024-11-03T12:30:12.081598Z
    &date_after=2024-11-02T12:30:12.081598Z
    &page=1
    &page_size=20
    &confirmed=true

Response:

{
  "page": 1,
  "page_size": 20,
  "total": 2,
  "total_pages": 1,
  "records": [
    {
      "id": "409e3900-c190-4dad-882d-ec2d40245329",
      "first_name": "John",
      "last_name": "Smith",
      "email": "john.smith@example.com",
      "confirmed": true,
      "created_at": "2024-11-03T12:30:12.081598Z",
      "updated_at": "2024-11-03T12:30:12.081598Z"
    },
    {
      "id": "9167d825-8944-4428-bf91-3c5531728b5e",
      "first_name": "Johnny",
      "last_name": "Doe",
      "email": "johnny.doe@example.com",
      "confirmed": true,
      "created_at": "2024-10-28T19:14:49.064626Z",
      "updated_at": "2024-10-28T19:14:49.064626Z"
    }
  ]
}
  1. Date range filter combined with two other custom filters

  • Notice the confirmed=true and first_name=Alex filters.
  • For the first_name filter the value will be an exact match (case-sensitive).
  • You can extend your struct as you please while the query parameters will also be available automatically.

Request:

GET /v1/internal/users
    ?date_before=2024-11-03T12:30:12.081598Z
    &date_after=2024-11-02T12:30:12.081598Z
    &confirmed=true
    &first_name=Alex

Response:

{
  "page": 1,
  "page_size": 20,
  "total": 1,
  "total_pages": 1,
  "records": [
    {
      "id": "509e3900-c190-4dad-882d-ec2d40245329",
      "first_name": "Alex",
      "last_name": "Johnson",
      "email": "alex.johnson@example.com",
      "confirmed": true,
      "created_at": "2024-11-02T12:30:12.081598Z"
    }
  ]
}

Performance Considerations

Query Pattern Optimization

Query Pattern Impact Recommendation
SELECT * ❌ High Impact Specify needed columns
Large Text Columns ❌ High Impact Use separate detail endpoint
Computed Columns ⚠️ Medium Impact Cache if possible
JSON Aggregation ⚠️ Medium Impact Limit array size
-- Text search
CREATE INDEX idx_users_name_gin ON users USING gin(to_tsvector('english', name));

-- Composite indexes for common queries
CREATE INDEX idx_users_confirmed_created ON users(confirmed, created_at);

-- JSON indexes
CREATE INDEX idx_users_metadata ON users USING gin(metadata);

Pagination Performance

Page Size Records Performance Impact
1-10 Optimal βœ… Best
11-50 Good βœ… Good
51-100 Caution ⚠️ Monitor
100+ Poor ❌ Not Recommended

Security Features

Input Sanitization

  • Search terms are cleaned and normalized
  • Parameter input values are trimmed and/or clamped against their defaults
  • Column names are validated against an allowlist:
    • The struct itself first;
    • Database specific table names second;
  • SQL injection patterns are blocked
  • System table access is prevented

Protected Patterns

  • System schemas (pg_, information_schema)
  • System columns (oid, xmin, etc.)
  • SQL injection attempts
  • Invalid characters in identifiers

Contributing

I warmly welcome contributions from the community! If you have ideas, improvements, or fixes, we encourage you to submit a Pull Request. Your input is highly valued, and I'm excited to collaborate with you to make this project even better.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Dependencies

~48MB
~842K SLoC