Database Cursor Patterns
Purpose: PostgreSQL-specific cursor implementation
Location: src/lib/db/api/ (consider renaming to src/lib/db/cursor/)
Last Updated: 2024-11-26
Overview
This documentation covers the PostgreSQL-specific implementation of the abstract cursor pattern. For the general cursor architecture, see Cursors Architecture.
Database cursors enable stateless pagination for PostgreSQL queries using token-based resumption. This implementation builds on the abstract cursor pattern defined in src/lib/cursor/.
Core principle: Database cursors implement the abstract Cursor<T> interface for PostgreSQL queries.
For complete examples, see Cursor Pagination Examples.
Relationship to Abstract Architecture
This is a concrete implementation of the abstract cursor pattern:
- Abstract:
src/lib/cursor/- See Cursors Architecture - Database-specific:
src/lib/db/api/- This documentation - Stream transformations:
src/lib/stream/- See Streams Architecture
Design Philosophy
Why Cursors?
Problem: Traditional pagination is stateful
- Server must remember query state
- Session-dependent
- Breaks in distributed systems
- Difficult to scale
Solution: Cursor-based pagination
- Encodes position in cursor string
- Stateless - no server memory required
- Works across multiple requests
- Scalable to distributed systems
- Resumable from any point
Use Cases
✅ Perfect for:
- Large result sets (thousands+ rows)
- API endpoints with pagination
- Infinite scroll interfaces
- Export operations with progress
- Multi-request workflows
❌ Not ideal for:
- Small result sets (< 100 rows)
- Single-page displays
- Real-time data (cursors can become stale)
Architecture
Components
1. Cursor Encoding (src/lib/cursor/encode.ts)
- Converts position to base64 string
- Includes query metadata
- Tamper-resistant
2. Cursor Decoding (src/lib/cursor/decode.ts)
- Extracts position from cursor
- Validates cursor integrity
- Handles corrupted cursors
3. Query Integration (src/lib/db/api/cursor.ts)
- Applies cursor to queries
- Manages page boundaries
- Handles edge cases
Cursor Structure
interface CursorData {
// Position tracking
offset?: number; // Offset-based position
lastId?: bigint; // ID-based position
lastValue?: unknown; // Value-based position
// Query metadata
direction: 'forward' | 'backward';
limit: number;
// Integrity
timestamp: number;
version: number;
}
Encoded cursor:
eyJvZmZzZXQiOjUwLCJsaW1pdCI6MjUsImRpcmVjdGlvbiI6ImZvcndhcmQifQ==
Implementation Patterns
For detailed implementation patterns, see Implementation Guide.
Pattern Types
Offset-Based: Simple pagination, order doesn't matter
- Pros: Simple to implement
- Cons: Poor performance with large offsets
ID-Based (Keyset): Large datasets, stable ordering
- Pros: Constant O(log n) performance
- Cons: Requires indexed column
Value-Based: Sorting by non-unique column
- Pros: Works with any sortable column
- Cons: Requires composite index
Quick Start
Basic Node Pagination
import { encodeCursor, decodeCursor } from '@/lib/cursor';
import { listNodes } from '@db/graph';
async function paginateNodes(client: PoolClient, cursor?: string) {
const limit = 50;
const cursorData = cursor ? decodeCursor(cursor) : null;
const nodes = await listNodes(client, {
limit: limit + 1,
after_id: cursorData?.lastId
});
const hasMore = nodes.length > limit;
const results = hasMore ? nodes.slice(0, limit) : nodes;
const nextCursor = hasMore
? encodeCursor({
lastId: results[results.length - 1].id,
limit,
direction: 'forward',
timestamp: Date.now(),
version: 1
})
: null;
return { nodes: results, cursor: nextCursor, hasMore };
}
Documentation Structure
- Implementation - Detailed cursor implementation patterns
- Performance - Optimization and indexing strategies
- API Integration - REST and GraphQL patterns
- Examples - Complete code examples
Related Documentation
- Best Practices - Using cursors with generated functions
- Patterns - General query patterns
- Examples - Complete cursor examples
Next steps: See Implementation Guide for detailed patterns, or Examples for complete code.