Pagination
hypequery provides robust support for cursor-based pagination, allowing you to efficiently navigate through large datasets without loading all records at once.
Cursor-Based Pagination
Unlike traditional offset-based pagination, cursor-based pagination uses a cursor (a pointer to a specific record) to determine where to start fetching the next set of results. This approach is more efficient and reliable, especially for large datasets.
import { createQueryBuilder } from '@hypequery/clickhouse';
const builder = createQueryBuilder(schema).table('users');
// Get the first page
const firstPage = await builder.paginate({
pageSize: 10, // Number of records per page
orderBy: [{ column: 'id', direction: 'ASC' }] // Must specify an order
});
// Get the next page using the end cursor from the first page
const nextPage = await builder.paginate({
pageSize: 10,
after: firstPage.pageInfo.endCursor, // Use the end cursor to fetch next page
orderBy: [{ column: 'id', direction: 'ASC' }]
});
// Get the previous page
const prevPage = await builder.paginate({
pageSize: 10,
before: firstPage.pageInfo.startCursor, // Use the start cursor to fetch previous page
orderBy: [{ column: 'id', direction: 'ASC' }]
});
Pagination Methods
hypequery provides several methods for pagination:
paginate()
The main pagination method that returns a page of results along with pagination metadata:
const page = await builder
.table('products')
.select(['id', 'name', 'price'])
.where('active', 'eq', 1)
.paginate({
pageSize: 20,
orderBy: [{ column: 'created_at', direction: 'DESC' }]
});
// Access the page data
page.data.forEach(product => {
console.log(product.name, product.price);
});
// Access pagination metadata
console.log(`Has next page: ${page.pageInfo.hasNextPage}`);
console.log(`Has previous page: ${page.pageInfo.hasPreviousPage}`);
firstPage()
A convenience method to get the first page with a specified page size:
const firstPage = await builder
.table('orders')
.select(['id', 'total', 'status'])
.firstPage(20);
iteratePages()
An async generator that allows you to iterate through all pages efficiently:
const iterator = builder
.table('events')
.select(['id', 'event_type', 'timestamp'])
.iteratePages(100);
// Process each page
for await (const page of iterator) {
console.log(`Processing page with ${page.data.length} events`);
// Process the page data
page.data.forEach(event => {
// Handle each event
});
}
Return Types
PaginatedResult
The pagination methods return a PaginatedResult
object with the following structure:
interface PaginatedResult<T> {
data: T[]; // The actual data for the current page
pageInfo: PageInfo; // Pagination metadata
}
interface PageInfo {
startCursor: string; // Cursor pointing to the first item in the current page
endCursor: string; // Cursor pointing to the last item in the current page
hasNextPage: boolean; // Whether there are more pages after the current page
hasPreviousPage: boolean; // Whether there are pages before the current page
}
Pagination Options
When calling the paginate()
method, you can provide the following options:
interface PaginationOptions<T> {
pageSize: number; // Number of records per page
after?: string; // Cursor for forward pagination
before?: string; // Cursor for backward pagination
orderBy?: Array<{ // Required for cursor-based pagination
column: keyof T; // The column to order by
direction: 'ASC' | 'DESC'; // The sort direction
}>;
}
Best Practices
-
Always specify orderBy: Cursor-based pagination requires a consistent ordering to work properly.
-
Store cursors for navigation: Save the
startCursor
andendCursor
values to allow users to navigate back and forth. -
Use iteratePages() for batch processing: When you need to process all records, use the
iteratePages()
method for efficient iteration. -
Limit page size: Keep page sizes reasonable (20-100 items) for optimal performance.
-
Use appropriate order direction: For newest-first listings, use
DESC
ordering on timestamp or ID fields.
Limitations
-
Order is required: You must specify at least one ordering column for cursor-based pagination.
-
Changing order affects cursors: Cursors are tied to specific ordering, changing the order will invalidate existing cursors.
-
Consistent schema: The columns used for ordering should be consistent across all records.
Example: User Interface Pagination
Here’s a complete example of implementing pagination in a user interface:
import { useState } from 'react';
import { createQueryBuilder } from '@hypequery/clickhouse';
function UserList() {
const [currentPage, setCurrentPage] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const loadFirstPage = async () => {
setIsLoading(true);
try {
const builder = createQueryBuilder(schema).table('users');
const page = await builder
.select(['id', 'name', 'email'])
.where('active', 'eq', 1)
.firstPage(20);
setCurrentPage(page);
} finally {
setIsLoading(false);
}
};
const loadNextPage = async () => {
if (!currentPage || !currentPage.pageInfo.hasNextPage) return;
setIsLoading(true);
try {
const builder = createQueryBuilder(schema).table('users');
const page = await builder
.select(['id', 'name', 'email'])
.where('active', 'eq', 1)
.paginate({
pageSize: 20,
after: currentPage.pageInfo.endCursor,
orderBy: [{ column: 'id', direction: 'ASC' }]
});
setCurrentPage(page);
} finally {
setIsLoading(false);
}
};
const loadPreviousPage = async () => {
if (!currentPage || !currentPage.pageInfo.hasPreviousPage) return;
setIsLoading(true);
try {
const builder = createQueryBuilder(schema).table('users');
const page = await builder
.select(['id', 'name', 'email'])
.where('active', 'eq', 1)
.paginate({
pageSize: 20,
before: currentPage.pageInfo.startCursor,
orderBy: [{ column: 'id', direction: 'ASC' }]
});
setCurrentPage(page);
} finally {
setIsLoading(false);
}
};
// Rest of component...
}
See Also
Advanced Pagination Patterns
For more complex pagination scenarios, you can combine multiple techniques:
import { createQueryBuilder } from '@hypequery/clickhouse';
// ... existing code ...