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
410 downloads per month
110KB
2K
SLoC
Paginated queries for SQLx
A flexible, type-safe SQLx query builder for dynamic web APIs, offering seamless pagination, searching, filtering, and sorting.
Table of Contents
- Features
- Database Support
- Market Analysis
- Installation
- Quick Start
- API Reference
- Query Examples
- Performance Considerations
- Security Features
- Contributing
- License
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
-
Query builders
- Diesel: Full ORM, can be heavyweight
- SeaQuery: Generic and can be verbose
- sqlbuilder: Basic SQL building without pagination or security
-
Missing features in existing solutions
- Easy integration with web frameworks
- Automatic type casting
- Typesafe search/filter/sort/pagination capabilities
Unique Selling Points
- 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))
}
- 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();
- 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
-
Primary users
- Rust web developers
- Teams needing secure query building
- Projects requiring pagination APIs
- SQLx users wanting higher-level abstractions
-
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>>,
}
-
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"
}
]
}
-
Date range filter combined with two other custom filters
- Notice the
confirmed=true
andfirst_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 |
Recommended Indexes
-- 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