Complete Documentation
All documentation pages compiled into a single view for easy reference and LLM consumption.
Table of Contents
Quick answers to common questions. For detailed guides, see Getting Started or Common Patterns.
Quick Decisions
Should I use CosmosQL?
Use CosmosQL if: TypeScript ✅ | Need type safety ✅ | Want minimal dependencies ✅
Use Azure SDK if: Need change feed/stored procedures ✅ | JavaScript only ✅
Can I migrate existing databases?
Yes. CosmosQL works with existing CosmosDB databases. No data migration needed—just define schemas matching your current documents.
Can I use both libraries?
Yes. CosmosQL and Azure SDK coexist—use CosmosQL for CRUD, Azure SDK for advanced features.
Features
What's supported?
- ✅ CRUD operations (create, read, update, delete)
- ✅ Partition-scoped and cross-partition queries
- ✅ Point reads, filtering, sorting, pagination
- ✅ Bulk operations within partitions
- ✅ Raw SQL queries
- ❌ Change feed processors
- ❌ Stored procedures
- ❌ Graph/MongoDB APIs
See Limitations for complete list.
Language & Runtime
JavaScript support?
No. CosmosQL requires TypeScript. The value is compile-time type safety.
Node.js version?
Node.js 18+. Uses modern built-in APIs.
Serverless?
Yes. Zero dependencies make CosmosQL ideal for Lambda, Azure Functions, Edge Runtime.
Performance
Overhead vs REST?
~1-2%. Direct REST API calls with compile-time type safety.
vs Azure SDK?
CosmosQL is typically faster due to zero runtime overhead. Azure SDK has some bulk operation optimizations.
How much does a query cost?
- Point read: ~1 RU
- Partition query: ~3-5 RU
- Cross-partition: ~50-100+ RU
Schema Changes
Adding fields?
Update your schema. Existing documents work fine—new fields are optional.
Removing fields?
Treat as optional in schema. Can't truly remove from existing documents. Consider versioning.
Changing field types?
Version documents or write migration scripts. See Migration Guide for strategies.
Migration
From Azure SDK?
Gradual migration recommended. Use CosmosQL for new features, keep SDK for advanced features. See Migration Guide.
Schema design?
Start with Core Concepts to understand partition keys. Then see Common Patterns for examples.
Production
Production ready?
Yes. Stable API, 249 tests, 100% coverage. Used in production by multiple teams.
Error handling?
Standard try/catch with CosmosError class. See Advanced Patterns for examples.
Support?
GitHub issues, discussions, and documentation. Maintainers are responsive.
Comparison
vs Prisma?
CosmosQL is CosmosDB-specific with partition key enforcement. Prisma supports many databases.
vs Mongoose?
CosmosQL is TypeScript-native, compile-time validation. Mongoose is MongoDB with runtime validation.
See Limitations for complete comparison table.
Need More Help?
- Getting started? → Getting Started
- Understanding basics? → Core Concepts
- Real-world examples? → Common Patterns
- Performance tips? → Performance Guide
- Edge cases? → Advanced Patterns
- API reference? → API Reference
A type-safe, zero-dependency library for Azure CosmosDB. Catch expensive query mistakes at compile time, not when your bill arrives.
The Problem
CosmosDB is powerful but unforgiving. Three common mistakes:
1. Forgotten Partition Keys → $2,400/month mistake
A developer wrote a query without a partition key. With 1,000 users it cost $5/month. With 100,000 users it cost $2,400/month. CosmosDB had to scan every partition.
2. Type Errors at Runtime → Production bugs
Try to access user.emial instead of user.email? JavaScript won't catch it. The bug ships, users get undefined emails.
3. Heavy Dependencies → Slow cold starts
The official Azure SDK pulls in 50+ packages. For serverless functions, this means slower cold starts and bigger bundles.
The Solution
CosmosQL prevents these at compile time:
- Partition keys required - TypeScript won't compile without them
- Type-safe queries - Autocomplete catches field name typos
- Zero dependencies - Just TypeScript, direct REST API calls
- Prisma-inspired API - Familiar patterns with CosmosDB-specific optimizations
import { createClient, container, field } from 'cosmosql';
const users = container('users', {
id: field.string(),
email: field.string(),
name: field.string()
}).partitionKey('email');
const db = await createClient({
connectionString: process.env.COSMOS_CONNECTION_STRING!,
database: 'myapp',
mode: 'auto-create'
}).withContainers({ users });
// Point read (1 RU - cheapest possible)
const user = await db.users.findUnique({
where: { id: 'user_123', email: 'john@example.com' }
});
// Query within partition (5 RU - still cheap)
const posts = await db.posts.findMany({
partitionKey: 'user_123',
where: { isPublished: true }
});
// TypeScript knows all return types automaticallyBuilt-In Features
CosmosQL includes everything you need for production:
Migrations - Structured schema evolution with rollback support:
const addPreferences = defineMigration({
version: 1,
name: 'add-user-preferences',
async up({ db, logger }) {
await db.users.updateMany({
where: {},
data: (doc) => ({ preferences: { theme: 'light' } }),
enableCrossPartitionQuery: true
});
}
});
await db.migrations.apply({ target: 'latest', confirm: true });Database Management - Health checks, schema validation, and container management:
// Health check
const health = await db.management.healthCheck();
console.log(`Health: ${health.overallHealth}`);
// Schema diff
const diff = await db.management.diffSchema();
if (diff.requiresAction) {
console.log('Schema drift detected!');
}Bulk Operations - Efficient batch updates and deletes with progress tracking:
// Update many documents
await db.users.updateMany({
where: { isActive: false },
data: { status: 'archived' },
enableCrossPartitionQuery: true,
onProgress: (stats) => console.log(`${stats.percentage}%`)
});Learn more: Migrations | Management | Bulk Operations
Is This For You?
Are you using TypeScript?
- No → Use the Azure SDK
- Yes ↓
Need change feed or stored procedures?
- Yes → Use the Azure SDK
- No ↓
Want type safety and minimal dependencies?
- Yes → Use CosmosQL ✓
Quick Start
Get working code in 5 minutes:
npm install cosmosqlSee the Quickstart Guide for your first query → understanding → customization flow.
Production Ready
Used in production by teams handling real traffic. Comprehensive test coverage ensures reliability.
Features:
- 100% Type-Safe - Compile-time error catching for all operations
- Zero Overhead - Direct REST API calls, minimal bundle size (43.6 kB packed)
- Partition Key Aware - Enforces partition key usage to avoid expensive cross-partition queries
- Zero Dependencies - Only TypeScript required, works with Node.js 18+ and Bun 1.0+
- Built-in Migrations - Schema evolution with versioning and rollback support
- Database Management - Health checks, schema validation, and container management
- Bulk Operations - Efficient batch updates and deletes with progress tracking
Next Steps:
- New to CosmosDB? → Core Concepts
- Ready to code? → Quickstart
- Need migrations? → Schema Migrations
- Database management? → Database Management
- Migrating from Azure SDK? → Migrating from Azure SDK
Contributing
Development setup, code quality, and contribution guidelines for CosmosQL
View on separate page →Thank you for your interest in contributing to CosmosQL! This guide will help you get started with development.
Development Setup
Prerequisites
- Node.js 18+ or Bun 1.0+
- TypeScript 5.0+
- Git for version control
Getting Started
- Clone the repository:
git clone https://github.com/Stoffberg/cosmosql.git
cd cosmosql- Install dependencies:
# Using Bun (recommended)
bun install
# Or using npm
npm install- Build the project:
bun run build
# Or
npm run build- Run tests:
# All tests
bun test
# Unit tests only
bun test tests/unit
# Integration tests only
bun test tests/integrationNote: Integration tests require a real Azure CosmosDB account. Set up environment variables:
COSMOS_ENDPOINT=https://your-account.documents.azure.com:443/
COSMOS_KEY=your-key
COSMOS_DATABASE=test-dbCode Quality
CosmosQL maintains high code quality standards:
Linting and Formatting
Biome is used for linting and formatting:
# Check for issues
bun run lint
# Auto-fix issues
bun run formatTypeScript
- Strict mode is enabled
- All code must be fully typed
- Avoid
anytypes unless absolutely necessary
Testing
- Comprehensive test suite required for all changes
- Maintain high test coverage
- Add tests for new features and bug fixes
Architecture
Understanding CosmosQL's architecture helps when contributing:
Core Principles
- Zero Dependencies - Only TypeScript, no external runtime dependencies
- Type-Level Programming - Leverage TypeScript's type system for schema inference
- REST API Direct - Direct HTTP calls to CosmosDB REST API, no SDK wrapper
- Bun Runtime Optimized - Built and tested with Bun, works with Node.js
Project Structure
cosmosql/
├── src/
│ ├── client/ # Client creation and configuration
│ ├── container/ # Container schema definitions
│ ├── query/ # Query builder and execution
│ ├── types/ # Type definitions and inference
│ └── utils/ # Utilities and helpers
├── tests/
│ ├── unit/ # Unit tests
│ └── integration/ # Integration tests with real CosmosDB
└── package.jsonKey Concepts
- Schema Definition: TypeScript-only schemas that don't exist at runtime
- Type Inference: Automatic type inference from schemas
- Partition Key Enforcement: Compile-time enforcement via TypeScript
- Query Builder: Type-safe SQL query generation
Making Changes
Workflow
- Create a branch:
git checkout -b feature/your-feature-name
# or
git checkout -b fix/your-bug-fix-
Make your changes - Write code, add tests
-
Run tests and linting:
bun test
bun run lint- Commit your changes:
git commit -m "feat: add new feature description"Follow Conventional Commits format:
feat:- New featurefix:- Bug fixdocs:- Documentation changestest:- Test additions/changesrefactor:- Code refactoringperf:- Performance improvements
- Push and create a PR:
git push origin feature/your-feature-namePull Request Guidelines
- Clear description of what changed and why
- Reference issues if fixing a bug or feature request
- Add tests for new features or bug fixes
- Update documentation if API changes or new features added
- Ensure all tests pass and linting is clean
Testing Requirements
- New features must include unit tests
- Integration tests for database operations
- Edge cases should be covered
- Maintain or improve test coverage
Areas for Contribution
High Priority
- Bug fixes - Report and fix bugs
- Documentation - Improve clarity and completeness
- Performance - Optimize queries and operations
- Type safety - Enhance type inference and error messages
Feature Ideas
- Aggregation functions (
count,sum,avg, etc.) - Cursor-based pagination
- Batch operations across partitions
- Additional query operators
Documentation
- Code examples
- Best practices guides
- Migration guides
- API reference improvements
Getting Help
If you need help:
- Check existing issues on GitHub
- Ask questions in discussions
- Join Discord community (if available)
- Read existing code to understand patterns
License
CosmosQL is licensed under the MIT License. By contributing, you agree that your contributions will be licensed under the same license.
Thank you for contributing to CosmosQL! Your efforts help make CosmosDB development safer and more enjoyable for everyone.
CosmosQL focuses on core CRUD operations and common query patterns that cover the majority of application use cases. This document outlines what's currently supported, what's missing, and workarounds for common scenarios.
Current Version Status
Production Ready: ✅ Yes - Core operations are stable and well-tested
Test Coverage: Comprehensive test suite covering all core functionality
Architecture:
- Zero dependencies - Only TypeScript required
- Type-level programming - Schema inference and type safety
- REST API direct - No SDK wrapper, minimal overhead
- Bun runtime - Optimized for Bun 1.0+ and Node.js 18+
Missing Features
1. Advanced Pagination
CosmosQL currently only supports offset-based pagination using take and skip. Cursor-based pagination and continuation tokens are not yet supported.
Current Support:
// Offset pagination (LIMIT/OFFSET)
const page1 = await db.posts.findMany({
partitionKey: 'user_123',
take: 20,
skip: 0
});
const page2 = await db.posts.findMany({
partitionKey: 'user_123',
take: 20,
skip: 20
});Limitations:
- No cursor-based pagination
- No continuation token support
- Cross-partition queries with large offsets can be expensive
Workaround: For large datasets, consider:
- Partition-scoped queries - Stay within partitions to minimize costs
- Date/time-based filtering - Use timestamp filters instead of large offsets
- Manual continuation tokens - Use raw SQL queries with continuation tokens (advanced)
Future Plans: Cursor-based pagination and continuation token support are planned.
2. Transaction Support
CosmosQL does not support:
- Multi-document transactions
- Batch operations beyond
createMany(which requires same partition key) - Transaction rollback capabilities
- Cross-partition transactions
Current Support:
// Single-partition batch operations only
await db.posts.createMany({
data: [
{ id: 'post_1', userId: 'user_123', title: 'Post 1' },
{ id: 'post_2', userId: 'user_123', title: 'Post 2' }
],
partitionKey: 'user_123' // All documents must share partition key
});Limitations:
- Cannot atomically update multiple documents across partitions
- No rollback support
- Limited to operations within a single partition
Workaround: For multi-document operations:
- Design for single-partition operations - Structure data so related documents share partition keys
- Use application-level transactions - Implement compensating actions for failures
- Use Azure SDK for transactions - For complex scenarios, use the official SDK alongside CosmosQL
Future Plans: Transaction support is under consideration for future releases.
3. Cross-Partition Query Limitations
Azure CosmosDB Restriction: Cross-partition queries are limited on empty containers. This is a CosmosDB limitation, not a CosmosQL limitation.
When attempting cross-partition queries on empty containers, CosmosDB returns an error. This is by design to prevent expensive operations on empty data.
Workaround: Ensure containers have at least one document before running cross-partition queries, or use partition-scoped queries when possible.
4. Advanced Query Features
The following CosmosDB features are not yet supported:
- Geospatial queries - Spatial indexing is supported in schema definition but full geospatial queries require raw SQL
- Full-text search - Integration with Azure Cognitive Search (not supported via REST API)
- JOIN operations - Not applicable to document databases, but no support for array joins
- Stored procedures - Cannot execute stored procedures (REST API focus)
- User-defined functions (UDFs) - Cannot use UDFs in queries (REST API focus)
Workaround: Use raw SQL queries for features not supported by the query builder:
// Geospatial example (when supported via raw SQL)
const result = await db.locations.query({
sql: `
SELECT * FROM c
WHERE ST_DISTANCE(c.location, {
'type': 'Point',
'coordinates': [@lng, @lat]
}) < @distance
`,
parameters: [
{ name: '@lng', value: -122.4194 },
{ name: '@lat', value: 37.7749 },
{ name: '@distance', value: 5000 }
]
});Future Plans: Advanced query features may be added based on community demand.
5. Performance Optimizations
Missing performance-related features:
- Query result caching - No built-in caching layer
- Query plan analysis - No automatic query optimization suggestions
- Automatic indexing suggestions - No recommendations for index optimization
- Connection pool management - Basic pooling exists, but limited configuration
Workaround:
- Implement caching at application level - Use Redis or in-memory caching for frequently accessed data
- Monitor RU usage - Track Request Unit consumption in Azure Portal
- Optimize partition keys - Ensure good partition key selection (documented in Performance Guide)
- Use point reads - Prefer
findUniqueoverfindManywhen querying single documents
Future Plans: Performance monitoring and optimization tools may be added.
6. Change Feed
CosmosQL does not support:
- Change feed processors
- Real-time change notifications
- Document change tracking
Workaround: Use the official Azure SDK for change feed processing:
// Using Azure SDK for change feed
import { CosmosClient } from '@azure/cosmos';
const client = new CosmosClient(connectionString);
const container = client.database('mydb').container('users');
// Use SDK for change feed
const changeFeedProcessor = container.items
.changeFeed({
startFromBeginning: false
})
.on('change', (changes) => {
// Handle changes
});Future Plans: Change feed support is being considered for future releases.
7. Multi-Database Operations
CosmosQL works with a single database per client instance. Cross-database operations are not supported.
Workaround: Create multiple client instances:
const productionDb = await createClient({
connectionString: process.env.COSMOS_CONNECTION_STRING!,
database: 'production',
mode: 'verify'
}).withContainers({ users, posts });
const analyticsDb = await createClient({
connectionString: process.env.COSMOS_CONNECTION_STRING!,
database: 'analytics',
mode: 'verify'
}).withContainers({ events, metrics });Feature Comparison
| Feature | CosmosQL | Azure SDK | Status |
|---|---|---|---|
| Type Safety | ✅ Full | ❌ Limited | CosmosQL advantage |
| CRUD Operations | ✅ Complete | ✅ Complete | Equivalent |
| Aggregations | ✅ Type-safe | ✅ Native | Equivalent |
| Transactions | ⚠️ Single partition | ✅ Full support | SDK advantage |
| Change Feed | ❌ Not supported | ✅ Supported | SDK advantage |
| Database Management | ✅ Full management | ✅ Full management | Equivalent |
| Zero Dependencies | ✅ Yes | ❌ 50+ packages | CosmosQL advantage |
| Query Builder | ✅ Type-safe | ❌ SQL strings | CosmosQL advantage |
When to Use CosmosQL vs Azure SDK
Use CosmosQL when:
- ✅ You prioritize type safety and developer experience
- ✅ You need zero dependencies for serverless/microservices
- ✅ Your use cases fit core CRUD operations
- ✅ You want compile-time guarantees for partition keys
- ✅ You're building new TypeScript applications
- ✅ You need database and container management (with
auto-create,verify, orskipmodes)
Use Azure SDK when:
- ✅ You need advanced features (change feed, stored procedures)
- ✅ You need cross-partition transactions
- ✅ You're working with JavaScript (not TypeScript)
- ✅ You need geospatial queries or full-text search
Use Both:
You can use CosmosQL alongside the Azure SDK for different use cases within the same project. They don't interfere with each other and work with the same database.
Roadmap
High Priority:
- ✅ Core CRUD operations (Completed)
- ✅ Type-safe query builder (Completed)
- ✅ Aggregation functions (Completed)
- 🔄 Cursor pagination (Planned)
- 🔄 Batch operations (Planned)
Medium Priority:
- ✅ Container management APIs (Completed)
- ⏳ Advanced querying (Geospatial, full-text) (Future)
- ⏳ Performance monitoring (Future)
- ⏳ Migration support (Future)
Lower Priority:
- ⏳ Multiple database support (Future)
- ⏳ Advanced connection pooling (Future)
- ⏳ Circuit breaker patterns (Future)
Reporting Issues
If you encounter limitations that impact your use case:
- Check existing issues on GitHub
- Create a feature request with use case details
- Contribute - Pull requests welcome!
- Use workarounds - Many limitations have documented workarounds
Getting Help
For questions about limitations or missing features:
- Check the FAQ for common questions
- Review Common Patterns for workarounds
- Open a GitHub issue for specific feature requests
- Join the community Discord for discussions
Migration
Learn how to migrate from the official Azure CosmosDB SDK to CosmosQL
View on separate page →Migration from Azure SDK
If you're currently using the official Azure CosmosDB SDK (@azure/cosmos), this guide will help you migrate to CosmosQL. The migration is straightforward—CosmosQL works with existing databases and data, so you can adopt it incrementally.
Why Migrate?
The Azure SDK is powerful, but CosmosQL provides significant advantages:
Key Benefits
- Type Safety: Compile-time guarantees instead of runtime errors
- Better Developer Experience: Cleaner API, autocomplete, fewer mistakes
- Performance: Zero runtime overhead, direct REST API calls
- Cost Prevention: Type system prevents expensive cross-partition queries
- Minimal Dependencies: No SDK bloat (50+ packages → zero dependencies)
Migration Strategy
You don't need to migrate everything at once. Here's a recommended approach:
Incremental Migration
CosmosQL and the Azure SDK can coexist in the same project—they use the same database and don't interfere. This allows you to migrate gradually without disruption.
- Start with New Code: Use CosmosQL for new features and endpoints
- Migrate High-Traffic Paths: Convert frequently-used operations to CosmosQL
- Gradual Replacement: Migrate remaining code incrementally
- Remove SDK: Once everything is migrated, remove the Azure SDK dependency
Basic Migration Examples
Creating a Client
Before (Azure SDK):
import { CosmosClient } from '@azure/cosmos';
const client = new CosmosClient(connectionString);
const database = client.database('mydb');
const container = database.container('users');After (CosmosQL):
import { createClient, container, field } from 'cosmosql';
// Define your schema
const users = container('users', {
id: field.string(),
email: field.string(),
name: field.string()
}).partitionKey('email');
// Create typed client (async - validates/creates containers)
const db = await createClient({
connectionString,
database: 'mydb',
mode: 'auto-create'
}).withContainers({ users });Key Differences:
Important Changes
- CosmosQL requires schema definitions (for type safety)
- Client creation is a one-time setup per database
db.usersis fully typed,containeris not
Querying Documents
Before (Azure SDK):
const { resources } = await container.items
.query({
query: 'SELECT * FROM c WHERE c.id = @id AND c.email = @email',
parameters: [
{ name: '@id', value: 'user_123' },
{ name: '@email', value: 'john@example.com' }
]
})
.fetchAll();
const user = resources[0]; // No type safety
// Type: any - could be undefined, wrong shape, etc.After (CosmosQL):
// Point read (most efficient)
const user = await db.users.findUnique({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
}
});
// Type: { id: string; email: string; name: string } | null
// TypeScript knows the exact shape and nullabilityBenefits:
- Type-safe: TypeScript knows the exact return type
- Simpler: No SQL strings, no parameter arrays
- Safer: Can't forget partition key (required by types)
- More efficient:
findUniqueuses point reads (faster than queries)
Querying Multiple Documents
Before (Azure SDK):
const { resources } = await container.items
.query({
query: 'SELECT * FROM c WHERE c.isActive = @active ORDER BY c.createdAt DESC',
parameters: [{ name: '@active', value: true }]
})
.fetchAll();
// No type safety, no partition key enforcementAfter (CosmosQL):
// Partition-scoped query (efficient)
const activeUsers = await db.users.findMany({
partitionKey: 'john@example.com', // Required!
where: {
isActive: true
},
orderBy: {
createdAt: 'desc'
}
});
// Type: Array<{ id: string; email: string; ... }>
// Partition key is enforced at compile timeCreating Documents
Before (Azure SDK):
const { resource: newUser } = await container.items.create({
id: 'user_123',
email: 'john@example.com',
name: 'John Doe'
});
// No type checking - could have typos, missing fields, etc.After (CosmosQL):
const newUser = await db.users.create({
data: {
id: 'user_123',
email: 'john@example.com',
name: 'John Doe'
// TypeScript ensures all required fields are present
// Autocomplete helps you discover available fields
}
});
// Type: { id: string; email: string; name: string }
// Fully typed resultUpdating Documents
Before (Azure SDK):
const { resource: updated } = await container
.item('user_123', 'john@example.com') // ID and partition key
.replace({
id: 'user_123',
email: 'john@example.com',
name: 'John Updated',
age: 30 // Could forget fields, wrong types, etc.
});After (CosmosQL):
// Update specific fields (partial update)
const updated = await db.users.update({
where: {
id: 'user_123',
email: 'john@example.com'
},
data: {
name: 'John Updated',
age: 30
// Only include fields you want to update
// TypeScript ensures types match schema
}
});
// Type: { id: string; email: string; name: string; age: number }Deleting Documents
Before (Azure SDK):
await container
.item('user_123', 'john@example.com')
.delete();After (CosmosQL):
await db.users.delete({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
}
});
// Type-safe: can't delete without partition keyHandling Common Patterns
Error Handling
Before (Azure SDK):
try {
await container.items.create(user);
} catch (error: any) {
if (error.code === 409) {
// Conflict
} else if (error.code === 429) {
// Rate limited
}
// Error types are loose, easy to miss cases
}After (CosmosQL):
import { CosmosError } from 'cosmosql';
try {
await db.users.create({ data: user });
} catch (error) {
if (error instanceof CosmosError) {
switch (error.code) {
case 409: // Conflict
case 429: // Rate limited
// Type-safe error handling
}
}
}Cross-Partition Queries
Before (Azure SDK):
// Easy to accidentally do expensive cross-partition query
const { resources } = await container.items
.query('SELECT * FROM c WHERE c.isActive = true')
.fetchAll();
// This scans ALL partitions - expensive!After (CosmosQL):
// Type system prevents accidental cross-partition queries
const users = await db.users.findMany({
enableCrossPartitionQuery: true, // Must explicitly opt in
where: { isActive: true }
});
// You're forced to think about the costMigration Checklist
When migrating code from Azure SDK to CosmosQL:
- Define schemas for all containers you use
- Identify partition keys for each container
- Replace client creation with CosmosQL client
- Convert queries from SQL strings to query builder
- Update error handling to use CosmosError
- Test thoroughly in staging before production
- Monitor RU usage to ensure queries are efficient
- Update types throughout your codebase
- Remove Azure SDK once migration is complete
Common Pitfalls
Forgetting Partition Keys: CosmosQL requires partition keys in where clauses. This is intentional—it prevents expensive cross-partition queries. Make sure you always provide partition keys.
Schema Mismatches: Your CosmosQL schema must match your actual document structure. Review existing documents to ensure field names and types align.
Type Assertions: Avoid using as any to bypass type checking. If you need to, it's often a sign your schema needs adjustment.
Gradual Migration: Don't try to migrate everything at once. Start small, test thoroughly, then expand.
Getting Help
If you run into issues during migration:
- Check the FAQ for common questions
- Review the API Reference for method details
- Look at Common Patterns for real-world examples
- Open a GitHub issue if you find bugs or need clarification
The migration process is straightforward, and you'll quickly see the benefits of type safety and better developer experience.
Performance Best Practices
1. Always Use Partition Keys When Possible
The Cost Difference
// ❌ BAD: Cross-partition query (expensive)
const user = await db.users.findMany({
enableCrossPartitionQuery: true,
where: { age: { gt: 18 } }
});
// Cost: ~50-100 RU
// ✅ GOOD: Partition-scoped query
const user = await db.users.findMany({
partitionKey: 'john@example.com',
where: { age: { gt: 18 } }
});
// Cost: ~5 RU (10-20x cheaper)2. Use Point Reads for Single Documents
Performance Tip
// ❌ OK: Query for single document
const user = await db.users.findMany({
partitionKey: 'john@example.com',
where: { id: 'user_123' },
take: 1
});
// Cost: ~3-5 RU
// ✅ BEST: Point read (direct lookup)
const user = await db.users.findUnique({
where: { id: 'user_123', email: 'john@example.com' }
});
// Cost: ~1 RU (5x cheaper)3. Select Only Needed Fields
// ❌ BAD: Fetch entire document
const user = await db.users.findUnique({
where: { id: 'user_123', email: 'john@example.com' }
});
// Returns all fields
// ✅ GOOD: Fetch only what you need
const user = await db.users.findUnique({
where: { id: 'user_123', email: 'john@example.com' },
select: { name: true, email: true }
});
// Smaller payload = faster transfer = lower latency4. Batch Operations in Same Partition
Batch Efficiency
// ❌ BAD: Individual creates (multiple requests)
for (const post of posts) {
await db.posts.create({ data: post });
}
// 10 posts = 10 HTTP requests
// ✅ GOOD: Batch create (single request)
await db.posts.createMany({
data: posts,
partitionKey: 'user_123'
});
// 10 posts = 1 HTTP request (10x faster)5. Use Appropriate Indexes
CosmosDB automatically indexes all properties by default, but you can optimize:
const users = container('users', {
id: field.string(),
email: field.string(),
name: field.string(),
metadata: field.object({ /* large nested object */ })
}).partitionKey('email')
.indexingPolicy({
automatic: true,
includedPaths: [
'/id/?',
'/email/?',
'/name/?'
],
excludedPaths: [
'/metadata/*' // Don't index large nested objects
]
});Core Concepts
Understand CosmosDB fundamentals, partition keys, and how CosmosQL prevents costly mistakes
View on separate page →Why this matters: CosmosDB has unique characteristics that shape how you query data. Understanding these fundamentals helps you write efficient, cost-effective code.
Skip this if: You're already a CosmosDB expert. Jump to Creating Documents.
Read this if: CosmosDB is new, or you want to understand the "why" behind CosmosQL's design.
The Distributed Database Problem
CosmosDB isn't one server—it's thousands of servers working together. Your data is split across them.
This creates a challenge: How do you query data when it's spread across hundreds of machines?
The solution: Partition keys.
Partition Keys: The $2,400 Question
Every document in CosmosDB has a partition key. This value determines which server stores it.
Why This Matters (Real Scenario)
A developer wrote this query:
// Find all active users
const users = await findWhere({ isActive: true });With 1,000 users: Cost $5/month, worked fine.
With 100,000 users: Cost $2,400/month, still worked fine.
What went wrong? No partition key = CosmosDB scans every server.
The Cost Breakdown
| Query Type | Servers Scanned | Request Units | Monthly Cost (1M queries) |
|---|---|---|---|
| With partition key | 1 | 1 RU | $24 |
| Without partition key | All (100+) | 100 RU | $2,400 |
100x difference.
How CosmosQL Prevents This
// ❌ TypeScript won't compile this
const users = await db.users.findMany({
where: { isActive: true }
// Error: Missing required property 'partitionKey'
});
// ✅ Forces you to be explicit
const users = await db.users.findMany({
partitionKey: 'user@example.com', // Scans one partition
where: { isActive: true }
});
// ✅ Or opt-in to cross-partition (expensive) queries
const users = await db.users.findMany({
enableCrossPartitionQuery: true, // "I know this is expensive"
where: { isActive: true }
});You cannot accidentally write expensive queries. TypeScript won't let your code compile.
Choosing a Good Partition Key
Good partition keys have three properties:
1. High Cardinality (Many Unique Values)
// ✅ Good: email addresses
const users = container('users', schema).partitionKey('email');
// ❌ Bad: boolean status
const users = container('users', schema).partitionKey('isActive');
// Only 2 values → only 2 partitions → poor distribution2. Even Distribution
// ✅ Good: user IDs spread evenly
const posts = container('posts', schema).partitionKey('userId');
// ⚠️ Risky: category (one category could be huge)
const products = container('products', schema).partitionKey('category');
// "Electronics" might be 80% of your data → hot partition3. Query Alignment
Choose keys that match your most common query patterns:
// If you always query by user
const posts = container('posts', schema).partitionKey('userId'); // ✅
// If you always query by date
const metrics = container('metrics', schema).partitionKey('timestamp'); // ✅
// If you query by multiple fields
const orders = container('orders', schema).partitionKey('customerId'); // ✅Request Units (RU): How CosmosDB Charges
CosmosDB uses Request Units (RUs) to measure and charge for operations.
Typical Costs
| Operation | Cost | Notes |
|---|---|---|
| Point read | 1 RU | Reading by ID + partition key |
| Partition query | 3-5 RU | Filtering within one partition |
| Cross-partition query | 50-100+ RU | Scanning all partitions |
| Write | 5-10 RU | Creating or updating |
Real Cost Example
If you process 1 million queries per month:
// Partition-scoped queries
1M queries × 3 RU = 3M RU/month ≈ $72/month
// Cross-partition queries
1M queries × 100 RU = 100M RU/month ≈ $2,400/monthThat's 33x more expensive.
Schema Definition
Schemas in CosmosQL are TypeScript-only - they don't exist at runtime.
import { container, field } from 'cosmosql';
const posts = container('posts', {
// Required fields
id: field.string(),
userId: field.string(),
title: field.string(),
// Optional fields
subtitle: field.string().optional(),
// Defaults
viewCount: field.number().default(0),
isPublished: field.boolean().default(false),
// Arrays
tags: field.array(field.string()),
// Nested objects
metadata: field.object({
source: field.string(),
version: field.number()
}).optional(),
// TTL (auto-delete after N seconds)
ttl: field.number().optional()
}).partitionKey('userId');
// Get TypeScript type
type Post = typeof posts.infer;Key point: This is purely for TypeScript. CosmosDB is schemaless - CosmosQL just adds type safety.
Field Types
field.string()→stringfield.number()→numberfield.boolean()→booleanfield.date()→Datefield.array(type)→Array<type>field.object(schema)→ nested object
Modifiers
.optional()→ field can beundefined.default(value)→ value used if not provided on create
Client Setup
import { createClient } from 'cosmosql';
const db = await createClient({
connectionString: process.env.COSMOS_CONNECTION_STRING!,
database: 'myapp',
mode: 'auto-create' // Validates and creates containers
}).withContainers({
users,
posts,
comments
});
// Now you have typed access
db.users // ContainerClient<User>
db.posts // ContainerClient<Post>
db.comments // ContainerClient<Comment>Client creation is async because it validates and optionally creates your database and containers. Choose a mode based on your environment:
auto-create- Development: creates missing containers automaticallyverify- Production: fails fast if containers don't existskip- Maximum performance: no validation
See Getting Started Guide for detailed mode documentation.
Connection management: HTTP/2 pooling with automatic retries. You don't manage connections.
Multiple databases: Create separate clients:
const prodDb = await createClient({...}).withContainers({users});
const analyticsDb = await createClient({...}).withContainers({events});Summary
Three key takeaways:
- Partition keys determine cost - Queries without them cost 10-100x more
- CosmosQL enforces them at compile time - TypeScript prevents expensive mistakes
- Schemas are TypeScript-only - Zero runtime overhead, all compile-time checks
Composite Partition Keys
When you need multi-tenant data or hierarchical structures:
const documents = container('documents', {
id: field.string(),
tenantId: field.string(),
organizationId: field.string(),
name: field.string(),
content: field.string()
}).partitionKey('tenantId', 'organizationId'); // Composite key
// Querying requires both parts
const docs = await db.documents.findMany({
partitionKey: ['tenant_1', 'org_a'], // Array for composite keys
where: { name: { startsWith: 'Report' } }
});Summary
Three key takeaways:
- Partition keys determine cost - Queries without them cost 10-100x more
- CosmosQL enforces them at compile time - TypeScript prevents expensive mistakes
- Schemas are TypeScript-only - Zero runtime overhead, all compile-time checks
Next: Apply these concepts in Creating Documents.
Time: 10 minutes
You'll learn: Installation → First query → How it works → Customization
Prerequisites
- Node.js 18+ or Bun 1.0+
- TypeScript 5.0+
- Azure CosmosDB account (create one | use emulator)
- Connection string (where to find it)
Note: CosmosQL is optimized for Bun runtime but works perfectly with Node.js. Both runtimes are fully supported.
Install
npm install cosmosqlCosmosQL has zero runtime dependencies beyond Node.js.
Your First Query
We're building: A simple user authentication system.
Copy and run this:
import { createClient, container, field } from 'cosmosql';
import dotenv from 'dotenv';
dotenv.config();
// Step 1: Define your schema
const users = container('users', {
id: field.string(),
email: field.string(),
name: field.string(),
age: field.number(),
isActive: field.boolean().default(true),
createdAt: field.date()
}).partitionKey('email');
// Step 2: Create a client (async - validates and creates containers)
const db = await createClient({
connectionString: process.env.COSMOS_CONNECTION_STRING!,
database: 'myapp',
mode: 'auto-create' // Creates database/containers if missing (dev default)
}).withContainers({ users });
// Step 3: Create a user
async function main() {
const newUser = await db.users.create({
data: {
id: 'user_123',
email: 'john@example.com',
name: 'John Doe',
age: 30,
createdAt: new Date()
// isActive automatically true (default)
}
});
console.log('Created user:', newUser.name);
// Step 4: Query by ID + partition key (1 RU - cheapest)
const user = await db.users.findUnique({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required!
}
});
console.log('Found user:', user?.name);
}
main().catch(console.error);Run it: npx tsx example.ts
Expected output:
Created user: John Doe
Found user: John DoeWhat Just Happened?
Let's break down each part:
1. Schema Definition
const users = container('users', {
id: field.string(),
email: field.string(),
// ...
}).partitionKey('email');This defines your data structure. Three key points:
- Not runtime validation - This is purely for TypeScript
- Partition key is required - More on why in Core Concepts
- Inference happens automatically - TypeScript knows the shape
2. Client Creation
const db = await createClient({
connectionString: process.env.COSMOS_CONNECTION_STRING!,
database: 'myapp',
mode: 'auto-create' // 'auto-create' | 'verify' | 'skip'
}).withContainers({ users });What happens: Client creation is async because it validates and optionally creates your database and containers.
Three modes:
auto-create(development default) - Creates database/containers if missing, updates indexing policies. Slower startup (~500ms-2s) but zero friction.verify(production default) - Checks everything exists, throws if misconfigured. Fast (~100-300ms), fail-fast on errors.skip(maximum performance) - No checks, instant. Use when infrastructure is guaranteed (e.g., after CI/CD deploy).
The client manages connections and provides typed access. After withContainers, db.users is fully typed.
3. Type-Safe Create
const newUser = await db.users.create({ data: { ... } });TypeScript enforces:
- All required fields are provided
- Field types match your schema
- Default values are applied automatically
4. Point Read (Fastest Query)
const user = await db.users.findUnique({
where: { id: 'user_123', email: 'john@example.com' }
});This costs ~1 RU (Request Unit) - the cheapest possible query. TypeScript requires both ID and the partition key (email).
Make It Yours
Change the example to match your data:
1. Update the schema: Replace fields with yours
const posts = container('posts', {
id: field.string(),
userId: field.string(),
title: field.string(),
content: field.string(),
publishedAt: field.date().optional()
}).partitionKey('userId');2. Change the partition key: See Choosing Partition Keys for guidance
3. Add your connection string: Store in .env file
COSMOS_CONNECTION_STRING="AccountEndpoint=https://..."Complete working example:
import { createClient, container, field } from 'cosmosql';
import dotenv from 'dotenv';
dotenv.config();
const posts = container('posts', {
id: field.string(),
userId: field.string(),
title: field.string(),
content: field.string(),
publishedAt: field.date().optional()
}).partitionKey('userId');
const db = await createClient({
connectionString: process.env.COSMOS_CONNECTION_STRING!,
database: 'myapp',
mode: process.env.NODE_ENV === 'production' ? 'verify' : 'auto-create'
}).withContainers({ posts });
async function main() {
// Create a post
const post = await db.posts.create({
data: {
id: 'post_123',
userId: 'user_456',
title: 'Hello CosmosQL',
content: 'This is my first post',
publishedAt: new Date()
}
});
console.log('Created:', post.title);
// Query posts for a user
const userPosts = await db.posts.findMany({
partitionKey: 'user_456',
where: { publishedAt: { ne: null } }
});
console.log(`Found ${userPosts.length} published posts`);
}
main().catch(console.error);Schema Configuration
You can configure container settings like throughput and indexing policies:
const users = container('users', {
id: field.string(),
email: field.string(),
name: field.string()
})
.partitionKey('email')
.throughput(400) // Optional: Set RU/s (auto-scale also available)
.indexing({ // Optional: Configure indexing policy
automatic: true,
excludePaths: ['/largeField/?'] // Exclude large fields from indexing
});
const db = await createClient({
connectionString: process.env.COSMOS_CONNECTION_STRING!,
database: 'myapp',
mode: 'auto-create' // Updates indexing in auto-create mode
}).withContainers({ users });Important: In auto-create mode, indexing policies are updated to match your schema. In verify mode, mismatches trigger warnings but containers aren't modified (production safety).
Partition Key Mismatches
If your schema defines a partition key that doesn't match an existing container, CosmosQL throws an error:
// Schema says: partitionKey('email')
// Existing container has: partitionKey('id')
// Throws: "Partition key mismatch: expected '/email', found '/id'"
// "Cannot modify partition key. Delete container or use different name."Solution: Delete the container manually (via Azure Portal/CLI) or use a different container name in your schema.
What You Learned
- ✓ Schemas define structure for TypeScript (not runtime)
- ✓ Partition keys are required and enforced
- ✓ Queries are fully typed automatically
- ✓ Point reads are cheapest (~1 RU vs ~5 RU for queries)
Next Steps
New to CosmosDB? → Read Core Concepts to understand partition keys and Request Units
Ready to build? → See Creating Documents for all operations
Need patterns? → Check Patterns & Use Cases for real scenarios
Getting Your Connection String
- Open Azure Portal
- Navigate to your CosmosDB account
- Go to Keys in the left sidebar
- Copy the Primary Connection String
The format looks like:
AccountEndpoint=https://your-account.documents.azure.com:443/;AccountKey=your-key==;Important: Never commit your connection string to version control. Always use environment variables or a secrets manager.
Advanced Patterns
Working with partition keys, TTL, raw queries, and error handling
View on separate page →Working with Partition Keys
Understanding the Trade-offs:
Cost Comparison
// ✅ GOOD: Query with partition key (1-5 RU)
const posts = await db.posts.findMany({
partitionKey: 'user_123',
where: { isPublished: true }
});
// ⚠️ EXPENSIVE: Cross-partition query (50-100+ RU)
const posts = await db.posts.findMany({
enableCrossPartitionQuery: true,
where: { isPublished: true }
});The cost difference is 10-100x higher for cross-partition queries.
Choosing a Good Partition Key:
Good Partition Keys Have
- High cardinality - Many unique values (e.g., userId, email)
- Even distribution - Data spread evenly across values
- Query alignment - Most queries filter by this field
// ✅ Good: User emails (high cardinality, even distribution)
const users = container('users', schema).partitionKey('email');
// ✅ Good: UserId for user-specific data
const posts = container('posts', schema).partitionKey('userId');
// ❌ Bad: Boolean field (only 2 values, uneven distribution)
const users = container('users', schema).partitionKey('isActive');
// ❌ Bad: Status with few values
const orders = container('orders', schema).partitionKey('status');Composite Partition Keys
When you need multi-tenant data or hierarchical structures:
const documents = container('documents', {
id: field.string(),
tenantId: field.string(),
organizationId: field.string(),
name: field.string(),
content: field.string()
}).partitionKey('tenantId', 'organizationId'); // Composite key
// Querying requires both parts
const docs = await db.documents.findMany({
partitionKey: ['tenant_1', 'org_a'], // Array for composite keys
where: { name: { startsWith: 'Report' } }
});TTL (Time-To-Live)
Auto-expire documents after a period:
const sessions = container('sessions', {
id: field.string(),
userId: field.string(),
token: field.string(),
createdAt: field.date(),
ttl: field.number() // Seconds until auto-delete
}).partitionKey('userId');
// Create session that expires in 1 hour
await db.sessions.create({
data: {
id: 'session_123',
userId: 'user_123',
token: 'abc...',
createdAt: new Date(),
ttl: 3600 // 1 hour in seconds
}
});
// CosmosDB automatically deletes after 1 hourRaw Queries
For advanced use cases beyond the query builder:
const result = await db.users.query<{ name: string; age: number }>({
sql: `
SELECT c.name, c.age
FROM c
WHERE c.age > @minAge
AND c.isActive = @active
`,
parameters: [
{ name: '@minAge', value: 18 },
{ name: '@active', value: true }
],
partitionKey: 'john@example.com' // Optional: for partition-scoped query
});
// Type: Array<{ name: string; age: number }>When to Use Raw Queries:
-
Complex aggregations
-
CosmosDB-specific functions (e.g., spatial queries)
-
Performance optimization with custom SQL
-
Features not yet supported by query builder
Error Handling
CosmosQL provides comprehensive error handling through the CosmosError class:
import { CosmosError, isCosmosError } from 'cosmosql';
try {
await db.users.create({ data: user });
} catch (error) {
if (isCosmosError(error)) {
// Type-safe error handling
switch (error.statusCode) {
case 400: // Bad Request
console.error('Invalid request:', error.message);
break;
case 401: // Unauthorized
console.error('Authentication failed - check your connection string');
break;
case 403: // Forbidden
console.error('Access denied - check permissions');
break;
case 404: // Not Found
console.error('Container or database not found');
break;
case 409: // Conflict (duplicate ID)
console.error('Document already exists with this ID');
break;
case 412: // Precondition Failed (ETag mismatch)
console.error('Document was modified, please retry');
break;
case 429: // Too Many Requests (rate limit)
console.error('Rate limited, retry after:', error.retryAfter, 'seconds');
// CosmosError includes retryAfter property
break;
case 500: // Internal Server Error
console.error('CosmosDB service error:', error.message);
break;
default:
console.error('CosmosDB error:', error.code, error.message);
}
} else {
// Not a CosmosDB error, rethrow
throw error;
}
}CosmosError Class:
class CosmosError extends Error {
statusCode: number; // HTTP status code (400, 404, 429, etc.)
code: string; // CosmosDB error code
message: string; // Error message
retryAfter?: number; // Seconds to wait before retry (for 429 errors)
}Common Error Codes:
| Code | Status | Description | Common Causes |
|---|---|---|---|
BadRequest | 400 | Invalid request | Malformed query syntax, invalid parameters |
Unauthorized | 401 | Authentication failed | Invalid connection string or key |
Forbidden | 403 | Access denied | Insufficient permissions |
NotFound | 404 | Resource not found | Container/database doesn't exist, document not found |
Conflict | 409 | Duplicate resource | Document with same ID already exists |
PreconditionFailed | 412 | ETag mismatch | Document was modified (optimistic concurrency) |
TooManyRequests | 429 | Rate limited | RU/s limit exceeded, automatic retry available |
InternalServerError | 500 | Service error | CosmosDB service issue |
CROSS_PARTITION_QUERY_ERROR | - | Cross-partition query error | Empty container cross-partition query |
Common Error Scenarios:
- 404: Document not found (returns
nullforfindUnique/findMany, throws forupdate/delete) - 429: Rate limited (automatic retry with backoff if configured via
retryOptions) - 401/403: Authentication/authorization failures (check connection string and keys)
- 400: Bad request (validation errors, invalid query syntax)
- 409: Conflict (duplicate ID on create - indicates business logic issue)
- 412: Precondition failed (ETag mismatch on updates - document was modified)
- CROSS_PARTITION_QUERY_ERROR: Cross-partition queries on empty containers (CosmosDB limitation)
Auto-Retry on 429:
CosmosQL automatically retries rate-limited requests with exponential backoff:
const db = await createClient({
connectionString: '...',
database: 'myapp',
mode: 'verify', // Production: fail-fast on misconfiguration
retryOptions: {
maxRetries: 3, // Maximum retry attempts
initialDelay: 100, // Initial delay in ms
maxDelay: 5000 // Maximum delay in ms
}
}).withContainers({ users });
// Automatically retries with exponential backoff on 429 errors
// Uses retryAfter from CosmosDB response when availableError Handling Best Practices:
- Always check error types - Use
isCosmosError()for type-safe error handling - Handle rate limits gracefully - Use built-in retry options or implement custom retry logic
- Log error details - Include
statusCode,code, andmessagein error logs for debugging - Don't ignore conflicts - 409 errors indicate business logic issues (duplicate IDs)
- Handle ETag mismatches - 412 errors mean document was modified; refetch and retry
- Check for cross-partition query errors - Ensure containers have data before cross-partition queries
Aggregations
Complete guide to using aggregation functions in CosmosQL - count, sum, average, min, max, and group by operations with full type safety.
View on separate page →Complete guide to using aggregation functions in CosmosQL - count, sum, average, min, max, and group by operations with full type safety.
Table of Contents
- Overview
- Count Operations
- Aggregate Operations
- findMany with Aggregations
- Group By Operations
- Convenience Methods
- Type Safety
- Real-World Examples
- Performance & Best Practices
Overview
CosmosQL provides a complete set of aggregation functions that are:
- ✅ Fully type-safe - TypeScript infers exact result types
- ✅ SQL-compatible - Translates to efficient Cosmos DB SQL
- ✅ Partition-aware - Optimized for Cosmos DB's partitioning
- ✅ Composable - Combine multiple aggregations in one query
Available Aggregations
| Operation | Description | Works With |
|---|---|---|
_count | Count documents | All fields |
_sum | Sum values | Number fields |
_avg | Average values | Number fields |
_min | Find minimum | All fields |
_max | Find maximum | All fields |
Count Operations
Basic Count
Count documents in a partition:
import { createClient, container, field } from 'cosmosql';
const orderSchema = container('orders', {
id: field.string(),
customerId: field.string(),
amount: field.number(),
status: field.string(),
}).partitionKey('customerId');
const client = createClient({
connectionString: process.env.COSMOS_CONNECTION_STRING!,
database: 'mydb',
});
const db = await client.withContainers({ orders: orderSchema });
// Count orders for a customer
const count = await db.orders.count({
partitionKey: 'customer-123',
});
// Result: 42Count with Filters
Add WHERE conditions to count specific documents:
// Count only completed orders
const completedCount = await db.orders.count({
partitionKey: 'customer-123',
where: {
status: 'completed',
},
});
// Count with complex filters
const recentHighValueCount = await db.orders.count({
partitionKey: 'customer-123',
where: {
status: 'completed',
amount: { gte: 100 },
createdAt: { gte: new Date('2024-01-01') },
},
});Cross-Partition Count
⚠️ Use sparingly - Cross-partition queries are expensive in Cosmos DB.
// Count ALL orders across all customers
const totalOrders = await db.orders.count({
enableCrossPartitionQuery: true,
});
// Cross-partition count with filter
const allCompletedOrders = await db.orders.count({
enableCrossPartitionQuery: true,
where: { status: 'completed' },
});Count Specific Fields
Count non-null values in specific fields:
const result = await db.orders.aggregate({
partitionKey: 'customer-123',
_count: {
select: {
amount: true, // Count orders with amount
discount: true, // Count orders with discount (might be optional)
trackingNumber: true,
},
},
});
// Result type is inferred:
// {
// _count: {
// amount: number;
// discount: number;
// trackingNumber: number;
// }
// }Aggregate Operations
Single Aggregation
// Sum all order amounts for a customer
const result = await db.orders.aggregate({
partitionKey: 'customer-123',
_sum: {
amount: true,
},
});
// Result: { _sum: { amount: 1250.50 | null } }Multiple Aggregations
Combine multiple operations in one query (more efficient than separate queries):
const stats = await db.orders.aggregate({
partitionKey: 'customer-123',
_count: true, // Total count
_sum: { amount: true }, // Total revenue
_avg: { amount: true }, // Average order value
_min: { amount: true }, // Smallest order
_max: { amount: true }, // Largest order
});
// Result type (fully inferred):
// {
// _count: number;
// _sum: { amount: number | null };
// _avg: { amount: number | null };
// _min: { amount: number | null };
// _max: { amount: number | null };
// }
console.log(`
Orders: ${stats._count}
Total Revenue: $${stats._sum.amount}
Average Order: $${stats._avg.amount}
Range: $${stats._min.amount} - $${stats._max.amount}
`);Aggregations with Filters
// Stats for completed orders only
const completedStats = await db.orders.aggregate({
partitionKey: 'customer-123',
where: { status: 'completed' },
_count: true,
_sum: { amount: true },
_avg: { amount: true },
});
// Year-to-date statistics
const ytdStats = await db.orders.aggregate({
partitionKey: 'customer-123',
where: {
createdAt: { gte: new Date('2024-01-01') },
},
_sum: { amount: true, tax: true, shipping: true },
_count: true,
});Multiple Field Aggregations
const productSchema = container('products', {
id: field.string(),
category: field.string(),
price: field.number(),
cost: field.number(),
stock: field.number(),
weight: field.number(),
}).partitionKey('category');
const stats = await db.products.aggregate({
partitionKey: 'electronics',
_sum: {
stock: true, // Total inventory
price: true, // Total value at retail
cost: true, // Total cost basis
},
_avg: {
price: true, // Average price
weight: true, // Average weight
},
_min: {
price: true, // Cheapest product
},
_max: {
price: true, // Most expensive product
},
});
// Calculate profit margin
const totalProfit = stats._sum.price! - stats._sum.cost!;
const margin = (totalProfit / stats._sum.price!) * 100;
console.log(`Profit margin: ${margin.toFixed(2)}%`);findMany with Aggregations
The findMany method supports aggregations, allowing you to fetch data and calculate statistics in a single, efficient query. This combines the power of data retrieval with aggregation operations.
Return Types
Without Aggregations (Backward Compatible):
// No aggregate parameter
const users = await db.users.findMany({ partitionKey: "john@example.com" });
// Type: User[]
const selectedUsers = await db.users.findMany({
partitionKey: "john@example.com",
select: { id: true, name: true }
});
// Type: Array<{ id: string, name: string }>With Aggregations:
// With aggregate parameter
const result = await db.users.findMany({
partitionKey: "john@example.com",
aggregate: { _count: true }
});
// Type: { data: User[], _count: number }Basic Usage
const result = await db.users.findMany({
partitionKey: "john@example.com",
where: { isActive: true },
aggregate: {
_count: true,
_avg: { age: true }
}
});
console.log(`Found ${result._count} active users`);
console.log(`Average age: ${result._avg.age}`);
console.log(`Users:`, result.data);Multiple Aggregations
Combine data retrieval with comprehensive statistics:
const stats = await db.orders.findMany({
partitionKey: "customer-123",
aggregate: {
_count: true,
_sum: { totalAmount: true },
_avg: { totalAmount: true },
_min: { createdAt: true },
_max: { createdAt: true }
}
});
console.log(`Order Statistics:`);
console.log(` Total orders: ${stats._count}`);
console.log(` Total revenue: ${stats._sum.totalAmount}`);
console.log(` Average order: ${stats._avg.totalAmount}`);
console.log(` First order: ${stats._min.createdAt}`);
console.log(` Latest order: ${stats._max.createdAt}`);
console.log(` Orders: ${stats.data.length} items`);With Filtering and Selection
const result = await db.products.findMany({
partitionKey: "category-electronics",
where: {
price: { gte: 100 },
inStock: true
},
select: { id: true, name: true, price: true },
aggregate: {
_count: true,
_avg: { price: true },
_min: { price: true },
_max: { price: true }
}
});
// Only selected fields in data
console.log(result.data); // Array<{ id: string, name: string, price: number }>
// All aggregations available
console.log(`Expensive products: ${result._count}`);
console.log(`Price range: ${result._min.price} - ${result._max.price}`);Paginated Results with Total Count
Perfect for building paginated UIs with accurate page counts:
const result = await db.users.findMany({
partitionKey: "john@example.com",
take: 10, // Get first 10 users
skip: 0,
aggregate: {
_count: true, // Count ALL users matching query
}
});
console.log(`Showing ${result.data.length} of ${result._count} total users`);
// Calculate total pages
const totalPages = Math.ceil(result._count / 10);
console.log(`Page 1 of ${totalPages}`);Cross-Partition Queries
const result = await db.products.findMany({
where: { category: "electronics" },
enableCrossPartitionQuery: true,
aggregate: {
_count: true,
_avg: { price: true }
}
});
console.log(`Electronics across all partitions:`);
console.log(` Total products: ${result._count}`);
console.log(` Average price: ${result._avg.price}`);Behavior & Performance
Parallel Execution:
- Data query and aggregation query execute in parallel
- Both queries use the same
where,partitionKey, andenableCrossPartitionQueryfilters - Results are combined into a single response object
Filtering Rules:
whereclause: Applied to both data and aggregation queriesorderBy,take,skip: Applied only to data query (aggregations count all matching records)select: Applied only to data query (aggregations work on full records)
Comparison with Separate Methods
// Before: Two separate queries
const users = await db.users.findMany({ partitionKey: "john@example.com" });
const stats = await db.users.aggregate({
partitionKey: "john@example.com",
_count: true,
_avg: { age: true }
});
// After: Single efficient query
const result = await db.users.findMany({
partitionKey: "john@example.com",
aggregate: {
_count: true,
_avg: { age: true }
}
});
// result.data contains users
// result._count and result._avg contain aggregationsWhen to Use Each Approach:
Use findMany with aggregate:
- When you need both data and aggregations
- For paginated results with total counts
- When filtering is shared between data and aggregations
Use separate aggregate() method:
- When you only need aggregations (no data)
- For complex group-by operations
- When data and aggregations have different filters
All Aggregation Operations
Count Records:
const result = await db.users.findMany({
partitionKey: "john@example.com",
aggregate: { _count: true }
});
// result._count: number
// Count specific fields (non-null values)
const result = await db.users.findMany({
partitionKey: "john@example.com",
aggregate: {
_count: { select: { name: true, email: true } }
}
});
// result._count: { name: number, email: number }Sum, Average, Min, Max:
const result = await db.orders.findMany({
partitionKey: "customer-123",
aggregate: {
_sum: { amount: true, tax: true },
_avg: { amount: true },
_min: { amount: true, createdAt: true },
_max: { amount: true, createdAt: true }
}
});
// All typed correctly based on field types
// number fields return number | null
// date fields return Date | nullFull TypeScript Support
const result = await db.users.findMany({
partitionKey: "john@example.com",
aggregate: {
_count: true,
_avg: { age: true },
_sum: { score: true }
}
});
// Type: {
// data: User[],
// _count: number,
// _avg: { age: number | null },
// _sum: { score: number | null }
// }Group By Operations
Group By is the most powerful aggregation feature - it splits data into groups and performs aggregations on each group.
Group by Single Field
const salesSchema = container('sales', {
id: field.string(),
storeId: field.string(),
category: field.string(),
amount: field.number(),
quantity: field.number(),
}).partitionKey('storeId');
// Sales by category
const categoryStats = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_count: true,
_sum: { amount: true, quantity: true },
_avg: { amount: true },
});
// Result type (fully inferred):
// Array<{
// category: string;
// _count: number;
// _sum: { amount: number | null; quantity: number | null };
// _avg: { amount: number | null };
// }>
// Use the results
for (const group of categoryStats) {
console.log(`${group.category}:
Sales: $${group._sum.amount}
Units: ${group._sum.quantity}
Avg: $${group._avg.amount}
`);
}Group by Multiple Fields
Use an array to group by multiple fields:
// Sales by store AND category
const detailedStats = await db.sales.groupBy({
by: ['storeId', 'category'] as const, // 'as const' for better type inference
enableCrossPartitionQuery: true,
_count: true,
_sum: { amount: true },
});
// Result type:
// Array<{
// storeId: string;
// category: string;
// _count: number;
// _sum: { amount: number | null };
// }>
// Find best performing store-category combination
const topPerformer = detailedStats.reduce((max, group) =>
(group._sum.amount || 0) > (max._sum.amount || 0) ? group : max
);
console.log(`Top: ${topPerformer.storeId} - ${topPerformer.category}`);Group By with Ordering
Sort results by aggregation values:
// Top categories by revenue
const topCategories = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_count: true,
_sum: { amount: true },
orderBy: {
_sum_amount: 'desc', // Order by aggregated field
},
take: 10, // Top 10
});
// Categories with most transactions
const popularCategories = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_count: true,
orderBy: {
_count: 'desc',
},
take: 5,
});Group By with Pagination
// Paginated category report
const page1 = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_sum: { amount: true },
orderBy: { category: 'asc' },
take: 20,
skip: 0,
});
const page2 = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_sum: { amount: true },
orderBy: { category: 'asc' },
take: 20,
skip: 20,
});Group By with Filters
Combine WHERE clauses with GROUP BY:
const userSchema = container('analytics', {
id: field.string(),
userId: field.string(),
eventType: field.string(),
country: field.string(),
timestamp: field.date(),
duration: field.number(),
}).partitionKey('userId');
// Events by type for a specific user in the last 30 days
const recentEvents = await db.analytics.groupBy({
by: 'eventType',
partitionKey: 'user-123',
where: {
timestamp: { gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) },
},
_count: true,
_avg: { duration: true },
});
// Cross-partition: Events by country for premium users
const countryStats = await db.analytics.groupBy({
by: 'country',
enableCrossPartitionQuery: true,
where: {
eventType: 'purchase',
},
_count: true,
_sum: { duration: true },
});Convenience Methods
For simple single-field aggregations, use convenience methods:
sum()
// Quick sum - cleaner than aggregate()
const totalRevenue = await db.orders.sum('amount', {
partitionKey: 'customer-123',
});
// Returns: number | null
// With filter
const completedRevenue = await db.orders.sum('amount', {
partitionKey: 'customer-123',
where: { status: 'completed' },
});avg()
// Average order value
const avgOrderValue = await db.orders.avg('amount', {
partitionKey: 'customer-123',
});
// Returns: number | null
// Average rating for a product
const avgRating = await db.reviews.avg('rating', {
partitionKey: 'product-456',
where: { verified: true },
});min()
// Find cheapest product
const cheapestPrice = await db.products.min('price', {
partitionKey: 'electronics',
});
// Returns: number | null
// Earliest order date
const firstOrder = await db.orders.min('createdAt', {
partitionKey: 'customer-123',
});
// Returns: Date | null (type inferred from field!)max()
// Most expensive product
const highestPrice = await db.products.max('price', {
partitionKey: 'electronics',
});
// Latest activity
const lastSeen = await db.users.max('lastLoginAt', {
partitionKey: 'tenant-1',
});
// Returns: Date | nullType Safety
CosmosQL provides complete type inference for all aggregation operations.
Automatic Type Inference
const orderSchema = container('orders', {
id: field.string(),
customerId: field.string(),
amount: field.number(),
tax: field.number().optional(),
status: field.string(),
createdAt: field.date(),
}).partitionKey('customerId');
// TypeScript knows exact types
const stats = await db.orders.aggregate({
partitionKey: 'customer-123',
_count: true,
_sum: { amount: true, tax: true },
_min: { createdAt: true },
_max: { createdAt: true },
});
// ✅ All these are correctly typed:
const count: number = stats._count;
const total: number | null = stats._sum.amount;
const taxTotal: number | null = stats._sum.tax;
const firstOrder: Date | null = stats._min.createdAt;
const lastOrder: Date | null = stats._max.createdAt;
// ❌ TypeScript errors:
// stats._sum.status; // Error: 'status' is not a number field
// stats._avg.createdAt; // Error: 'createdAt' is not a number fieldGroup By Type Inference
// Single field grouping
const result1 = await db.orders.groupBy({
by: 'status',
enableCrossPartitionQuery: true,
_count: true,
_sum: { amount: true },
});
// Type: Array<{ status: string; _count: number; _sum: { amount: number | null } }>
// Multiple field grouping
const result2 = await db.orders.groupBy({
by: ['customerId', 'status'] as const,
enableCrossPartitionQuery: true,
_count: true,
});
// Type: Array<{ customerId: string; status: string; _count: number }>
// Access fields with full type safety
for (const group of result1) {
console.log(group.status); // ✅ string
console.log(group._count); // ✅ number
console.log(group._sum.amount); // ✅ number | null
}Optional Fields
const productSchema = container('products', {
id: field.string(),
category: field.string(),
price: field.number(),
salePrice: field.number().optional(), // Optional field
}).partitionKey('category');
// TypeScript knows salePrice might be undefined
const result = await db.products.aggregate({
partitionKey: 'electronics',
_sum: { price: true, salePrice: true }, // ✅ Works with optional fields
_avg: { salePrice: true },
});
// All aggregation results are nullable
const total: number | null = result._sum.price;
const saleTotal: number | null = result._sum.salePrice;Type-Safe Field Selection
// Only number fields can be summed/averaged
await db.orders.aggregate({
partitionKey: 'customer-123',
_sum: {
amount: true, // ✅ number field
// status: true, // ❌ Error: 'status' is not a number
},
_avg: {
amount: true, // ✅ number field
// createdAt: true, // ❌ Error: dates can't be averaged
},
});
// Any field can be used with min/max/count
await db.orders.aggregate({
partitionKey: 'customer-123',
_min: {
amount: true, // ✅ number
status: true, // ✅ string
createdAt: true, // ✅ date
},
});Real-World Examples
E-Commerce Analytics Dashboard
// Schema
const orderSchema = container('orders', {
id: field.string(),
customerId: field.string(),
status: field.string(),
totalAmount: field.number(),
itemCount: field.number(),
discount: field.number().optional(),
createdAt: field.date(),
}).partitionKey('customerId');
// Customer lifetime value
async function getCustomerLTV(customerId: string) {
const stats = await db.orders.aggregate({
partitionKey: customerId,
where: { status: 'completed' },
_count: true,
_sum: { totalAmount: true, itemCount: true, discount: true },
_avg: { totalAmount: true },
_min: { createdAt: true },
_max: { createdAt: true },
});
return {
totalOrders: stats._count,
lifetimeValue: stats._sum.totalAmount || 0,
totalItems: stats._sum.itemCount || 0,
totalSavings: stats._sum.discount || 0,
avgOrderValue: stats._avg.totalAmount || 0,
firstOrderDate: stats._min.createdAt,
lastOrderDate: stats._max.createdAt,
customerSince: stats._min.createdAt
? Math.floor((Date.now() - stats._min.createdAt.getTime()) / (1000 * 60 * 60 * 24))
: 0,
};
}
// Sales by status report
async function getSalesStatusReport() {
const statusReport = await db.orders.groupBy({
by: 'status',
enableCrossPartitionQuery: true,
_count: true,
_sum: { totalAmount: true, itemCount: true },
_avg: { totalAmount: true },
});
return statusReport.map(group => ({
status: group.status,
count: group._count,
revenue: group._sum.totalAmount || 0,
items: group._sum.itemCount || 0,
avgValue: group._avg.totalAmount || 0,
}));
}
// Top customers
async function getTopCustomers(limit: number = 10) {
const customers = await db.orders.groupBy({
by: 'customerId',
enableCrossPartitionQuery: true,
_count: true,
_sum: { totalAmount: true },
orderBy: { _sum_totalAmount: 'desc' },
take: limit,
});
return customers.map(c => ({
customerId: c.customerId,
orders: c._count,
spent: c._sum.totalAmount || 0,
}));
}Analytics Platform
const eventSchema = container('events', {
id: field.string(),
userId: field.string(),
eventType: field.string(),
page: field.string(),
duration: field.number(),
timestamp: field.date(),
}).partitionKey('userId');
// User engagement metrics
async function getUserEngagement(userId: string, days: number = 30) {
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
const metrics = await db.events.aggregate({
partitionKey: userId,
where: { timestamp: { gte: since } },
_count: true,
_sum: { duration: true },
_avg: { duration: true },
});
const eventBreakdown = await db.events.groupBy({
by: 'eventType',
partitionKey: userId,
where: { timestamp: { gte: since } },
_count: true,
_avg: { duration: true },
});
return {
totalEvents: metrics._count,
totalTime: metrics._sum.duration || 0,
avgSessionTime: metrics._avg.duration || 0,
eventTypes: eventBreakdown,
};
}
// Platform-wide statistics
async function getPlatformStats() {
// Events by type
const eventTypes = await db.events.groupBy({
by: 'eventType',
enableCrossPartitionQuery: true,
where: {
timestamp: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
},
_count: true,
_avg: { duration: true },
orderBy: { _count: 'desc' },
});
// Page engagement
const pages = await db.events.groupBy({
by: 'page',
enableCrossPartitionQuery: true,
where: {
timestamp: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
},
_count: true,
_sum: { duration: true },
orderBy: { _count: 'desc' },
take: 10,
});
return { eventTypes, topPages: pages };
}Inventory Management
const productSchema = container('products', {
id: field.string(),
warehouseId: field.string(),
category: field.string(),
stock: field.number(),
reorderPoint: field.number(),
cost: field.number(),
price: field.number(),
}).partitionKey('warehouseId');
// Warehouse inventory summary
async function getWarehouseSummary(warehouseId: string) {
const summary = await db.products.aggregate({
partitionKey: warehouseId,
_count: true,
_sum: { stock: true, cost: true, price: true },
});
const categoryBreakdown = await db.products.groupBy({
by: 'category',
partitionKey: warehouseId,
_count: true,
_sum: { stock: true },
_avg: { price: true },
});
const inventoryValue = (summary._sum.cost || 0) * (summary._sum.stock || 0);
const retailValue = (summary._sum.price || 0) * (summary._sum.stock || 0);
return {
totalProducts: summary._count,
totalStock: summary._sum.stock || 0,
inventoryValue,
retailValue,
potentialProfit: retailValue - inventoryValue,
byCategory: categoryBreakdown,
};
}
// Low stock alerts
async function getLowStockItems(warehouseId: string) {
const items = await db.products.findMany({
partitionKey: warehouseId,
where: {
stock: { lte: field.number() }, // Stock <= reorderPoint
},
});
// Get summary stats
const stats = await db.products.aggregate({
partitionKey: warehouseId,
where: {
stock: { lte: field.number() },
},
_count: true,
_sum: { stock: true },
});
return {
items,
totalLowStockItems: stats._count,
totalLowStockUnits: stats._sum.stock || 0,
};
}Performance & Best Practices
✅ DO: Use Partition Keys
Always provide a partition key when possible:
// ✅ GOOD - Single partition query (fast & cheap)
const count = await db.orders.count({
partitionKey: 'customer-123',
});
// ⚠️ AVOID - Cross-partition query (slow & expensive)
const count = await db.orders.count({
enableCrossPartitionQuery: true,
});✅ DO: Combine Multiple Aggregations
More efficient to get multiple aggregations in one query:
// ✅ GOOD - Single query
const stats = await db.orders.aggregate({
partitionKey: 'customer-123',
_count: true,
_sum: { amount: true },
_avg: { amount: true },
});
// ❌ BAD - Three separate queries
const count = await db.orders.count({ partitionKey: 'customer-123' });
const sum = await db.orders.sum('amount', { partitionKey: 'customer-123' });
const avg = await db.orders.avg('amount', { partitionKey: 'customer-123' });✅ DO: Use WHERE Clauses
Filter data to reduce computation:
// ✅ GOOD - Only aggregate what you need
const completedStats = await db.orders.aggregate({
partitionKey: 'customer-123',
where: { status: 'completed' },
_sum: { amount: true },
});
// ⚠️ AVOID - Aggregating everything then filtering
const allStats = await db.orders.aggregate({
partitionKey: 'customer-123',
_sum: { amount: true },
});
// Then filtering in application code✅ DO: Use Pagination for Large Results
// ✅ GOOD - Paginate large group by results
async function* getAllGroups() {
let skip = 0;
const pageSize = 100;
while (true) {
const page = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_sum: { amount: true },
take: pageSize,
skip,
});
if (page.length === 0) break;
yield* page;
skip += pageSize;
}
}⚠️ AVOID: Unnecessary Cross-Partition Queries
// ❌ BAD - Expensive cross-partition query
const total = await db.orders.sum('amount', {
enableCrossPartitionQuery: true,
});
// ✅ BETTER - Store aggregates separately if needed frequently
// Use a separate container or materialized views⚠️ AVOID: Aggregating Large Date Ranges Without Filters
// ❌ BAD - Scanning entire history
const allTimeStats = await db.orders.aggregate({
enableCrossPartitionQuery: true,
_sum: { amount: true },
});
// ✅ BETTER - Limit to relevant time period
const recentStats = await db.orders.aggregate({
enableCrossPartitionQuery: true,
where: {
createdAt: { gte: new Date('2024-01-01') },
},
_sum: { amount: true },
});Understanding NULL Results
Aggregations return null when there are no rows:
const stats = await db.orders.aggregate({
partitionKey: 'new-customer',
_sum: { amount: true },
_avg: { amount: true },
});
// stats._sum.amount might be null (no orders)
// Always handle null case:
const total = stats._sum.amount ?? 0;
const average = stats._avg.amount ?? 0;
// Or use nullish coalescing in display:
console.log(`Total: $${stats._sum.amount || 0}`);Cosmos DB RU Costs
Aggregation operations consume Request Units (RUs):
- Single Partition: ~2-5 RUs (very efficient)
- Cross-Partition: 5-100+ RUs depending on data size
- Group By: Higher RU cost, especially cross-partition
Monitor your RU consumption and optimize queries accordingly.
Advanced Patterns
Combining Aggregations with Find
// Get both detail and summary
async function getOrderReport(customerId: string) {
const [orders, stats] = await Promise.all([
db.orders.findMany({
partitionKey: customerId,
orderBy: { createdAt: 'desc' },
take: 10,
}),
db.orders.aggregate({
partitionKey: customerId,
_count: true,
_sum: { amount: true },
_avg: { amount: true },
}),
]);
return { recentOrders: orders, summary: stats };
}Calculating Percentages
const statusBreakdown = await db.orders.groupBy({
by: 'status',
enableCrossPartitionQuery: true,
_count: true,
});
const total = statusBreakdown.reduce((sum, g) => sum + g._count, 0);
const withPercentages = statusBreakdown.map(group => ({
status: group.status,
count: group._count,
percentage: ((group._count / total) * 100).toFixed(2) + '%',
}));Time-Series Aggregations
// Daily sales for last 30 days
async function getDailySales(days: number = 30) {
const results = [];
for (let i = 0; i < days; i++) {
const date = new Date();
date.setDate(date.getDate() - i);
const startOfDay = new Date(date.setHours(0, 0, 0, 0));
const endOfDay = new Date(date.setHours(23, 59, 59, 999));
const stats = await db.orders.aggregate({
enableCrossPartitionQuery: true,
where: {
createdAt: { gte: startOfDay, lte: endOfDay },
status: 'completed',
},
_count: true,
_sum: { amount: true },
});
results.push({
date: startOfDay,
orders: stats._count,
revenue: stats._sum.amount || 0,
});
}
return results;
}Error Handling
import { CosmosError } from 'cosmosql';
try {
const stats = await db.orders.aggregate({
// partitionKey missing!
_sum: { amount: true },
});
} catch (error) {
if (error instanceof CosmosError) {
if (error.statusCode === 429) {
// Rate limited - retry with backoff
console.log('Rate limited, retry after:', error.retryAfter);
} else if (error.code === 'PARTITION_KEY_REQUIRED') {
// Missing partition key
console.error('Must provide partition key or enable cross-partition query');
}
}
throw error;
}Migration from Raw Cosmos DB
If you're migrating from raw Cosmos DB SDK:
Before (Raw SDK)
const querySpec = {
query: 'SELECT COUNT(1) as count, SUM(c.amount) as total FROM c WHERE c.customerId = @customerId',
parameters: [{ name: '@customerId', value: 'customer-123' }],
};
const { resources } = await container.items.query(querySpec).fetchAll();
const result = resources[0];After (CosmosQL)
const result = await db.orders.aggregate({
partitionKey: 'customer-123',
_count: true,
_sum: { amount: true },
});
// Result is fully typed!Summary
CosmosQL's aggregation functions provide:
- Type Safety - Full TypeScript inference for all operations
- Clean API - Intuitive, composable operations
- Performance - Optimized queries with partition awareness
- Flexibility - From simple counts to complex group by operations
- Developer Experience - IntelliSense, autocompletion, and compile-time checks
Start with simple count() operations, then explore aggregate() for multiple operations, and finally groupBy() for advanced analytics. Always consider partition keys and cross-partition costs when designing your queries.
For more examples, see the API reference or join our community for support.
Common Patterns
Real-world usage patterns for authentication, multi-tenant apps, time-series data, and more
View on separate page →Real-world solutions to common problems with CosmosQL.
Organized by use case:
- Authentication - User login and registration
- Multi-Tenant SaaS - Data isolation per tenant
- E-Commerce - Product catalogs and carts
- Social Platform - User-generated content
- Time-Series Data - Metrics and logs
Authentication
The pattern: Use email as both ID and partition key for user documents.
Why this works:
- Login queries are partition-scoped (fast + cheap)
- Natural 1:1 mapping between user and partition
- Scales to millions of users
- No "hot partitions" (even distribution)
When to avoid:
- Users can change email addresses → Use immutable
userIdinstead - Multiple authentication providers → Requires composite keys
Schema Design
const users = container('users', {
id: field.string(), // Using email as ID
email: field.string(), // Also the partition key
passwordHash: field.string(),
profile: field.object({
name: field.string(),
avatar: field.string().optional()
}),
createdAt: field.date(),
lastLoginAt: field.date().optional()
}).partitionKey('email'); // Key decision: email is partition keyWhy email as both ID and partition key?
- Point reads during login (fastest possible: 1 RU)
- No duplicate emails (ID uniqueness)
- Even distribution across partitions
Registration
async function register(email: string, password: string) {
const passwordHash = await bcrypt.hash(password, 10);
try {
const user = await db.users.create({
data: {
id: email, // Using email as ID
email: email, // And partition key
passwordHash,
profile: { name: email.split('@')[0] },
createdAt: new Date()
}
});
return { success: true, user };
} catch (error) {
if (isCosmosError(error) && error.statusCode === 409) {
// Conflict = duplicate email
return { success: false, error: 'Email already exists' };
}
throw error;
}
}What makes this efficient:
- One partition write (5 RU)
- Automatic duplicate detection (409 conflict)
- Type-safe return values
Login
async function login(email: string, password: string) {
// Point read: fastest possible query (1 RU)
const user = await db.users.findUnique({
where: {
id: email,
email: email // Partition key
},
select: {
id: true,
email: true,
passwordHash: true,
profile: true
}
});
if (!user) {
return { success: false, error: 'Invalid credentials' };
}
// Verify password
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) {
return { success: false, error: 'Invalid credentials' };
}
// Update last login (separate operation to keep read fast)
await db.users.update({
where: { id: email, email },
data: { lastLoginAt: new Date() }
});
return { success: true, user };
}Performance breakdown:
- Read: 1 RU (point read)
- Update: 5 RU (partition-scoped write)
- Total: 6 RU per login
Alternative: Skip the update to save 5 RU if you don't need lastLoginAt.
Email Change (Advanced)
Problem: User wants to change email, but email is the partition key.
Solution: Create new document, migrate data, delete old.
async function changeEmail(oldEmail: string, newEmail: string) {
// 1. Check if new email exists
const existing = await db.users.findUnique({
where: { id: newEmail, email: newEmail }
});
if (existing) {
return { success: false, error: 'Email already in use' };
}
// 2. Get current user
const user = await db.users.findUnique({
where: { id: oldEmail, email: oldEmail }
});
if (!user) {
return { success: false, error: 'User not found' };
}
// 3. Create new document with new email
await db.users.create({
data: {
...user,
id: newEmail,
email: newEmail
}
});
// 4. Delete old document
await db.users.delete({
where: { id: oldEmail, email: oldEmail }
});
return { success: true };
}Cost: ~15 RU (read + create + delete)
Caveat: Not atomic—handle failures appropriately
Multi-Tenant SaaS
The pattern: Use tenantId as partition key to ensure data isolation.
Why this works:
- Complete isolation between tenants
- All queries automatically scoped to a single tenant
- Prevents cross-tenant data leakage
- Scales as tenants grow
Schema Design
const organizations = container('organizations', {
id: field.string(),
name: field.string(),
plan: field.string(),
settings: field.object({
theme: field.string().default('light'),
features: field.array(field.string())
})
}).partitionKey('id'); // Organization ID as partition key
const users = container('users', {
id: field.string(),
organizationId: field.string(),
email: field.string(),
role: field.string(),
profile: field.object({
name: field.string(),
department: field.string().optional()
})
}).partitionKey('organizationId'); // All users for an org in same partition
const documents = container('documents', {
id: field.string(),
tenantId: field.string(),
name: field.string(),
content: field.string(),
createdBy: field.string(),
createdAt: field.date()
}).partitionKey('tenantId');Querying Tenant Data
// All queries automatically scoped to tenant
async function getDocuments(tenantId: string) {
return await db.documents.findMany({
partitionKey: tenantId,
orderBy: { createdAt: 'desc' }
});
}
async function getOrgUsers(organizationId: string) {
return await db.users.findMany({
partitionKey: organizationId,
where: {
role: 'admin'
},
select: {
id: true,
email: true,
profile: { name: true }
}
});
}E-Commerce
The pattern: Partition by category for efficient product queries.
Why this works:
- Fast category browsing
- Good distribution across partitions
- Cross-partition queries only for global search
Schema Design
const products = container('products', {
id: field.string(),
category: field.string(), // Partition key
name: field.string(),
price: field.number(),
inventory: field.number(),
tags: field.array(field.string()),
metadata: field.object({
brand: field.string(),
weight: field.number().optional(),
dimensions: field.object({
length: field.number(),
width: field.number(),
height: field.number()
}).optional()
})
}).partitionKey('category');Product Queries
// Query products by category with filtering
const electronics = await db.products.findMany({
partitionKey: 'electronics',
where: {
price: { lte: 999 },
inventory: { gt: 0 },
tags: { contains: 'wireless' }
},
select: {
id: true,
name: true,
price: true,
metadata: {
brand: true
}
},
orderBy: { price: 'asc' },
take: 20
});Social Platform
The pattern: Partition by userId to keep all user content together.
Why this works:
- Fast queries for a user's posts, comments, activity
- All related data in one partition
- Efficient for user profiles and feeds
Schema Design
const posts = container('posts', {
id: field.string(),
userId: field.string(), // Partition key
content: field.string(),
likes: field.number().default(0),
createdAt: field.date(),
tags: field.array(field.string()),
author: field.object({
name: field.string(),
avatar: field.string().optional()
})
}).partitionKey('userId');User Content Queries
// Find user's recent posts with engagement
const userPosts = await db.posts.findMany({
partitionKey: 'user_123',
where: {
createdAt: { gte: new Date('2024-01-01') },
likes: { gt: 10 }
},
select: {
id: true,
content: true,
likes: true,
author: {
name: true
}
},
orderBy: { createdAt: 'desc' },
take: 50
});Time-Series Data
The pattern: Use deviceId as partition key with TTL for automatic cleanup.
Why this works:
- Fast queries per device
- Automatic expiration with TTL
- Good distribution across devices
Schema Design
const metrics = container('metrics', {
id: field.string(),
deviceId: field.string(), // Partition key
timestamp: field.date(),
temperature: field.number(),
humidity: field.number(),
ttl: field.number() // Auto-delete old data
}).partitionKey('deviceId');Time-Series Queries
// Ingest metric (auto-expires after 30 days)
await db.metrics.create({
data: {
id: generateId(),
deviceId: 'device_123',
timestamp: new Date(),
temperature: 22.5,
humidity: 45,
ttl: 30 * 24 * 60 * 60 // 30 days in seconds
}
});
// Query recent metrics for device
const recent = await db.metrics.findMany({
partitionKey: 'device_123',
where: {
timestamp: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) }
},
orderBy: { timestamp: 'desc' }
});Shopping Cart
The pattern: Use userId as partition key with TTL for abandoned carts.
const carts = container('carts', {
id: field.string(),
userId: field.string(),
items: field.array(field.object({
productId: field.string(),
quantity: field.number(),
price: field.number()
})),
total: field.number(),
updatedAt: field.date(),
ttl: field.number() // Expire abandoned carts
}).partitionKey('userId');Cart Operations
async function addToCart(userId: string, item: CartItem) {
const cart = await db.carts.findUnique({
where: { id: userId, userId }
});
if (!cart) {
return await db.carts.create({
data: {
id: userId,
userId,
items: [item],
total: item.price * item.quantity,
updatedAt: new Date(),
ttl: 7 * 24 * 60 * 60 // Expire after 7 days
}
});
}
// Update existing cart
const newItems = [...cart.items, item];
const newTotal = newItems.reduce((sum, i) => sum + i.price * i.quantity, 0);
return await db.carts.update({
where: { id: userId, userId },
data: {
items: newItems,
total: newTotal,
updatedAt: new Date()
}
});
}Next Steps
- Review Advanced Patterns for complex scenarios
- See Performance Guide for optimization tips
- Check API Reference for complete method documentation
Core Concepts
Understand CosmosDB fundamentals, partition keys, and how CosmosQL prevents costly mistakes
View on separate page →Why this matters: CosmosDB has unique characteristics that shape how you query data. Understanding these fundamentals helps you write efficient, cost-effective code.
Skip this if: You're already a CosmosDB expert. Jump to Creating Documents.
Read this if: CosmosDB is new, or you want to understand the "why" behind CosmosQL's design.
The Distributed Database Problem
CosmosDB isn't one server—it's thousands of servers working together. Your data is split across them.
This creates a challenge: How do you query data when it's spread across hundreds of machines?
The solution: Partition keys.
Partition Keys: The $2,400 Question
Every document in CosmosDB has a partition key. This value determines which server stores it.
Why This Matters (Real Scenario)
A developer wrote this query:
// Find all active users
const users = await findWhere({ isActive: true });With 1,000 users: Cost $5/month, worked fine.
With 100,000 users: Cost $2,400/month, still worked fine.
What went wrong? No partition key = CosmosDB scans every server.
The Cost Breakdown
| Query Type | Servers Scanned | Request Units | Monthly Cost (1M queries) |
|---|---|---|---|
| With partition key | 1 | 1 RU | $24 |
| Without partition key | All (100+) | 100 RU | $2,400 |
100x difference.
How CosmosQL Prevents This
// ❌ TypeScript won't compile this
const users = await db.users.findMany({
where: { isActive: true }
// Error: Missing required property 'partitionKey'
});
// ✅ Forces you to be explicit
const users = await db.users.findMany({
partitionKey: 'user@example.com', // Scans one partition
where: { isActive: true }
});
// ✅ Or opt-in to cross-partition (expensive) queries
const users = await db.users.findMany({
enableCrossPartitionQuery: true, // "I know this is expensive"
where: { isActive: true }
});You cannot accidentally write expensive queries. TypeScript won't let your code compile.
Choosing a Good Partition Key
Good partition keys have three properties:
1. High Cardinality (Many Unique Values)
// ✅ Good: email addresses
const users = container('users', schema).partitionKey('email');
// ❌ Bad: boolean status
const users = container('users', schema).partitionKey('isActive');
// Only 2 values → only 2 partitions → poor distribution2. Even Distribution
// ✅ Good: user IDs spread evenly
const posts = container('posts', schema).partitionKey('userId');
// ⚠️ Risky: category (one category could be huge)
const products = container('products', schema).partitionKey('category');
// "Electronics" might be 80% of your data → hot partition3. Query Alignment
Choose keys that match your most common query patterns:
// If you always query by user
const posts = container('posts', schema).partitionKey('userId'); // ✅
// If you always query by date
const metrics = container('metrics', schema).partitionKey('timestamp'); // ✅
// If you query by multiple fields
const orders = container('orders', schema).partitionKey('customerId'); // ✅Request Units (RU): How CosmosDB Charges
CosmosDB uses Request Units (RUs) to measure and charge for operations.
Typical Costs
| Operation | Cost | Notes |
|---|---|---|
| Point read | 1 RU | Reading by ID + partition key |
| Partition query | 3-5 RU | Filtering within one partition |
| Cross-partition query | 50-100+ RU | Scanning all partitions |
| Write | 5-10 RU | Creating or updating |
Real Cost Example
If you process 1 million queries per month:
// Partition-scoped queries
1M queries × 3 RU = 3M RU/month ≈ $72/month
// Cross-partition queries
1M queries × 100 RU = 100M RU/month ≈ $2,400/monthThat's 33x more expensive.
Schema Definition
Schemas in CosmosQL are TypeScript-only - they don't exist at runtime.
import { container, field } from 'cosmosql';
const posts = container('posts', {
// Required fields
id: field.string(),
userId: field.string(),
title: field.string(),
// Optional fields
subtitle: field.string().optional(),
// Defaults
viewCount: field.number().default(0),
isPublished: field.boolean().default(false),
// Arrays
tags: field.array(field.string()),
// Nested objects
metadata: field.object({
source: field.string(),
version: field.number()
}).optional(),
// TTL (auto-delete after N seconds)
ttl: field.number().optional()
}).partitionKey('userId');
// Get TypeScript type
type Post = typeof posts.infer;Key point: This is purely for TypeScript. CosmosDB is schemaless - CosmosQL just adds type safety.
Field Types
field.string()→stringfield.number()→numberfield.boolean()→booleanfield.date()→Datefield.array(type)→Array<type>field.object(schema)→ nested object
Modifiers
.optional()→ field can beundefined.default(value)→ value used if not provided on create
Client Setup
import { createClient } from 'cosmosql';
const db = await createClient({
connectionString: process.env.COSMOS_CONNECTION_STRING!,
database: 'myapp',
mode: 'auto-create' // Validates and creates containers
}).withContainers({
users,
posts,
comments
});
// Now you have typed access
db.users // ContainerClient<User>
db.posts // ContainerClient<Post>
db.comments // ContainerClient<Comment>Client creation is async because it validates and optionally creates your database and containers. Choose a mode based on your environment:
auto-create- Development: creates missing containers automaticallyverify- Production: fails fast if containers don't existskip- Maximum performance: no validation
See Getting Started Guide for detailed mode documentation.
Connection management: HTTP/2 pooling with automatic retries. You don't manage connections.
Multiple databases: Create separate clients:
const prodDb = await createClient({...}).withContainers({users});
const analyticsDb = await createClient({...}).withContainers({events});Summary
Three key takeaways:
- Partition keys determine cost - Queries without them cost 10-100x more
- CosmosQL enforces them at compile time - TypeScript prevents expensive mistakes
- Schemas are TypeScript-only - Zero runtime overhead, all compile-time checks
Next: Apply these concepts in Creating Documents.
Creating Documents
Learn how to create and insert documents into CosmosDB with CosmosQL
View on separate page →Complete guide to inserting documents into CosmosDB.
Navigation:
- Single Document - Basic create operations
- Bulk Operations - Creating multiple documents
- Default Values - Using schema defaults
Single Document
Use when: Creating one document at a time.
Example scenario: User registration.
const newUser = await db.users.create({
data: {
id: 'user_123',
email: 'john@example.com',
name: 'John Doe',
age: 30,
createdAt: new Date()
// Optional fields with defaults are automatically applied
}
});
// Returns: Fully typed User objectWhat happens:
- CosmosQL generates a POST request to CosmosDB REST API
- Document is stored in the partition determined by
email - Return value is fully typed based on your schema
Cost: ~5 RU per document
Default Values
Use when: Your schema defines defaults for optional fields.
const users = container('users', {
id: field.string(),
email: field.string(),
isActive: field.boolean().default(true), // Default value
createdAt: field.date()
}).partitionKey('email');
// No need to provide isActive
const user = await db.users.create({
data: {
id: 'user_123',
email: 'john@example.com',
createdAt: new Date()
// isActive automatically true
}
});Bulk Operations
Use when: Creating multiple documents in one request.
Requirement: All documents MUST share the same partition key.
Why: CosmosDB batch operations only work within a single partition.
Example scenario: Importing user posts.
await db.posts.createMany({
data: [
{ id: 'post_1', userId: 'user_123', title: 'First', content: '...' },
{ id: 'post_2', userId: 'user_123', title: 'Second', content: '...' },
{ id: 'post_3', userId: 'user_123', title: 'Third', content: '...' }
],
partitionKey: 'user_123' // Must be same for all documents
});Cost: ~5 RU per document (same as single creates)
Performance: Single HTTP request vs multiple
Error handling: If any document fails, none are created (atomic within partition).
Best Practices
1. Always Provide IDs Explicitly
// ✅ Good: Explicit ID
const user = await db.users.create({
data: {
id: `user_${Date.now()}`, // Predictable
email: 'user@example.com',
// ...
}
});2. Use Meaningful IDs
// ✅ Good: Human-readable
'user_john@example.com'
'order_2024-01-15-abc123'
// ❌ Bad: Random UUIDs (unless you need them)
'a3f5b8c2-d4e6-7890-abcd-ef1234567890'3. Set Timestamps Explicitly
const user = await db.users.create({
data: {
id: 'user_123',
email: 'john@example.com',
createdAt: new Date(), // Server time
updatedAt: new Date()
}
});Error Handling
try {
const user = await db.users.create({
data: {
id: 'user_123',
email: 'john@example.com',
// ...
}
});
} catch (error) {
if (error.code === 409) {
// Document with this ID already exists
console.error('User already exists');
} else if (error.code === 429) {
// Rate limit exceeded
console.error('Rate limit exceeded, please retry');
} else {
console.error('Failed to create user:', error);
}
}Common Patterns
Pattern: Multi-Tenant Applications
// Partition by tenant ID
const posts = container('posts', {
id: field.string(),
tenantId: field.string(),
title: field.string(),
// ...
}).partitionKey('tenantId');
// Create posts scoped to a tenant
await db.posts.createMany({
data: [
{ id: 'post_1', tenantId: 'tenant_abc', title: '...' },
{ id: 'post_2', tenantId: 'tenant_abc', title: '...' }
],
partitionKey: 'tenant_abc'
});Pattern: Hierarchical Data
// Create parent-child relationships
const category = await db.categories.create({
data: {
id: 'cat_tech',
name: 'Technology',
parentId: null,
createdAt: new Date()
}
});
const subcategory = await db.categories.create({
data: {
id: 'cat_tech_ai',
name: 'Artificial Intelligence',
parentId: 'cat_tech',
createdAt: new Date()
}
});Next Steps
- Read Reading Documents to learn efficient query patterns
- See Common Patterns for real-world scenarios
- Review Update Operations for modifying documents
Deleting Documents
Learn how to delete documents and implement soft delete patterns
View on separate page →Deleting documents requires careful consideration. Use hard deletes for performance, soft deletes for recovery.
Navigation:
- Hard Delete - Permanent removal
- Soft Delete - Recoverable deletion
- Bulk Operations - Delete multiple documents efficiently
Hard Delete
The basic delete operation removes a document permanently:
await db.users.delete({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
}
});Important: Deletion is permanent and cannot be undone. Always ensure you have the partition key in the where clause—this is enforced at compile time.
Cost: ~5 RU
Soft Delete
Instead of permanently deleting documents, mark them as deleted:
// Instead of deleting, mark as inactive
await db.users.update({
where: { id: 'user_123', email: 'john@example.com' },
data: {
isActive: false,
deletedAt: new Date()
}
});
// Query only active users
const activeUsers = await db.users.findMany({
partitionKey: 'tenant_123',
where: {
isActive: true
}
});Why Soft Delete?
- Data Recovery: Can restore deleted data
- Audit Trail: Keep history of what was deleted and when
- Referential Integrity: Other documents can still reference deleted items
- Analytics: Analyze deleted data patterns
Implementing Soft Delete
Complete implementation:
// Define schema with soft delete fields
const users = container('users', {
id: field.string(),
email: field.string(),
name: field.string(),
isDeleted: field.boolean().default(false),
deletedAt: field.date().optional(),
deletedBy: field.string().optional()
}).partitionKey('email');
// Helper functions
async function softDeleteUser(id: string, email: string, deletedBy: string) {
return await db.users.update({
where: { id, email },
data: {
isDeleted: true,
deletedAt: new Date(),
deletedBy
}
});
}
async function restoreUser(id: string, email: string) {
return await db.users.update({
where: { id, email },
data: {
isDeleted: false,
deletedAt: null,
deletedBy: null
}
});
}
// Query excluding soft-deleted items
async function findAllUsers(partitionKey: string) {
return await db.users.findMany({
partitionKey,
where: {
isDeleted: false
}
});
}Cleanup Old Soft Deletes
Automatically clean up old soft-deleted items:
async function cleanupOldSoftDeletes() {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const deletedUsers = await db.users.findMany({
enableCrossPartitionQuery: true,
where: {
isDeleted: true,
deletedAt: { lt: thirtyDaysAgo }
}
});
// Permanently delete old soft-deleted items
for (const user of deletedUsers) {
await db.users.delete({
where: { id: user.id, email: user.email }
});
}
}Cascade Delete Pattern
When deleting a parent document, you may need to delete related documents:
async function deleteUserWithRelatedData(userId: string, email: string) {
// Find all related posts
const posts = await db.posts.findMany({
partitionKey: userId
});
// Delete all posts
for (const post of posts) {
await db.posts.delete({
where: { id: post.id, userId }
});
}
// Finally, delete the user
await db.users.delete({
where: { id: userId, email }
});
}Note: CosmosDB doesn't support foreign key constraints or cascade deletes at the database level. You must handle this logic in your application code.
Bulk Operations
For deleting multiple documents at once, use deleteMany:
// Delete old posts
const result = await db.posts.deleteMany({
where: { createdAt: { lt: oneYearAgo } },
confirm: true, // Safety: must explicitly confirm
partitionKey: 'user123',
onProgress: (stats) => {
console.log(`Deleted ${stats.updated}/${stats.total}`);
}
});
console.log(`Deleted ${result.deleted} documents`);
console.log(`Failed: ${result.failed}`);
console.log(`RU consumed: ${result.performance.ruConsumed}`);Key Options:
where: Query to match documentsconfirm: Must betrueto execute (safety requirement)partitionKeyorenableCrossPartitionQuery: Required (one or the other)batchSize: Documents per batch (default: 50)maxConcurrency: Parallel batches (default: 5)continueOnError: Keep going if some fail (default: false)onProgress: Progress callback with stats
Result includes:
deleted: Number of successfully deleted documentsfailed: Number of failed deletionserrors: Array of error detailsperformance: RU consumption, duration, and throughput metrics
Best Practices:
- Always use
confirm: true- This prevents accidental deletions - Start with small batches when testing (e.g.,
batchSize: 10) - Use
continueOnError: truefor large operations where some failures are acceptable - Monitor progress using
onProgresscallbacks - Use partition keys when possible (much faster than cross-partition queries)
Note: For deleting documents by specific IDs in the same partition, you can still use individual deletes:
async function deleteManyPosts(postIds: string[], userId: string) {
const deletePromises = postIds.map(id =>
db.posts.delete({
where: { id, userId }
})
);
await Promise.all(deletePromises);
}However, deleteMany is more efficient for query-based deletions with built-in progress tracking and error handling.
Bulk operations are built into CosmosQL and work seamlessly with your existing containers.
Delete by Query
For query-based deletions, use deleteMany instead of manually querying and deleting:
// ✅ Recommended: Use deleteMany
const result = await db.posts.deleteMany({
where: {
createdAt: { lt: cutoffDate },
isPublished: false
},
confirm: true,
enableCrossPartitionQuery: true,
onProgress: (stats) => {
console.log(`Progress: ${stats.percentage}%`);
}
});This is more efficient than querying and deleting individually, as it includes built-in progress tracking, error handling, and retry logic.
Error Handling
try {
await db.users.delete({
where: { id: 'user_123', email: 'john@example.com' }
});
} catch (error) {
if (error.code === 404) {
// Document not found
console.error('User not found');
} else if (error.code === 429) {
// Rate limit exceeded
console.error('Rate limit exceeded, please retry');
} else {
console.error('Failed to delete user:', error);
}
}Performance Considerations
1. Always Include Partition Key
// ✅ Good: Includes partition key (fast, ~5 RU)
await db.users.delete({
where: { id: 'user_123', email: 'john@example.com' }
});2. Use Soft Delete for Frequent Deletions
Hard deletes are expensive if done repeatedly. Soft deletes allow you to batch hard deletes later:
// Mark as deleted immediately (fast)
await db.users.update({
where: { id: 'user_123', email: 'john@example.com' },
data: { isDeleted: true, deletedAt: new Date() }
});
// Batch permanent deletion later (background job)
setInterval(async () => {
await cleanupOldSoftDeletes();
}, 60 * 60 * 1000); // Run every hourCommon Patterns
Pattern: Recycle Bin
async function moveToRecycleBin(postId: string, userId: string) {
// Copy to recycle bin
const post = await db.posts.findUnique({
where: { id: postId, userId }
});
await db.recycleBin.create({
data: {
id: `bin_${postId}`,
deletedItemId: postId,
deletedItemType: 'post',
deletedItemData: post,
deletedAt: new Date()
}
});
// Remove from original location
await db.posts.delete({
where: { id: postId, userId }
});
}
async function restoreFromRecycleBin(itemId: string) {
const binItem = await db.recycleBin.findUnique({
where: { id: itemId }
});
if (binItem.deletedItemType === 'post') {
await db.posts.create({
data: binItem.deletedItemData
});
}
await db.recycleBin.delete({ where: { id: itemId } });
}Next Steps
- Read Update Operations for modifying documents
- See Common Patterns for real-world scenarios
- Review Performance Guide for optimization tips
Reading Documents
Learn how to query and read documents from CosmosDB efficiently
View on separate page →Reading data is the most common operation. CosmosQL provides three methods optimized for different scenarios.
Navigation:
- Point Read - By ID and partition key
- Query Within Partition - Filtering and sorting
- Cross-Partition Query - Admin and analytics
- Aggregations with findMany - Data + statistics in one query
Point Read (Fastest)
Use when: You know the exact ID and partition key.
Why it's fastest: Direct lookup, no scanning required.
Cost: ~1 RU (cheapest possible)
Example scenario: Loading a user's profile.
const user = await db.users.findUnique({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
}
});
// Returns: User | nullType safety: TypeScript enforces both ID and partition key.
// ❌ This won't compile
const user = await db.users.findUnique({
where: { id: 'user_123' }
// Error: Property 'email' is missing
});Query Within Partition
Use when: Filtering, sorting, or paginating within one partition.
Cost: ~3-5 RU (depends on result size)
Example scenario: Getting a user's posts.
const posts = await db.posts.findMany({
partitionKey: 'user_123', // Required
where: {
isPublished: true,
createdAt: { gte: new Date('2024-01-01') }
},
orderBy: { createdAt: 'desc' },
take: 10
});
// Returns: Post[]findMany with Aggregations
findMany can also return aggregations alongside your data - perfect for paginated lists with counts or statistics.
Example: Pagination with total count
const result = await db.posts.findMany({
partitionKey: 'user_123',
where: { isPublished: true },
orderBy: { createdAt: 'desc' },
take: 10,
skip: 0,
aggregate: {
_count: true // Count ALL matching posts
}
});
console.log(`Showing ${result.data.length} of ${result._count} posts`);
console.log(`Page 1 of ${Math.ceil(result._count / 10)}`);
// Returns: { data: Post[], _count: number }Example: Data with statistics
const result = await db.orders.findMany({
partitionKey: 'customer-123',
where: { status: 'completed' },
take: 5, // Last 5 orders
orderBy: { createdAt: 'desc' },
aggregate: {
_count: true,
_sum: { amount: true },
_avg: { amount: true },
_min: { createdAt: true },
_max: { createdAt: true }
}
});
// Access both data and stats
console.log(`Recent orders:`, result.data);
console.log(`Total completed orders: ${result._count}`);
console.log(`Total revenue: $${result._sum.amount}`);
console.log(`Average order value: $${result._avg.amount}`);
console.log(`Customer since: ${result._min.createdAt}`);
// Returns: {
// data: Order[],
// _count: number,
// _sum: { amount: number | null },
// _avg: { amount: number | null },
// _min: { createdAt: Date | null },
// _max: { createdAt: Date | null }
// }How it works:
- Data query and aggregation execute in parallel
- Both use the same
wherefilter - Pagination (
take/skip) only applies todata, not aggregations - Full type safety: TypeScript infers the complete return type
See full aggregations documentation below for more examples.
Cross-Partition Query
Use when: You need data across multiple partitions.
Cost: ~50-100+ RU (10-100x more expensive)
Requires: Explicit opt-in via enableCrossPartitionQuery: true
Example scenario: Global search across all users.
const recentUsers = await db.users.findMany({
enableCrossPartitionQuery: true, // Must be explicit
where: {
createdAt: { gte: new Date('2024-01-01') }
},
orderBy: { createdAt: 'desc' },
take: 20
});When to use: Admin dashboards, analytics, reports—not user-facing queries.
Selecting Specific Fields
Use when: You don't need the full document.
Why: Reduces data transfer, faster queries, lower latency.
const user = await db.users.findUnique({
where: { id: 'user_123', email: 'john@example.com' },
select: {
name: true,
email: true
}
});
// Returns: { name: string; email: string } | nullNested fields: Select specific fields from nested objects.
const post = await db.posts.findUnique({
where: { id: 'post_1', userId: 'user_123' },
select: {
title: true,
metadata: {
source: true
}
}
});
// Returns: { title: string; metadata?: { source: string } } | nullFiltering with Where Clauses
Comprehensive filtering options:
await db.users.findMany({
partitionKey: 'john@example.com',
where: {
// Exact match
isActive: true,
// Number comparisons
age: { gt: 21 }, // Greater than
score: { gte: 85 }, // Greater than or equal
rating: { lt: 5 }, // Less than
count: { lte: 100 }, // Less than or equal
age: { gte: 18, lte: 65 }, // Range
// String operations
name: { startsWith: 'John' }, // STARTSWITH(c.name, 'John')
email: { contains: 'gmail' }, // CONTAINS(c.email, 'gmail')
bio: { endsWith: 'developer' }, // ENDSWITH(c.bio, 'developer')
// Array operations
tags: { contains: 'premium' }, // CONTAINS array element
// Note: containsAny and containsAll are planned features
// For now, use contains for single checks or raw SQL for complex operations
// Date comparisons
createdAt: { gte: new Date('2024-01-01') },
lastLoginAt: { lt: new Date() }
}
});Pagination
Implement pagination with take and skip:
const ITEMS_PER_PAGE = 20;
// Page 1
const page1 = await db.posts.findMany({
partitionKey: 'user_123',
take: ITEMS_PER_PAGE,
skip: 0
});
// Page 2
const page2 = await db.posts.findMany({
partitionKey: 'user_123',
take: ITEMS_PER_PAGE,
skip: ITEMS_PER_PAGE
});Note: Large offsets can be expensive. For better performance, use cursor-based pagination with date filters.
Ordering Results
Sort results using orderBy:
// Single field ordering
await db.posts.findMany({
partitionKey: 'user_123',
orderBy: {
createdAt: 'desc' // or 'asc'
}
});
// Multiple field ordering
await db.posts.findMany({
partitionKey: 'user_123',
orderBy: [
{ createdAt: 'desc' },
{ title: 'asc' }
]
});Raw SQL Queries
Use when: The query builder doesn't support your use case.
Example: Complex joins, CosmosDB-specific functions.
const result = await db.users.query<{ count: number }>({
sql: `
SELECT COUNT(1) as count
FROM c
WHERE c.isActive = @active
AND c.age > @minAge
`,
parameters: [
{ name: '@active', value: true },
{ name: '@minAge', value: 18 }
],
partitionKey: 'john@example.com' // Optional: for partition-scoped query
});Aggregations with findMany
Use when: You need both data and statistics in a single query.
Benefit: Combines data retrieval with aggregations efficiently.
findMany now supports aggregations, allowing you to fetch documents and calculate statistics (count, sum, avg, min, max) in a single query with full type safety.
Basic Example
const result = await db.orders.findMany({
partitionKey: 'customer-123',
where: { status: 'completed' },
aggregate: {
_count: true,
_sum: { amount: true },
_avg: { amount: true }
}
});
console.log(`Found ${result._count} orders`);
console.log(`Total: $${result._sum.amount}`);
console.log(`Average: $${result._avg.amount}`);
console.log(`Orders:`, result.data);Return Type Changes
Without aggregations:
const orders = await db.orders.findMany({ partitionKey: 'customer-123' });
// Type: Order[]With aggregations:
const result = await db.orders.findMany({
partitionKey: 'customer-123',
aggregate: { _count: true }
});
// Type: { data: Order[], _count: number }Pagination with Total Count
Perfect for building paginated UIs:
const result = await db.users.findMany({
partitionKey: 'tenant-1',
take: 20,
skip: 0,
aggregate: { _count: true } // Counts ALL matching records
});
console.log(`Page 1 of ${Math.ceil(result._count / 20)}`);
console.log(`Showing ${result.data.length} of ${result._count} users`);Available Aggregations
All aggregation operations are supported:
const result = await db.products.findMany({
partitionKey: 'category-electronics',
aggregate: {
_count: true, // Count all records
_sum: { price: true, stock: true }, // Sum numeric fields
_avg: { price: true }, // Average values
_min: { price: true, createdAt: true }, // Minimum values
_max: { price: true, createdAt: true } // Maximum values
}
});See the Aggregations Guide for complete documentation.
Performance Best Practices
1. Always Prefer Point Reads
// ✅ Good: Point read (~1 RU)
const user = await db.users.findUnique({
where: { id: 'user_123', email: 'john@example.com' }
});
// ⚠️ Slower: Query even with unique result (~5 RU)
const user = await db.users.findMany({
partitionKey: 'john@example.com',
where: { id: 'user_123' },
take: 1
});2. Use Partition Keys Efficiently
// ✅ Good: All queries scoped to partition
await db.posts.findMany({
partitionKey: 'user_123',
where: { isPublished: true }
});
// ❌ Bad: Cross-partition query when not needed
await db.posts.findMany({
where: { userId: 'user_123', isPublished: true },
enableCrossPartitionQuery: true
});3. Limit Result Sets
// ✅ Good: Limit to what you need
await db.posts.findMany({
partitionKey: 'user_123',
take: 10 // Only fetch 10 results
});4. Use Select to Reduce Data Transfer
// ✅ Good: Only fetch needed fields
await db.users.findMany({
partitionKey: 'tenant_123',
select: { id: true, name: true }
});Common Query Patterns
Pattern: Getting a Count
const count = await db.posts.query<{ count: number }>({
sql: `
SELECT VALUE COUNT(1)
FROM c
WHERE c.isPublished = true
AND c.publishedAt > @date
`,
parameters: [{ name: '@date', value: new Date('2024-01-01') }],
enableCrossPartitionQuery: true
});Pattern: Search Within Partition
const matchingPosts = await db.posts.findMany({
partitionKey: 'user_123',
where: {
title: { contains: searchTerm },
isPublished: true
}
});Pattern: Conditional Queries
const where: any = { isPublished: true };
if (minDate) {
where.publishedAt = { gte: minDate };
}
if (category) {
where.category = category;
}
const posts = await db.posts.findMany({
partitionKey: 'user_123',
where
});Next Steps
- See Update Operations for modifying documents
- Check Common Patterns for real-world scenarios
- Review Performance Guide for optimization tips
Type System Deep Dive
Understanding how CosmosQL's type inference works and leveraging it effectively
View on separate page →How Type Inference Works
CosmosQL leverages TypeScript's powerful type inference to provide compile-time safety:
const users = container('users', {
id: field.string(),
email: field.string(),
name: field.string().optional(),
age: field.number()
}).partitionKey('email');
// The type is automatically inferred:
type User = typeof users.infer;
// {
// id: string;
// email: string;
// name?: string;
// age: number;
// }
// Query results are typed automatically
const user = await db.users.findUnique({
where: { id: '123', email: 'john@example.com' }
});
// Type: User | null (no manual type annotation needed!)
// Select creates a new type
const partial = await db.users.findUnique({
where: { id: '123', email: 'john@example.com' },
select: { name: true, age: true }
});
// Type: { name?: string; age: number } | nullCustom Type Errors
When you make a mistake, CosmosQL provides helpful error messages:
// Missing partition key
const user = await db.users.findUnique({
where: { id: '123' }
});
// Error message in IDE:
// ❌ PARTITION KEY REQUIRED
//
// This operation requires a partition key to avoid expensive cross-partition queries.
//
// 💡 Fix: Add the partition key to your where clause:
// where: { id: '123', email: 'john@example.com' }
//
// 📖 Learn more about partition keys: https://cosmosql.dev/docs/partition-keysType Guards
Runtime type checking when needed:
import { isCosmosError } from 'cosmosql';
try {
await db.users.create({ data: user });
} catch (error) {
if (isCosmosError(error)) {
// error is now typed as CosmosError
console.log(error.code, error.statusCode, error.retryAfter);
}
}CosmosQL provides flexible and type-safe ways to update documents in CosmosDB.
Navigation:
- Single Update - Modify existing documents
- Upsert - Insert or update
- Atomic Operations - Increment/decrement
- Bulk Operations - Update multiple documents efficiently
Single Update
The basic update operation modifies an existing document:
await db.users.update({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
},
data: {
age: 31,
name: 'John Updated'
}
});Important: The partition key must be included in the where clause. This is enforced at compile time for type safety.
Partial Updates
You can update only the fields you need—other fields remain unchanged:
await db.users.update({
where: { id: 'user_123', email: 'john@example.com' },
data: {
age: 31
// Other fields remain unchanged
}
});This is efficient as CosmosDB only updates the specified fields, not the entire document.
Upsert Operations
The upsert operation inserts if the document doesn't exist, or updates if it does:
await db.users.upsert({
where: {
id: 'user_123',
email: 'john@example.com'
},
create: {
// Full user object for creation
id: 'user_123',
email: 'john@example.com',
name: 'John Doe',
age: 30,
isActive: true,
createdAt: new Date()
},
update: {
// Partial update
age: 31,
name: 'John Updated'
}
});Why Upsert? This pattern is common in distributed systems where you want idempotent operations—operations that can be safely retried without unintended side effects.
Atomic Operations
Perform atomic increment/decrement operations:
await db.posts.update({
where: { id: 'post_1', userId: 'user_123' },
data: {
viewCount: { increment: 1 },
likeCount: { increment: 5 },
dislikeCount: { decrement: 2 }
}
});This is an atomic operation performed server-side, making it safe for concurrent updates.
Update Patterns
Pattern: Touch Pattern (Update Timestamp)
await db.posts.update({
where: { id: 'post_1', userId: 'user_123' },
data: {
title: 'New Title',
updatedAt: new Date()
}
});Pattern: Status Transitions
// Update status with validation
const validTransitions = {
draft: ['published', 'archived'],
published: ['archived'],
archived: [] // terminal state
};
function canTransition(from: string, to: string): boolean {
return validTransitions[from]?.includes(to) ?? false;
}
async function updatePostStatus(postId: string, userId: string, newStatus: string) {
const post = await db.posts.findUnique({
where: { id: postId, userId }
});
if (!post || !canTransition(post.status, newStatus)) {
throw new Error('Invalid status transition');
}
await db.posts.update({
where: { id: postId, userId },
data: {
status: newStatus,
updatedAt: new Date()
}
});
}Pattern: Array Operations
While CosmosQL doesn't provide built-in array manipulation helpers, you can update entire arrays:
// Replace the entire tags array
await db.posts.update({
where: { id: 'post_1', userId: 'user_123' },
data: {
tags: ['javascript', 'react', 'tutorial']
}
});
// For append operations, fetch, modify, and update
const post = await db.posts.findUnique({
where: { id: 'post_1', userId: 'user_123' }
});
await db.posts.update({
where: { id: 'post_1', userId: 'user_123' },
data: {
tags: [...post.tags, 'new-tag']
}
});Bulk Operations
For updating multiple documents at once, use updateMany:
// Static update - update all inactive users
const result = await db.users.updateMany({
where: { isActive: false },
data: { status: 'archived' },
partitionKey: 'user@email.com' // or enableCrossPartitionQuery: true
});
console.log(`Updated ${result.updated} documents`);
console.log(`Failed: ${result.failed}`);
console.log(`RU consumed: ${result.performance.ruConsumed}`);Dynamic update with function:
// Update email domains for all users
const result = await db.users.updateMany({
where: { email: { contains: '@old.com' } },
data: (doc) => ({
email: doc.email.replace('@old.com', '@new.com'),
migratedAt: new Date()
}),
enableCrossPartitionQuery: true,
batchSize: 50,
maxConcurrency: 5,
onProgress: (stats) => {
console.log(`${stats.percentage}% - ${stats.ruConsumed} RU`);
},
onError: (error) => {
console.error(`Failed: ${error.documentId}`, error.error);
}
});Key Options:
where: Query to match documentsdata: Static object or function that returns updatespartitionKeyorenableCrossPartitionQuery: Required (one or the other)batchSize: Documents per batch (default: 50)maxConcurrency: Parallel batches (default: 5)continueOnError: Keep going if some fail (default: false)maxRetries: Retry attempts for retriable errors (default: 3)onProgress: Progress callback with statsonError: Error callback for individual failures
Result includes:
updated: Number of successfully updated documentsfailed: Number of failed updatesskipped: Number of skipped documentserrors: Array of error detailsperformance: RU consumption, duration, and throughput metrics
Best Practices:
- Start with small batches when testing (e.g.,
batchSize: 10) - Use
continueOnError: truefor large operations where some failures are acceptable - Monitor RU consumption using
onProgresscallbacks - Use partition keys when possible (much faster than cross-partition queries)
- Test with dry runs if implementing custom logic
Bulk operations are built into CosmosQL and work seamlessly with your existing containers.
Error Handling
try {
await db.users.update({
where: { id: 'user_123', email: 'john@example.com' },
data: { age: 31 }
});
} catch (error) {
if (error.code === 404) {
// Document not found
console.error('User not found');
} else if (error.code === 429) {
// Rate limit exceeded
console.error('Rate limit exceeded, please retry');
} else if (error.code === 412) {
// Precondition failed (e.g., ETag mismatch)
console.error('Document was modified, please retry');
} else {
console.error('Failed to update user:', error);
}
}Performance Considerations
1. Use Partial Updates
// ✅ Good: Only update what changed
await db.posts.update({
where: { id: 'post_1', userId: 'user_123' },
data: { title: 'New Title' }
});2. Batch Related Updates
// ✅ Good: Single atomic operation
await db.posts.update({
where: { id: 'post_1', userId: 'user_123' },
data: {
viewCount: { increment: 1 },
lastViewedAt: new Date()
}
});Next Steps
- Read Deleting Documents to learn deletion patterns
- See Common Patterns for real-world scenarios
- Review Performance Guide for optimization tips
Aggregations
Complete guide to using aggregation functions in CosmosQL - count, sum, average, min, max, and group by operations with full type safety.
View on separate page →Complete guide to using aggregation functions in CosmosQL - count, sum, average, min, max, and group by operations with full type safety.
Table of Contents
- Overview
- Count Operations
- Aggregate Operations
- findMany with Aggregations
- Group By Operations
- Convenience Methods
- Type Safety
- Real-World Examples
- Performance & Best Practices
Overview
CosmosQL provides a complete set of aggregation functions that are:
- ✅ Fully type-safe - TypeScript infers exact result types
- ✅ SQL-compatible - Translates to efficient Cosmos DB SQL
- ✅ Partition-aware - Optimized for Cosmos DB's partitioning
- ✅ Composable - Combine multiple aggregations in one query
Available Aggregations
| Operation | Description | Works With |
|---|---|---|
_count | Count documents | All fields |
_sum | Sum values | Number fields |
_avg | Average values | Number fields |
_min | Find minimum | All fields |
_max | Find maximum | All fields |
Count Operations
Basic Count
Count documents in a partition:
import { createClient, container, field } from 'cosmosql';
const orderSchema = container('orders', {
id: field.string(),
customerId: field.string(),
amount: field.number(),
status: field.string(),
}).partitionKey('customerId');
const client = createClient({
connectionString: process.env.COSMOS_CONNECTION_STRING!,
database: 'mydb',
});
const db = await client.withContainers({ orders: orderSchema });
// Count orders for a customer
const count = await db.orders.count({
partitionKey: 'customer-123',
});
// Result: 42Count with Filters
Add WHERE conditions to count specific documents:
// Count only completed orders
const completedCount = await db.orders.count({
partitionKey: 'customer-123',
where: {
status: 'completed',
},
});
// Count with complex filters
const recentHighValueCount = await db.orders.count({
partitionKey: 'customer-123',
where: {
status: 'completed',
amount: { gte: 100 },
createdAt: { gte: new Date('2024-01-01') },
},
});Cross-Partition Count
⚠️ Use sparingly - Cross-partition queries are expensive in Cosmos DB.
// Count ALL orders across all customers
const totalOrders = await db.orders.count({
enableCrossPartitionQuery: true,
});
// Cross-partition count with filter
const allCompletedOrders = await db.orders.count({
enableCrossPartitionQuery: true,
where: { status: 'completed' },
});Count Specific Fields
Count non-null values in specific fields:
const result = await db.orders.aggregate({
partitionKey: 'customer-123',
_count: {
select: {
amount: true, // Count orders with amount
discount: true, // Count orders with discount (might be optional)
trackingNumber: true,
},
},
});
// Result type is inferred:
// {
// _count: {
// amount: number;
// discount: number;
// trackingNumber: number;
// }
// }Aggregate Operations
Single Aggregation
// Sum all order amounts for a customer
const result = await db.orders.aggregate({
partitionKey: 'customer-123',
_sum: {
amount: true,
},
});
// Result: { _sum: { amount: 1250.50 | null } }Multiple Aggregations
Combine multiple operations in one query (more efficient than separate queries):
const stats = await db.orders.aggregate({
partitionKey: 'customer-123',
_count: true, // Total count
_sum: { amount: true }, // Total revenue
_avg: { amount: true }, // Average order value
_min: { amount: true }, // Smallest order
_max: { amount: true }, // Largest order
});
// Result type (fully inferred):
// {
// _count: number;
// _sum: { amount: number | null };
// _avg: { amount: number | null };
// _min: { amount: number | null };
// _max: { amount: number | null };
// }
console.log(`
Orders: ${stats._count}
Total Revenue: $${stats._sum.amount}
Average Order: $${stats._avg.amount}
Range: $${stats._min.amount} - $${stats._max.amount}
`);Aggregations with Filters
// Stats for completed orders only
const completedStats = await db.orders.aggregate({
partitionKey: 'customer-123',
where: { status: 'completed' },
_count: true,
_sum: { amount: true },
_avg: { amount: true },
});
// Year-to-date statistics
const ytdStats = await db.orders.aggregate({
partitionKey: 'customer-123',
where: {
createdAt: { gte: new Date('2024-01-01') },
},
_sum: { amount: true, tax: true, shipping: true },
_count: true,
});Multiple Field Aggregations
const productSchema = container('products', {
id: field.string(),
category: field.string(),
price: field.number(),
cost: field.number(),
stock: field.number(),
weight: field.number(),
}).partitionKey('category');
const stats = await db.products.aggregate({
partitionKey: 'electronics',
_sum: {
stock: true, // Total inventory
price: true, // Total value at retail
cost: true, // Total cost basis
},
_avg: {
price: true, // Average price
weight: true, // Average weight
},
_min: {
price: true, // Cheapest product
},
_max: {
price: true, // Most expensive product
},
});
// Calculate profit margin
const totalProfit = stats._sum.price! - stats._sum.cost!;
const margin = (totalProfit / stats._sum.price!) * 100;
console.log(`Profit margin: ${margin.toFixed(2)}%`);findMany with Aggregations
The findMany method supports aggregations, allowing you to fetch data and calculate statistics in a single, efficient query. This combines the power of data retrieval with aggregation operations.
Return Types
Without Aggregations (Backward Compatible):
// No aggregate parameter
const users = await db.users.findMany({ partitionKey: "john@example.com" });
// Type: User[]
const selectedUsers = await db.users.findMany({
partitionKey: "john@example.com",
select: { id: true, name: true }
});
// Type: Array<{ id: string, name: string }>With Aggregations:
// With aggregate parameter
const result = await db.users.findMany({
partitionKey: "john@example.com",
aggregate: { _count: true }
});
// Type: { data: User[], _count: number }Basic Usage
const result = await db.users.findMany({
partitionKey: "john@example.com",
where: { isActive: true },
aggregate: {
_count: true,
_avg: { age: true }
}
});
console.log(`Found ${result._count} active users`);
console.log(`Average age: ${result._avg.age}`);
console.log(`Users:`, result.data);Multiple Aggregations
Combine data retrieval with comprehensive statistics:
const stats = await db.orders.findMany({
partitionKey: "customer-123",
aggregate: {
_count: true,
_sum: { totalAmount: true },
_avg: { totalAmount: true },
_min: { createdAt: true },
_max: { createdAt: true }
}
});
console.log(`Order Statistics:`);
console.log(` Total orders: ${stats._count}`);
console.log(` Total revenue: ${stats._sum.totalAmount}`);
console.log(` Average order: ${stats._avg.totalAmount}`);
console.log(` First order: ${stats._min.createdAt}`);
console.log(` Latest order: ${stats._max.createdAt}`);
console.log(` Orders: ${stats.data.length} items`);With Filtering and Selection
const result = await db.products.findMany({
partitionKey: "category-electronics",
where: {
price: { gte: 100 },
inStock: true
},
select: { id: true, name: true, price: true },
aggregate: {
_count: true,
_avg: { price: true },
_min: { price: true },
_max: { price: true }
}
});
// Only selected fields in data
console.log(result.data); // Array<{ id: string, name: string, price: number }>
// All aggregations available
console.log(`Expensive products: ${result._count}`);
console.log(`Price range: ${result._min.price} - ${result._max.price}`);Paginated Results with Total Count
Perfect for building paginated UIs with accurate page counts:
const result = await db.users.findMany({
partitionKey: "john@example.com",
take: 10, // Get first 10 users
skip: 0,
aggregate: {
_count: true, // Count ALL users matching query
}
});
console.log(`Showing ${result.data.length} of ${result._count} total users`);
// Calculate total pages
const totalPages = Math.ceil(result._count / 10);
console.log(`Page 1 of ${totalPages}`);Cross-Partition Queries
const result = await db.products.findMany({
where: { category: "electronics" },
enableCrossPartitionQuery: true,
aggregate: {
_count: true,
_avg: { price: true }
}
});
console.log(`Electronics across all partitions:`);
console.log(` Total products: ${result._count}`);
console.log(` Average price: ${result._avg.price}`);Behavior & Performance
Parallel Execution:
- Data query and aggregation query execute in parallel
- Both queries use the same
where,partitionKey, andenableCrossPartitionQueryfilters - Results are combined into a single response object
Filtering Rules:
whereclause: Applied to both data and aggregation queriesorderBy,take,skip: Applied only to data query (aggregations count all matching records)select: Applied only to data query (aggregations work on full records)
Comparison with Separate Methods
// Before: Two separate queries
const users = await db.users.findMany({ partitionKey: "john@example.com" });
const stats = await db.users.aggregate({
partitionKey: "john@example.com",
_count: true,
_avg: { age: true }
});
// After: Single efficient query
const result = await db.users.findMany({
partitionKey: "john@example.com",
aggregate: {
_count: true,
_avg: { age: true }
}
});
// result.data contains users
// result._count and result._avg contain aggregationsWhen to Use Each Approach:
Use findMany with aggregate:
- When you need both data and aggregations
- For paginated results with total counts
- When filtering is shared between data and aggregations
Use separate aggregate() method:
- When you only need aggregations (no data)
- For complex group-by operations
- When data and aggregations have different filters
All Aggregation Operations
Count Records:
const result = await db.users.findMany({
partitionKey: "john@example.com",
aggregate: { _count: true }
});
// result._count: number
// Count specific fields (non-null values)
const result = await db.users.findMany({
partitionKey: "john@example.com",
aggregate: {
_count: { select: { name: true, email: true } }
}
});
// result._count: { name: number, email: number }Sum, Average, Min, Max:
const result = await db.orders.findMany({
partitionKey: "customer-123",
aggregate: {
_sum: { amount: true, tax: true },
_avg: { amount: true },
_min: { amount: true, createdAt: true },
_max: { amount: true, createdAt: true }
}
});
// All typed correctly based on field types
// number fields return number | null
// date fields return Date | nullFull TypeScript Support
const result = await db.users.findMany({
partitionKey: "john@example.com",
aggregate: {
_count: true,
_avg: { age: true },
_sum: { score: true }
}
});
// Type: {
// data: User[],
// _count: number,
// _avg: { age: number | null },
// _sum: { score: number | null }
// }Group By Operations
Group By is the most powerful aggregation feature - it splits data into groups and performs aggregations on each group.
Group by Single Field
const salesSchema = container('sales', {
id: field.string(),
storeId: field.string(),
category: field.string(),
amount: field.number(),
quantity: field.number(),
}).partitionKey('storeId');
// Sales by category
const categoryStats = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_count: true,
_sum: { amount: true, quantity: true },
_avg: { amount: true },
});
// Result type (fully inferred):
// Array<{
// category: string;
// _count: number;
// _sum: { amount: number | null; quantity: number | null };
// _avg: { amount: number | null };
// }>
// Use the results
for (const group of categoryStats) {
console.log(`${group.category}:
Sales: $${group._sum.amount}
Units: ${group._sum.quantity}
Avg: $${group._avg.amount}
`);
}Group by Multiple Fields
Use an array to group by multiple fields:
// Sales by store AND category
const detailedStats = await db.sales.groupBy({
by: ['storeId', 'category'] as const, // 'as const' for better type inference
enableCrossPartitionQuery: true,
_count: true,
_sum: { amount: true },
});
// Result type:
// Array<{
// storeId: string;
// category: string;
// _count: number;
// _sum: { amount: number | null };
// }>
// Find best performing store-category combination
const topPerformer = detailedStats.reduce((max, group) =>
(group._sum.amount || 0) > (max._sum.amount || 0) ? group : max
);
console.log(`Top: ${topPerformer.storeId} - ${topPerformer.category}`);Group By with Ordering
Sort results by aggregation values:
// Top categories by revenue
const topCategories = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_count: true,
_sum: { amount: true },
orderBy: {
_sum_amount: 'desc', // Order by aggregated field
},
take: 10, // Top 10
});
// Categories with most transactions
const popularCategories = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_count: true,
orderBy: {
_count: 'desc',
},
take: 5,
});Group By with Pagination
// Paginated category report
const page1 = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_sum: { amount: true },
orderBy: { category: 'asc' },
take: 20,
skip: 0,
});
const page2 = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_sum: { amount: true },
orderBy: { category: 'asc' },
take: 20,
skip: 20,
});Group By with Filters
Combine WHERE clauses with GROUP BY:
const userSchema = container('analytics', {
id: field.string(),
userId: field.string(),
eventType: field.string(),
country: field.string(),
timestamp: field.date(),
duration: field.number(),
}).partitionKey('userId');
// Events by type for a specific user in the last 30 days
const recentEvents = await db.analytics.groupBy({
by: 'eventType',
partitionKey: 'user-123',
where: {
timestamp: { gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) },
},
_count: true,
_avg: { duration: true },
});
// Cross-partition: Events by country for premium users
const countryStats = await db.analytics.groupBy({
by: 'country',
enableCrossPartitionQuery: true,
where: {
eventType: 'purchase',
},
_count: true,
_sum: { duration: true },
});Convenience Methods
For simple single-field aggregations, use convenience methods:
sum()
// Quick sum - cleaner than aggregate()
const totalRevenue = await db.orders.sum('amount', {
partitionKey: 'customer-123',
});
// Returns: number | null
// With filter
const completedRevenue = await db.orders.sum('amount', {
partitionKey: 'customer-123',
where: { status: 'completed' },
});avg()
// Average order value
const avgOrderValue = await db.orders.avg('amount', {
partitionKey: 'customer-123',
});
// Returns: number | null
// Average rating for a product
const avgRating = await db.reviews.avg('rating', {
partitionKey: 'product-456',
where: { verified: true },
});min()
// Find cheapest product
const cheapestPrice = await db.products.min('price', {
partitionKey: 'electronics',
});
// Returns: number | null
// Earliest order date
const firstOrder = await db.orders.min('createdAt', {
partitionKey: 'customer-123',
});
// Returns: Date | null (type inferred from field!)max()
// Most expensive product
const highestPrice = await db.products.max('price', {
partitionKey: 'electronics',
});
// Latest activity
const lastSeen = await db.users.max('lastLoginAt', {
partitionKey: 'tenant-1',
});
// Returns: Date | nullType Safety
CosmosQL provides complete type inference for all aggregation operations.
Automatic Type Inference
const orderSchema = container('orders', {
id: field.string(),
customerId: field.string(),
amount: field.number(),
tax: field.number().optional(),
status: field.string(),
createdAt: field.date(),
}).partitionKey('customerId');
// TypeScript knows exact types
const stats = await db.orders.aggregate({
partitionKey: 'customer-123',
_count: true,
_sum: { amount: true, tax: true },
_min: { createdAt: true },
_max: { createdAt: true },
});
// ✅ All these are correctly typed:
const count: number = stats._count;
const total: number | null = stats._sum.amount;
const taxTotal: number | null = stats._sum.tax;
const firstOrder: Date | null = stats._min.createdAt;
const lastOrder: Date | null = stats._max.createdAt;
// ❌ TypeScript errors:
// stats._sum.status; // Error: 'status' is not a number field
// stats._avg.createdAt; // Error: 'createdAt' is not a number fieldGroup By Type Inference
// Single field grouping
const result1 = await db.orders.groupBy({
by: 'status',
enableCrossPartitionQuery: true,
_count: true,
_sum: { amount: true },
});
// Type: Array<{ status: string; _count: number; _sum: { amount: number | null } }>
// Multiple field grouping
const result2 = await db.orders.groupBy({
by: ['customerId', 'status'] as const,
enableCrossPartitionQuery: true,
_count: true,
});
// Type: Array<{ customerId: string; status: string; _count: number }>
// Access fields with full type safety
for (const group of result1) {
console.log(group.status); // ✅ string
console.log(group._count); // ✅ number
console.log(group._sum.amount); // ✅ number | null
}Optional Fields
const productSchema = container('products', {
id: field.string(),
category: field.string(),
price: field.number(),
salePrice: field.number().optional(), // Optional field
}).partitionKey('category');
// TypeScript knows salePrice might be undefined
const result = await db.products.aggregate({
partitionKey: 'electronics',
_sum: { price: true, salePrice: true }, // ✅ Works with optional fields
_avg: { salePrice: true },
});
// All aggregation results are nullable
const total: number | null = result._sum.price;
const saleTotal: number | null = result._sum.salePrice;Type-Safe Field Selection
// Only number fields can be summed/averaged
await db.orders.aggregate({
partitionKey: 'customer-123',
_sum: {
amount: true, // ✅ number field
// status: true, // ❌ Error: 'status' is not a number
},
_avg: {
amount: true, // ✅ number field
// createdAt: true, // ❌ Error: dates can't be averaged
},
});
// Any field can be used with min/max/count
await db.orders.aggregate({
partitionKey: 'customer-123',
_min: {
amount: true, // ✅ number
status: true, // ✅ string
createdAt: true, // ✅ date
},
});Real-World Examples
E-Commerce Analytics Dashboard
// Schema
const orderSchema = container('orders', {
id: field.string(),
customerId: field.string(),
status: field.string(),
totalAmount: field.number(),
itemCount: field.number(),
discount: field.number().optional(),
createdAt: field.date(),
}).partitionKey('customerId');
// Customer lifetime value
async function getCustomerLTV(customerId: string) {
const stats = await db.orders.aggregate({
partitionKey: customerId,
where: { status: 'completed' },
_count: true,
_sum: { totalAmount: true, itemCount: true, discount: true },
_avg: { totalAmount: true },
_min: { createdAt: true },
_max: { createdAt: true },
});
return {
totalOrders: stats._count,
lifetimeValue: stats._sum.totalAmount || 0,
totalItems: stats._sum.itemCount || 0,
totalSavings: stats._sum.discount || 0,
avgOrderValue: stats._avg.totalAmount || 0,
firstOrderDate: stats._min.createdAt,
lastOrderDate: stats._max.createdAt,
customerSince: stats._min.createdAt
? Math.floor((Date.now() - stats._min.createdAt.getTime()) / (1000 * 60 * 60 * 24))
: 0,
};
}
// Sales by status report
async function getSalesStatusReport() {
const statusReport = await db.orders.groupBy({
by: 'status',
enableCrossPartitionQuery: true,
_count: true,
_sum: { totalAmount: true, itemCount: true },
_avg: { totalAmount: true },
});
return statusReport.map(group => ({
status: group.status,
count: group._count,
revenue: group._sum.totalAmount || 0,
items: group._sum.itemCount || 0,
avgValue: group._avg.totalAmount || 0,
}));
}
// Top customers
async function getTopCustomers(limit: number = 10) {
const customers = await db.orders.groupBy({
by: 'customerId',
enableCrossPartitionQuery: true,
_count: true,
_sum: { totalAmount: true },
orderBy: { _sum_totalAmount: 'desc' },
take: limit,
});
return customers.map(c => ({
customerId: c.customerId,
orders: c._count,
spent: c._sum.totalAmount || 0,
}));
}Analytics Platform
const eventSchema = container('events', {
id: field.string(),
userId: field.string(),
eventType: field.string(),
page: field.string(),
duration: field.number(),
timestamp: field.date(),
}).partitionKey('userId');
// User engagement metrics
async function getUserEngagement(userId: string, days: number = 30) {
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
const metrics = await db.events.aggregate({
partitionKey: userId,
where: { timestamp: { gte: since } },
_count: true,
_sum: { duration: true },
_avg: { duration: true },
});
const eventBreakdown = await db.events.groupBy({
by: 'eventType',
partitionKey: userId,
where: { timestamp: { gte: since } },
_count: true,
_avg: { duration: true },
});
return {
totalEvents: metrics._count,
totalTime: metrics._sum.duration || 0,
avgSessionTime: metrics._avg.duration || 0,
eventTypes: eventBreakdown,
};
}
// Platform-wide statistics
async function getPlatformStats() {
// Events by type
const eventTypes = await db.events.groupBy({
by: 'eventType',
enableCrossPartitionQuery: true,
where: {
timestamp: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
},
_count: true,
_avg: { duration: true },
orderBy: { _count: 'desc' },
});
// Page engagement
const pages = await db.events.groupBy({
by: 'page',
enableCrossPartitionQuery: true,
where: {
timestamp: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
},
_count: true,
_sum: { duration: true },
orderBy: { _count: 'desc' },
take: 10,
});
return { eventTypes, topPages: pages };
}Inventory Management
const productSchema = container('products', {
id: field.string(),
warehouseId: field.string(),
category: field.string(),
stock: field.number(),
reorderPoint: field.number(),
cost: field.number(),
price: field.number(),
}).partitionKey('warehouseId');
// Warehouse inventory summary
async function getWarehouseSummary(warehouseId: string) {
const summary = await db.products.aggregate({
partitionKey: warehouseId,
_count: true,
_sum: { stock: true, cost: true, price: true },
});
const categoryBreakdown = await db.products.groupBy({
by: 'category',
partitionKey: warehouseId,
_count: true,
_sum: { stock: true },
_avg: { price: true },
});
const inventoryValue = (summary._sum.cost || 0) * (summary._sum.stock || 0);
const retailValue = (summary._sum.price || 0) * (summary._sum.stock || 0);
return {
totalProducts: summary._count,
totalStock: summary._sum.stock || 0,
inventoryValue,
retailValue,
potentialProfit: retailValue - inventoryValue,
byCategory: categoryBreakdown,
};
}
// Low stock alerts
async function getLowStockItems(warehouseId: string) {
const items = await db.products.findMany({
partitionKey: warehouseId,
where: {
stock: { lte: field.number() }, // Stock <= reorderPoint
},
});
// Get summary stats
const stats = await db.products.aggregate({
partitionKey: warehouseId,
where: {
stock: { lte: field.number() },
},
_count: true,
_sum: { stock: true },
});
return {
items,
totalLowStockItems: stats._count,
totalLowStockUnits: stats._sum.stock || 0,
};
}Performance & Best Practices
✅ DO: Use Partition Keys
Always provide a partition key when possible:
// ✅ GOOD - Single partition query (fast & cheap)
const count = await db.orders.count({
partitionKey: 'customer-123',
});
// ⚠️ AVOID - Cross-partition query (slow & expensive)
const count = await db.orders.count({
enableCrossPartitionQuery: true,
});✅ DO: Combine Multiple Aggregations
More efficient to get multiple aggregations in one query:
// ✅ GOOD - Single query
const stats = await db.orders.aggregate({
partitionKey: 'customer-123',
_count: true,
_sum: { amount: true },
_avg: { amount: true },
});
// ❌ BAD - Three separate queries
const count = await db.orders.count({ partitionKey: 'customer-123' });
const sum = await db.orders.sum('amount', { partitionKey: 'customer-123' });
const avg = await db.orders.avg('amount', { partitionKey: 'customer-123' });✅ DO: Use WHERE Clauses
Filter data to reduce computation:
// ✅ GOOD - Only aggregate what you need
const completedStats = await db.orders.aggregate({
partitionKey: 'customer-123',
where: { status: 'completed' },
_sum: { amount: true },
});
// ⚠️ AVOID - Aggregating everything then filtering
const allStats = await db.orders.aggregate({
partitionKey: 'customer-123',
_sum: { amount: true },
});
// Then filtering in application code✅ DO: Use Pagination for Large Results
// ✅ GOOD - Paginate large group by results
async function* getAllGroups() {
let skip = 0;
const pageSize = 100;
while (true) {
const page = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_sum: { amount: true },
take: pageSize,
skip,
});
if (page.length === 0) break;
yield* page;
skip += pageSize;
}
}⚠️ AVOID: Unnecessary Cross-Partition Queries
// ❌ BAD - Expensive cross-partition query
const total = await db.orders.sum('amount', {
enableCrossPartitionQuery: true,
});
// ✅ BETTER - Store aggregates separately if needed frequently
// Use a separate container or materialized views⚠️ AVOID: Aggregating Large Date Ranges Without Filters
// ❌ BAD - Scanning entire history
const allTimeStats = await db.orders.aggregate({
enableCrossPartitionQuery: true,
_sum: { amount: true },
});
// ✅ BETTER - Limit to relevant time period
const recentStats = await db.orders.aggregate({
enableCrossPartitionQuery: true,
where: {
createdAt: { gte: new Date('2024-01-01') },
},
_sum: { amount: true },
});Understanding NULL Results
Aggregations return null when there are no rows:
const stats = await db.orders.aggregate({
partitionKey: 'new-customer',
_sum: { amount: true },
_avg: { amount: true },
});
// stats._sum.amount might be null (no orders)
// Always handle null case:
const total = stats._sum.amount ?? 0;
const average = stats._avg.amount ?? 0;
// Or use nullish coalescing in display:
console.log(`Total: $${stats._sum.amount || 0}`);Cosmos DB RU Costs
Aggregation operations consume Request Units (RUs):
- Single Partition: ~2-5 RUs (very efficient)
- Cross-Partition: 5-100+ RUs depending on data size
- Group By: Higher RU cost, especially cross-partition
Monitor your RU consumption and optimize queries accordingly.
Advanced Patterns
Combining Aggregations with Find
// Get both detail and summary
async function getOrderReport(customerId: string) {
const [orders, stats] = await Promise.all([
db.orders.findMany({
partitionKey: customerId,
orderBy: { createdAt: 'desc' },
take: 10,
}),
db.orders.aggregate({
partitionKey: customerId,
_count: true,
_sum: { amount: true },
_avg: { amount: true },
}),
]);
return { recentOrders: orders, summary: stats };
}Calculating Percentages
const statusBreakdown = await db.orders.groupBy({
by: 'status',
enableCrossPartitionQuery: true,
_count: true,
});
const total = statusBreakdown.reduce((sum, g) => sum + g._count, 0);
const withPercentages = statusBreakdown.map(group => ({
status: group.status,
count: group._count,
percentage: ((group._count / total) * 100).toFixed(2) + '%',
}));Time-Series Aggregations
// Daily sales for last 30 days
async function getDailySales(days: number = 30) {
const results = [];
for (let i = 0; i < days; i++) {
const date = new Date();
date.setDate(date.getDate() - i);
const startOfDay = new Date(date.setHours(0, 0, 0, 0));
const endOfDay = new Date(date.setHours(23, 59, 59, 999));
const stats = await db.orders.aggregate({
enableCrossPartitionQuery: true,
where: {
createdAt: { gte: startOfDay, lte: endOfDay },
status: 'completed',
},
_count: true,
_sum: { amount: true },
});
results.push({
date: startOfDay,
orders: stats._count,
revenue: stats._sum.amount || 0,
});
}
return results;
}Error Handling
import { CosmosError } from 'cosmosql';
try {
const stats = await db.orders.aggregate({
// partitionKey missing!
_sum: { amount: true },
});
} catch (error) {
if (error instanceof CosmosError) {
if (error.statusCode === 429) {
// Rate limited - retry with backoff
console.log('Rate limited, retry after:', error.retryAfter);
} else if (error.code === 'PARTITION_KEY_REQUIRED') {
// Missing partition key
console.error('Must provide partition key or enable cross-partition query');
}
}
throw error;
}Migration from Raw Cosmos DB
If you're migrating from raw Cosmos DB SDK:
Before (Raw SDK)
const querySpec = {
query: 'SELECT COUNT(1) as count, SUM(c.amount) as total FROM c WHERE c.customerId = @customerId',
parameters: [{ name: '@customerId', value: 'customer-123' }],
};
const { resources } = await container.items.query(querySpec).fetchAll();
const result = resources[0];After (CosmosQL)
const result = await db.orders.aggregate({
partitionKey: 'customer-123',
_count: true,
_sum: { amount: true },
});
// Result is fully typed!Summary
CosmosQL's aggregation functions provide:
- Type Safety - Full TypeScript inference for all operations
- Clean API - Intuitive, composable operations
- Performance - Optimized queries with partition awareness
- Flexibility - From simple counts to complex group by operations
- Developer Experience - IntelliSense, autocompletion, and compile-time checks
Start with simple count() operations, then explore aggregate() for multiple operations, and finally groupBy() for advanced analytics. Always consider partition keys and cross-partition costs when designing your queries.
For more examples, see the API reference or join our community for support.
Creating Documents
Learn how to create and insert documents into CosmosDB with CosmosQL
View on separate page →Complete guide to inserting documents into CosmosDB.
Navigation:
- Single Document - Basic create operations
- Bulk Operations - Creating multiple documents
- Default Values - Using schema defaults
Single Document
Use when: Creating one document at a time.
Example scenario: User registration.
const newUser = await db.users.create({
data: {
id: 'user_123',
email: 'john@example.com',
name: 'John Doe',
age: 30,
createdAt: new Date()
// Optional fields with defaults are automatically applied
}
});
// Returns: Fully typed User objectWhat happens:
- CosmosQL generates a POST request to CosmosDB REST API
- Document is stored in the partition determined by
email - Return value is fully typed based on your schema
Cost: ~5 RU per document
Default Values
Use when: Your schema defines defaults for optional fields.
const users = container('users', {
id: field.string(),
email: field.string(),
isActive: field.boolean().default(true), // Default value
createdAt: field.date()
}).partitionKey('email');
// No need to provide isActive
const user = await db.users.create({
data: {
id: 'user_123',
email: 'john@example.com',
createdAt: new Date()
// isActive automatically true
}
});Bulk Operations
Use when: Creating multiple documents in one request.
Requirement: All documents MUST share the same partition key.
Why: CosmosDB batch operations only work within a single partition.
Example scenario: Importing user posts.
await db.posts.createMany({
data: [
{ id: 'post_1', userId: 'user_123', title: 'First', content: '...' },
{ id: 'post_2', userId: 'user_123', title: 'Second', content: '...' },
{ id: 'post_3', userId: 'user_123', title: 'Third', content: '...' }
],
partitionKey: 'user_123' // Must be same for all documents
});Cost: ~5 RU per document (same as single creates)
Performance: Single HTTP request vs multiple
Error handling: If any document fails, none are created (atomic within partition).
Best Practices
1. Always Provide IDs Explicitly
// ✅ Good: Explicit ID
const user = await db.users.create({
data: {
id: `user_${Date.now()}`, // Predictable
email: 'user@example.com',
// ...
}
});2. Use Meaningful IDs
// ✅ Good: Human-readable
'user_john@example.com'
'order_2024-01-15-abc123'
// ❌ Bad: Random UUIDs (unless you need them)
'a3f5b8c2-d4e6-7890-abcd-ef1234567890'3. Set Timestamps Explicitly
const user = await db.users.create({
data: {
id: 'user_123',
email: 'john@example.com',
createdAt: new Date(), // Server time
updatedAt: new Date()
}
});Error Handling
try {
const user = await db.users.create({
data: {
id: 'user_123',
email: 'john@example.com',
// ...
}
});
} catch (error) {
if (error.code === 409) {
// Document with this ID already exists
console.error('User already exists');
} else if (error.code === 429) {
// Rate limit exceeded
console.error('Rate limit exceeded, please retry');
} else {
console.error('Failed to create user:', error);
}
}Common Patterns
Pattern: Multi-Tenant Applications
// Partition by tenant ID
const posts = container('posts', {
id: field.string(),
tenantId: field.string(),
title: field.string(),
// ...
}).partitionKey('tenantId');
// Create posts scoped to a tenant
await db.posts.createMany({
data: [
{ id: 'post_1', tenantId: 'tenant_abc', title: '...' },
{ id: 'post_2', tenantId: 'tenant_abc', title: '...' }
],
partitionKey: 'tenant_abc'
});Pattern: Hierarchical Data
// Create parent-child relationships
const category = await db.categories.create({
data: {
id: 'cat_tech',
name: 'Technology',
parentId: null,
createdAt: new Date()
}
});
const subcategory = await db.categories.create({
data: {
id: 'cat_tech_ai',
name: 'Artificial Intelligence',
parentId: 'cat_tech',
createdAt: new Date()
}
});Next Steps
- Read Reading Documents to learn efficient query patterns
- See Common Patterns for real-world scenarios
- Review Update Operations for modifying documents
CRUD Operations
Overview of Create, Read, Update, and Delete operations in CosmosQL
View on separate page →CosmosQL provides a complete set of CRUD (Create, Read, Update, Delete) operations with full type safety. This page provides an overview of all operations.
Create Operations
- Creating Documents - Create single and multiple documents
create()- Create a single documentcreateMany()- Create multiple documents in one operation
Read Operations
- Reading Documents - Query and retrieve documents
findUnique()- Get a single document by ID + partition key (fastest: 1 RU)findMany()- Query multiple documents with filtering, sorting, and paginationquery()- Execute raw SQL queries for advanced use cases
Update Operations
- Updating Documents - Modify existing documents
update()- Update a single documentupdateMany()- Update multiple documents matching criteriaupsert()- Update if exists, create if doesn't
Delete Operations
- Deleting Documents - Remove documents
delete()- Delete a single documentdeleteMany()- Delete multiple documents matching criteria
Common Patterns
All operations follow consistent patterns:
Partition Key Requirement
Most operations require a partition key to ensure efficient queries:
// ✅ Good: Partition-scoped operation
const user = await db.users.findUnique({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
}
});
// ⚠️ Expensive: Cross-partition (must explicitly opt-in)
const users = await db.users.findMany({
enableCrossPartitionQuery: true,
where: { isActive: true }
});Type Safety
All operations are fully typed:
// TypeScript knows the exact return type
const user = await db.users.create({
data: {
id: 'user_123',
email: 'john@example.com',
name: 'John Doe',
age: 30
}
});
// user is typed as: { id: string; email: string; name: string; age: number; ... }Error Handling
All operations can throw CosmosError:
import { isCosmosError } from 'cosmosql';
try {
await db.users.create({ data: user });
} catch (error) {
if (isCosmosError(error)) {
// Handle CosmosDB-specific errors
if (error.statusCode === 409) {
// Duplicate ID
}
}
}See Error Handling for complete error handling guide.
Next Steps
- Learn about Creating Documents
- Learn about Reading Documents
- Learn about Updating Documents
- Learn about Deleting Documents
Database Management
Comprehensive tools for managing and monitoring your CosmosDB resources
View on separate page →Manage and monitor your CosmosDB database with built-in tools for health checks, schema validation, and container management.
Navigation:
- Database Information - Get database details
- Health Checks - Monitor database health
- Schema Diff - Compare schema with database
- Container Pruning - Remove orphaned containers
- Delete Containers - Delete specific containers
Database Information
Get detailed information about your database:
const info = await db.management.getDatabaseInfo();
console.log(`Database: ${info.id}`);
console.log(`Containers: ${info.containersCount}`);
console.log(`Total documents: ${info.storage.totalDocuments}`);
console.log(`Storage: ${info.storage.totalSizeGB} GB`);
// Container details
info.containers.forEach(c => {
console.log(`\n${c.id}:`);
console.log(` Documents: ${c.statistics.documentCount}`);
console.log(` Partition Key: ${c.partitionKey.paths[0]}`);
console.log(` Registered: ${c.schema?.registered}`);
});Health Checks
Run health checks on your database:
const health = await db.management.healthCheck();
console.log(`Overall Health: ${health.overallHealth}`); // 'healthy', 'warning', or 'critical'
health.containers.forEach(c => {
if (!c.healthy) {
console.log(`⚠️ ${c.container}:`);
c.issues.forEach(issue => {
console.log(` [${issue.severity}] ${issue.message}`);
if (issue.recommendation) {
console.log(` 💡 ${issue.recommendation}`);
}
});
}
});
// Recommendations
health.recommendations.forEach(r => console.log(`💡 ${r}`));Issue Types:
missing_index: Container has very few indexed pathslarge_documents: Average document size is highorphaned: Container not registered in schema
Schema Diff
Compare your schema with the actual database:
const diff = await db.management.diffSchema();
if (diff.requiresAction) {
console.log('⚠️ Schema drift detected!');
// Containers in database but not schema
if (diff.containers.orphaned.length > 0) {
console.log(`Orphaned: ${diff.containers.orphaned.join(', ')}`);
}
// Containers in schema but not database
if (diff.containers.missing.length > 0) {
console.log(`Missing: ${diff.containers.missing.join(', ')}`);
}
// Configuration differences
diff.containers.modified.forEach(mod => {
console.log(`${mod.container} has differences:`);
console.log(JSON.stringify(mod.differences, null, 2));
});
} else {
console.log('✅ Schema is in sync');
}Container Pruning
Remove containers that aren't in your schema:
// List orphaned containers
const orphaned = await db.management.listOrphanedContainers();
console.log(`Found ${orphaned.length} orphaned containers`);
// Preview what would be deleted
const preview = await db.management.pruneContainers({
confirm: false,
dryRun: true
});
console.log(`Would delete: ${preview.pruned.join(', ')}`);
// Actually delete
const result = await db.management.pruneContainers({
confirm: true,
exclude: ['keep-this-one'] // Optional: containers to keep
});
console.log(`Deleted: ${result.pruned.join(', ')}`);Delete Containers
Delete containers by name:
const result = await db.management.deleteContainers(
['temp-container', 'old-data'],
{ confirm: true }
);
console.log(`Deleted: ${result.deleted.join(', ')}`);
console.log(`Failed: ${result.failed.length}`);Best Practices
- Run health checks regularly to catch issues early
- Use schema diff to detect drift between code and database
- Backup before pruning containers in production
- Monitor orphaned containers - they still consume resources
- Review recommendations from health checks
Performance Tips
- Database info queries are cached where possible
- Health checks run queries to gather statistics
- Schema diff is lightweight (metadata only)
Troubleshooting
"Must set confirm: true"
Cause: Safety check for destructive operations
Solution: Add confirm: true to the options
Next Steps
- Learn about Migrations for schema evolution
- See Common Patterns for real-world scenarios
- Review Performance Guide for optimization tips
Deleting Documents
Learn how to delete documents and implement soft delete patterns
View on separate page →Deleting documents requires careful consideration. Use hard deletes for performance, soft deletes for recovery.
Navigation:
- Hard Delete - Permanent removal
- Soft Delete - Recoverable deletion
- Bulk Operations - Delete multiple documents efficiently
Hard Delete
The basic delete operation removes a document permanently:
await db.users.delete({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
}
});Important: Deletion is permanent and cannot be undone. Always ensure you have the partition key in the where clause—this is enforced at compile time.
Cost: ~5 RU
Soft Delete
Instead of permanently deleting documents, mark them as deleted:
// Instead of deleting, mark as inactive
await db.users.update({
where: { id: 'user_123', email: 'john@example.com' },
data: {
isActive: false,
deletedAt: new Date()
}
});
// Query only active users
const activeUsers = await db.users.findMany({
partitionKey: 'tenant_123',
where: {
isActive: true
}
});Why Soft Delete?
- Data Recovery: Can restore deleted data
- Audit Trail: Keep history of what was deleted and when
- Referential Integrity: Other documents can still reference deleted items
- Analytics: Analyze deleted data patterns
Implementing Soft Delete
Complete implementation:
// Define schema with soft delete fields
const users = container('users', {
id: field.string(),
email: field.string(),
name: field.string(),
isDeleted: field.boolean().default(false),
deletedAt: field.date().optional(),
deletedBy: field.string().optional()
}).partitionKey('email');
// Helper functions
async function softDeleteUser(id: string, email: string, deletedBy: string) {
return await db.users.update({
where: { id, email },
data: {
isDeleted: true,
deletedAt: new Date(),
deletedBy
}
});
}
async function restoreUser(id: string, email: string) {
return await db.users.update({
where: { id, email },
data: {
isDeleted: false,
deletedAt: null,
deletedBy: null
}
});
}
// Query excluding soft-deleted items
async function findAllUsers(partitionKey: string) {
return await db.users.findMany({
partitionKey,
where: {
isDeleted: false
}
});
}Cleanup Old Soft Deletes
Automatically clean up old soft-deleted items:
async function cleanupOldSoftDeletes() {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const deletedUsers = await db.users.findMany({
enableCrossPartitionQuery: true,
where: {
isDeleted: true,
deletedAt: { lt: thirtyDaysAgo }
}
});
// Permanently delete old soft-deleted items
for (const user of deletedUsers) {
await db.users.delete({
where: { id: user.id, email: user.email }
});
}
}Cascade Delete Pattern
When deleting a parent document, you may need to delete related documents:
async function deleteUserWithRelatedData(userId: string, email: string) {
// Find all related posts
const posts = await db.posts.findMany({
partitionKey: userId
});
// Delete all posts
for (const post of posts) {
await db.posts.delete({
where: { id: post.id, userId }
});
}
// Finally, delete the user
await db.users.delete({
where: { id: userId, email }
});
}Note: CosmosDB doesn't support foreign key constraints or cascade deletes at the database level. You must handle this logic in your application code.
Bulk Operations
For deleting multiple documents at once, use deleteMany:
// Delete old posts
const result = await db.posts.deleteMany({
where: { createdAt: { lt: oneYearAgo } },
confirm: true, // Safety: must explicitly confirm
partitionKey: 'user123',
onProgress: (stats) => {
console.log(`Deleted ${stats.updated}/${stats.total}`);
}
});
console.log(`Deleted ${result.deleted} documents`);
console.log(`Failed: ${result.failed}`);
console.log(`RU consumed: ${result.performance.ruConsumed}`);Key Options:
where: Query to match documentsconfirm: Must betrueto execute (safety requirement)partitionKeyorenableCrossPartitionQuery: Required (one or the other)batchSize: Documents per batch (default: 50)maxConcurrency: Parallel batches (default: 5)continueOnError: Keep going if some fail (default: false)onProgress: Progress callback with stats
Result includes:
deleted: Number of successfully deleted documentsfailed: Number of failed deletionserrors: Array of error detailsperformance: RU consumption, duration, and throughput metrics
Best Practices:
- Always use
confirm: true- This prevents accidental deletions - Start with small batches when testing (e.g.,
batchSize: 10) - Use
continueOnError: truefor large operations where some failures are acceptable - Monitor progress using
onProgresscallbacks - Use partition keys when possible (much faster than cross-partition queries)
Note: For deleting documents by specific IDs in the same partition, you can still use individual deletes:
async function deleteManyPosts(postIds: string[], userId: string) {
const deletePromises = postIds.map(id =>
db.posts.delete({
where: { id, userId }
})
);
await Promise.all(deletePromises);
}However, deleteMany is more efficient for query-based deletions with built-in progress tracking and error handling.
Bulk operations are built into CosmosQL and work seamlessly with your existing containers.
Delete by Query
For query-based deletions, use deleteMany instead of manually querying and deleting:
// ✅ Recommended: Use deleteMany
const result = await db.posts.deleteMany({
where: {
createdAt: { lt: cutoffDate },
isPublished: false
},
confirm: true,
enableCrossPartitionQuery: true,
onProgress: (stats) => {
console.log(`Progress: ${stats.percentage}%`);
}
});This is more efficient than querying and deleting individually, as it includes built-in progress tracking, error handling, and retry logic.
Error Handling
try {
await db.users.delete({
where: { id: 'user_123', email: 'john@example.com' }
});
} catch (error) {
if (error.code === 404) {
// Document not found
console.error('User not found');
} else if (error.code === 429) {
// Rate limit exceeded
console.error('Rate limit exceeded, please retry');
} else {
console.error('Failed to delete user:', error);
}
}Performance Considerations
1. Always Include Partition Key
// ✅ Good: Includes partition key (fast, ~5 RU)
await db.users.delete({
where: { id: 'user_123', email: 'john@example.com' }
});2. Use Soft Delete for Frequent Deletions
Hard deletes are expensive if done repeatedly. Soft deletes allow you to batch hard deletes later:
// Mark as deleted immediately (fast)
await db.users.update({
where: { id: 'user_123', email: 'john@example.com' },
data: { isDeleted: true, deletedAt: new Date() }
});
// Batch permanent deletion later (background job)
setInterval(async () => {
await cleanupOldSoftDeletes();
}, 60 * 60 * 1000); // Run every hourCommon Patterns
Pattern: Recycle Bin
async function moveToRecycleBin(postId: string, userId: string) {
// Copy to recycle bin
const post = await db.posts.findUnique({
where: { id: postId, userId }
});
await db.recycleBin.create({
data: {
id: `bin_${postId}`,
deletedItemId: postId,
deletedItemType: 'post',
deletedItemData: post,
deletedAt: new Date()
}
});
// Remove from original location
await db.posts.delete({
where: { id: postId, userId }
});
}
async function restoreFromRecycleBin(itemId: string) {
const binItem = await db.recycleBin.findUnique({
where: { id: itemId }
});
if (binItem.deletedItemType === 'post') {
await db.posts.create({
data: binItem.deletedItemData
});
}
await db.recycleBin.delete({ where: { id: itemId } });
}Next Steps
- Read Update Operations for modifying documents
- See Common Patterns for real-world scenarios
- Review Performance Guide for optimization tips
Error Handling
A comprehensive guide to handling CosmosError and common issues
View on separate page →CosmosQL provides comprehensive error handling through the CosmosError class:
import { CosmosError, isCosmosError } from 'cosmosql';
try {
await db.users.create({ data: user });
} catch (error) {
if (isCosmosError(error)) {
// Type-safe error handling
switch (error.statusCode) {
case 400: // Bad Request
console.error('Invalid request:', error.message);
break;
case 401: // Unauthorized
console.error('Authentication failed - check your connection string');
break;
case 403: // Forbidden
console.error('Access denied - check permissions');
break;
case 404: // Not Found
console.error('Container or database not found');
break;
case 409: // Conflict (duplicate ID)
console.error('Document already exists with this ID');
break;
case 412: // Precondition Failed (ETag mismatch)
console.error('Document was modified, please retry');
break;
case 429: // Too Many Requests (rate limit)
console.error('Rate limited, retry after:', error.retryAfter, 'seconds');
// CosmosError includes retryAfter property
break;
case 500: // Internal Server Error
console.error('CosmosDB service error:', error.message);
break;
default:
console.error('CosmosDB error:', error.code, error.message);
}
} else {
// Not a CosmosDB error, rethrow
throw error;
}
}CosmosError Class
class CosmosError extends Error {
statusCode: number; // HTTP status code (400, 404, 429, etc.)
code: string; // CosmosDB error code
message: string; // Error message
retryAfter?: number; // Seconds to wait before retry (for 429 errors)
}Common Error Codes
| Code | Status | Description | Common Causes |
|---|---|---|---|
BadRequest | 400 | Invalid request | Malformed query syntax, invalid parameters |
Unauthorized | 401 | Authentication failed | Invalid connection string or key |
Forbidden | 403 | Access denied | Insufficient permissions |
NotFound | 404 | Resource not found | Container/database doesn't exist, document not found |
Conflict | 409 | Duplicate resource | Document with same ID already exists |
PreconditionFailed | 412 | ETag mismatch | Document was modified (optimistic concurrency) |
TooManyRequests | 429 | Rate limited | RU/s limit exceeded, automatic retry available |
InternalServerError | 500 | Service error | CosmosDB service issue |
CROSS_PARTITION_QUERY_ERROR | - | Cross-partition query error | Empty container cross-partition query |
Common Error Scenarios
- 404: Document not found (returns
nullforfindUnique/findMany, throws forupdate/delete) - 429: Rate limited (automatic retry with backoff if configured via
retryOptions) - 401/403: Authentication/authorization failures (check connection string and keys)
- 400: Bad request (validation errors, invalid query syntax)
- 409: Conflict (duplicate ID on create - indicates business logic issue)
- 412: Precondition failed (ETag mismatch on updates - document was modified)
- CROSS_PARTITION_QUERY_ERROR: Cross-partition queries on empty containers (CosmosDB limitation)
Auto-Retry on 429
CosmosQL automatically retries rate-limited requests with exponential backoff:
const db = await createClient({
connectionString: '...',
database: 'myapp',
mode: 'verify', // Production: fail-fast on misconfiguration
retryOptions: {
maxRetries: 3, // Maximum retry attempts
initialDelay: 100, // Initial delay in ms
maxDelay: 5000 // Maximum delay in ms
}
}).withContainers({ users });
// Automatically retries with exponential backoff on 429 errors
// Uses retryAfter from CosmosDB response when availableError Handling Best Practices
- Always check error types - Use
isCosmosError()for type-safe error handling - Handle rate limits gracefully - Use built-in retry options or implement custom retry logic
- Log error details - Include
statusCode,code, andmessagein error logs for debugging - Don't ignore conflicts - 409 errors indicate business logic issues (duplicate IDs)
- Handle ETag mismatches - 412 errors mean document was modified; refetch and retry
- Check for cross-partition query errors - Ensure containers have data before cross-partition queries
Example: Registration with Error Handling
async function register(email: string, password: string) {
const passwordHash = await bcrypt.hash(password, 10);
try {
const user = await db.users.create({
data: {
id: email,
email: email,
passwordHash,
profile: { name: email.split('@')[0] },
createdAt: new Date()
}
});
return { success: true, user };
} catch (error) {
if (isCosmosError(error) && error.statusCode === 409) {
// Conflict = duplicate email
return { success: false, error: 'Email already exists' };
}
throw error;
}
}Performance Best Practices
1. Always Use Partition Keys When Possible
The Cost Difference
// ❌ BAD: Cross-partition query (expensive)
const user = await db.users.findMany({
enableCrossPartitionQuery: true,
where: { age: { gt: 18 } }
});
// Cost: ~50-100 RU
// ✅ GOOD: Partition-scoped query
const user = await db.users.findMany({
partitionKey: 'john@example.com',
where: { age: { gt: 18 } }
});
// Cost: ~5 RU (10-20x cheaper)2. Use Point Reads for Single Documents
Performance Tip
// ❌ OK: Query for single document
const user = await db.users.findMany({
partitionKey: 'john@example.com',
where: { id: 'user_123' },
take: 1
});
// Cost: ~3-5 RU
// ✅ BEST: Point read (direct lookup)
const user = await db.users.findUnique({
where: { id: 'user_123', email: 'john@example.com' }
});
// Cost: ~1 RU (5x cheaper)3. Select Only Needed Fields
// ❌ BAD: Fetch entire document
const user = await db.users.findUnique({
where: { id: 'user_123', email: 'john@example.com' }
});
// Returns all fields
// ✅ GOOD: Fetch only what you need
const user = await db.users.findUnique({
where: { id: 'user_123', email: 'john@example.com' },
select: { name: true, email: true }
});
// Smaller payload = faster transfer = lower latency4. Batch Operations in Same Partition
Batch Efficiency
// ❌ BAD: Individual creates (multiple requests)
for (const post of posts) {
await db.posts.create({ data: post });
}
// 10 posts = 10 HTTP requests
// ✅ GOOD: Batch create (single request)
await db.posts.createMany({
data: posts,
partitionKey: 'user_123'
});
// 10 posts = 1 HTTP request (10x faster)5. Use Appropriate Indexes
CosmosDB automatically indexes all properties by default, but you can optimize:
const users = container('users', {
id: field.string(),
email: field.string(),
name: field.string(),
metadata: field.object({ /* large nested object */ })
}).partitionKey('email')
.indexingPolicy({
automatic: true,
includedPaths: [
'/id/?',
'/email/?',
'/name/?'
],
excludedPaths: [
'/metadata/*' // Don't index large nested objects
]
});Reading Documents
Learn how to query and read documents from CosmosDB efficiently
View on separate page →Reading data is the most common operation. CosmosQL provides three methods optimized for different scenarios.
Navigation:
- Point Read - By ID and partition key
- Query Within Partition - Filtering and sorting
- Cross-Partition Query - Admin and analytics
- Aggregations with findMany - Data + statistics in one query
Point Read (Fastest)
Use when: You know the exact ID and partition key.
Why it's fastest: Direct lookup, no scanning required.
Cost: ~1 RU (cheapest possible)
Example scenario: Loading a user's profile.
const user = await db.users.findUnique({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
}
});
// Returns: User | nullType safety: TypeScript enforces both ID and partition key.
// ❌ This won't compile
const user = await db.users.findUnique({
where: { id: 'user_123' }
// Error: Property 'email' is missing
});Query Within Partition
Use when: Filtering, sorting, or paginating within one partition.
Cost: ~3-5 RU (depends on result size)
Example scenario: Getting a user's posts.
const posts = await db.posts.findMany({
partitionKey: 'user_123', // Required
where: {
isPublished: true,
createdAt: { gte: new Date('2024-01-01') }
},
orderBy: { createdAt: 'desc' },
take: 10
});
// Returns: Post[]findMany with Aggregations
findMany can also return aggregations alongside your data - perfect for paginated lists with counts or statistics.
Example: Pagination with total count
const result = await db.posts.findMany({
partitionKey: 'user_123',
where: { isPublished: true },
orderBy: { createdAt: 'desc' },
take: 10,
skip: 0,
aggregate: {
_count: true // Count ALL matching posts
}
});
console.log(`Showing ${result.data.length} of ${result._count} posts`);
console.log(`Page 1 of ${Math.ceil(result._count / 10)}`);
// Returns: { data: Post[], _count: number }Example: Data with statistics
const result = await db.orders.findMany({
partitionKey: 'customer-123',
where: { status: 'completed' },
take: 5, // Last 5 orders
orderBy: { createdAt: 'desc' },
aggregate: {
_count: true,
_sum: { amount: true },
_avg: { amount: true },
_min: { createdAt: true },
_max: { createdAt: true }
}
});
// Access both data and stats
console.log(`Recent orders:`, result.data);
console.log(`Total completed orders: ${result._count}`);
console.log(`Total revenue: $${result._sum.amount}`);
console.log(`Average order value: $${result._avg.amount}`);
console.log(`Customer since: ${result._min.createdAt}`);
// Returns: {
// data: Order[],
// _count: number,
// _sum: { amount: number | null },
// _avg: { amount: number | null },
// _min: { createdAt: Date | null },
// _max: { createdAt: Date | null }
// }How it works:
- Data query and aggregation execute in parallel
- Both use the same
wherefilter - Pagination (
take/skip) only applies todata, not aggregations - Full type safety: TypeScript infers the complete return type
See full aggregations documentation below for more examples.
Cross-Partition Query
Use when: You need data across multiple partitions.
Cost: ~50-100+ RU (10-100x more expensive)
Requires: Explicit opt-in via enableCrossPartitionQuery: true
Example scenario: Global search across all users.
const recentUsers = await db.users.findMany({
enableCrossPartitionQuery: true, // Must be explicit
where: {
createdAt: { gte: new Date('2024-01-01') }
},
orderBy: { createdAt: 'desc' },
take: 20
});When to use: Admin dashboards, analytics, reports—not user-facing queries.
Selecting Specific Fields
Use when: You don't need the full document.
Why: Reduces data transfer, faster queries, lower latency.
const user = await db.users.findUnique({
where: { id: 'user_123', email: 'john@example.com' },
select: {
name: true,
email: true
}
});
// Returns: { name: string; email: string } | nullNested fields: Select specific fields from nested objects.
const post = await db.posts.findUnique({
where: { id: 'post_1', userId: 'user_123' },
select: {
title: true,
metadata: {
source: true
}
}
});
// Returns: { title: string; metadata?: { source: string } } | nullFiltering with Where Clauses
Comprehensive filtering options:
await db.users.findMany({
partitionKey: 'john@example.com',
where: {
// Exact match
isActive: true,
// Number comparisons
age: { gt: 21 }, // Greater than
score: { gte: 85 }, // Greater than or equal
rating: { lt: 5 }, // Less than
count: { lte: 100 }, // Less than or equal
age: { gte: 18, lte: 65 }, // Range
// String operations
name: { startsWith: 'John' }, // STARTSWITH(c.name, 'John')
email: { contains: 'gmail' }, // CONTAINS(c.email, 'gmail')
bio: { endsWith: 'developer' }, // ENDSWITH(c.bio, 'developer')
// Array operations
tags: { contains: 'premium' }, // CONTAINS array element
// Note: containsAny and containsAll are planned features
// For now, use contains for single checks or raw SQL for complex operations
// Date comparisons
createdAt: { gte: new Date('2024-01-01') },
lastLoginAt: { lt: new Date() }
}
});Pagination
Implement pagination with take and skip:
const ITEMS_PER_PAGE = 20;
// Page 1
const page1 = await db.posts.findMany({
partitionKey: 'user_123',
take: ITEMS_PER_PAGE,
skip: 0
});
// Page 2
const page2 = await db.posts.findMany({
partitionKey: 'user_123',
take: ITEMS_PER_PAGE,
skip: ITEMS_PER_PAGE
});Note: Large offsets can be expensive. For better performance, use cursor-based pagination with date filters.
Ordering Results
Sort results using orderBy:
// Single field ordering
await db.posts.findMany({
partitionKey: 'user_123',
orderBy: {
createdAt: 'desc' // or 'asc'
}
});
// Multiple field ordering
await db.posts.findMany({
partitionKey: 'user_123',
orderBy: [
{ createdAt: 'desc' },
{ title: 'asc' }
]
});Raw SQL Queries
Use when: The query builder doesn't support your use case.
Example: Complex joins, CosmosDB-specific functions.
const result = await db.users.query<{ count: number }>({
sql: `
SELECT COUNT(1) as count
FROM c
WHERE c.isActive = @active
AND c.age > @minAge
`,
parameters: [
{ name: '@active', value: true },
{ name: '@minAge', value: 18 }
],
partitionKey: 'john@example.com' // Optional: for partition-scoped query
});Aggregations with findMany
Use when: You need both data and statistics in a single query.
Benefit: Combines data retrieval with aggregations efficiently.
findMany now supports aggregations, allowing you to fetch documents and calculate statistics (count, sum, avg, min, max) in a single query with full type safety.
Basic Example
const result = await db.orders.findMany({
partitionKey: 'customer-123',
where: { status: 'completed' },
aggregate: {
_count: true,
_sum: { amount: true },
_avg: { amount: true }
}
});
console.log(`Found ${result._count} orders`);
console.log(`Total: $${result._sum.amount}`);
console.log(`Average: $${result._avg.amount}`);
console.log(`Orders:`, result.data);Return Type Changes
Without aggregations:
const orders = await db.orders.findMany({ partitionKey: 'customer-123' });
// Type: Order[]With aggregations:
const result = await db.orders.findMany({
partitionKey: 'customer-123',
aggregate: { _count: true }
});
// Type: { data: Order[], _count: number }Pagination with Total Count
Perfect for building paginated UIs:
const result = await db.users.findMany({
partitionKey: 'tenant-1',
take: 20,
skip: 0,
aggregate: { _count: true } // Counts ALL matching records
});
console.log(`Page 1 of ${Math.ceil(result._count / 20)}`);
console.log(`Showing ${result.data.length} of ${result._count} users`);Available Aggregations
All aggregation operations are supported:
const result = await db.products.findMany({
partitionKey: 'category-electronics',
aggregate: {
_count: true, // Count all records
_sum: { price: true, stock: true }, // Sum numeric fields
_avg: { price: true }, // Average values
_min: { price: true, createdAt: true }, // Minimum values
_max: { price: true, createdAt: true } // Maximum values
}
});See the Aggregations Guide for complete documentation.
Performance Best Practices
1. Always Prefer Point Reads
// ✅ Good: Point read (~1 RU)
const user = await db.users.findUnique({
where: { id: 'user_123', email: 'john@example.com' }
});
// ⚠️ Slower: Query even with unique result (~5 RU)
const user = await db.users.findMany({
partitionKey: 'john@example.com',
where: { id: 'user_123' },
take: 1
});2. Use Partition Keys Efficiently
// ✅ Good: All queries scoped to partition
await db.posts.findMany({
partitionKey: 'user_123',
where: { isPublished: true }
});
// ❌ Bad: Cross-partition query when not needed
await db.posts.findMany({
where: { userId: 'user_123', isPublished: true },
enableCrossPartitionQuery: true
});3. Limit Result Sets
// ✅ Good: Limit to what you need
await db.posts.findMany({
partitionKey: 'user_123',
take: 10 // Only fetch 10 results
});4. Use Select to Reduce Data Transfer
// ✅ Good: Only fetch needed fields
await db.users.findMany({
partitionKey: 'tenant_123',
select: { id: true, name: true }
});Common Query Patterns
Pattern: Getting a Count
const count = await db.posts.query<{ count: number }>({
sql: `
SELECT VALUE COUNT(1)
FROM c
WHERE c.isPublished = true
AND c.publishedAt > @date
`,
parameters: [{ name: '@date', value: new Date('2024-01-01') }],
enableCrossPartitionQuery: true
});Pattern: Search Within Partition
const matchingPosts = await db.posts.findMany({
partitionKey: 'user_123',
where: {
title: { contains: searchTerm },
isPublished: true
}
});Pattern: Conditional Queries
const where: any = { isPublished: true };
if (minDate) {
where.publishedAt = { gte: minDate };
}
if (category) {
where.category = category;
}
const posts = await db.posts.findMany({
partitionKey: 'user_123',
where
});Next Steps
- See Update Operations for modifying documents
- Check Common Patterns for real-world scenarios
- Review Performance Guide for optimization tips
Migrations provide a structured way to evolve your database schema and data over time. Define migrations, track their status, apply them safely, and rollback when needed.
Navigation:
- Defining Migrations - Create migration files
- Registering Migrations - Register with your client
- Migration Status - Check applied migrations
- Planning Migrations - Preview changes
- Applying Migrations - Run migrations
- Rolling Back - Undo migrations
Defining Migrations
Create migrations using defineMigration:
import { defineMigration } from 'cosmosql';
export const addPreferences = defineMigration({
version: 1,
name: 'add-user-preferences',
description: 'Add preferences object to all users',
async up({ db, logger, progress }) {
logger.info('Adding preferences to users...');
const result = await db.users.updateMany({
where: {},
data: (doc) => ({
preferences: {
theme: doc.oldTheme || 'light',
notifications: true
}
}),
enableCrossPartitionQuery: true,
onProgress: progress.track('users')
});
logger.info(`Updated ${result.updated} users`);
},
async down({ db, logger }) {
logger.info('Removing preferences from users...');
await db.users.updateMany({
where: {},
data: { preferences: undefined },
enableCrossPartitionQuery: true
});
}
});Migration Context:
db: Your database client with all containerslogger: Logger withinfo,warn,error,debugmethodsprogress: Progress tracker for bulk operationsdryRun: Boolean indicating if this is a dry run
Rules:
- Versions must be sequential integers (1, 2, 3...)
- Names must be lowercase alphanumeric with hyphens
up()is required,down()is optional (but recommended)
Registering Migrations
Pass migrations when creating the client:
const db = await createClient({
connectionString: process.env.COSMOS_CONNECTION_STRING!,
database: 'myapp',
migrations: [addPreferences, migration2, migration3]
}).withContainers({ users, posts });Migration Status
Check which migrations are applied:
const status = await db.migrations.status();
console.log(`Current version: ${status.current?.version}`);
console.log(`Pending: ${status.pending.length}`);
console.log(`Can rollback: ${status.canRollback}`);
// List all applied migrations
status.applied.forEach(m => {
console.log(`v${m.version}: ${m.name} (${m.ruConsumed} RU)`);
});Planning Migrations
Preview what will happen before applying:
const plan = await db.migrations.plan({ dryRun: true });
console.log(`Will apply ${plan.migrationsToApply.length} migrations`);
console.log(`Estimated cost: ${plan.totalEstimatedRU} RU`);
console.log(`Estimated duration: ${plan.totalEstimatedDuration}`);
// Warnings
plan.warnings.forEach(w => console.log(`⚠️ ${w}`));Applying Migrations
Apply pending migrations:
const result = await db.migrations.apply({
target: 'latest', // or specific version number
confirm: true, // Safety confirmation
onProgress: (p) => {
console.log(
`[Migration ${p.migration.version}] ${p.status} - ` +
`${p.percentage}% (${p.ruConsumed} RU)`
);
}
});
console.log(`Applied ${result.applied.length} migrations`);
console.log(`Total RU: ${result.performance.totalRuConsumed}`);Dry Run:
Test migrations without applying them:
await db.migrations.apply({
target: 'latest',
dryRun: true,
onProgress: (p) => console.log(`Would apply: ${p.migration.name}`)
});Rolling Back
Rollback to a previous version:
await db.migrations.rollback({
to: 3, // Roll back to version 3
confirm: true, // Safety confirmation
onProgress: (p) => {
console.log(`Rolling back: ${p.migration.name}`);
}
});Requirements:
- All migrations being rolled back must have
down()functions - Migrations are rolled back in reverse order
Best Practices
- Never modify applied migrations - the system detects changes via checksums
- Always provide
down()functions for rollback capability - Test migrations on dev/staging before production
- Use
dryRun: trueto preview changes - Keep migrations small and focused - one logical change per migration
- Version sequentially - no gaps in version numbers
Performance Tips
- Migrations are tracked in a special
_migrationscontainer - Progress tracking adds minimal overhead
- Use bulk operations within migrations for efficiency
Troubleshooting
"Migrations must be sequential"
Cause: Gap in version numbers
Solution: Ensure versions are 1, 2, 3... with no gaps
"Cannot rollback: migration has no down() function"
Cause: Trying to rollback a migration without down()
Solution: Add down() function or remove the migration
Next Steps
- Learn about Bulk Operations for efficient data migrations
- See Database Management for health checks and monitoring
- Review Common Patterns for real-world scenarios
CosmosQL provides flexible and type-safe ways to update documents in CosmosDB.
Navigation:
- Single Update - Modify existing documents
- Upsert - Insert or update
- Atomic Operations - Increment/decrement
- Bulk Operations - Update multiple documents efficiently
Single Update
The basic update operation modifies an existing document:
await db.users.update({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
},
data: {
age: 31,
name: 'John Updated'
}
});Important: The partition key must be included in the where clause. This is enforced at compile time for type safety.
Partial Updates
You can update only the fields you need—other fields remain unchanged:
await db.users.update({
where: { id: 'user_123', email: 'john@example.com' },
data: {
age: 31
// Other fields remain unchanged
}
});This is efficient as CosmosDB only updates the specified fields, not the entire document.
Upsert Operations
The upsert operation inserts if the document doesn't exist, or updates if it does:
await db.users.upsert({
where: {
id: 'user_123',
email: 'john@example.com'
},
create: {
// Full user object for creation
id: 'user_123',
email: 'john@example.com',
name: 'John Doe',
age: 30,
isActive: true,
createdAt: new Date()
},
update: {
// Partial update
age: 31,
name: 'John Updated'
}
});Why Upsert? This pattern is common in distributed systems where you want idempotent operations—operations that can be safely retried without unintended side effects.
Atomic Operations
Perform atomic increment/decrement operations:
await db.posts.update({
where: { id: 'post_1', userId: 'user_123' },
data: {
viewCount: { increment: 1 },
likeCount: { increment: 5 },
dislikeCount: { decrement: 2 }
}
});This is an atomic operation performed server-side, making it safe for concurrent updates.
Update Patterns
Pattern: Touch Pattern (Update Timestamp)
await db.posts.update({
where: { id: 'post_1', userId: 'user_123' },
data: {
title: 'New Title',
updatedAt: new Date()
}
});Pattern: Status Transitions
// Update status with validation
const validTransitions = {
draft: ['published', 'archived'],
published: ['archived'],
archived: [] // terminal state
};
function canTransition(from: string, to: string): boolean {
return validTransitions[from]?.includes(to) ?? false;
}
async function updatePostStatus(postId: string, userId: string, newStatus: string) {
const post = await db.posts.findUnique({
where: { id: postId, userId }
});
if (!post || !canTransition(post.status, newStatus)) {
throw new Error('Invalid status transition');
}
await db.posts.update({
where: { id: postId, userId },
data: {
status: newStatus,
updatedAt: new Date()
}
});
}Pattern: Array Operations
While CosmosQL doesn't provide built-in array manipulation helpers, you can update entire arrays:
// Replace the entire tags array
await db.posts.update({
where: { id: 'post_1', userId: 'user_123' },
data: {
tags: ['javascript', 'react', 'tutorial']
}
});
// For append operations, fetch, modify, and update
const post = await db.posts.findUnique({
where: { id: 'post_1', userId: 'user_123' }
});
await db.posts.update({
where: { id: 'post_1', userId: 'user_123' },
data: {
tags: [...post.tags, 'new-tag']
}
});Bulk Operations
For updating multiple documents at once, use updateMany:
// Static update - update all inactive users
const result = await db.users.updateMany({
where: { isActive: false },
data: { status: 'archived' },
partitionKey: 'user@email.com' // or enableCrossPartitionQuery: true
});
console.log(`Updated ${result.updated} documents`);
console.log(`Failed: ${result.failed}`);
console.log(`RU consumed: ${result.performance.ruConsumed}`);Dynamic update with function:
// Update email domains for all users
const result = await db.users.updateMany({
where: { email: { contains: '@old.com' } },
data: (doc) => ({
email: doc.email.replace('@old.com', '@new.com'),
migratedAt: new Date()
}),
enableCrossPartitionQuery: true,
batchSize: 50,
maxConcurrency: 5,
onProgress: (stats) => {
console.log(`${stats.percentage}% - ${stats.ruConsumed} RU`);
},
onError: (error) => {
console.error(`Failed: ${error.documentId}`, error.error);
}
});Key Options:
where: Query to match documentsdata: Static object or function that returns updatespartitionKeyorenableCrossPartitionQuery: Required (one or the other)batchSize: Documents per batch (default: 50)maxConcurrency: Parallel batches (default: 5)continueOnError: Keep going if some fail (default: false)maxRetries: Retry attempts for retriable errors (default: 3)onProgress: Progress callback with statsonError: Error callback for individual failures
Result includes:
updated: Number of successfully updated documentsfailed: Number of failed updatesskipped: Number of skipped documentserrors: Array of error detailsperformance: RU consumption, duration, and throughput metrics
Best Practices:
- Start with small batches when testing (e.g.,
batchSize: 10) - Use
continueOnError: truefor large operations where some failures are acceptable - Monitor RU consumption using
onProgresscallbacks - Use partition keys when possible (much faster than cross-partition queries)
- Test with dry runs if implementing custom logic
Bulk operations are built into CosmosQL and work seamlessly with your existing containers.
Error Handling
try {
await db.users.update({
where: { id: 'user_123', email: 'john@example.com' },
data: { age: 31 }
});
} catch (error) {
if (error.code === 404) {
// Document not found
console.error('User not found');
} else if (error.code === 429) {
// Rate limit exceeded
console.error('Rate limit exceeded, please retry');
} else if (error.code === 412) {
// Precondition failed (e.g., ETag mismatch)
console.error('Document was modified, please retry');
} else {
console.error('Failed to update user:', error);
}
}Performance Considerations
1. Use Partial Updates
// ✅ Good: Only update what changed
await db.posts.update({
where: { id: 'post_1', userId: 'user_123' },
data: { title: 'New Title' }
});2. Batch Related Updates
// ✅ Good: Single atomic operation
await db.posts.update({
where: { id: 'post_1', userId: 'user_123' },
data: {
viewCount: { increment: 1 },
lastViewedAt: new Date()
}
});Next Steps
- Read Deleting Documents to learn deletion patterns
- See Common Patterns for real-world scenarios
- Review Performance Guide for optimization tips
Database Management
Comprehensive tools for managing and monitoring your CosmosDB resources
View on separate page →Manage and monitor your CosmosDB database with built-in tools for health checks, schema validation, and container management.
Navigation:
- Database Information - Get database details
- Health Checks - Monitor database health
- Schema Diff - Compare schema with database
- Container Pruning - Remove orphaned containers
- Delete Containers - Delete specific containers
Database Information
Get detailed information about your database:
const info = await db.management.getDatabaseInfo();
console.log(`Database: ${info.id}`);
console.log(`Containers: ${info.containersCount}`);
console.log(`Total documents: ${info.storage.totalDocuments}`);
console.log(`Storage: ${info.storage.totalSizeGB} GB`);
// Container details
info.containers.forEach(c => {
console.log(`\n${c.id}:`);
console.log(` Documents: ${c.statistics.documentCount}`);
console.log(` Partition Key: ${c.partitionKey.paths[0]}`);
console.log(` Registered: ${c.schema?.registered}`);
});Health Checks
Run health checks on your database:
const health = await db.management.healthCheck();
console.log(`Overall Health: ${health.overallHealth}`); // 'healthy', 'warning', or 'critical'
health.containers.forEach(c => {
if (!c.healthy) {
console.log(`⚠️ ${c.container}:`);
c.issues.forEach(issue => {
console.log(` [${issue.severity}] ${issue.message}`);
if (issue.recommendation) {
console.log(` 💡 ${issue.recommendation}`);
}
});
}
});
// Recommendations
health.recommendations.forEach(r => console.log(`💡 ${r}`));Issue Types:
missing_index: Container has very few indexed pathslarge_documents: Average document size is highorphaned: Container not registered in schema
Schema Diff
Compare your schema with the actual database:
const diff = await db.management.diffSchema();
if (diff.requiresAction) {
console.log('⚠️ Schema drift detected!');
// Containers in database but not schema
if (diff.containers.orphaned.length > 0) {
console.log(`Orphaned: ${diff.containers.orphaned.join(', ')}`);
}
// Containers in schema but not database
if (diff.containers.missing.length > 0) {
console.log(`Missing: ${diff.containers.missing.join(', ')}`);
}
// Configuration differences
diff.containers.modified.forEach(mod => {
console.log(`${mod.container} has differences:`);
console.log(JSON.stringify(mod.differences, null, 2));
});
} else {
console.log('✅ Schema is in sync');
}Container Pruning
Remove containers that aren't in your schema:
// List orphaned containers
const orphaned = await db.management.listOrphanedContainers();
console.log(`Found ${orphaned.length} orphaned containers`);
// Preview what would be deleted
const preview = await db.management.pruneContainers({
confirm: false,
dryRun: true
});
console.log(`Would delete: ${preview.pruned.join(', ')}`);
// Actually delete
const result = await db.management.pruneContainers({
confirm: true,
exclude: ['keep-this-one'] // Optional: containers to keep
});
console.log(`Deleted: ${result.pruned.join(', ')}`);Delete Containers
Delete containers by name:
const result = await db.management.deleteContainers(
['temp-container', 'old-data'],
{ confirm: true }
);
console.log(`Deleted: ${result.deleted.join(', ')}`);
console.log(`Failed: ${result.failed.length}`);Best Practices
- Run health checks regularly to catch issues early
- Use schema diff to detect drift between code and database
- Backup before pruning containers in production
- Monitor orphaned containers - they still consume resources
- Review recommendations from health checks
Performance Tips
- Database info queries are cached where possible
- Health checks run queries to gather statistics
- Schema diff is lightweight (metadata only)
Troubleshooting
"Must set confirm: true"
Cause: Safety check for destructive operations
Solution: Add confirm: true to the options
Next Steps
- Learn about Migrations for schema evolution
- See Common Patterns for real-world scenarios
- Review Performance Guide for optimization tips
Migrations provide a structured way to evolve your database schema and data over time. Define migrations, track their status, apply them safely, and rollback when needed.
Navigation:
- Defining Migrations - Create migration files
- Registering Migrations - Register with your client
- Migration Status - Check applied migrations
- Planning Migrations - Preview changes
- Applying Migrations - Run migrations
- Rolling Back - Undo migrations
Defining Migrations
Create migrations using defineMigration:
import { defineMigration } from 'cosmosql';
export const addPreferences = defineMigration({
version: 1,
name: 'add-user-preferences',
description: 'Add preferences object to all users',
async up({ db, logger, progress }) {
logger.info('Adding preferences to users...');
const result = await db.users.updateMany({
where: {},
data: (doc) => ({
preferences: {
theme: doc.oldTheme || 'light',
notifications: true
}
}),
enableCrossPartitionQuery: true,
onProgress: progress.track('users')
});
logger.info(`Updated ${result.updated} users`);
},
async down({ db, logger }) {
logger.info('Removing preferences from users...');
await db.users.updateMany({
where: {},
data: { preferences: undefined },
enableCrossPartitionQuery: true
});
}
});Migration Context:
db: Your database client with all containerslogger: Logger withinfo,warn,error,debugmethodsprogress: Progress tracker for bulk operationsdryRun: Boolean indicating if this is a dry run
Rules:
- Versions must be sequential integers (1, 2, 3...)
- Names must be lowercase alphanumeric with hyphens
up()is required,down()is optional (but recommended)
Registering Migrations
Pass migrations when creating the client:
const db = await createClient({
connectionString: process.env.COSMOS_CONNECTION_STRING!,
database: 'myapp',
migrations: [addPreferences, migration2, migration3]
}).withContainers({ users, posts });Migration Status
Check which migrations are applied:
const status = await db.migrations.status();
console.log(`Current version: ${status.current?.version}`);
console.log(`Pending: ${status.pending.length}`);
console.log(`Can rollback: ${status.canRollback}`);
// List all applied migrations
status.applied.forEach(m => {
console.log(`v${m.version}: ${m.name} (${m.ruConsumed} RU)`);
});Planning Migrations
Preview what will happen before applying:
const plan = await db.migrations.plan({ dryRun: true });
console.log(`Will apply ${plan.migrationsToApply.length} migrations`);
console.log(`Estimated cost: ${plan.totalEstimatedRU} RU`);
console.log(`Estimated duration: ${plan.totalEstimatedDuration}`);
// Warnings
plan.warnings.forEach(w => console.log(`⚠️ ${w}`));Applying Migrations
Apply pending migrations:
const result = await db.migrations.apply({
target: 'latest', // or specific version number
confirm: true, // Safety confirmation
onProgress: (p) => {
console.log(
`[Migration ${p.migration.version}] ${p.status} - ` +
`${p.percentage}% (${p.ruConsumed} RU)`
);
}
});
console.log(`Applied ${result.applied.length} migrations`);
console.log(`Total RU: ${result.performance.totalRuConsumed}`);Dry Run:
Test migrations without applying them:
await db.migrations.apply({
target: 'latest',
dryRun: true,
onProgress: (p) => console.log(`Would apply: ${p.migration.name}`)
});Rolling Back
Rollback to a previous version:
await db.migrations.rollback({
to: 3, // Roll back to version 3
confirm: true, // Safety confirmation
onProgress: (p) => {
console.log(`Rolling back: ${p.migration.name}`);
}
});Requirements:
- All migrations being rolled back must have
down()functions - Migrations are rolled back in reverse order
Best Practices
- Never modify applied migrations - the system detects changes via checksums
- Always provide
down()functions for rollback capability - Test migrations on dev/staging before production
- Use
dryRun: trueto preview changes - Keep migrations small and focused - one logical change per migration
- Version sequentially - no gaps in version numbers
Performance Tips
- Migrations are tracked in a special
_migrationscontainer - Progress tracking adds minimal overhead
- Use bulk operations within migrations for efficiency
Troubleshooting
"Migrations must be sequential"
Cause: Gap in version numbers
Solution: Ensure versions are 1, 2, 3... with no gaps
"Cannot rollback: migration has no down() function"
Cause: Trying to rollback a migration without down()
Solution: Add down() function or remove the migration
Next Steps
- Learn about Bulk Operations for efficient data migrations
- See Database Management for health checks and monitoring
- Review Common Patterns for real-world scenarios
The pattern: Use email as both ID and partition key for user documents.
Why this works:
- Login queries are partition-scoped (fast + cheap)
- Natural 1:1 mapping between user and partition
- Scales to millions of users
- No "hot partitions" (even distribution)
When to avoid:
- Users can change email addresses → Use immutable
userIdinstead - Multiple authentication providers → Requires composite keys
Schema Design
const users = container('users', {
id: field.string(), // Using email as ID
email: field.string(), // Also the partition key
passwordHash: field.string(),
profile: field.object({
name: field.string(),
avatar: field.string().optional()
}),
createdAt: field.date(),
lastLoginAt: field.date().optional()
}).partitionKey('email'); // Key decision: email is partition keyWhy email as both ID and partition key?
- Point reads during login (fastest possible: 1 RU)
- No duplicate emails (ID uniqueness)
- Even distribution across partitions
Registration
async function register(email: string, password: string) {
const passwordHash = await bcrypt.hash(password, 10);
try {
const user = await db.users.create({
data: {
id: email, // Using email as ID
email: email, // And partition key
passwordHash,
profile: { name: email.split('@')[0] },
createdAt: new Date()
}
});
return { success: true, user };
} catch (error) {
if (isCosmosError(error) && error.statusCode === 409) {
// Conflict = duplicate email
return { success: false, error: 'Email already exists' };
}
throw error;
}
}What makes this efficient:
- One partition write (5 RU)
- Automatic duplicate detection (409 conflict)
- Type-safe return values
Login
async function login(email: string, password: string) {
// Point read: fastest possible query (1 RU)
const user = await db.users.findUnique({
where: {
id: email,
email: email // Partition key
},
select: {
id: true,
email: true,
passwordHash: true,
profile: true
}
});
if (!user) {
return { success: false, error: 'Invalid credentials' };
}
// Verify password
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) {
return { success: false, error: 'Invalid credentials' };
}
// Update last login (separate operation to keep read fast)
await db.users.update({
where: { id: email, email },
data: { lastLoginAt: new Date() }
});
return { success: true, user };
}Performance breakdown:
- Read: 1 RU (point read)
- Update: 5 RU (partition-scoped write)
- Total: 6 RU per login
Alternative: Skip the update to save 5 RU if you don't need lastLoginAt.
Email Change (Advanced)
Problem: User wants to change email, but email is the partition key.
Solution: Create new document, migrate data, delete old.
async function changeEmail(oldEmail: string, newEmail: string) {
// 1. Check if new email exists
const existing = await db.users.findUnique({
where: { id: newEmail, email: newEmail }
});
if (existing) {
return { success: false, error: 'Email already in use' };
}
// 2. Get current user
const user = await db.users.findUnique({
where: { id: oldEmail, email: oldEmail }
});
if (!user) {
return { success: false, error: 'User not found' };
}
// 3. Create new document with new email
await db.users.create({
data: {
...user,
id: newEmail,
email: newEmail
}
});
// 4. Delete old document
await db.users.delete({
where: { id: oldEmail, email: oldEmail }
});
return { success: true };
}Cost: ~15 RU (read + create + delete)
Caveat: Not atomic—handle failures appropriately
The pattern: Partition by category for efficient product queries.
Why this works:
- Fast category browsing
- Good distribution across partitions
- Cross-partition queries only for global search
Schema Design
const products = container('products', {
id: field.string(),
category: field.string(), // Partition key
name: field.string(),
price: field.number(),
inventory: field.number(),
tags: field.array(field.string()),
metadata: field.object({
brand: field.string(),
weight: field.number().optional(),
dimensions: field.object({
length: field.number(),
width: field.number(),
height: field.number()
}).optional()
})
}).partitionKey('category');Product Queries
// Query products by category with filtering
const electronics = await db.products.findMany({
partitionKey: 'electronics',
where: {
price: { lte: 999 },
inventory: { gt: 0 },
tags: { contains: 'wireless' }
},
select: {
id: true,
name: true,
price: true,
metadata: {
brand: true
}
},
orderBy: { price: 'asc' },
take: 20
});Global Search
For global search across all categories, use cross-partition queries (expensive but necessary):
// Global product search (requires cross-partition query)
const searchResults = await db.products.findMany({
enableCrossPartitionQuery: true,
where: {
name: { contains: searchTerm },
inventory: { gt: 0 }
},
orderBy: { price: 'asc' },
take: 50
});Inventory Management
// Update inventory after purchase
await db.products.update({
where: {
id: productId,
category: productCategory
},
data: {
inventory: { decrement: quantity }
}
});Best Practices
- Category selection - Choose categories with good distribution (avoid "Other" being too large)
- Cache popular categories - Frequently browsed categories benefit from caching
- Monitor hot partitions - If one category has 80% of products, consider sub-categorization
The pattern: Use tenantId as partition key to ensure data isolation.
Why this works:
- Complete isolation between tenants
- All queries automatically scoped to a single tenant
- Prevents cross-tenant data leakage
- Scales as tenants grow
Schema Design
const organizations = container('organizations', {
id: field.string(),
name: field.string(),
plan: field.string(),
settings: field.object({
theme: field.string().default('light'),
features: field.array(field.string())
})
}).partitionKey('id'); // Organization ID as partition key
const users = container('users', {
id: field.string(),
organizationId: field.string(),
email: field.string(),
role: field.string(),
profile: field.object({
name: field.string(),
department: field.string().optional()
})
}).partitionKey('organizationId'); // All users for an org in same partition
const documents = container('documents', {
id: field.string(),
tenantId: field.string(),
name: field.string(),
content: field.string(),
createdBy: field.string(),
createdAt: field.date()
}).partitionKey('tenantId');Querying Tenant Data
// All queries automatically scoped to tenant
async function getDocuments(tenantId: string) {
return await db.documents.findMany({
partitionKey: tenantId,
orderBy: { createdAt: 'desc' }
});
}
async function getOrgUsers(organizationId: string) {
return await db.users.findMany({
partitionKey: organizationId,
where: {
role: 'admin'
},
select: {
id: true,
email: true,
profile: { name: true }
}
});
}Security Best Practices
- Always validate tenant context - Ensure the authenticated user's tenant matches the partition key
- Use middleware - Automatically inject tenant ID from request context
- Prevent cross-tenant access - Never allow user-provided tenant IDs without validation
// Example: Express middleware to enforce tenant isolation
function tenantMiddleware(req: Request, res: Response, next: NextFunction) {
const tenantId = req.user?.organizationId;
if (!tenantId) {
return res.status(401).json({ error: 'Unauthorized' });
}
req.tenantId = tenantId; // Attach to request
next();
}
// Use in routes
app.get('/documents', tenantMiddleware, async (req, res) => {
const documents = await db.documents.findMany({
partitionKey: req.tenantId, // Always use validated tenant ID
orderBy: { createdAt: 'desc' }
});
res.json(documents);
});The pattern: Use userId as partition key with TTL for abandoned carts.
Schema Design
const carts = container('carts', {
id: field.string(),
userId: field.string(),
items: field.array(field.object({
productId: field.string(),
quantity: field.number(),
price: field.number()
})),
total: field.number(),
updatedAt: field.date(),
ttl: field.number() // Expire abandoned carts
}).partitionKey('userId');Cart Operations
async function addToCart(userId: string, item: CartItem) {
const cart = await db.carts.findUnique({
where: { id: userId, userId }
});
if (!cart) {
return await db.carts.create({
data: {
id: userId,
userId,
items: [item],
total: item.price * item.quantity,
updatedAt: new Date(),
ttl: 7 * 24 * 60 * 60 // Expire after 7 days
}
});
}
// Update existing cart
const newItems = [...cart.items, item];
const newTotal = newItems.reduce((sum, i) => sum + i.price * i.quantity, 0);
return await db.carts.update({
where: { id: userId, userId },
data: {
items: newItems,
total: newTotal,
updatedAt: new Date()
}
});
}Cart Retrieval
// Get user's cart (fast point read)
const cart = await db.carts.findUnique({
where: {
id: userId,
userId
}
});
if (!cart) {
// Cart expired or doesn't exist
return { items: [], total: 0 };
}Checkout
async function checkout(userId: string) {
const cart = await db.carts.findUnique({
where: { id: userId, userId }
});
if (!cart || cart.items.length === 0) {
throw new Error('Cart is empty');
}
// Create order (in different container)
const order = await db.orders.create({
data: {
id: generateId(),
userId: userId,
items: cart.items,
total: cart.total,
status: 'pending',
createdAt: new Date()
}
});
// Clear cart (or let TTL expire it)
await db.carts.delete({
where: { id: userId, userId }
});
return order;
}Best Practices
- TTL for abandoned carts - Automatically clean up old carts
- Update TTL on cart activity - Refresh expiration when user interacts
- Partition by userId - All cart operations scoped to user
- Consider cart expiration - Match business requirements (7-30 days typical)
The pattern: Partition by userId to keep all user content together.
Why this works:
- Fast queries for a user's posts, comments, activity
- All related data in one partition
- Efficient for user profiles and feeds
Schema Design
const posts = container('posts', {
id: field.string(),
userId: field.string(), // Partition key
content: field.string(),
likes: field.number().default(0),
createdAt: field.date(),
tags: field.array(field.string()),
author: field.object({
name: field.string(),
avatar: field.string().optional()
})
}).partitionKey('userId');User Content Queries
// Find user's recent posts with engagement
const userPosts = await db.posts.findMany({
partitionKey: 'user_123',
where: {
createdAt: { gte: new Date('2024-01-01') },
likes: { gt: 10 }
},
select: {
id: true,
content: true,
likes: true,
author: {
name: true
}
},
orderBy: { createdAt: 'desc' },
take: 50
});Global Feed (Cross-Partition)
For a global feed showing posts from all users:
// Global feed (requires cross-partition query)
const globalFeed = await db.posts.findMany({
enableCrossPartitionQuery: true,
where: {
createdAt: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) } // Last 7 days
},
orderBy: { likes: 'desc' },
take: 100
});Engagement Tracking
// Increment likes (atomic update)
await db.posts.update({
where: {
id: postId,
userId: userId
},
data: {
likes: { increment: 1 }
}
});Best Practices
- Prefer user-scoped queries - Most social platforms show user-specific content
- Cache global feeds - Cross-partition queries are expensive; cache frequently
- Consider denormalization - Store author info in posts to avoid joins
The pattern: Use deviceId as partition key with TTL for automatic cleanup.
Why this works:
- Fast queries per device
- Automatic expiration with TTL
- Good distribution across devices
Schema Design
const metrics = container('metrics', {
id: field.string(),
deviceId: field.string(), // Partition key
timestamp: field.date(),
temperature: field.number(),
humidity: field.number(),
ttl: field.number() // Auto-delete old data
}).partitionKey('deviceId');Time-Series Queries
// Ingest metric (auto-expires after 30 days)
await db.metrics.create({
data: {
id: generateId(),
deviceId: 'device_123',
timestamp: new Date(),
temperature: 22.5,
humidity: 45,
ttl: 30 * 24 * 60 * 60 // 30 days in seconds
}
});
// Query recent metrics for device
const recent = await db.metrics.findMany({
partitionKey: 'device_123',
where: {
timestamp: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) }
},
orderBy: { timestamp: 'desc' }
});Aggregations
// Get average temperature over time period
const stats = await db.metrics.aggregate({
partitionKey: 'device_123',
where: {
timestamp: {
gte: new Date('2024-01-01'),
lte: new Date('2024-01-31')
}
},
_avg: { temperature: true, humidity: true },
_min: { temperature: true },
_max: { temperature: true },
_count: true
});Best Practices
- Use TTL for automatic cleanup - Avoids manual deletion jobs
- Partition by device/entity - Keeps related data together
- Index timestamp - Ensure efficient time-range queries
- Consider retention policies - Set TTL based on data retention requirements
Contributing
Development setup, code quality, and contribution guidelines for CosmosQL
View on separate page →Thank you for your interest in contributing to CosmosQL! This guide will help you get started with development.
Development Setup
Prerequisites
- Node.js 18+ or Bun 1.0+
- TypeScript 5.0+
- Git for version control
Getting Started
- Clone the repository:
git clone https://github.com/Stoffberg/cosmosql.git
cd cosmosql- Install dependencies:
# Using Bun (recommended)
bun install
# Or using npm
npm install- Build the project:
bun run build
# Or
npm run build- Run tests:
# All tests
bun test
# Unit tests only
bun test tests/unit
# Integration tests only
bun test tests/integrationNote: Integration tests require a real Azure CosmosDB account. Set up environment variables:
COSMOS_ENDPOINT=https://your-account.documents.azure.com:443/
COSMOS_KEY=your-key
COSMOS_DATABASE=test-dbCode Quality
CosmosQL maintains high code quality standards:
Linting and Formatting
Biome is used for linting and formatting:
# Check for issues
bun run lint
# Auto-fix issues
bun run formatTypeScript
- Strict mode is enabled
- All code must be fully typed
- Avoid
anytypes unless absolutely necessary
Testing
- Comprehensive test suite required for all changes
- Maintain high test coverage
- Add tests for new features and bug fixes
Architecture
Understanding CosmosQL's architecture helps when contributing:
Core Principles
- Zero Dependencies - Only TypeScript, no external runtime dependencies
- Type-Level Programming - Leverage TypeScript's type system for schema inference
- REST API Direct - Direct HTTP calls to CosmosDB REST API, no SDK wrapper
- Bun Runtime Optimized - Built and tested with Bun, works with Node.js
Project Structure
cosmosql/
├── src/
│ ├── client/ # Client creation and configuration
│ ├── container/ # Container schema definitions
│ ├── query/ # Query builder and execution
│ ├── types/ # Type definitions and inference
│ └── utils/ # Utilities and helpers
├── tests/
│ ├── unit/ # Unit tests
│ └── integration/ # Integration tests with real CosmosDB
└── package.jsonKey Concepts
- Schema Definition: TypeScript-only schemas that don't exist at runtime
- Type Inference: Automatic type inference from schemas
- Partition Key Enforcement: Compile-time enforcement via TypeScript
- Query Builder: Type-safe SQL query generation
Making Changes
Workflow
- Create a branch:
git checkout -b feature/your-feature-name
# or
git checkout -b fix/your-bug-fix-
Make your changes - Write code, add tests
-
Run tests and linting:
bun test
bun run lint- Commit your changes:
git commit -m "feat: add new feature description"Follow Conventional Commits format:
feat:- New featurefix:- Bug fixdocs:- Documentation changestest:- Test additions/changesrefactor:- Code refactoringperf:- Performance improvements
- Push and create a PR:
git push origin feature/your-feature-namePull Request Guidelines
- Clear description of what changed and why
- Reference issues if fixing a bug or feature request
- Add tests for new features or bug fixes
- Update documentation if API changes or new features added
- Ensure all tests pass and linting is clean
Testing Requirements
- New features must include unit tests
- Integration tests for database operations
- Edge cases should be covered
- Maintain or improve test coverage
Areas for Contribution
High Priority
- Bug fixes - Report and fix bugs
- Documentation - Improve clarity and completeness
- Performance - Optimize queries and operations
- Type safety - Enhance type inference and error messages
Feature Ideas
- Aggregation functions (
count,sum,avg, etc.) - Cursor-based pagination
- Batch operations across partitions
- Additional query operators
Documentation
- Code examples
- Best practices guides
- Migration guides
- API reference improvements
Getting Help
If you need help:
- Check existing issues on GitHub
- Ask questions in discussions
- Join Discord community (if available)
- Read existing code to understand patterns
License
CosmosQL is licensed under the MIT License. By contributing, you agree that your contributions will be licensed under the same license.
Thank you for contributing to CosmosQL! Your efforts help make CosmosDB development safer and more enjoyable for everyone.
Quick answers to common questions. For detailed guides, see Quickstart or Patterns & Use Cases.
Quick Decisions
Should I use CosmosQL?
Use CosmosQL if: TypeScript ✅ | Need type safety ✅ | Want minimal dependencies ✅
Use Azure SDK if: Need change feed/stored procedures ✅ | JavaScript only ✅
Can I migrate existing databases?
Yes. CosmosQL works with existing CosmosDB databases. No data migration needed—just define schemas matching your current documents.
Can I use both libraries?
Yes. CosmosQL and Azure SDK coexist—use CosmosQL for CRUD, Azure SDK for advanced features.
Features
What's supported?
- ✅ CRUD operations (create, read, update, delete)
- ✅ Partition-scoped and cross-partition queries
- ✅ Point reads, filtering, sorting, pagination
- ✅ Bulk operations within partitions
- ✅ Raw SQL queries
- ❌ Change feed processors
- ❌ Stored procedures
- ❌ Graph/MongoDB APIs
See Limitations for complete list.
Language & Runtime
JavaScript support?
No. CosmosQL requires TypeScript. The value is compile-time type safety.
Node.js version?
Node.js 18+. Uses modern built-in APIs.
Serverless?
Yes. Zero dependencies make CosmosQL ideal for Lambda, Azure Functions, Edge Runtime.
Performance
Overhead vs REST?
~1-2%. Direct REST API calls with compile-time type safety.
vs Azure SDK?
CosmosQL is typically faster due to zero runtime overhead. Azure SDK has some bulk operation optimizations.
How much does a query cost?
- Point read: ~1 RU
- Partition query: ~3-5 RU
- Cross-partition: ~50-100+ RU
Schema Changes
Adding fields?
Update your schema. Existing documents work fine—new fields are optional.
Removing fields?
Treat as optional in schema. Can't truly remove from existing documents. Consider versioning.
Changing field types?
Version documents or write migration scripts. See Migrating from Azure SDK for strategies.
Migration
From Azure SDK?
Gradual migration recommended. Use CosmosQL for new features, keep SDK for advanced features. See Migrating from Azure SDK.
Schema design?
Start with Core Concepts to understand partition keys. Then see Patterns & Use Cases for examples.
Production
Production ready?
Yes. Stable API, 249 tests, 100% coverage. Used in production by multiple teams.
Error handling?
Standard try/catch with CosmosError class. See Error Handling for examples.
Support?
GitHub issues, discussions, and documentation. Maintainers are responsive.
Comparison
vs Prisma?
CosmosQL is CosmosDB-specific with partition key enforcement. Prisma supports many databases.
vs Mongoose?
CosmosQL is TypeScript-native, compile-time validation. Mongoose is MongoDB with runtime validation.
See Limitations for complete comparison table.
Need More Help?
- Getting started? → Quickstart
- Understanding basics? → Core Concepts
- Real-world examples? → Patterns & Use Cases
- Performance tips? → Performance Guide
- Error handling? → Error Handling
- API reference? → API Reference
CosmosQL focuses on core CRUD operations and common query patterns that cover the majority of application use cases. This document outlines what's currently supported, what's missing, and workarounds for common scenarios.
Current Version Status
Production Ready: ✅ Yes - Core operations are stable and well-tested
Test Coverage: Comprehensive test suite covering all core functionality
Architecture:
- Zero dependencies - Only TypeScript required
- Type-level programming - Schema inference and type safety
- REST API direct - No SDK wrapper, minimal overhead
- Bun runtime - Optimized for Bun 1.0+ and Node.js 18+
Missing Features
1. Advanced Pagination
CosmosQL currently only supports offset-based pagination using take and skip. Cursor-based pagination and continuation tokens are not yet supported.
Current Support:
// Offset pagination (LIMIT/OFFSET)
const page1 = await db.posts.findMany({
partitionKey: 'user_123',
take: 20,
skip: 0
});
const page2 = await db.posts.findMany({
partitionKey: 'user_123',
take: 20,
skip: 20
});Limitations:
- No cursor-based pagination
- No continuation token support
- Cross-partition queries with large offsets can be expensive
Workaround: For large datasets, consider:
- Partition-scoped queries - Stay within partitions to minimize costs
- Date/time-based filtering - Use timestamp filters instead of large offsets
- Manual continuation tokens - Use raw SQL queries with continuation tokens (advanced)
Future Plans: Cursor-based pagination and continuation token support are planned.
2. Transaction Support
CosmosQL does not support:
- Multi-document transactions
- Batch operations beyond
createMany(which requires same partition key) - Transaction rollback capabilities
- Cross-partition transactions
Current Support:
// Single-partition batch operations only
await db.posts.createMany({
data: [
{ id: 'post_1', userId: 'user_123', title: 'Post 1' },
{ id: 'post_2', userId: 'user_123', title: 'Post 2' }
],
partitionKey: 'user_123' // All documents must share partition key
});Limitations:
- Cannot atomically update multiple documents across partitions
- No rollback support
- Limited to operations within a single partition
Workaround: For multi-document operations:
- Design for single-partition operations - Structure data so related documents share partition keys
- Use application-level transactions - Implement compensating actions for failures
- Use Azure SDK for transactions - For complex scenarios, use the official SDK alongside CosmosQL
Future Plans: Transaction support is under consideration for future releases.
3. Cross-Partition Query Limitations
Azure CosmosDB Restriction: Cross-partition queries are limited on empty containers. This is a CosmosDB limitation, not a CosmosQL limitation.
When attempting cross-partition queries on empty containers, CosmosDB returns an error. This is by design to prevent expensive operations on empty data.
Workaround: Ensure containers have at least one document before running cross-partition queries, or use partition-scoped queries when possible.
4. Advanced Query Features
The following CosmosDB features are not yet supported:
- Geospatial queries - Spatial indexing is supported in schema definition but full geospatial queries require raw SQL
- Full-text search - Integration with Azure Cognitive Search (not supported via REST API)
- JOIN operations - Not applicable to document databases, but no support for array joins
- Stored procedures - Cannot execute stored procedures (REST API focus)
- User-defined functions (UDFs) - Cannot use UDFs in queries (REST API focus)
Workaround: Use raw SQL queries for features not supported by the query builder:
// Geospatial example (when supported via raw SQL)
const result = await db.locations.query({
sql: `
SELECT * FROM c
WHERE ST_DISTANCE(c.location, {
'type': 'Point',
'coordinates': [@lng, @lat]
}) < @distance
`,
parameters: [
{ name: '@lng', value: -122.4194 },
{ name: '@lat', value: 37.7749 },
{ name: '@distance', value: 5000 }
]
});Future Plans: Advanced query features may be added based on community demand.
5. Performance Optimizations
Missing performance-related features:
- Query result caching - No built-in caching layer
- Query plan analysis - No automatic query optimization suggestions
- Automatic indexing suggestions - No recommendations for index optimization
- Connection pool management - Basic pooling exists, but limited configuration
Workaround:
- Implement caching at application level - Use Redis or in-memory caching for frequently accessed data
- Monitor RU usage - Track Request Unit consumption in Azure Portal
- Optimize partition keys - Ensure good partition key selection (documented in Performance Guide)
- Use point reads - Prefer
findUniqueoverfindManywhen querying single documents
Future Plans: Performance monitoring and optimization tools may be added.
6. Change Feed
CosmosQL does not support:
- Change feed processors
- Real-time change notifications
- Document change tracking
Workaround: Use the official Azure SDK for change feed processing:
// Using Azure SDK for change feed
import { CosmosClient } from '@azure/cosmos';
const client = new CosmosClient(connectionString);
const container = client.database('mydb').container('users');
// Use SDK for change feed
const changeFeedProcessor = container.items
.changeFeed({
startFromBeginning: false
})
.on('change', (changes) => {
// Handle changes
});Future Plans: Change feed support is being considered for future releases.
7. Multi-Database Operations
CosmosQL works with a single database per client instance. Cross-database operations are not supported.
Workaround: Create multiple client instances:
const productionDb = await createClient({
connectionString: process.env.COSMOS_CONNECTION_STRING!,
database: 'production',
mode: 'verify'
}).withContainers({ users, posts });
const analyticsDb = await createClient({
connectionString: process.env.COSMOS_CONNECTION_STRING!,
database: 'analytics',
mode: 'verify'
}).withContainers({ events, metrics });Feature Comparison
| Feature | CosmosQL | Azure SDK | Status |
|---|---|---|---|
| Type Safety | ✅ Full | ❌ Limited | CosmosQL advantage |
| CRUD Operations | ✅ Complete | ✅ Complete | Equivalent |
| Aggregations | ✅ Type-safe | ✅ Native | Equivalent |
| Transactions | ⚠️ Single partition | ✅ Full support | SDK advantage |
| Change Feed | ❌ Not supported | ✅ Supported | SDK advantage |
| Database Management | ✅ Full management | ✅ Full management | Equivalent |
| Zero Dependencies | ✅ Yes | ❌ 50+ packages | CosmosQL advantage |
| Query Builder | ✅ Type-safe | ❌ SQL strings | CosmosQL advantage |
When to Use CosmosQL vs Azure SDK
Use CosmosQL when:
- ✅ You prioritize type safety and developer experience
- ✅ You need zero dependencies for serverless/microservices
- ✅ Your use cases fit core CRUD operations
- ✅ You want compile-time guarantees for partition keys
- ✅ You're building new TypeScript applications
- ✅ You need database and container management (with
auto-create,verify, orskipmodes)
Use Azure SDK when:
- ✅ You need advanced features (change feed, stored procedures)
- ✅ You need cross-partition transactions
- ✅ You're working with JavaScript (not TypeScript)
- ✅ You need geospatial queries or full-text search
Use Both:
You can use CosmosQL alongside the Azure SDK for different use cases within the same project. They don't interfere with each other and work with the same database.
Roadmap
High Priority:
- ✅ Core CRUD operations (Completed)
- ✅ Type-safe query builder (Completed)
- ✅ Aggregation functions (Completed)
- 🔄 Cursor pagination (Planned)
- 🔄 Batch operations (Planned)
Medium Priority:
- ✅ Container management APIs (Completed)
- ⏳ Advanced querying (Geospatial, full-text) (Future)
- ⏳ Performance monitoring (Future)
- ⏳ Migration support (Future)
Lower Priority:
- ⏳ Multiple database support (Future)
- ⏳ Advanced connection pooling (Future)
- ⏳ Circuit breaker patterns (Future)
Reporting Issues
If you encounter limitations that impact your use case:
- Check existing issues on GitHub
- Create a feature request with use case details
- Contribute - Pull requests welcome!
- Use workarounds - Many limitations have documented workarounds
Getting Help
For questions about limitations or missing features:
- Check the FAQ for common questions
- Review Patterns & Use Cases for workarounds
- Open a GitHub issue for specific feature requests
- Join the community Discord for discussions
Migrating from Azure SDK
Learn how to migrate from the official Azure CosmosDB SDK to CosmosQL
View on separate page →Migration from Azure SDK
If you're currently using the official Azure CosmosDB SDK (@azure/cosmos), this guide will help you migrate to CosmosQL. The migration is straightforward—CosmosQL works with existing databases and data, so you can adopt it incrementally.
Why Migrate?
The Azure SDK is powerful, but CosmosQL provides significant advantages:
Key Benefits
- Type Safety: Compile-time guarantees instead of runtime errors
- Better Developer Experience: Cleaner API, autocomplete, fewer mistakes
- Performance: Zero runtime overhead, direct REST API calls
- Cost Prevention: Type system prevents expensive cross-partition queries
- Minimal Dependencies: No SDK bloat (50+ packages → zero dependencies)
Migration Strategy
You don't need to migrate everything at once. Here's a recommended approach:
Incremental Migration
CosmosQL and the Azure SDK can coexist in the same project—they use the same database and don't interfere. This allows you to migrate gradually without disruption.
- Start with New Code: Use CosmosQL for new features and endpoints
- Migrate High-Traffic Paths: Convert frequently-used operations to CosmosQL
- Gradual Replacement: Migrate remaining code incrementally
- Remove SDK: Once everything is migrated, remove the Azure SDK dependency
Basic Migration Examples
Creating a Client
Before (Azure SDK):
import { CosmosClient } from '@azure/cosmos';
const client = new CosmosClient(connectionString);
const database = client.database('mydb');
const container = database.container('users');After (CosmosQL):
import { createClient, container, field } from 'cosmosql';
// Define your schema
const users = container('users', {
id: field.string(),
email: field.string(),
name: field.string()
}).partitionKey('email');
// Create typed client (async - validates/creates containers)
const db = await createClient({
connectionString,
database: 'mydb',
mode: 'auto-create'
}).withContainers({ users });Key Differences:
Important Changes
- CosmosQL requires schema definitions (for type safety)
- Client creation is a one-time setup per database
db.usersis fully typed,containeris not
Querying Documents
Before (Azure SDK):
const { resources } = await container.items
.query({
query: 'SELECT * FROM c WHERE c.id = @id AND c.email = @email',
parameters: [
{ name: '@id', value: 'user_123' },
{ name: '@email', value: 'john@example.com' }
]
})
.fetchAll();
const user = resources[0]; // No type safety
// Type: any - could be undefined, wrong shape, etc.After (CosmosQL):
// Point read (most efficient)
const user = await db.users.findUnique({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
}
});
// Type: { id: string; email: string; name: string } | null
// TypeScript knows the exact shape and nullabilityBenefits:
- Type-safe: TypeScript knows the exact return type
- Simpler: No SQL strings, no parameter arrays
- Safer: Can't forget partition key (required by types)
- More efficient:
findUniqueuses point reads (faster than queries)
Querying Multiple Documents
Before (Azure SDK):
const { resources } = await container.items
.query({
query: 'SELECT * FROM c WHERE c.isActive = @active ORDER BY c.createdAt DESC',
parameters: [{ name: '@active', value: true }]
})
.fetchAll();
// No type safety, no partition key enforcementAfter (CosmosQL):
// Partition-scoped query (efficient)
const activeUsers = await db.users.findMany({
partitionKey: 'john@example.com', // Required!
where: {
isActive: true
},
orderBy: {
createdAt: 'desc'
}
});
// Type: Array<{ id: string; email: string; ... }>
// Partition key is enforced at compile timeCreating Documents
Before (Azure SDK):
const { resource: newUser } = await container.items.create({
id: 'user_123',
email: 'john@example.com',
name: 'John Doe'
});
// No type checking - could have typos, missing fields, etc.After (CosmosQL):
const newUser = await db.users.create({
data: {
id: 'user_123',
email: 'john@example.com',
name: 'John Doe'
// TypeScript ensures all required fields are present
// Autocomplete helps you discover available fields
}
});
// Type: { id: string; email: string; name: string }
// Fully typed resultUpdating Documents
Before (Azure SDK):
const { resource: updated } = await container
.item('user_123', 'john@example.com') // ID and partition key
.replace({
id: 'user_123',
email: 'john@example.com',
name: 'John Updated',
age: 30 // Could forget fields, wrong types, etc.
});After (CosmosQL):
// Update specific fields (partial update)
const updated = await db.users.update({
where: {
id: 'user_123',
email: 'john@example.com'
},
data: {
name: 'John Updated',
age: 30
// Only include fields you want to update
// TypeScript ensures types match schema
}
});
// Type: { id: string; email: string; name: string; age: number }Deleting Documents
Before (Azure SDK):
await container
.item('user_123', 'john@example.com')
.delete();After (CosmosQL):
await db.users.delete({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
}
});
// Type-safe: can't delete without partition keyHandling Common Patterns
Error Handling
Before (Azure SDK):
try {
await container.items.create(user);
} catch (error: any) {
if (error.code === 409) {
// Conflict
} else if (error.code === 429) {
// Rate limited
}
// Error types are loose, easy to miss cases
}After (CosmosQL):
import { CosmosError } from 'cosmosql';
try {
await db.users.create({ data: user });
} catch (error) {
if (error instanceof CosmosError) {
switch (error.code) {
case 409: // Conflict
case 429: // Rate limited
// Type-safe error handling
}
}
}Cross-Partition Queries
Before (Azure SDK):
// Easy to accidentally do expensive cross-partition query
const { resources } = await container.items
.query('SELECT * FROM c WHERE c.isActive = true')
.fetchAll();
// This scans ALL partitions - expensive!After (CosmosQL):
// Type system prevents accidental cross-partition queries
const users = await db.users.findMany({
enableCrossPartitionQuery: true, // Must explicitly opt in
where: { isActive: true }
});
// You're forced to think about the costMigration Checklist
When migrating code from Azure SDK to CosmosQL:
- Define schemas for all containers you use
- Identify partition keys for each container
- Replace client creation with CosmosQL client
- Convert queries from SQL strings to query builder
- Update error handling to use CosmosError
- Test thoroughly in staging before production
- Monitor RU usage to ensure queries are efficient
- Update types throughout your codebase
- Remove Azure SDK once migration is complete
Common Pitfalls
Forgetting Partition Keys: CosmosQL requires partition keys in where clauses. This is intentional—it prevents expensive cross-partition queries. Make sure you always provide partition keys.
Schema Mismatches: Your CosmosQL schema must match your actual document structure. Review existing documents to ensure field names and types align.
Type Assertions: Avoid using as any to bypass type checking. If you need to, it's often a sign your schema needs adjustment.
Gradual Migration: Don't try to migrate everything at once. Start small, test thoroughly, then expand.
Getting Help
If you run into issues during migration:
- Check the FAQ for common questions
- Review the API Reference for method details
- Look at Patterns & Use Cases for real-world examples
- Open a GitHub issue if you find bugs or need clarification
The migration process is straightforward, and you'll quickly see the benefits of type safety and better developer experience.
Aggregation Methods
Complete reference for count, aggregate, groupBy, and convenience methods
View on separate page →Aggregation Operations
count(options)
Count documents matching criteria.
const count = await db.users.count({
partitionKey: 'john@example.com',
where: { isActive: true }
});
// Returns: numberaggregate(options)
Perform aggregation operations (count, sum, avg, min, max).
const stats = await db.orders.aggregate({
partitionKey: 'customer-123',
where: { status: 'completed' },
_count: true,
_sum: { amount: true, tax: true },
_avg: { amount: true },
_min: { createdAt: true },
_max: { amount: true }
});
// Returns: {
// _count: number,
// _sum: { amount: number | null, tax: number | null },
// _avg: { amount: number | null },
// _min: { createdAt: Date | null },
// _max: { amount: number | null }
// }groupBy(options)
Group data and perform aggregations on each group.
const salesByCategory = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_count: true,
_sum: { amount: true },
_avg: { amount: true },
orderBy: { _sum_amount: 'desc' },
take: 10
});
// Returns: Array<{
// category: string,
// _count: number,
// _sum: { amount: number | null },
// _avg: { amount: number | null }
// }>Convenience Methods
Quick aggregations for single fields:
// Sum
const totalRevenue = await db.orders.sum('amount', {
partitionKey: 'customer-123',
where: { status: 'completed' }
});
// Returns: number | null
// Average
const avgAge = await db.users.avg('age', {
partitionKey: 'tenant-1'
});
// Returns: number | null
// Minimum
const minPrice = await db.products.min('price', {
partitionKey: 'category-electronics'
});
// Returns: number | null
// Maximum
const maxPrice = await db.products.max('price', {
partitionKey: 'category-electronics'
});
// Returns: number | nullAggregation Options
partitionKey - Scope to partition
partitionKey: 'john@example.com' // Single value
partitionKey: ['tenant_1', 'org_a'] // Composite partition key (array)where - Filter conditions
where: {
status: 'completed',
amount: { gte: 100 },
createdAt: { gte: new Date('2024-01-01') }
}enableCrossPartitionQuery - Allow cross-partition aggregations
enableCrossPartitionQuery: true // Required for groupBy across partitionsAggregation Fields
_count: true- Count documents_sum: { field: true }- Sum numeric fields_avg: { field: true }- Average numeric fields_min: { field: true }- Minimum value_max: { field: true }- Maximum value
See Aggregations Guide for complete documentation with examples.
API Reference
Complete API reference for CosmosQL field types, container methods, and query options
View on separate page →Complete reference for CosmosQL's API, including field types, container methods, query operators, and configuration options.
Field Types
CosmosQL provides a fluent API for defining field types in your schema:
// Primitive types
field.string() // String type
field.number() // Number type
field.boolean() // Boolean type
field.date() // Date type (stored as ISO strings)
// Complex types
field.array(type) // Array of any field type
field.object(schema) // Nested object with its own schema
// Modifiers
.optional() // Makes field optional (can be undefined)
.default(value) // Provides default value for optional fieldsExample Schema with All Field Types:
const users = container('users', {
// Required fields
id: field.string(),
email: field.string(),
name: field.string(),
age: field.number(),
isActive: field.boolean(),
createdAt: field.date(),
// Optional fields
bio: field.string().optional(),
lastLoginAt: field.date().optional(),
// Fields with defaults
viewCount: field.number().default(0),
settings: field.object({
theme: field.string().default('light'),
notifications: field.boolean().default(true)
}).optional(),
// Arrays
tags: field.array(field.string()),
scores: field.array(field.number()),
// Nested objects
profile: field.object({
website: field.string().optional(),
location: field.string().optional()
}).optional()
}).partitionKey('email');Container Configuration
Containers can be configured with throughput, indexing policies, and more:
const posts = container('posts', {
id: field.string(),
userId: field.string(),
title: field.string(),
content: field.string(),
tags: field.array(field.string()),
createdAt: field.date()
})
.partitionKey('userId')
.throughput(400) // Optional: Set RU/s (Request Units per second)
.indexing({ // Optional: Configure indexing policy
automatic: true,
includedPaths: [
{ path: '/title/?' },
{ path: '/tags/[]/?' }
],
excludedPaths: [
{ path: '/content/?' }
],
compositeIndexes: [
[
{ path: '/createdAt', order: 'ascending' },
{ path: '/userId', order: 'ascending' }
]
]
});Container Configuration Options:
.partitionKey(field)- Required: Specifies the partition key field.throughput(ru)- Optional: Sets RU/s for the container (default: auto-scale).indexing(policy)- Optional: Configures indexing policy
Indexing Policy Options:
indexing: {
automatic: boolean; // Enable automatic indexing
includedPaths?: Array<{ // Paths to index
path: string; // Path pattern (e.g., '/title/?')
indexes?: Array<{
kind: 'Range' | 'Hash'; // Index kind
precision?: number; // Precision for range indexes
dataType: 'String' | 'Number' // Data type
}>
}>;
excludedPaths?: Array<{ // Paths to exclude from indexing
path: string;
}>;
compositeIndexes?: Array< // Composite indexes for multi-field sorting
Array<{
path: string;
order: 'ascending' | 'descending';
}>
>;
spatialIndexes?: Array<{ // Spatial indexes (advanced)
path: string;
types: Array<'Point' | 'LineString' | 'Polygon' | 'MultiPolygon'>;
}>;
}Container Methods
Query Operations
findUnique(options)
Retrieves a single document by ID and partition key. This is the most efficient operation (point read).
const user = await db.users.findUnique({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
},
select: {
name: true,
email: true
}
});
// Returns: { name: string; email: string } | nullfindMany(options)
Queries multiple documents with filtering, sorting, and pagination. Optionally include aggregations to get both data and statistics in a single query.
// Without aggregations
const users = await db.users.findMany({
partitionKey: 'john@example.com',
where: {
isActive: true,
age: { gte: 18 }
},
select: { id: true, name: true },
orderBy: { age: 'desc' },
take: 10,
skip: 0
});
// Returns: Array<{ id: string; name: string }>
// With aggregations
const result = await db.users.findMany({
partitionKey: 'john@example.com',
where: { isActive: true },
aggregate: {
_count: true,
_avg: { age: true },
_sum: { score: true },
_min: { createdAt: true },
_max: { createdAt: true }
}
});
// Returns: {
// data: User[],
// _count: number,
// _avg: { age: number | null },
// _sum: { score: number | null },
// _min: { createdAt: Date | null },
// _max: { createdAt: Date | null }
// }See Aggregations Guide for complete documentation.
query<T>(options)
Execute raw SQL queries for advanced use cases.
const result = await db.users.query<{ count: number }>({
sql: 'SELECT COUNT(1) as count FROM c WHERE c.isActive = true',
parameters: [
{ name: '@active', value: true }
],
partitionKey: 'john@example.com' // Optional: for partition-scoped queries
});
// Returns: Array<T>Write Operations
create(options)
Creates a single document.
const user = await db.users.create({
data: {
id: 'user_123',
email: 'john@example.com',
name: 'John Doe',
age: 30,
createdAt: new Date()
// isActive will use default value if defined
}
});
// Returns: Full user object (fully typed)createMany(options)
Creates multiple documents in a single operation. All documents must share the same partition key.
await db.users.createMany({
data: [
{ id: 'user_1', email: 'user1@test.com', name: 'User 1', age: 25 },
{ id: 'user_2', email: 'user2@test.com', name: 'User 2', age: 30 }
],
partitionKey: 'shared@email.com' // All must share partition key
});update(options)
Updates a single document by ID and partition key.
await db.users.update({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
},
data: {
age: 31,
name: 'John Smith'
}
});
// Returns: Updated user objectupsert(options)
Updates a document if it exists, creates it if it doesn't.
await db.users.upsert({
where: {
id: 'user_123',
email: 'john@example.com'
},
create: {
id: 'user_123',
email: 'john@example.com',
name: 'New User',
age: 25
},
update: {
age: 26,
name: 'Updated Name'
}
});delete(options)
Deletes a single document by ID and partition key.
await db.users.delete({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
}
});Aggregation Operations
count(options)
Count documents matching criteria.
const count = await db.users.count({
partitionKey: 'john@example.com',
where: { isActive: true }
});
// Returns: numberaggregate(options)
Perform aggregation operations (count, sum, avg, min, max).
const stats = await db.orders.aggregate({
partitionKey: 'customer-123',
where: { status: 'completed' },
_count: true,
_sum: { amount: true, tax: true },
_avg: { amount: true },
_min: { createdAt: true },
_max: { amount: true }
});
// Returns: {
// _count: number,
// _sum: { amount: number | null, tax: number | null },
// _avg: { amount: number | null },
// _min: { createdAt: Date | null },
// _max: { amount: number | null }
// }groupBy(options)
Group data and perform aggregations on each group.
const salesByCategory = await db.sales.groupBy({
by: 'category',
enableCrossPartitionQuery: true,
_count: true,
_sum: { amount: true },
_avg: { amount: true },
orderBy: { _sum_amount: 'desc' },
take: 10
});
// Returns: Array<{
// category: string,
// _count: number,
// _sum: { amount: number | null },
// _avg: { amount: number | null }
// }>Convenience Methods:
// Quick aggregations for single fields
const totalRevenue = await db.orders.sum('amount', {
partitionKey: 'customer-123',
where: { status: 'completed' }
});
// Returns: number | null
const avgAge = await db.users.avg('age', {
partitionKey: 'tenant-1'
});
// Returns: number | null
const minPrice = await db.products.min('price', {
partitionKey: 'category-electronics'
});
// Returns: number | null
const maxPrice = await db.products.max('price', {
partitionKey: 'category-electronics'
});
// Returns: number | nullSee Aggregations Guide for complete documentation.
Query Operators
String Operators
where: {
name: { contains: 'John' }, // CONTAINS(c.name, 'John')
email: { startsWith: 'user' }, // STARTSWITH(c.email, 'user')
bio: { endsWith: 'developer' } // ENDSWITH(c.bio, 'developer')
}Number Operators
where: {
age: { gt: 21 }, // c.age > 21 (greater than)
score: { gte: 85 }, // c.score >= 85 (greater than or equal)
rating: { lt: 5 }, // c.rating < 5 (less than)
count: { lte: 100 }, // c.count <= 100 (less than or equal)
// Multiple conditions (range)
age: { gte: 18, lte: 65 } // 18 <= c.age <= 65
}Array Operators
where: {
tags: { contains: 'javascript' }, // CONTAINS(c.tags, 'javascript')
skills: { containsAny: ['react', 'node'] }, // Any element matches (planned)
permissions: { containsAll: ['read', 'write'] } // All elements must match (planned)
}Note: containsAny and containsAll are planned features. Currently, use contains for single element checks or raw SQL queries for complex array operations.
Boolean Operators
where: {
isActive: true, // Exact match
isActive: { equals: true }, // Explicit equals (same as above)
isVerified: false
}Date Operators
where: {
createdAt: { gte: new Date('2024-01-01') }, // Greater than or equal
publishedAt: { lt: new Date() }, // Less than
lastLoginAt: { lte: new Date() } // Less than or equal
}Nested Object Queries
where: {
profile: {
settings: {
theme: 'dark',
notifications: true
}
}
}Combining Operators
where: {
// Multiple conditions (AND logic)
isActive: true,
age: { gte: 18, lte: 65 },
name: { startsWith: 'John' },
tags: { contains: 'developer' }
}Query Options
where - Filter conditions
where: {
// Exact match
isActive: true,
// Comparisons
age: { gte: 18, lte: 65 },
// String operations
name: { startsWith: 'John' },
bio: { contains: 'developer' },
// Array operations
tags: { contains: 'javascript' },
// Nested objects
profile: {
settings: { theme: 'dark' }
}
}select - Choose fields to return
select: {
id: true,
name: true,
email: true,
profile: {
website: true
}
}
// Returns only selected fieldsorderBy - Sort results
orderBy: {
age: 'desc', // Sort by age descending
name: 'asc' // Then by name ascending
}take - Limit results (maximum number)
take: 10 // Return maximum 10 documentsskip - Offset results
skip: 20 // Skip first 20 documents (for pagination)partitionKey - Scope query to partition
partitionKey: 'john@example.com' // Single value
partitionKey: ['tenant_1', 'org_a'] // Composite partition key (array)enableCrossPartitionQuery - Allow expensive cross-partition queries
enableCrossPartitionQuery: true // Must explicitly opt-inClient Configuration
Connection Options:
const db = await createClient({
// Option 1: Connection string (recommended)
connectionString: process.env.COSMOS_CONNECTION_STRING!,
// Option 2: Explicit endpoint + key
endpoint: 'https://myaccount.documents.azure.com:443/',
key: process.env.COSMOS_KEY!,
// Required: Database name
database: 'myapp',
// Optional: Container validation mode
mode: 'auto-create', // 'auto-create' | 'verify' | 'skip'
// Optional: Retry configuration
retryOptions: {
maxRetries: 3, // Default: 3
initialDelay: 100, // Default: 100ms
maxDelay: 5000 // Default: 5000ms
}
}).withContainers({ users, posts });Configuration Options:
| Option | Type | Default | Description |
|---|---|---|---|
endpoint | string | - | Azure CosmosDB endpoint URL |
key | string | - | Azure CosmosDB master key |
connectionString | string | - | Full connection string (alternative to endpoint+key) |
database | string | Required | Database name |
mode | ContainerMode | 'verify' | Container management mode |
retryOptions | object | See below | Retry configuration |
Container Modes:
'auto-create'- Automatically creates database and containers if they don't exist'verify'- Validates that database and containers exist with correct configuration (production default)'skip'- Skips all checks for maximum performance
Retry Options:
retryOptions: {
maxRetries?: number; // Default: 3
initialDelay?: number; // Default: 100ms
maxDelay?: number; // Default: 5000ms
}Note: Client creation is async. It validates and optionally creates containers based on the mode parameter. See Getting Started Guide for detailed mode documentation.
Container Registration:
.withContainers({
users,
posts,
comments
// Add all your container schemas here
})Database Operations
Container Management
listOrphanedContainers()
Lists containers in the database that are not registered in your schema. Useful for identifying containers that may need cleanup.
const orphaned = await db.listOrphanedContainers();
// Returns: string[] - Array of container names not in schemadeleteContainers(names)
Deletes specific containers by name. Use with caution as this permanently removes containers and all their data.
await db.deleteContainers(['old_container', 'temp_container']);pruneContainers(options)
Removes all orphaned containers (containers not in your schema). Requires explicit confirmation.
// Prune all orphaned containers (requires confirmation)
await db.pruneContainers({ confirm: true });
// Get list of containers that would be pruned without deleting
const orphaned = await db.listOrphanedContainers();
console.log('Would delete:', orphaned);⚠️ Warning: These operations permanently delete containers and all their data. Always back up important data before using these methods.
This documentation provides comprehensive coverage of CosmosQL's API. The library enforces CosmosDB best practices at the type level, preventing costly mistakes before they reach production.
Next Steps:
-
Install CosmosQL:
npm install cosmosql -
Define your schema using the field types above
-
Start querying with full type safety
For questions or issues, visit the GitHub repository or Discord community.
Client & Configuration
Complete reference for creating and configuring CosmosQL clients
View on separate page →Client Creation
Create a CosmosQL client with connection options:
const db = await createClient({
// Option 1: Connection string (recommended)
connectionString: process.env.COSMOS_CONNECTION_STRING!,
// Option 2: Explicit endpoint + key
endpoint: 'https://myaccount.documents.azure.com:443/',
key: process.env.COSMOS_KEY!,
// Required: Database name
database: 'myapp',
// Optional: Container validation mode
mode: 'auto-create', // 'auto-create' | 'verify' | 'skip'
// Optional: Retry configuration
retryOptions: {
maxRetries: 3, // Default: 3
initialDelay: 100, // Default: 100ms
maxDelay: 5000 // Default: 5000ms
}
}).withContainers({ users, posts });Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
endpoint | string | - | Azure CosmosDB endpoint URL |
key | string | - | Azure CosmosDB master key |
connectionString | string | - | Full connection string (alternative to endpoint+key) |
database | string | Required | Database name |
mode | ContainerMode | 'verify' | Container management mode |
retryOptions | object | See below | Retry configuration |
Container Modes
'auto-create'- Automatically creates database and containers if they don't exist'verify'- Validates that database and containers exist with correct configuration (production default)'skip'- Skips all checks for maximum performance
Retry Options
retryOptions: {
maxRetries?: number; // Default: 3
initialDelay?: number; // Default: 100ms
maxDelay?: number; // Default: 5000ms
}Note: Client creation is async. It validates and optionally creates containers based on the mode parameter. See Getting Started for detailed mode documentation.
Container Registration
.withContainers({
users,
posts,
comments
// Add all your container schemas here
})Database Operations
Container Management
listOrphanedContainers()
Lists containers in the database that are not registered in your schema. Useful for identifying containers that may need cleanup.
const orphaned = await db.listOrphanedContainers();
// Returns: string[] - Array of container names not in schemadeleteContainers(names)
Deletes specific containers by name. Use with caution as this permanently removes containers and all their data.
await db.deleteContainers(['old_container', 'temp_container']);pruneContainers(options)
Removes all orphaned containers (containers not in your schema). Requires explicit confirmation.
// Prune all orphaned containers (requires confirmation)
await db.pruneContainers({ confirm: true });
// Get list of containers that would be pruned without deleting
const orphaned = await db.listOrphanedContainers();
console.log('Would delete:', orphaned);⚠️ Warning: These operations permanently delete containers and all their data. Always back up important data before using these methods.
Query Methods
Complete reference for querying documents with findUnique, findMany, and query
View on separate page →Query Operations
findUnique(options)
Retrieves a single document by ID and partition key. This is the most efficient operation (point read).
const user = await db.users.findUnique({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
},
select: {
name: true,
email: true
}
});
// Returns: { name: string; email: string } | nullfindMany(options)
Queries multiple documents with filtering, sorting, and pagination. Optionally include aggregations to get both data and statistics in a single query.
// Without aggregations
const users = await db.users.findMany({
partitionKey: 'john@example.com',
where: {
isActive: true,
age: { gte: 18 }
},
select: { id: true, name: true },
orderBy: { age: 'desc' },
take: 10,
skip: 0
});
// Returns: Array<{ id: string; name: string }>
// With aggregations
const result = await db.users.findMany({
partitionKey: 'john@example.com',
where: { isActive: true },
aggregate: {
_count: true,
_avg: { age: true },
_sum: { score: true },
_min: { createdAt: true },
_max: { createdAt: true }
}
});
// Returns: {
// data: User[],
// _count: number,
// _avg: { age: number | null },
// _sum: { score: number | null },
// _min: { createdAt: Date | null },
// _max: { createdAt: Date | null }
// }See Aggregations Guide for complete documentation.
query<T>(options)
Execute raw SQL queries for advanced use cases.
const result = await db.users.query<{ count: number }>({
sql: 'SELECT COUNT(1) as count FROM c WHERE c.isActive = true',
parameters: [
{ name: '@active', value: true }
],
partitionKey: 'john@example.com' // Optional: for partition-scoped queries
});
// Returns: Array<T>Query Options
where - Filter conditions
where: {
// Exact match
isActive: true,
// Comparisons
age: { gte: 18, lte: 65 },
// String operations
name: { startsWith: 'John' },
bio: { contains: 'developer' },
// Array operations
tags: { contains: 'javascript' },
// Nested objects
profile: {
settings: { theme: 'dark' }
}
}select - Choose fields to return
select: {
id: true,
name: true,
email: true,
profile: {
website: true
}
}
// Returns only selected fieldsorderBy - Sort results
orderBy: {
age: 'desc', // Sort by age descending
name: 'asc' // Then by name ascending
}take - Limit results (maximum number)
take: 10 // Return maximum 10 documentsskip - Offset results
skip: 20 // Skip first 20 documents (for pagination)partitionKey - Scope query to partition
partitionKey: 'john@example.com' // Single value
partitionKey: ['tenant_1', 'org_a'] // Composite partition key (array)enableCrossPartitionQuery - Allow expensive cross-partition queries
enableCrossPartitionQuery: true // Must explicitly opt-inQuery Operators
String Operators
where: {
name: { contains: 'John' }, // CONTAINS(c.name, 'John')
email: { startsWith: 'user' }, // STARTSWITH(c.email, 'user')
bio: { endsWith: 'developer' } // ENDSWITH(c.bio, 'developer')
}Number Operators
where: {
age: { gt: 21 }, // c.age > 21 (greater than)
score: { gte: 85 }, // c.score >= 85 (greater than or equal)
rating: { lt: 5 }, // c.rating < 5 (less than)
count: { lte: 100 }, // c.count <= 100 (less than or equal)
// Multiple conditions (range)
age: { gte: 18, lte: 65 } // 18 <= c.age <= 65
}Array Operators
where: {
tags: { contains: 'javascript' }, // CONTAINS(c.tags, 'javascript')
skills: { containsAny: ['react', 'node'] }, // Any element matches (planned)
permissions: { containsAll: ['read', 'write'] } // All elements must match (planned)
}Note: containsAny and containsAll are planned features. Currently, use contains for single element checks or raw SQL queries for complex array operations.
Boolean Operators
where: {
isActive: true, // Exact match
isActive: { equals: true }, // Explicit equals (same as above)
isVerified: false
}Date Operators
where: {
createdAt: { gte: new Date('2024-01-01') }, // Greater than or equal
publishedAt: { lt: new Date() }, // Less than
lastLoginAt: { lte: new Date() } // Less than or equal
}Nested Object Queries
where: {
profile: {
settings: {
theme: 'dark',
notifications: true
}
}
}Combining Operators
where: {
// Multiple conditions (AND logic)
isActive: true,
age: { gte: 18, lte: 65 },
name: { startsWith: 'John' },
tags: { contains: 'developer' }
}Schema Definition
Complete reference for field types and container configurations
View on separate page →Field Types
CosmosQL provides a fluent API for defining field types in your schema:
// Primitive types
field.string() // String type
field.number() // Number type
field.boolean() // Boolean type
field.date() // Date type (stored as ISO strings)
// Complex types
field.array(type) // Array of any field type
field.object(schema) // Nested object with its own schema
// Modifiers
.optional() // Makes field optional (can be undefined)
.default(value) // Provides default value for optional fieldsExample Schema with All Field Types
const users = container('users', {
// Required fields
id: field.string(),
email: field.string(),
name: field.string(),
age: field.number(),
isActive: field.boolean(),
createdAt: field.date(),
// Optional fields
bio: field.string().optional(),
lastLoginAt: field.date().optional(),
// Fields with defaults
viewCount: field.number().default(0),
settings: field.object({
theme: field.string().default('light'),
notifications: field.boolean().default(true)
}).optional(),
// Arrays
tags: field.array(field.string()),
scores: field.array(field.number()),
// Nested objects
profile: field.object({
website: field.string().optional(),
location: field.string().optional()
}).optional()
}).partitionKey('email');Container Configuration
Containers can be configured with throughput, indexing policies, and more:
const posts = container('posts', {
id: field.string(),
userId: field.string(),
title: field.string(),
content: field.string(),
tags: field.array(field.string()),
createdAt: field.date()
})
.partitionKey('userId')
.throughput(400) // Optional: Set RU/s (Request Units per second)
.indexing({ // Optional: Configure indexing policy
automatic: true,
includedPaths: [
{ path: '/title/?' },
{ path: '/tags/[]/?' }
],
excludedPaths: [
{ path: '/content/?' }
],
compositeIndexes: [
[
{ path: '/createdAt', order: 'ascending' },
{ path: '/userId', order: 'ascending' }
]
]
});Container Configuration Options
.partitionKey(field)- Required: Specifies the partition key field.throughput(ru)- Optional: Sets RU/s for the container (default: auto-scale).indexing(policy)- Optional: Configures indexing policy
Indexing Policy Options
indexing: {
automatic: boolean; // Enable automatic indexing
includedPaths?: Array<{ // Paths to index
path: string; // Path pattern (e.g., '/title/?')
indexes?: Array<{
kind: 'Range' | 'Hash'; // Index kind
precision?: number; // Precision for range indexes
dataType: 'String' | 'Number' // Data type
}>
}>;
excludedPaths?: Array<{ // Paths to exclude from indexing
path: string;
}>;
compositeIndexes?: Array< // Composite indexes for multi-field sorting
Array<{
path: string;
order: 'ascending' | 'descending';
}>
>;
spatialIndexes?: Array<{ // Spatial indexes (advanced)
path: string;
types: Array<'Point' | 'LineString' | 'Polygon' | 'MultiPolygon'>;
}>;
}See Core Concepts for guidance on choosing partition keys.
Type System Deep Dive
Understanding how CosmosQL's type inference works and leveraging it effectively
View on separate page →How Type Inference Works
CosmosQL leverages TypeScript's powerful type inference to provide compile-time safety:
const users = container('users', {
id: field.string(),
email: field.string(),
name: field.string().optional(),
age: field.number()
}).partitionKey('email');
// The type is automatically inferred:
type User = typeof users.infer;
// {
// id: string;
// email: string;
// name?: string;
// age: number;
// }
// Query results are typed automatically
const user = await db.users.findUnique({
where: { id: '123', email: 'john@example.com' }
});
// Type: User | null (no manual type annotation needed!)
// Select creates a new type
const partial = await db.users.findUnique({
where: { id: '123', email: 'john@example.com' },
select: { name: true, age: true }
});
// Type: { name?: string; age: number } | nullCustom Type Errors
When you make a mistake, CosmosQL provides helpful error messages:
// Missing partition key
const user = await db.users.findUnique({
where: { id: '123' }
});
// Error message in IDE:
// ❌ PARTITION KEY REQUIRED
//
// This operation requires a partition key to avoid expensive cross-partition queries.
//
// 💡 Fix: Add the partition key to your where clause:
// where: { id: '123', email: 'john@example.com' }
//
// 📖 Learn more about partition keys: https://cosmosql.dev/docs/partition-keysType Guards
Runtime type checking when needed:
import { isCosmosError } from 'cosmosql';
try {
await db.users.create({ data: user });
} catch (error) {
if (isCosmosError(error)) {
// error is now typed as CosmosError
console.log(error.code, error.statusCode, error.retryAfter);
}
}Write Methods
Complete reference for creating, updating, and deleting documents
View on separate page →Write Operations
create(options)
Creates a single document.
const user = await db.users.create({
data: {
id: 'user_123',
email: 'john@example.com',
name: 'John Doe',
age: 30,
createdAt: new Date()
// isActive will use default value if defined
}
});
// Returns: Full user object (fully typed)createMany(options)
Creates multiple documents in a single operation. All documents must share the same partition key.
await db.users.createMany({
data: [
{ id: 'user_1', email: 'user1@test.com', name: 'User 1', age: 25 },
{ id: 'user_2', email: 'user2@test.com', name: 'User 2', age: 30 }
],
partitionKey: 'shared@email.com' // All must share partition key
});update(options)
Updates a single document by ID and partition key.
await db.users.update({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
},
data: {
age: 31,
name: 'John Smith'
}
});
// Returns: Updated user objectupdateMany(options)
Updates multiple documents matching criteria. Can be used within a partition or across partitions.
// Update within a partition
await db.users.updateMany({
partitionKey: 'tenant-1',
where: { isActive: false },
data: { status: 'archived' }
});
// Update across partitions (expensive)
await db.users.updateMany({
enableCrossPartitionQuery: true,
where: { lastLogin: { lt: new Date('2024-01-01') } },
data: { isActive: false }
});upsert(options)
Updates a document if it exists, creates it if it doesn't.
await db.users.upsert({
where: {
id: 'user_123',
email: 'john@example.com'
},
create: {
id: 'user_123',
email: 'john@example.com',
name: 'New User',
age: 25
},
update: {
age: 26,
name: 'Updated Name'
}
});delete(options)
Deletes a single document by ID and partition key.
await db.users.delete({
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
}
});deleteMany(options)
Deletes multiple documents matching criteria. Can be used within a partition or across partitions.
// Delete within a partition
const deletedCount = await db.users.deleteMany({
partitionKey: 'tenant-1',
where: {
isActive: false,
createdAt: { lt: new Date('2023-01-01') }
}
});
// Delete across partitions (expensive)
await db.users.deleteMany({
enableCrossPartitionQuery: true,
where: { status: 'deleted' }
});Write Options
where - Document identification
For single-document operations (update, delete, upsert):
where: {
id: 'user_123',
email: 'john@example.com' // Partition key required
}For multi-document operations (updateMany, deleteMany):
where: {
isActive: false,
createdAt: { lt: new Date('2023-01-01') }
}data - Update data
data: {
// Direct field updates
name: 'New Name',
age: 31,
// Dynamic updates (using function)
views: (doc) => doc.views + 1,
// Nested object updates
profile: {
name: 'John',
avatar: 'url'
}
}partitionKey - Scope to partition
partitionKey: 'john@example.com' // Single value
partitionKey: ['tenant_1', 'org_a'] // Composite partition key (array)enableCrossPartitionQuery - Allow cross-partition operations
enableCrossPartitionQuery: true // Must explicitly opt-in for multi-document operationsSee Creating Documents, Updating Documents, and Deleting Documents for detailed guides.