# Competitive Analysis

pypaginate v0.2.0 vs the Python pagination ecosystem -- an honest, benchmark-backed assessment.

---

## The Ecosystem

| Library | Stars | Focus | Backends |
|---|---|---|---|
| [fastapi-pagination](https://github.com/uriyyo/fastapi-pagination) | 1,635 | Pagination | 19 (SA, Beanie, Tortoise...) |
| [fastapi-filter](https://github.com/arthurio/fastapi-filter) | 302 | SA filtering | SQLAlchemy, MongoEngine |
| [sqlakeyset](https://github.com/djrobstep/sqlakeyset) | -- | Keyset pagination | SQLAlchemy |
| [sqlalchemy-pagination](https://github.com/wizeline/sqlalchemy-pagination) | 80 | Offset pagination | SQLAlchemy |
| [paginate](https://github.com/Pylons/paginate) | -- | In-memory pagination | Any sequence |
| **pypaginate** | -- | **All-in-one** | Memory, SQLAlchemy, FastAPI |

---

## Feature Comparison

| Feature | pypaginate | fastapi-pagination | fastapi-filter | paginate-lib |
|---|---|---|---|---|
| Offset pagination | yes | yes | no | yes |
| Cursor/keyset pagination | yes | yes | no | no |
| In-memory filtering (20 ops) | yes | no | no | no |
| In-memory sorting (null-aware) | yes | no | no | no |
| In-memory search (fuzzy+Unicode) | yes | no | no | no |
| SA filtering (WHERE) | yes | no | yes | no |
| SA sorting (ORDER BY) | yes | no | no | no |
| SA search (LIKE/ILIKE) | yes | no | no | no |
| Full pipeline (filter+sort+search+paginate) | yes | no | no | no |
| Nested filter groups (And/Or) | yes | no | partial | no |
| Sync + async auto-detection | yes | yes | no | no |
| FastAPI integration | yes | yes | yes | no |
| msgspec fast pages | yes | no | no | no |
| Type-safe (Elysia-style inference) | yes | partial | no | no |
| `mypy --strict` | yes | yes | no | no |

pypaginate is the only library that does filter + sort + search + paginate in one call.

---

## Performance Benchmarks (10K items)

All benchmarks run on the same machine, same data, same operations.
Competitors that cannot filter/sort use raw Python for those steps plus their own pagination.

### Paginate Only (all libraries can do this)

| Rank | Library | Median | vs #1 |
|---|---|---|---|
| **#1** | **pypaginate** | **1.2 us** | -- |
| #2 | paginate-lib | 1.4 us | 1.2x |
| #3 | fastapi-pagination | 44.5 us | 37.6x |

### SA Paginate Sync (COUNT + OFFSET/LIMIT on same SQLite DB)

| Rank | Library | Median | vs #1 |
|---|---|---|---|
| **#1** | **pypaginate sync** | **330 us** | -- |
| #2 | sqlakeyset | 379 us | 1.1x |
| #3 | sqlalchemy-pagination | 487 us | 1.5x |
| #4 | fastapi-pagination SA | 572 us | 1.7x |

### Full Pipeline: filter + sort + paginate

All libraries do the same work. Competitors use raw Python for filter+sort, then their paginate.

| Rank | Library | Median | How filter+sort is done |
|---|---|---|---|
| #1 | paginate-lib | 682 us | Raw Python list comp + sorted() |
| #2 | fastapi-pagination | 756 us | Raw Python list comp + sorted() |
| **#3** | **pypaginate** | **2.50 ms** | Built-in compiled pipeline |

**Why the gap?** Competitors use raw Python list comprehension + `sorted()` for filter+sort -- zero abstraction overhead. pypaginate uses compiled predicates + null-aware sorting -- features that have a cost in CPython.

### SA Filter (WHERE clause + pagination)

| Rank | Library | Median | Note |
|---|---|---|---|
| #1 | fastapi-filter | 320 us | Builds WHERE clause only (no COUNT/OFFSET) |
| **#2** | **pypaginate SA** | **816 us** | Full pipeline (WHERE + COUNT + OFFSET/LIMIT) |

**Why the gap?** fastapi-filter only builds the SQL WHERE clause -- no database execution. pypaginate does the full pipeline: WHERE + COUNT + OFFSET/LIMIT.

---

## Overhead Breakdown (10K items)

Paginate + serialize adds near-zero overhead. The HTTP layer dominates.

| Step | Filter | Sort | Search | Paginate |
|---|---|---|---|---|
| Ops only | 928 us | 1.73 ms | 8.98 ms | 1.2 us |
| + Paginate | 942 us (+14us) | 1.71 ms (+0) | 7.76 ms (+0) | -- |
| + Serialize | 918 us (+0) | 1.78 ms (+71us) | 8.94 ms (+0) | 3.9 us (+2.7us) |
| + Full HTTP | 11.51 ms (+10.6ms) | 10.43 ms (+8.7ms) | 18.69 ms (+9.8ms) | 8.91 ms (+8.9ms) |

**Key finding:** Paginate and serialize add essentially **zero overhead**. The HTTP layer (FastAPI request/response encoding) adds roughly 9ms constant -- this is FastAPI's cost, not pypaginate's.

---

## DX Comparison: Endpoint Code

### With fastapi-pagination + fastapi-filter (2 libraries)

```python
from fastapi_pagination import Page, add_pagination, paginate
from fastapi_filter import FilterDepends
from fastapi_filter.contrib.sqlalchemy import Filter

app = FastAPI()
add_pagination(app)

class UserFilter(Filter):
    name__ilike: str | None = None
    age__gte: int | None = None
    class Constants(Filter.Constants):
        model = User

@app.get("/users", response_model=Page[UserSchema])
async def get_users(
    user_filter: UserFilter = FilterDepends(UserFilter),
):
    query = user_filter.filter(select(User))
    return await paginate(query)
```

### With pypaginate v0.2.0 (1 library)

```python
from pypaginate import paginate, OffsetParams, FilterSpec
from pypaginate.adapters.fastapi import OffsetDep
from pypaginate.adapters.sqlalchemy import SQLAlchemyBackend, SQLAlchemyFilterBackend
from pypaginate.engine.pipeline import AsyncPipeline
from pypaginate.engine.paginator import AsyncPaginator

@app.get("/users")
async def get_users(
    params: OffsetDep,
    session: AsyncSession = Depends(get_session),
    name: str | None = Query(None),
    min_age: int | None = Query(None),
):
    filters = []
    if name:
        filters.append(FilterSpec(field="name", operator="contains", value=name))
    if min_age:
        filters.append(FilterSpec(field="age", operator="gte", value=min_age))

    backend = SQLAlchemyBackend(session)
    pipeline = AsyncPipeline(
        AsyncPaginator(backend),
        filter_backend=SQLAlchemyFilterBackend(),
    )
    return await pipeline.execute(select(User), params, filters=filters)
```

### Comparison

| Aspect | fastapi-pagination + filter | pypaginate |
|---|---|---|
| Libraries needed | 2 | 1 |
| Filter declaration | Declarative (class) | Declarative (`FilterDep` with `FilterField`) |
| Sorting | Built into filter class | `SortDep` (`?sort=name,-age`) |
| Search | Basic (ilike only) | `SearchDep` + full fuzzy/Unicode/tokens |
| Type safety | Partial (string operators) | Full (Literal types, protocols) |
| In-memory support | No | Yes (same API) |
| Auto OpenAPI for filters | Yes | Yes (`FilterDep` generates OpenAPI via Pydantic) |
| Nested filter groups | No | Yes (And/Or builders) |

---

## Where pypaginate Wins

1. **Performance**: #1 for pagination (1.2us vs paginate-lib 1.4us vs fp 44.5us).
2. **SA Performance**: Faster than sqlakeyset and raw SA pagination (330us vs 379us).
3. **Unified library**: Filter + sort + search + paginate in one `pip install`.
4. **Search**: Only library with fuzzy matching, Unicode normalization, accent removal.
5. **Type safety**: `mypy --strict`, Elysia-style type inference, Literal operator types.
6. **Architecture**: Protocol-based backends, compile-once strategy, `__slots__` everywhere.
7. **msgspec acceleration**: Near-zero page construction with `pypaginate[fast]`.
8. **Nested filters**: `And()` / `Or()` groups up to 5 levels deep.

## Where pypaginate Lags

1. **DB backends**: SQLAlchemy only (competitors support 19 backends).
2. **Full pipeline speed**: Behind competitors that use raw Python for filter+sort.
3. **Zero-config middleware**: No `add_pagination(app)` -- explicit setup required.

## Roadmap

| Version | Focus | Status |
|---|---|---|
| **v0.2.0** | Hexagonal architecture, protocol backends, unified paginate(), FilterDep, SortDep, SearchDep | **Done** |
| **v0.3.0** | JSON Logic parser, `add_pagination(app)`, HATEOAS links | Planned |
| **v0.4.0** | Multi-backend (Beanie, Tortoise), additional ORM support | Planned |
| **v1.0.0** | Rust core extension for 10x filter/search speed | Planned |
