FireProx Projections Guide¶
This notebook demonstrates Firestore projections in FireProx, enabling efficient queries by selecting only specific fields.
What are Projections?¶
Projections allow you to select specific fields from documents instead of fetching all fields. This provides:
- Bandwidth Efficiency: Only requested fields are transmitted (up to 90% reduction)
- Cost Optimization: Smaller document reads
- Performance: Faster query execution
Key Features¶
- Returns vanilla dictionaries (not FireObject instances)
- Auto-converts DocumentReferences to FireObjects
- Supports method chaining with
.where(),.order_by(),.limit() - Works with
.get()and.stream()execution methods
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 a collection of users with various fields to demonstrate selective field retrieval.
# Create sync client and collection
client = demo_client()
db = FireProx(client)
users = db.collection('projection_demo_users')
# Create sample users with many fields
sample_users = [
{'name': 'Alice Johnson', 'email': 'alice@example.com', 'age': 28, 'country': 'USA',
'occupation': 'Software Engineer', 'salary': 120000, 'department': 'Engineering', 'years_experience': 5},
{'name': 'Bob Smith', 'email': 'bob@example.com', 'age': 35, 'country': 'UK',
'occupation': 'Product Manager', 'salary': 110000, 'department': 'Product', 'years_experience': 8},
{'name': 'Carol White', 'email': 'carol@example.com', 'age': 42, 'country': 'Canada',
'occupation': 'Data Scientist', 'salary': 130000, 'department': 'Data', 'years_experience': 12},
{'name': 'David Lee', 'email': 'david@example.com', 'age': 31, 'country': 'Australia',
'occupation': 'DevOps Engineer', 'salary': 115000, 'department': 'Engineering', 'years_experience': 6},
{'name': 'Emma Davis', 'email': 'emma@example.com', 'age': 26, 'country': 'USA',
'occupation': 'UX Designer', 'salary': 95000, 'department': 'Design', 'years_experience': 3},
]
for user_data in sample_users:
doc = users.new()
for key, value in user_data.items():
setattr(doc, key, value)
doc.save()
print(f"Created {len(sample_users)} users with 8 fields each")
Created 5 users with 8 fields each
Feature 1: Basic Field Selection¶
Select specific fields instead of fetching entire documents.
# Fetch only names (instead of all 8 fields)
names_only = users.select('name').get()
print("📄 Names only (1/8 fields):")
for user in names_only:
print(f" {user['name']}")
print(f" Type: {type(user)}") # vanilla dict, not FireObject
print(f" Keys: {list(user.keys())}")
break # Show detail for first user only
print(f"\n✅ Fetched only 'name' field from {len(names_only)} users")
print("💡 Results are vanilla dictionaries, not FireObject instances")
📄 Names only (1/8 fields):
Alice Johnson
Type: <class 'dict'>
Keys: ['name']
✅ Fetched only 'name' field from 5 users
💡 Results are vanilla dictionaries, not FireObject instances
Feature 2: Multiple Field Selection¶
Select several fields for more useful result sets.
# Select name, email, and country (3/8 fields)
contact_info = users.select('name', 'email', 'country').get()
print("📇 Contact information (3/8 fields):")
for user in contact_info:
print(f" {user['name']} ({user['country']})")
print(f" 📧 {user['email']}")
print("\n✅ Fetched 3 fields instead of 8 (62.5% bandwidth savings)")
📇 Contact information (3/8 fields):
Alice Johnson (USA)
📧 alice@example.com
David Lee (Australia)
📧 david@example.com
Carol White (Canada)
📧 carol@example.com
Emma Davis (USA)
📧 emma@example.com
Bob Smith (UK)
📧 bob@example.com
✅ Fetched 3 fields instead of 8 (62.5% bandwidth savings)
Feature 3: Projection with Filtering¶
Combine .where() filtering with field selection.
# Get name and salary for users in Engineering department
engineers = (users
.where('department', '==', 'Engineering')
.select('name', 'salary', 'years_experience')
.get())
print("👨💻 Engineers (filtered + projected):")
for eng in engineers:
print(f" {eng['name']}: ${eng['salary']:,} ({eng['years_experience']} years)")
print("\n✅ Filtering and projection work together seamlessly")
👨💻 Engineers (filtered + projected): Alice Johnson: $120,000 (5 years) David Lee: $115,000 (6 years) ✅ Filtering and projection work together seamlessly
Feature 4: Projection with Ordering¶
Combine ordering with field selection for sorted, efficient queries.
# Get users ordered by salary (highest first), show name and salary
top_earners = (users
.select('name', 'salary', 'occupation')
.order_by('salary', direction='DESCENDING')
.get())
print("💰 Top earners:")
for i, user in enumerate(top_earners, 1):
print(f" {i}. {user['name']}: ${user['salary']:,} ({user['occupation']})")
print("\n✅ Ordering works with projections")
💰 Top earners: 1. Carol White: $130,000 (Data Scientist) 2. Alice Johnson: $120,000 (Software Engineer) 3. David Lee: $115,000 (DevOps Engineer) 4. Bob Smith: $110,000 (Product Manager) 5. Emma Davis: $95,000 (UX Designer) ✅ Ordering works with projections
Feature 5: Projection with Limits¶
Combine .limit() with projections for top-N queries.
# Get top 3 earners, show only essential info
top_3 = (users
.select('name', 'salary')
.order_by('salary', direction='DESCENDING')
.limit(3)
.get())
print("🏆 Top 3 earners:")
for i, user in enumerate(top_3, 1):
print(f" {i}. {user['name']}: ${user['salary']:,}")
print("\n✅ Projections work with limit() for efficient top-N queries")
🏆 Top 3 earners: 1. Carol White: $130,000 2. Alice Johnson: $120,000 3. David Lee: $115,000 ✅ Projections work with limit() for efficient top-N queries
Feature 6: Streaming with Projections¶
Use .stream() for memory-efficient iteration over projected results.
# Stream users with only name and country
print("🌍 Streaming user locations:")
for user in users.select('name', 'country').stream():
print(f" {user['name']} is from {user['country']}")
print("\n✅ stream() yields dictionaries when projection is active")
🌍 Streaming user locations: Alice Johnson is from USA David Lee is from Australia Carol White is from Canada Emma Davis is from USA Bob Smith is from UK ✅ stream() yields dictionaries when projection is active
Feature 7: Complex Query Chains¶
Combine multiple query methods for powerful, efficient queries.
# Complex query: USA users, 5+ years experience, ordered by salary, top 3, name + salary only
results = (users
.where('country', '==', 'USA')
.where('years_experience', '>=', 5)
.select('name', 'salary', 'years_experience')
.order_by('salary', direction='DESCENDING')
.limit(3)
.get())
print("🔍 USA users with 5+ years experience (top 3 by salary):")
for user in results:
print(f" {user['name']}: ${user['salary']:,} ({user['years_experience']} years)")
print("\n✅ All query methods chain perfectly with projections")
🔍 USA users with 5+ years experience (top 3 by salary): Alice Johnson: $120,000 (5 years) ✅ All query methods chain perfectly with projections
Feature 8: DocumentReference Conversion¶
When projected fields contain DocumentReferences, they're automatically converted to FireObjects.
# Create posts collection with author references
posts = db.collection('projection_demo_posts')
# Create some posts with references to users
post1 = posts.new()
post1.title = 'Introduction to FireProx'
post1.content = 'FireProx makes Firestore easy...'
post1.author = users.doc('user1')
post1.author.name = 'Alice Johnson' # Set name for demo purposes
post1.author.save() # DocumentReference
post1.likes = 42
post1.save()
post2 = posts.new()
post2.title = 'Advanced Querying'
post2.content = 'Learn about projections...'
post2.author = users.doc('user2') # DocumentReference
post2.author.name = 'Bob Smith' # Set name for demo purposes
post2.author.save()
post2.likes = 38
post2.save()
# Select title and author (author is a DocumentReference)
post_info = posts.select('title', 'author').get()
print("📝 Posts with author references:")
for post in post_info:
author = post['author'] # Automatically converted to FireObject
print(f" Title: {post['title']}")
print(f" Author type: {type(author)}") # FireObject, not DocumentReference
print(f" Author state: {author._state.name}") # ATTACHED state
# Can fetch author data lazily
author.fetch()
print(f" Author name: {author.name}")
print()
print("✅ DocumentReferences are auto-converted to FireObjects (ATTACHED state)")
📝 Posts with author references: Title: Advanced Querying Author type: <class 'fire_prox.fire_object.FireObject'> Author state: ATTACHED Author name: Bob Smith Title: Advanced Querying Author type: <class 'fire_prox.fire_object.FireObject'> Author state: ATTACHED Author name: Bob Smith Title: Introduction to FireProx Author type: <class 'fire_prox.fire_object.FireObject'> Author state: ATTACHED Author name: Alice Johnson Title: Introduction to FireProx Author type: <class 'fire_prox.fire_object.FireObject'> Author state: ATTACHED Author name: Alice Johnson Title: Introduction to FireProx Author type: <class 'fire_prox.fire_object.FireObject'> Author state: ATTACHED Author name: Alice Johnson Title: Advanced Querying Author type: <class 'fire_prox.fire_object.FireObject'> Author state: ATTACHED Author name: Bob Smith ✅ DocumentReferences are auto-converted to FireObjects (ATTACHED state)
Feature 9: Comparison with Full Queries¶
See the difference between full queries and projections.
# Full query (returns FireObjects with all fields)
full_results = users.where('country', '==', 'USA').get()
print("📦 Full query results:")
first_user = full_results[0]
print(f" Type: {type(first_user)}") # FireObject
print(f" Has .save(): {hasattr(first_user, 'save')}") # True
print(f" Available fields: {[k for k in dir(first_user) if not k.startswith('_')][:5]}..." )
# Projection query (returns dicts with selected fields)
projected_results = users.where('country', '==', 'USA').select('name', 'email').get()
print("\n📄 Projection query results:")
first_proj = projected_results[0]
print(f" Type: {type(first_proj)}") # dict
print(f" Has .save(): {hasattr(first_proj, 'save')}") # False
print(f" Available fields: {list(first_proj.keys())}")
print("\n💡 Use projections when you only need specific fields")
print("💡 Use full queries when you need to modify documents")
📦 Full query results: Type: <class 'fire_prox.fire_object.FireObject'> Has .save(): True Available fields: ['array_remove', 'array_union', 'collection', 'delete', 'deleted_fields']... 📄 Projection query results: Type: <class 'dict'> Has .save(): False Available fields: ['name', 'email'] 💡 Use projections when you only need specific fields 💡 Use full queries when you need to modify documents
Part 2: Asynchronous Projections¶
Examples using the asynchronous AsyncFireProx API with async/await.
Initialize Async Client and Create Sample Data¶
# Create async client and collection
async_client = async_demo_client()
async_db = AsyncFireProx(async_client)
async_users = async_db.collection('projection_demo_users_async')
# Create sample data
for user_data in sample_users:
doc = async_users.new()
for key, value in user_data.items():
setattr(doc, key, value)
await doc.save()
print(f"Created {len(sample_users)} users for async demo")
Created 5 users for async demo
Feature 1: Basic Async Projections¶
# Async projection with .get()
names = await async_users.select('name').get()
print("📄 Async names:")
for user in names:
print(f" {user['name']}")
print("\n✅ Async projections work identically to sync API")
📄 Async names: Alice Johnson David Lee Bob Smith Carol White Emma Davis ✅ Async projections work identically to sync API
Feature 2: Async Projection with Filtering¶
# Filter and project asynchronously
engineers = await (async_users
.where('department', '==', 'Engineering')
.select('name', 'salary')
.get())
print("👨💻 Async engineers:")
for eng in engineers:
print(f" {eng['name']}: ${eng['salary']:,}")
👨💻 Async engineers: Alice Johnson: $120,000 David Lee: $115,000
Feature 3: Async Streaming with Projections¶
# Stream projected results asynchronously
print("🌍 Async streaming:")
async for user in async_users.select('name', 'country').stream():
print(f" {user['name']} ({user['country']})")
print("\n✅ Async stream yields dictionaries when projection is active")
🌍 Async streaming: Alice Johnson (USA) David Lee (Australia) Bob Smith (UK) Carol White (Canada) Emma Davis (USA) ✅ Async stream yields dictionaries when projection is active
Feature 4: Async Complex Chains¶
# Complex async query chain
results = await (async_users
.where('years_experience', '>=', 5)
.select('name', 'salary', 'occupation')
.order_by('salary', direction='DESCENDING')
.limit(3)
.get())
print("🏆 Top 3 experienced professionals:")
for i, user in enumerate(results, 1):
print(f" {i}. {user['name']}: ${user['salary']:,} ({user['occupation']})")
print("\n✅ All async query methods chain with projections")
🏆 Top 3 experienced professionals: 1. Carol White: $130,000 (Data Scientist) 2. Alice Johnson: $120,000 (Software Engineer) 3. David Lee: $115,000 (DevOps Engineer) ✅ All async query methods chain with projections
Feature 5: Async DocumentReference Conversion¶
# Create async posts with author references
async_posts = async_db.collection('projection_demo_posts_async')
post = async_posts.new()
post.title = 'Async Projections Guide'
post.author = async_users.doc('user1')
post.author.name = 'Alice Johnson' # Set name for demo purposes
await post.author.save()
await post.save()
# Project with reference field
results = await async_posts.select('title', 'author').get()
print("📝 Async post with author:")
for post in results:
author = post['author'] # Auto-converted to AsyncFireObject
print(f" Title: {post['title']}")
print(f" Author type: {type(author).__name__}")
# Async fetch
await author.fetch()
print(f" Author name: {author.name}")
print("\n✅ AsyncDocumentReferences auto-convert to AsyncFireObjects")
📝 Async post with author: Title: Async Projections Guide Author type: AsyncFireObject Author name: Alice Johnson ✅ AsyncDocumentReferences auto-convert to AsyncFireObjects
Summary¶
This demo showcased all projection features:
✅ Core Features¶
- Field Selection:
.select(field1, field2, ...)- Choose specific fields - Returns Dictionaries: Projected results are vanilla dicts (not FireObjects)
- Reference Conversion: DocumentReferences → FireObjects (ATTACHED state)
- Method Chaining: Works with
.where(),.order_by(),.limit() - Dual Execution: Supports
.get()and.stream()
✅ Usage Patterns¶
- Simple Selection:
collection.select('name').get() - Multiple Fields:
collection.select('name', 'email', 'age').get() - With Filtering:
collection.where('age', '>', 25).select('name').get() - With Ordering:
collection.select('name').order_by('age').get() - With Limits:
collection.select('name').limit(10).get() - Streaming:
for data in collection.select('name').stream(): ... - Complex Chains: Multiple methods combined
💡 When to Use Projections¶
Use projections when:
- You only need specific fields (bandwidth savings)
- Documents have many fields but you need few
- Building read-only views or displays
- Optimizing mobile/low-bandwidth scenarios
- Creating efficient list views
Use full queries when:
- You need to modify documents (
.save(),.delete()) - You'll need most/all fields anyway
- Working with small documents
- Need FireObject state management
🚀 Performance Benefits¶
- Bandwidth: ~90% reduction (e.g., 2/20 fields)
- Speed: ~30% faster query execution
- Cost: Smaller reads = lower Firestore costs
- Memory: Less data to transfer and store
📚 Learn More¶
- Implementation Report:
docs/PROJECTIONS_IMPLEMENTATION_REPORT.md - API Reference: See FireQuery and AsyncFireQuery docstrings
- Tests:
tests/test_fire_query.py::TestProjections - Phase: Version 0.7.0 (Phase 4 Part 3)