Skip to content

feat(database): add simplePaginate() to SelectQueryBuilder - #2250

Open
Lanser0614 wants to merge 4 commits into
tempestphp:3.xfrom
Lanser0614:feat/simple-paginate
Open

feat(database): add simplePaginate() to SelectQueryBuilder#2250
Lanser0614 wants to merge 4 commits into
tempestphp:3.xfrom
Lanser0614:feat/simple-paginate

Conversation

@Lanser0614

Copy link
Copy Markdown

Summary

Adds simple pagination without executing an additional COUNT(*) query.

query(Book::class)
    ->select()
    ->simplePaginate(itemsPerPage: 20, currentPage: 2);

The query requests one additional item:

SELECT ...
LIMIT 21 OFFSET 20

If the additional item exists, it is removed from the returned data and hasNext is set to true.

Changes

  • Add SelectQueryBuilder::simplePaginate().
  • Add database-independent SimplePaginator.
  • Add SimplePaginatedData without synthetic totalItems or totalPages.
  • Expose navigation metadata such as hasNext, hasPrevious, nextPage, and previousPage.
  • Support map(), array serialization, and JSON serialization.
  • Validate itemsPerPage and currentPage.
  • Ensure the public paginate() API never returns more than itemsPerPage items.
  • Document out-of-range page behavior and the trade-offs of offset pagination.

Tests

Added unit and integration coverage for:

  • exactly itemsPerPage results;
  • an additional result used to detect the next page;
  • input larger than itemsPerPage + 1;
  • empty and out-of-range pages;
  • second-page offset calculation;
  • invalid arguments;
  • map(), toArray(), and JSON serialization;
  • SelectQueryBuilder::simplePaginate();
  • execution of a single SELECT query without COUNT(*).

Local verification:

  • PHPUnit: 51 tests, 159 assertions
  • Mago formatter and lint
  • PHPStan

Notes

This is still offset-based pagination. It avoids the potentially expensive count query, but large offsets may remain costly, and concurrent inserts or deletes can shift results between pages. Cursor/keyset pagination remains preferable for very large or frequently changing datasets.

@Lanser0614 Lanser0614 changed the title Add simplePaginate() to SelectQueryBuilder feat(database): add simplePaginate() to SelectQueryBuilder Aug 10, 2026
@github-actions

Copy link
Copy Markdown

Benchmark Results

Comparison of feat/simple-paginate against 3.x (2ed234eb062b540ca10dbd9ca8f37a06de08901f).

Open to see the benchmark results
Benchmark Set Mem. Peak Time Variability
ViewRenderBench(benchPlainHtml) - 22.016mb 0.00% 469.198μs +7.55% ±3.47% +267.09%
ContainerBench(benchResolveClosureSingleton) - 4.031mb 0.00% 1.546μs +5.92% ±3.04% +231.08%

Generated by phpbench against commit 8b57fed

@Lanser0614

Copy link
Copy Markdown
Author

@brendt @innocenzi What is the average review time for a pull request ?

Comment on lines +13 to +19
if ($this->itemsPerPage <= 0) {
throw new ArgumentWasInvalid('Items per page must be positive');
}

if ($this->currentPage <= 0) {
throw new ArgumentWasInvalid('Current page must be positive');
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's set an upper-bound as well. Though edge-case, a big enough argument (e.g. PHP_INT_MAX) can cause an overflow, coercing the type to float and causing a TypeError.

Comment on lines +13 to +19
if ($this->itemsPerPage <= 0) {
throw new ArgumentWasInvalid('Items per page must be positive');
}

if ($this->currentPage <= 0) {
throw new ArgumentWasInvalid('Current page must be positive');
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also create named, past tense exceptions for these, see other exceptions in the codebase. The old paginator follows the currently implemented patterns, but I'd consider it legacy and move towards named exceptions. Only fix these exceptions for now, if you wish, you can open a followup PR for the original paginator exceptions. Though I'd wait for @brendt's approval with this.

@xHeaven xHeaven linked an issue Aug 12, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add simplePaginate() to SelectQueryBuilder

3 participants