sqlx-paginated
A blazingly fast, type-safe, fluid query builder for dynamic APIs, offering seamless pagination, sorting and dynamic filtering on top of SQLx.
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 multi-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 syntax 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 | Stable |
| SQLite | Testing | 3.35+ | All features supported | Stable |
| MySQL | Planned | 8.0+ | Core features planned | On roadmap, development starting in Q2 2026 |
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 Postgres;
use ;
use ;
async
- Type safety & ergonomics for parameter configuration
let params = new
.with_pagination
.with_sort
.with_search
.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
.with_params
.with_query_builder
Target audience
-
Primary users
- Rust web developers or API teams
- Teams needing quick and secure query building
- Projects requiring pagination and dynamic filtering APIs
- SQLx users wanting higher-level abstractions for repetitive tasks
-
Use cases
- REST APIs with pagination
- Complex backend filtering
- Admin panels
- Data exploration interfaces
Installation
Add to Cargo.toml:
For PostgreSQL:
[]
= { = "0.3.1", = ["postgres"] }
For SQLite:
[]
= { = "0.3.1", = ["sqlite"] }
For both:
[]
= { = "0.3.1", = ["postgres", "sqlite"] }
Quick start
Basic usage
PostgreSQL:
use ;
use ;
async
SQLite:
use ;
use ;
async
Examples
Complete working example
See examples/sqlx-paginated-sqlite-example for a complete working REST API demonstrating:
- Actix-web integration with SQLite
- User and product endpoints with full CRUD operations
- Migrations and seed data
- Bruno API collection for testing
- Production-ready error handling
Run with: cd examples/sqlx-paginated-sqlite-example && cargo run
Query building code examples
For detailed query building patterns, see src/paginated_query_as/examples:
- query_filters_examples.rs - Examples of all filter operators including comparison operators, IN/NOT IN, NULL checks, LIKE patterns, and complex filtering scenarios
- query_builder_examples.rs - Query builder examples showing safe defaults and custom query construction for PostgreSQL and SQLite
- paginated_query_builder_advanced_examples.rs - Advanced query builder usage with custom conditions and protection disabling
Response example
API reference
Parameter overview
| Feature | HTTP parameters | Builder method |
|---|---|---|
| Pagination | page, page_size |
.with_pagination(page, size) |
| Sorting | sort_column, sort_direction |
.with_sort(column, direction) |
| Search | search, search_columns |
.with_search(term, columns) |
| Date range | date_after, date_before, date_column |
.with_date_range(after, before, column) |
| Filters | field=value, field[op]=value |
.with_filter_operator(field, operator, value) |
Pagination
| Parameter | Type | Default | Range | Description |
|---|---|---|---|---|
page |
integer | 1 |
1+ | Page number (1-indexed) |
page_size |
integer | 10 |
10-50 | Records per page |
GET /users?page=2&page_size=20
.with_pagination
Sorting
| Parameter | Type | Default | Values | Description |
|---|---|---|---|---|
sort_column |
string | created_at |
Any valid column | Column to sort by |
sort_direction |
string | descending |
ascending, descending |
Sort order |
GET /users?sort_column=last_name&sort_direction=ascending
use QuerySortDirection;
.with_sort
Search
| Parameter | Type | Default | Constraint | Description |
|---|---|---|---|---|
search |
string | null | Max 100 chars | Search term (sanitized: alphanumeric, spaces, hyphens) |
search_columns |
string | name,description |
Comma-separated | Columns to search |
GET /users?search=john&search_columns=first_name,last_name,email
.with_search
Generated SQL:
WHERE (LOWER("first_name") LIKE LOWER('%john%')
OR LOWER("last_name") LIKE LOWER('%john%')
OR LOWER("email") LIKE LOWER('%john%'))
Date range
| Parameter | Type | Default | Format | Description |
|---|---|---|---|---|
date_after |
datetime | null | ISO 8601 | Range start (>=) |
date_before |
datetime | null | ISO 8601 | Range end (<=) |
date_column |
string | created_at |
Column name | Target column |
GET /users?date_after=2024-01-01T00:00:00Z&date_before=2024-12-31T23:59:59Z
use ;
let start = parse_from_rfc3339.unwrap.into;
let end = parse_from_rfc3339.unwrap.into;
.with_date_range
Filter operators
Operator reference
| Operator | HTTP syntax | Rust method | SQL output |
|---|---|---|---|
| Equal | field=value |
.with_filter("field", Some("value")) |
field = $1 |
| Not equal | field[ne]=value |
.with_filter_operator("field", NotEqual, "value") |
field != $1 |
| Greater than | field[gt]=value |
.with_filter_operator("field", GreaterThan, "value") |
field > $1 |
| Greater or equal | field[gte]=value |
.with_filter_operator("field", GreaterOrEqual, "value") |
field >= $1 |
| Less than | field[lt]=value |
.with_filter_operator("field", LessThan, "value") |
field < $1 |
| Less or equal | field[lte]=value |
.with_filter_operator("field", LessOrEqual, "value") |
field <= $1 |
| IN | field[in]=a,b,c |
.with_filter_in("field", vec!["a","b","c"]) |
field IN ($1,$2,$3) |
| NOT IN | field[nin]=a,b |
.with_filter_not_in("field", vec!["a","b"]) |
field NOT IN ($1,$2) |
| Is null | field[is_null]= |
.with_filter_null("field", true) |
field IS NULL |
| Is not null | field[is_not_null]= |
.with_filter_null("field", false) |
field IS NOT NULL |
| LIKE | field[like]=%pattern% |
.with_filter_like("field", "%pattern%") |
field LIKE $1 |
| Not like | field[nlike]=%pattern% |
.with_filter_not_like("field", "%pattern%") |
field NOT LIKE $1 |
HTTP examples
GET /products?price[gte]=10&price[lte]=100
GET /users?role[in]=admin,moderator&status[ne]=banned
GET /users?deleted_at[is_null]=&email[is_not_null]=
GET /users?email[like]=%@company.com
Rust examples
use ;
// Basic operators
new
.with_filter_operator
.with_filter_operator
.with_filter
.build
// Convenience methods
new
.with_filter_in
.with_filter_null
.build
// Using QueryFilterCondition
use QueryFilterCondition;
use HashMap;
let mut filters = new;
filters.insert;
filters.insert;
new
.with_filter_conditions
.build
Web framework integration
use ;
use ;
use Postgres;
async
Axum:
use ;
use ;
use ;
async
Complete example
HTTP:
GET /products?search=laptop&search_columns=name,description
&price[gte]=500&price[lte]=2000&stock[gt]=0
&category[in]=computers,electronics
&status=active&deleted_at[is_null]=
&sort_column=price&sort_direction=ascending
&page=1&page_size=24
Rust:
use ;
let params = new
.with_search
.with_filter_operator
.with_filter_operator
.with_filter_operator
.with_filter_in
.with_filter
.with_filter_null
.with_sort
.with_pagination
.build;
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.
-
Combined search, sort, date range, pagination and custom filter
- Notice the
confirmed=truefilter.
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:
-
Date range filter combined with two other custom filters
- Notice the
confirmed=trueandfirst_name=Alexfilters. - For the
first_namefilter 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:
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
(to_tsvector('english', name));
-- Composite indexes for common queries
(confirmed, created_at);
-- JSON indexes
(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.