FireProx Pagination Guide¶
This notebook demonstrates cursor-based pagination in FireProx, enabling efficient navigation through large result sets.
Pagination Methods¶
start_at(cursor)- Start at cursor position (inclusive)start_after(cursor)- Start after cursor position (exclusive)end_at(cursor)- End at cursor position (inclusive)end_before(cursor)- End before cursor position (exclusive)
Cursor Types¶
- Field Value Dict:
{'field_name': value}- Must match order_by field - DocumentSnapshot: Obtained from
doc._doc_ref.get()
The demo is split into two sections:
- Synchronous API examples
- Asynchronous API examples
Setup¶
Import modules and create sample data.
from fire_prox import AsyncFireProx, FireProx
from fire_prox.testing import async_demo_client, demo_client
Initialize Client and Create Sample Data¶
We'll create 15 users spanning from 1643 to 1955 to demonstrate pagination.
# Create sync client and collection
client = demo_client()
db = FireProx(client)
scientists = db.collection('pagination_demo_scientists')
# Create sample data - 15 famous scientists/mathematicians
sample_data = [
{'name': 'Isaac Newton', 'birth_year': 1643, 'field': 'Physics'},
{'name': 'Gottfried Leibniz', 'birth_year': 1646, 'field': 'Mathematics'},
{'name': 'Leonhard Euler', 'birth_year': 1707, 'field': 'Mathematics'},
{'name': 'Carl Gauss', 'birth_year': 1777, 'field': 'Mathematics'},
{'name': 'Charles Babbage', 'birth_year': 1791, 'field': 'Computer Science'},
{'name': 'Ada Lovelace', 'birth_year': 1815, 'field': 'Computer Science'},
{'name': 'James Maxwell', 'birth_year': 1831, 'field': 'Physics'},
{'name': 'Henri Poincaré', 'birth_year': 1854, 'field': 'Mathematics'},
{'name': 'Nikola Tesla', 'birth_year': 1856, 'field': 'Engineering'},
{'name': 'Emmy Noether', 'birth_year': 1882, 'field': 'Mathematics'},
{'name': 'Albert Einstein', 'birth_year': 1879, 'field': 'Physics'},
{'name': 'John von Neumann', 'birth_year': 1903, 'field': 'Mathematics'},
{'name': 'Grace Hopper', 'birth_year': 1906, 'field': 'Computer Science'},
{'name': 'Alan Turing', 'birth_year': 1912, 'field': 'Computer Science'},
{'name': 'Katherine Johnson', 'birth_year': 1918, 'field': 'Mathematics'},
]
for data in sample_data:
doc = scientists.new()
for key, value in data.items():
setattr(doc, key, value)
doc.save()
print(f"Created {len(sample_data)} scientists for pagination demo")
Created 15 scientists for pagination demo
Feature 1: Basic Pagination with start_after()¶
The most common pagination pattern: fetch pages sequentially using start_after() to exclude the last document from the previous page.
# Page 1: Get first 5 scientists ordered by birth year
page1_query = scientists.order_by('birth_year').limit(5)
page1 = page1_query.get()
print("📄 Page 1 (first 5):")
for scientist in page1:
print(f" {scientist.birth_year}: {scientist.name}")
# Page 2: Start after the last person from page 1
last_year = page1[-1].birth_year
page2_query = scientists.order_by('birth_year').start_after({'birth_year': last_year}).limit(5)
page2 = page2_query.get()
print("\n📄 Page 2 (next 5):")
for scientist in page2:
print(f" {scientist.birth_year}: {scientist.name}")
# Page 3: Continue from page 2
last_year = page2[-1].birth_year
page3_query = scientists.order_by('birth_year').start_after({'birth_year': last_year}).limit(5)
page3 = page3_query.get()
print("\n📄 Page 3 (last 5):")
for scientist in page3:
print(f" {scientist.birth_year}: {scientist.name}")
print(f"\n✅ Total: {len(page1) + len(page2) + len(page3)} scientists across 3 pages")
📄 Page 1 (first 5): 1643: Isaac Newton 1646: Gottfried Leibniz 1707: Leonhard Euler 1777: Carl Gauss 1791: Charles Babbage 📄 Page 2 (next 5): 1815: Ada Lovelace 1831: James Maxwell 1854: Henri Poincaré 1856: Nikola Tesla 1879: Albert Einstein 📄 Page 3 (last 5): 1882: Emmy Noether 1903: John von Neumann 1906: Grace Hopper 1912: Alan Turing 1918: Katherine Johnson ✅ Total: 15 scientists across 3 pages
Feature 2: Inclusive vs Exclusive Cursors¶
Understand the difference between start_at (inclusive) and start_after (exclusive).
# Get scientists from year 1800 onwards
cursor_year = 1800
# start_at - INCLUDES the cursor document
inclusive_query = scientists.order_by('birth_year').start_at({'birth_year': cursor_year})
inclusive_results = inclusive_query.get()
print(f"📌 start_at({cursor_year}) - INCLUSIVE (includes {cursor_year}):")
for s in inclusive_results[:3]:
print(f" {s.birth_year}: {s.name}")
print(f" ... ({len(inclusive_results)} total)")
# start_after - EXCLUDES the cursor document
exclusive_query = scientists.order_by('birth_year').start_after({'birth_year': cursor_year})
exclusive_results = exclusive_query.get()
print(f"\n📌 start_after({cursor_year}) - EXCLUSIVE (excludes {cursor_year}):")
for s in exclusive_results[:3]:
print(f" {s.birth_year}: {s.name}")
print(f" ... ({len(exclusive_results)} total)")
print(f"\n💡 Difference: {len(inclusive_results) - len(exclusive_results)} document(s)")
📌 start_at(1800) - INCLUSIVE (includes 1800): 1815: Ada Lovelace 1831: James Maxwell 1854: Henri Poincaré ... (10 total) 📌 start_after(1800) - EXCLUSIVE (excludes 1800): 1815: Ada Lovelace 1831: James Maxwell 1854: Henri Poincaré ... (10 total) 💡 Difference: 0 document(s)
Feature 3: Range Queries with end_at() and end_before()¶
Limit results to a specific range by combining start and end cursors.
# Get scientists born between 1800 and 1900 (inclusive)
range_query = (scientists
.order_by('birth_year')
.start_at({'birth_year': 1800})
.end_at({'birth_year': 1900}))
range_results = range_query.get()
print("📊 Scientists born between 1800-1900 (inclusive):")
for scientist in range_results:
print(f" {scientist.birth_year}: {scientist.name}")
# Get scientists in the 19th century (1800-1899)
century_query = (scientists
.order_by('birth_year')
.start_at({'birth_year': 1800})
.end_before({'birth_year': 1900}))
century_results = century_query.get()
print("\n📊 Scientists in 19th century (1800-1899):")
for scientist in century_results:
print(f" {scientist.birth_year}: {scientist.name}")
print("\n💡 Using end_at vs end_before affects boundary inclusion")
📊 Scientists born between 1800-1900 (inclusive): 1815: Ada Lovelace 1831: James Maxwell 1854: Henri Poincaré 1856: Nikola Tesla 1879: Albert Einstein 1882: Emmy Noether 📊 Scientists in 19th century (1800-1899): 1815: Ada Lovelace 1831: James Maxwell 1854: Henri Poincaré 1856: Nikola Tesla 1879: Albert Einstein 1882: Emmy Noether 💡 Using end_at vs end_before affects boundary inclusion
Feature 4: Document Snapshot Cursors¶
Use DocumentSnapshot objects as cursors for more reliable pagination (handles duplicate field values).
# Get first page
page1 = scientists.order_by('birth_year').limit(5).get()
print("📄 Page 1:")
for s in page1:
print(f" {s.birth_year}: {s.name}")
# Get the DocumentSnapshot of the last document
last_doc_ref = page1[-1]._doc_ref
last_snapshot = last_doc_ref.get()
# Use snapshot as cursor for next page
page2 = (scientists
.order_by('birth_year')
.start_after(last_snapshot)
.limit(5)
.get())
print("\n📄 Page 2 (using DocumentSnapshot cursor):")
for s in page2:
print(f" {s.birth_year}: {s.name}")
print("\n💡 DocumentSnapshot cursors are more reliable than field values")
print(" They work correctly even when multiple documents have the same field value")
📄 Page 1: 1643: Isaac Newton 1646: Gottfried Leibniz 1707: Leonhard Euler 1777: Carl Gauss 1791: Charles Babbage 📄 Page 2 (using DocumentSnapshot cursor): 1815: Ada Lovelace 1831: James Maxwell 1854: Henri Poincaré 1856: Nikola Tesla 1879: Albert Einstein 💡 DocumentSnapshot cursors are more reliable than field values They work correctly even when multiple documents have the same field value
Feature 5: Descending Order Pagination¶
Pagination works with descending order - useful for "newest first" or "highest score" scenarios.
# Get most recent scientists (descending order)
page1_desc = (scientists
.order_by('birth_year', direction='DESCENDING')
.limit(5)
.get())
print("📄 Newest scientists first (page 1):")
for s in page1_desc:
print(f" {s.birth_year}: {s.name}")
# Continue pagination in descending order
last_year = page1_desc[-1].birth_year
page2_desc = (scientists
.order_by('birth_year', direction='DESCENDING')
.start_after({'birth_year': last_year})
.limit(5)
.get())
print("\n📄 Next page (descending):")
for s in page2_desc:
print(f" {s.birth_year}: {s.name}")
print("\n💡 Pagination works seamlessly with both ascending and descending order")
📄 Newest scientists first (page 1): 1918: Katherine Johnson 1912: Alan Turing 1906: Grace Hopper 1903: John von Neumann 1882: Emmy Noether 📄 Next page (descending): 1879: Albert Einstein 1856: Nikola Tesla 1854: Henri Poincaré 1831: James Maxwell 1815: Ada Lovelace 💡 Pagination works seamlessly with both ascending and descending order
Feature 6: Filtered Pagination¶
Combine filtering with pagination using where() + pagination cursors.
# Paginate through mathematicians only
math_page1 = (scientists
.where('field', '==', 'Mathematics')
.order_by('birth_year')
.limit(3)
.get())
print("📄 Mathematicians - Page 1:")
for s in math_page1:
print(f" {s.birth_year}: {s.name}")
# Next page of mathematicians
last_year = math_page1[-1].birth_year
math_page2 = (scientists
.where('field', '==', 'Mathematics')
.order_by('birth_year')
.start_after({'birth_year': last_year})
.limit(3)
.get())
print("\n📄 Mathematicians - Page 2:")
for s in math_page2:
print(f" {s.birth_year}: {s.name}")
print("\n💡 Pagination works with filtered queries using where()")
📄 Mathematicians - Page 1: 1646: Gottfried Leibniz 1707: Leonhard Euler 1777: Carl Gauss 📄 Mathematicians - Page 2: 1854: Henri Poincaré 1882: Emmy Noether 1903: John von Neumann 💡 Pagination works with filtered queries using where()
Feature 7: Practical Pagination Helper¶
A reusable pagination pattern for real applications.
def paginate(collection, page_size=5, order_field='birth_year'):
"""
Generator that yields pages of results.
Usage:
for page_num, page in paginate(scientists, page_size=5):
print(f"Page {page_num}: {len(page)} items")
"""
query = collection.order_by(order_field).limit(page_size)
page_num = 1
while True:
results = query.get()
if not results:
break
yield page_num, results
# Prepare next page
last_value = getattr(results[-1], order_field)
query = (collection
.order_by(order_field)
.start_after({order_field: last_value})
.limit(page_size))
page_num += 1
# Use the helper
print("📚 Paginating through all scientists:")
total_count = 0
for page_num, page in paginate(scientists, page_size=5):
print(f"\nPage {page_num}: {len(page)} scientists")
for s in page:
print(f" {s.birth_year}: {s.name}")
total_count += len(page)
print(f"\n✅ Total: {total_count} scientists processed")
📚 Paginating through all scientists: Page 1: 5 scientists 1643: Isaac Newton 1646: Gottfried Leibniz 1707: Leonhard Euler 1777: Carl Gauss 1791: Charles Babbage Page 2: 5 scientists 1815: Ada Lovelace 1831: James Maxwell 1854: Henri Poincaré 1856: Nikola Tesla 1879: Albert Einstein Page 3: 5 scientists 1882: Emmy Noether 1903: John von Neumann 1906: Grace Hopper 1912: Alan Turing 1918: Katherine Johnson ✅ Total: 15 scientists processed
Initialize Async Client and Create Sample Data¶
# Create async client and collection
async_client = async_demo_client()
async_db = AsyncFireProx(async_client)
async_scientists = async_db.collection('pagination_demo_scientists_async')
# Create sample data
for data in sample_data:
doc = async_scientists.new()
for key, value in data.items():
setattr(doc, key, value)
await doc.save()
print(f"Created {len(sample_data)} scientists for async pagination demo")
Created 15 scientists for async pagination demo
Feature 1: Basic Async Pagination¶
# Page 1
page1_query = async_scientists.order_by('birth_year').limit(5)
page1 = await page1_query.get()
print("📄 Async Page 1:")
for scientist in page1:
print(f" {scientist.birth_year}: {scientist.name}")
# Page 2
last_year = page1[-1].birth_year
page2_query = async_scientists.order_by('birth_year').start_after({'birth_year': last_year}).limit(5)
page2 = await page2_query.get()
print("\n📄 Async Page 2:")
for scientist in page2:
print(f" {scientist.birth_year}: {scientist.name}")
print("\n✅ Async pagination works identically to sync API")
📄 Async Page 1: 1643: Isaac Newton 1646: Gottfried Leibniz 1707: Leonhard Euler 1777: Carl Gauss 1791: Charles Babbage 📄 Async Page 2: 1815: Ada Lovelace 1831: James Maxwell 1854: Henri Poincaré 1856: Nikola Tesla 1879: Albert Einstein ✅ Async pagination works identically to sync API
Feature 2: Async Range Queries¶
# Get scientists born between 1800 and 1900
range_query = (async_scientists
.order_by('birth_year')
.start_at({'birth_year': 1800})
.end_at({'birth_year': 1900}))
range_results = await range_query.get()
print("📊 Async range query (1800-1900):")
for scientist in range_results:
print(f" {scientist.birth_year}: {scientist.name}")
📊 Async range query (1800-1900): 1815: Ada Lovelace 1831: James Maxwell 1854: Henri Poincaré 1856: Nikola Tesla 1879: Albert Einstein 1882: Emmy Noether
Feature 3: Async Document Snapshot Cursors¶
# Get first page
page1 = await async_scientists.order_by('birth_year').limit(5).get()
print("📄 Async Page 1:")
for s in page1:
print(f" {s.birth_year}: {s.name}")
# Get DocumentSnapshot and use as cursor
last_doc_ref = page1[-1]._doc_ref
last_snapshot = await last_doc_ref.get()
page2 = await (async_scientists
.order_by('birth_year')
.start_after(last_snapshot)
.limit(5)
.get())
print("\n📄 Async Page 2 (using DocumentSnapshot):")
for s in page2:
print(f" {s.birth_year}: {s.name}")
📄 Async Page 1: 1643: Isaac Newton 1646: Gottfried Leibniz 1707: Leonhard Euler 1777: Carl Gauss 1791: Charles Babbage 📄 Async Page 2 (using DocumentSnapshot): 1815: Ada Lovelace 1831: James Maxwell 1854: Henri Poincaré 1856: Nikola Tesla 1879: Albert Einstein
Feature 4: Async Pagination Helper¶
async def async_paginate(collection, page_size=5, order_field='birth_year'):
"""
Async generator that yields pages of results.
Usage:
async for page_num, page in async_paginate(async_scientists, page_size=5):
print(f"Page {page_num}: {len(page)} items")
"""
query = collection.order_by(order_field).limit(page_size)
page_num = 1
while True:
results = await query.get()
if not results:
break
yield page_num, results
# Prepare next page
last_value = getattr(results[-1], order_field)
query = (collection
.order_by(order_field)
.start_after({order_field: last_value})
.limit(page_size))
page_num += 1
# Use the async helper
print("📚 Async paginating through all scientists:")
total_count = 0
async for page_num, page in async_paginate(async_scientists, page_size=5):
print(f"\nPage {page_num}: {len(page)} scientists")
for s in page:
print(f" {s.birth_year}: {s.name}")
total_count += len(page)
print(f"\n✅ Total: {total_count} scientists processed (async)")
📚 Async paginating through all scientists: Page 1: 5 scientists 1643: Isaac Newton 1646: Gottfried Leibniz 1707: Leonhard Euler 1777: Carl Gauss 1791: Charles Babbage Page 2: 5 scientists 1815: Ada Lovelace 1831: James Maxwell 1854: Henri Poincaré 1856: Nikola Tesla 1879: Albert Einstein Page 3: 5 scientists 1882: Emmy Noether 1903: John von Neumann 1906: Grace Hopper 1912: Alan Turing 1918: Katherine Johnson ✅ Total: 15 scientists processed (async)
Summary¶
This demo showcased all pagination cursor features:
✅ Pagination Methods¶
start_at(cursor)- Start at position (inclusive)start_after(cursor)- Start after position (exclusive)end_at(cursor)- End at position (inclusive)end_before(cursor)- End before position (exclusive)
✅ Cursor Types¶
- Field Value Dict:
{'field': value}- Simple, matches order_by field - DocumentSnapshot:
doc._doc_ref.get()- Reliable, handles duplicates
✅ Common Patterns¶
- Basic Pagination:
start_after()+limit() - Range Queries:
start_at()+end_at() - Filtered Pagination:
where()+ pagination cursors - Descending Order: Works with
direction='DESCENDING'
💡 Best Practices¶
- Use
start_after()for pagination (excludes duplicate of last item) - Use DocumentSnapshot cursors when field values might have duplicates
- Always use
order_by()with pagination cursors - Cursor field must match the
order_by()field
🚀 Performance Benefits¶
- Efficient: Only fetches requested page, not all results
- Scalable: Works with millions of documents
- Cost-effective: Reduces Firestore read operations
- Fast: Firestore uses indexes for cursor-based pagination
📚 Learn More¶
- API Reference: See
docs/PHASE2_5_IMPLEMENTATION_REPORT.md - Query Builder: See
demos/phase2_5/demo.ipynb - Tests: See
tests/test_fire_query.pyfor more examples