# SkillPatch skill: azure-cosmos-ts

This skill provides comprehensive guidance for using the Azure Cosmos DB JavaScript/TypeScript SDK (@azure/cosmos) for data plane operations. It covers authentication (Entra token and key-based), CRUD operations on documents (create, read, update/replace, upsert, delete), database and container setup, and includes ready-to-use TypeScript code snippets. It targets the NoSQL API and distinguishes itself from the ARM management SDK.

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/azure-cosmos-ts
curl -sSL https://skillpatch.dev/install_skill/azure-cosmos-ts | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (3)

- `SKILL.md`
- `references/bulk-operations.md`
- `references/query-patterns.md`


### `SKILL.md`

````markdown
---
name: azure-cosmos-ts
description: |
  Azure Cosmos DB JavaScript/TypeScript SDK (@azure/cosmos) for data plane operations. Use for CRUD operations on documents, queries, bulk operations, and container management. Triggers: "Cosmos DB", "@azure/cosmos", "CosmosClient", "document CRUD", "NoSQL queries", "bulk operations", "partition key", "container.items".
license: MIT
metadata:
  author: Microsoft
  version: "1.0.0"
  package: '@azure/cosmos'
---

# @azure/cosmos (TypeScript/JavaScript)

Data plane SDK for Azure Cosmos DB NoSQL API operations — CRUD on documents, queries, bulk operations.

> **⚠️ Data vs Management Plane**
> - **This SDK (@azure/cosmos)**: CRUD operations on documents, queries, stored procedures
> - **Management SDK (@azure/arm-cosmosdb)**: Create accounts, databases, containers via ARM

## Installation

```bash
npm install @azure/cosmos @azure/identity
```

**Current Version**: 4.9.0  
**Node.js**: >= 20.0.0

## Environment Variables

```bash
COSMOS_ENDPOINT=https://<account>.documents.azure.com:443/
COSMOS_DATABASE=<database-name>
COSMOS_CONTAINER=<container-name>
# For key-based auth only (prefer AAD)
COSMOS_KEY=<account-key>
AZURE_TOKEN_CREDENTIALS=prod # Required only if DefaultAzureCredential is used in production
```

## Authentication

### Microsoft Entra Token Credential (Recommended)

```typescript
import { CosmosClient } from "@azure/cosmos";
import { DefaultAzureCredential, ManagedIdentityCredential } from "@azure/identity";

// Local dev: DefaultAzureCredential. Production: set AZURE_TOKEN_CREDENTIALS=prod or AZURE_TOKEN_CREDENTIALS=<specific_credential>
const credential = new DefaultAzureCredential({requiredEnvVars: ["AZURE_TOKEN_CREDENTIALS"]});
// Or use a specific credential directly in production:
// See https://learn.microsoft.com/javascript/api/overview/azure/identity-readme?view=azure-node-latest#credential-classes
// const credential = new ManagedIdentityCredential();

const client = new CosmosClient({
  endpoint: process.env.COSMOS_ENDPOINT!,
  aadCredentials: credential,
});
```

### Key-Based Authentication

```typescript
import { CosmosClient } from "@azure/cosmos";

// Option 1: Endpoint + Key
const client = new CosmosClient({
  endpoint: process.env.COSMOS_ENDPOINT!,
  key: process.env.COSMOS_KEY!,
});

// Option 2: Connection String
const client = new CosmosClient(process.env.COSMOS_CONNECTION_STRING!);
```

## Resource Hierarchy

```
CosmosClient
└── Database
    └── Container
        ├── Items (documents)
        ├── Scripts (stored procedures, triggers, UDFs)
        └── Conflicts
```

## Core Operations

### Database & Container Setup

```typescript
const { database } = await client.databases.createIfNotExists({
  id: "my-database",
});

const { container } = await database.containers.createIfNotExists({
  id: "my-container",
  partitionKey: { paths: ["/partitionKey"] },
});
```

### Create Document

```typescript
interface Product {
  id: string;
  partitionKey: string;
  name: string;
  price: number;
}

const item: Product = {
  id: "product-1",
  partitionKey: "electronics",
  name: "Laptop",
  price: 999.99,
};

const { resource } = await container.items.create<Product>(item);
```

### Read Document

```typescript
const { resource } = await container
  .item("product-1", "electronics") // id, partitionKey
  .read<Product>();

if (resource) {
  console.log(resource.name);
}
```

### Update Document (Replace)

```typescript
const { resource: existing } = await container
  .item("product-1", "electronics")
  .read<Product>();

if (existing) {
  existing.price = 899.99;
  const { resource: updated } = await container
    .item("product-1", "electronics")
    .replace<Product>(existing);
}
```

### Upsert Document

```typescript
const item: Product = {
  id: "product-1",
  partitionKey: "electronics",
  name: "Laptop Pro",
  price: 1299.99,
};

const { resource } = await container.items.upsert<Product>(item);
```

### Delete Document

```typescript
await container.item("product-1", "electronics").delete();
```

### Patch Document (Partial Update)

```typescript
import { PatchOperation } from "@azure/cosmos";

const operations: PatchOperation[] = [
  { op: "replace", path: "/price", value: 799.99 },
  { op: "add", path: "/discount", value: true },
  { op: "remove", path: "/oldField" },
];

const { resource } = await container
  .item("product-1", "electronics")
  .patch<Product>(operations);
```

## Queries

### Simple Query

```typescript
const { resources } = await container.items
  .query<Product>("SELECT * FROM c WHERE c.price < 1000")
  .fetchAll();
```

### Parameterized Query (Recommended)

```typescript
import { SqlQuerySpec } from "@azure/cosmos";

const querySpec: SqlQuerySpec = {
  query: "SELECT * FROM c WHERE c.partitionKey = @category AND c.price < @maxPrice",
  parameters: [
    { name: "@category", value: "electronics" },
    { name: "@maxPrice", value: 1000 },
  ],
};

const { resources } = await container.items
  .query<Product>(querySpec)
  .fetchAll();
```

### Query with Pagination

```typescript
const queryIterator = container.items.query<Product>(querySpec, {
  maxItemCount: 10, // Items per page
});

while (queryIterator.hasMoreResults()) {
  const { resources, continuationToken } = await queryIterator.fetchNext();
  console.log(`Page with ${resources?.length} items`);
  // Use continuationToken for next page if needed
}
```

### Cross-Partition Query

```typescript
const { resources } = await container.items
  .query<Product>(
    "SELECT * FROM c WHERE c.price > 500",
    { enableCrossPartitionQuery: true }
  )
  .fetchAll();
```

## Bulk Operations

### Execute Bulk Operations

```typescript
import { BulkOperationType, OperationInput } from "@azure/cosmos";

const operations: OperationInput[] = [
  {
    operationType: BulkOperationType.Create,
    resourceBody: { id: "1", partitionKey: "cat-a", name: "Item 1" },
  },
  {
    operationType: BulkOperationType.Upsert,
    resourceBody: { id: "2", partitionKey: "cat-a", name: "Item 2" },
  },
  {
    operationType: BulkOperationType.Read,
    id: "3",
    partitionKey: "cat-b",
  },
  {
    operationType: BulkOperationType.Replace,
    id: "4",
    partitionKey: "cat-b",
    resourceBody: { id: "4", partitionKey: "cat-b", name: "Updated" },
  },
  {
    operationType: BulkOperationType.Delete,
    id: "5",
    partitionKey: "cat-c",
  },
  {
    operationType: BulkOperationType.Patch,
    id: "6",
    partitionKey: "cat-c",
    resourceBody: {
      operations: [{ op: "replace", path: "/name", value: "Patched" }],
    },
  },
];

const response = await container.items.executeBulkOperations(operations);

response.forEach((result, index) => {
  if (result.statusCode >= 200 && result.statusCode < 300) {
    console.log(`Operation ${index} succeeded`);
  } else {
    console.error(`Operation ${index} failed: ${result.statusCode}`);
  }
});
```

## Partition Keys

### Simple Partition Key

```typescript
const { container } = await database.containers.createIfNotExists({
  id: "products",
  partitionKey: { paths: ["/category"] },
});
```

### Hierarchical Partition Key (MultiHash)

```typescript
import { PartitionKeyDefinitionVersion, PartitionKeyKind } from "@azure/cosmos";

const { container } = await database.containers.createIfNotExists({
  id: "orders",
  partitionKey: {
    paths: ["/tenantId", "/userId", "/sessionId"],
    version: PartitionKeyDefinitionVersion.V2,
    kind: PartitionKeyKind.MultiHash,
  },
});

// Operations require array of partition key values
const { resource } = await container.items.create({
  id: "order-1",
  tenantId: "tenant-a",
  userId: "user-123",
  sessionId: "session-xyz",
  total: 99.99,
});

// Read with hierarchical partition key
const { resource: order } = await container
  .item("order-1", ["tenant-a", "user-123", "session-xyz"])
  .read();
```

## Error Handling

```typescript
import { ErrorResponse } from "@azure/cosmos";

try {
  const { resource } = await container.item("missing", "pk").read();
} catch (error) {
  if (error instanceof ErrorResponse) {
    switch (error.code) {
      case 404:
        console.log("Document not found");
        break;
      case 409:
        console.log("Conflict - document already exists");
        break;
      case 412:
        console.log("Precondition failed (ETag mismatch)");
        break;
      case 429:
        console.log("Rate limited - retry after:", error.retryAfterInMs);
        break;
      default:
        console.error(`Cosmos error ${error.code}: ${error.message}`);
    }
  }
  throw error;
}
```

## Optimistic Concurrency (ETags)

```typescript
// Read with ETag
const { resource, etag } = await container
  .item("product-1", "electronics")
  .read<Product>();

if (resource && etag) {
  resource.price = 899.99;
  
  try {
    // Replace only if ETag matches
    await container.item("product-1", "electronics").replace(resource, {
      accessCondition: { type: "IfMatch", condition: etag },
    });
  } catch (error) {
    if (error instanceof ErrorResponse && error.code === 412) {
      console.log("Document was modified by another process");
    }
  }
}
```

## TypeScript Types Reference

```typescript
import {
  // Client & Resources
  CosmosClient,
  Database,
  Container,
  Item,
  Items,
  
  // Operations
  OperationInput,
  BulkOperationType,
  PatchOperation,
  
  // Queries
  SqlQuerySpec,
  SqlParameter,
  FeedOptions,
  
  // Partition Keys
  PartitionKeyDefinition,
  PartitionKeyDefinitionVersion,
  PartitionKeyKind,
  
  // Responses
  ItemResponse,
  FeedResponse,
  ResourceResponse,
  
  // Errors
  ErrorResponse,
} from "@azure/cosmos";
```

## Best Practices

1. **Use Microsoft Entra Token Credential** — Use `DefaultAzureCredential` for local development; use `ManagedIdentityCredential` or `WorkloadIdentityCredential` for production
2. **Always use parameterized queries** — Prevents injection, improves plan caching
3. **Specify partition key** — Avoid cross-partition queries when possible
4. **Use bulk operations** — For multiple writes, use `executeBulkOperations`
5. **Handle 429 errors** — Implement retry logic with exponential backoff
6. **Use ETags for concurrency** — Prevent lost updates in concurrent scenarios
7. **Close client on shutdown** — Call `client.dispose()` in cleanup

## Common Patterns

### Service Layer Pattern

```typescript
export class ProductService {
  private container: Container;

  constructor(client: CosmosClient) {
    this.container = client
      .database(process.env.COSMOS_DATABASE!)
      .container(process.env.COSMOS_CONTAINER!);
  }

  async getById(id: string, category: string): Promise<Product | null> {
    try {
      const { resource } = await this.container
        .item(id, category)
        .read<Product>();
      return resource ?? null;
    } catch (error) {
      if (error instanceof ErrorResponse && error.code === 404) {
        return null;
      }
      throw error;
    }
  }

  async create(product: Omit<Product, "id">): Promise<Product> {
    const item = { ...product, id: crypto.randomUUID() };
    const { resource } = await this.container.items.create<Product>(item);
    return resource!;
  }

  async findByCategory(category: string): Promise<Product[]> {
    const querySpec: SqlQuerySpec = {
      query: "SELECT * FROM c WHERE c.partitionKey = @category",
      parameters: [{ name: "@category", value: category }],
    };
    const { resources } = await this.container.items
      .query<Product>(querySpec)
      .fetchAll();
    return resources;
  }
}
```

## Related SDKs

| SDK | Purpose | Install |
|-----|---------|---------|
| `@azure/cosmos` | Data plane (this SDK) | `npm install @azure/cosmos` |
| `@azure/arm-cosmosdb` | Management plane (ARM) | `npm install @azure/arm-cosmosdb` |
| `@azure/identity` | Authentication | `npm install @azure/identity` |

````


### `references/bulk-operations.md`

````markdown
# Bulk Operations Reference

High-throughput bulk operations for Azure Cosmos DB using the @azure/cosmos TypeScript SDK.

## Overview

Bulk operations enable efficient batch processing of Create, Upsert, Read, Replace, Delete, and Patch operations with automatic batching and retry handling.

## BulkOperationType Enum

```typescript
import { BulkOperationType } from "@azure/cosmos";

enum BulkOperationType {
  Create = "Create",
  Upsert = "Upsert",
  Read = "Read",
  Replace = "Replace",
  Delete = "Delete",
  Patch = "Patch"
}
```

## OperationInput Interface

```typescript
import { OperationInput, PatchOperation } from "@azure/cosmos";

// Base operation types
interface CreateOperationInput {
  operationType: BulkOperationType.Create;
  resourceBody: Record<string, unknown>;
  partitionKey?: PartitionKey;
}

interface UpsertOperationInput {
  operationType: BulkOperationType.Upsert;
  resourceBody: Record<string, unknown>;
  partitionKey?: PartitionKey;
}

interface ReadOperationInput {
  operationType: BulkOperationType.Read;
  id: string;
  partitionKey: PartitionKey;
}

interface ReplaceOperationInput {
  operationType: BulkOperationType.Replace;
  id: string;
  resourceBody: Record<string, unknown>;
  partitionKey?: PartitionKey;
}

interface DeleteOperationInput {
  operationType: BulkOperationType.Delete;
  id: string;
  partitionKey: PartitionKey;
}

interface PatchOperationInput {
  operationType: BulkOperationType.Patch;
  id: string;
  partitionKey: PartitionKey;
  resourceBody: {
    operations: PatchOperation[];
    condition?: string;
  };
}

type OperationInput = 
  | CreateOperationInput
  | UpsertOperationInput
  | ReadOperationInput
  | ReplaceOperationInput
  | DeleteOperationInput
  | PatchOperationInput;
```

## Basic Bulk Operations

```typescript
import { 
  CosmosClient, 
  BulkOperationType, 
  OperationInput,
  BulkOperationResponse 
} from "@azure/cosmos";

const client = new CosmosClient({ endpoint, key });
const container = client.database("mydb").container("mycontainer");

// Mixed bulk operations
const operations: OperationInput[] = [
  // Create
  {
    operationType: BulkOperationType.Create,
    resourceBody: { 
      id: "item-1", 
      partitionKey: "category-a", 
      name: "Item 1",
      price: 10.99
    }
  },
  // Upsert
  {
    operationType: BulkOperationType.Upsert,
    resourceBody: { 
      id: "item-2", 
      partitionKey: "category-a", 
      name: "Item 2",
      price: 20.99
    }
  },
  // Read
  {
    operationType: BulkOperationType.Read,
    id: "item-3",
    partitionKey: "category-b"
  },
  // Replace
  {
    operationType: BulkOperationType.Replace,
    id: "item-4",
    partitionKey: "category-b",
    resourceBody: { 
      id: "item-4", 
      partitionKey: "category-b", 
      name: "Updated Item 4",
      price: 15.99
    }
  },
  // Delete
  {
    operationType: BulkOperationType.Delete,
    id: "item-5",
    partitionKey: "category-c"
  }
];

const response: BulkOperationResponse = await container.items.bulk(operations);

// Process results
response.forEach((result, index) => {
  if (result.statusCode >= 200 && result.statusCode < 300) {
    console.log(`Operation ${index} succeeded: ${result.statusCode}`);
    if (result.resourceBody) {
      console.log(`  Resource: ${JSON.stringify(result.resourceBody)}`);
    }
  } else {
    console.error(`Operation ${index} failed: ${result.statusCode}`);
  }
});
```

## Bulk Create Pattern

```typescript
interface Product {
  id: string;
  partitionKey: string;
  name: string;
  price: number;
}

async function bulkCreate(
  container: Container,
  items: Product[]
): Promise<{ succeeded: number; failed: number }> {
  const operations: OperationInput[] = items.map(item => ({
    operationType: BulkOperationType.Create,
    resourceBody: item
  }));

  const response = await container.items.bulk(operations);

  let succeeded = 0;
  let failed = 0;

  response.forEach((result, index) => {
    if (result.statusCode === 201) {
      succeeded++;
    } else {
      failed++;
      console.error(`Failed to create item ${items[index].id}: ${result.statusCode}`);
    }
  });

  return { succeeded, failed };
}

// Usage
const products: Product[] = [
  { id: "p1", partitionKey: "electronics", name: "Laptop", price: 999 },
  { id: "p2", partitionKey: "electronics", name: "Phone", price: 599 },
  { id: "p3", partitionKey: "clothing", name: "Shirt", price: 29 },
];

const result = await bulkCreate(container, products);
console.log(`Created: ${result.succeeded}, Failed: ${result.failed}`);
```

## Bulk Upsert Pattern

```typescript
async function bulkUpsert<T extends { id: string }>(
  container: Container,
  items: T[]
): Promise<BulkOperationResponse> {
  const operations: OperationInput[] = items.map(item => ({
    operationType: BulkOperationType.Upsert,
    resourceBody: item as Record<string, unknown>
  }));

  return container.items.bulk(operations);
}

// Usage - creates if not exists, updates if exists
const updates = [
  { id: "p1", partitionKey: "electronics", name: "Laptop Pro", price: 1299 },
  { id: "p4", partitionKey: "electronics", name: "Tablet", price: 399 },
];

await bulkUpsert(container, updates);
```

## Bulk Delete Pattern

```typescript
interface DeleteItem {
  id: string;
  partitionKey: string;
}

async function bulkDelete(
  container: Container,
  items: DeleteItem[]
): Promise<{ deleted: number; notFound: number; errors: number }> {
  const operations: OperationInput[] = items.map(item => ({
    operationType: BulkOperationType.Delete,
    id: item.id,
    partitionKey: item.partitionKey
  }));

  const response = await container.items.bulk(operations);

  let deleted = 0;
  let notFound = 0;
  let errors = 0;

  response.forEach((result) => {
    if (result.statusCode === 204) {
      deleted++;
    } else if (result.statusCode === 404) {
      notFound++;
    } else {
      errors++;
    }
  });

  return { deleted, notFound, errors };
}

// Usage
const toDelete: DeleteItem[] = [
  { id: "p1", partitionKey: "electronics" },
  { id: "p2", partitionKey: "electronics" },
];

const deleteResult = await bulkDelete(container, toDelete);
console.log(`Deleted: ${deleteResult.deleted}, Not found: ${deleteResult.notFound}`);
```

## Bulk Patch Pattern

```typescript
import { PatchOperation } from "@azure/cosmos";

interface PatchItem {
  id: string;
  partitionKey: string;
  operations: PatchOperation[];
}

async function bulkPatch(
  container: Container,
  items: PatchItem[]
): Promise<BulkOperationResponse> {
  const operations: OperationInput[] = items.map(item => ({
    operationType: BulkOperationType.Patch,
    id: item.id,
    partitionKey: item.partitionKey,
    resourceBody: {
      operations: item.operations
    }
  }));

  return container.items.bulk(operations);
}

// Usage - partial updates
const patches: PatchItem[] = [
  {
    id: "p1",
    partitionKey: "electronics",
    operations: [
      { op: "replace", path: "/price", value: 899 },
      { op: "add", path: "/onSale", value: true }
    ]
  },
  {
    id: "p2",
    partitionKey: "electronics",
    operations: [
      { op: "incr", path: "/viewCount", value: 1 }
    ]
  }
];

await bulkPatch(container, patches);
```

## Chunked Bulk Operations

For very large datasets, process in chunks to manage memory and handle rate limiting:

```typescript
async function bulkOperationsChunked<T extends Record<string, unknown>>(
  container: Container,
  items: T[],
  operationType: BulkOperationType.Create | BulkOperationType.Upsert,
  chunkSize: number = 100
): Promise<{ succeeded: number; failed: number }> {
  let succeeded = 0;
  let failed = 0;

  // Process in chunks
  for (let i = 0; i < items.length; i += chunkSize) {
    const chunk = items.slice(i, i + chunkSize);
    
    const operations: OperationInput[] = chunk.map(item => ({
      operationType,
      resourceBody: item
    }));

    const response = await container.items.bulk(operations);

    response.forEach(result => {
      if (result.statusCode >= 200 && result.statusCode < 300) {
        succeeded++;
      } else {
        failed++;
      }
    });

    console.log(`Processed ${Math.min(i + chunkSize, items.length)}/${items.length}`);
  }

  return { succeeded, failed };
}

// Usage
const largeDataset = generateItems(10000);
const result = await bulkOperationsChunked(container, largeDataset, BulkOperationType.Create, 100);
```

## Bulk Operation Response

```typescript
interface BulkOperationResponse extends Array<OperationResponse> {}

interface OperationResponse {
  /** HTTP status code */
  statusCode: number;
  
  /** Request charge (RUs) for this operation */
  requestCharge: number;
  
  /** ETag of the resource (for successful operations) */
  eTag?: string;
  
  /** Resource body (for read/create/upsert/replace) */
  resourceBody?: Record<string, unknown>;
}

// Common status codes
// 200 - Read successful
// 201 - Create successful
// 204 - Delete successful
// 400 - Bad request
// 404 - Not found
// 409 - Conflict (duplicate id on create)
// 412 - Precondition failed
// 429 - Rate limited
```

## Error Handling

```typescript
async function bulkWithRetry(
  container: Container,
  operations: OperationInput[],
  maxRetries: number = 3
): Promise<BulkOperationResponse> {
  let response = await container.items.bulk(operations);
  let retryCount = 0;

  while (retryCount < maxRetries) {
    const failedOps: OperationInput[] = [];
    const failedIndices: number[] = [];

    response.forEach((result, index) => {
      if (result.statusCode === 429) {
        // Rate limited - retry these
        failedOps.push(operations[index]);
        failedIndices.push(index);
      }
    });

    if (failedOps.length === 0) {
      break; // All succeeded
    }

    console.log(`Retrying ${failedOps.length} rate-limited operations...`);
    
    // Exponential backoff
    await new Promise(resolve => 
      setTimeout(resolve, Math.pow(2, retryCount) * 1000)
    );

    const retryResponse = await container.items.bulk(failedOps);
    
    // Update original response with retry results
    retryResponse.forEach((result, i) => {
      response[failedIndices[i]] = result;
    });

    retryCount++;
  }

  return response;
}
```

## Best Practices

1. **Batch by partition key** — Operations in the same partition are more efficient
2. **Use appropriate chunk sizes** — 100-500 items per batch is typical
3. **Handle 429 errors** — Implement retry with exponential backoff
4. **Monitor RU consumption** — Check `requestCharge` in responses
5. **Use Upsert for idempotency** — Safer than Create for retry scenarios
6. **Validate before bulk** — Check data before submitting large batches
7. **Process results** — Always check individual operation status codes

## Performance Considerations

| Factor | Recommendation |
|--------|----------------|
| Chunk size | 100-500 items per batch |
| Partition distribution | Spread across partitions for parallelism |
| Operation type | Upsert is safer than Create for retries |
| Error handling | Implement retry logic for 429s |
| Memory | Stream large datasets, don't load all into memory |

## See Also

- [Query Patterns Reference](./query-patterns.md)
- [Cosmos DB Bulk Execution](https://learn.microsoft.com/azure/cosmos-db/nosql/tutorial-dotnet-bulk-import)
- [Request Units (RUs)](https://learn.microsoft.com/azure/cosmos-db/request-units)

````


### `references/query-patterns.md`

````markdown
# Query Patterns Reference

Advanced query patterns for Azure Cosmos DB using the @azure/cosmos TypeScript SDK.

## Overview

Cosmos DB supports SQL-like queries with support for JSON documents. This reference covers parameterized queries, pagination, cross-partition queries, and advanced query patterns.

## SqlQuerySpec Interface

```typescript
import { SqlQuerySpec, SqlParameter } from "@azure/cosmos";

interface SqlQuerySpec {
  /** SQL query text */
  query: string;
  /** Array of parameters */
  parameters?: SqlParameter[];
}

interface SqlParameter {
  /** Parameter name (including @) */
  name: string;
  /** Parameter value */
  value: unknown;
}
```

## Parameterized Queries (Recommended)

Always use parameterized queries to prevent injection and improve plan caching.

```typescript
import { SqlQuerySpec, Container } from "@azure/cosmos";

interface Product {
  id: string;
  category: string;
  name: string;
  price: number;
  inStock: boolean;
}

// Single parameter
const querySpec: SqlQuerySpec = {
  query: "SELECT * FROM c WHERE c.category = @category",
  parameters: [
    { name: "@category", value: "electronics" }
  ]
};

const { resources } = await container.items
  .query<Product>(querySpec)
  .fetchAll();

// Multiple parameters
const rangeQuery: SqlQuerySpec = {
  query: `
    SELECT * FROM c 
    WHERE c.category = @category 
      AND c.price >= @minPrice 
      AND c.price <= @maxPrice
      AND c.inStock = @inStock
  `,
  parameters: [
    { name: "@category", value: "electronics" },
    { name: "@minPrice", value: 100 },
    { name: "@maxPrice", value: 1000 },
    { name: "@inStock", value: true }
  ]
};

const { resources: filtered } = await container.items
  .query<Product>(rangeQuery)
  .fetchAll();
```

## Pagination with Continuation Tokens

```typescript
import { FeedOptions } from "@azure/cosmos";

interface PagedResult<T> {
  items: T[];
  continuationToken?: string;
  hasMore: boolean;
}

async function queryWithPagination<T>(
  container: Container,
  querySpec: SqlQuerySpec,
  pageSize: number,
  continuationToken?: string
): Promise<PagedResult<T>> {
  const options: FeedOptions = {
    maxItemCount: pageSize,
    continuationToken
  };

  const queryIterator = container.items.query<T>(querySpec, options);
  const { resources, continuationToken: nextToken } = await queryIterator.fetchNext();

  return {
    items: resources || [],
    continuationToken: nextToken,
    hasMore: !!nextToken
  };
}

// Usage
let page = await queryWithPagination<Product>(
  container,
  { query: "SELECT * FROM c ORDER BY c.createdAt DESC" },
  10
);

console.log(`Page 1: ${page.items.length} items`);

while (page.hasMore) {
  page = await queryWithPagination<Product>(
    container,
    { query: "SELECT * FROM c ORDER BY c.createdAt DESC" },
    10,
    page.continuationToken
  );
  console.log(`Next page: ${page.items.length} items`);
}
```

## Async Iterator Pattern

```typescript
async function* queryAll<T>(
  container: Container,
  querySpec: SqlQuerySpec
): AsyncGenerator<T> {
  const queryIterator = container.items.query<T>(querySpec);
  
  while (queryIterator.hasMoreResults()) {
    const { resources } = await queryIterator.fetchNext();
    if (resources) {
      for (const item of resources) {
        yield item;
      }
    }
  }
}

// Usage
for await (const product of queryAll<Product>(container, querySpec)) {
  console.log(product.name);
}
```

## Cross-Partition Queries

```typescript
// Enable cross-partition query when partition key is not specified
const crossPartitionQuery: SqlQuerySpec = {
  query: "SELECT * FROM c WHERE c.price > @minPrice",
  parameters: [{ name: "@minPrice", value: 500 }]
};

const { resources } = await container.items
  .query<Product>(crossPartitionQuery, {
    enableCrossPartitionQuery: true
  })
  .fetchAll();

// Aggregate across partitions
const aggregateQuery: SqlQuerySpec = {
  query: "SELECT VALUE COUNT(1) FROM c WHERE c.category = @category",
  parameters: [{ name: "@category", value: "electronics" }]
};

const { resources: countResult } = await container.items
  .query<number>(aggregateQuery, { enableCrossPartitionQuery: true })
  .fetchAll();

console.log(`Total count: ${countResult[0]}`);
```

## Projection Queries

```typescript
// Select specific fields
const projectionQuery: SqlQuerySpec = {
  query: `
    SELECT c.id, c.name, c.price, c.category
    FROM c
    WHERE c.inStock = true
  `
};

interface ProductSummary {
  id: string;
  name: string;
  price: number;
  category: string;
}

const { resources } = await container.items
  .query<ProductSummary>(projectionQuery)
  .fetchAll();

// Computed properties
const computedQuery: SqlQuerySpec = {
  query: `
    SELECT 
      c.id,
      c.name,
      c.price,
      c.price * 0.9 AS discountedPrice,
      CONCAT(c.category, "-", c.id) AS sku
    FROM c
  `
};
```

## Array Queries (JOIN)

```typescript
interface Order {
  id: string;
  customerId: string;
  items: OrderItem[];
}

interface OrderItem {
  productId: string;
  quantity: number;
  price: number;
}

// Query items within arrays
const arrayQuery: SqlQuerySpec = {
  query: `
    SELECT 
      o.id AS orderId,
      o.customerId,
      i.productId,
      i.quantity,
      i.price
    FROM orders o
    JOIN i IN o.items
    WHERE i.quantity > @minQuantity
  `,
  parameters: [{ name: "@minQuantity", value: 5 }]
};

// Check if array contains value
const containsQuery: SqlQuerySpec = {
  query: `
    SELECT * FROM c 
    WHERE ARRAY_CONTAINS(c.tags, @tag)
  `,
  parameters: [{ name: "@tag", value: "featured" }]
};
```

## Aggregate Functions

```typescript
// COUNT
const countQuery = { query: "SELECT VALUE COUNT(1) FROM c" };

// SUM, AVG, MIN, MAX
const statsQuery: SqlQuerySpec = {
  query: `
    SELECT 
      COUNT(1) AS totalProducts,
      SUM(c.price) AS totalValue,
      AVG(c.price) AS averagePrice,
      MIN(c.price) AS minPrice,
      MAX(c.price) AS maxPrice
    FROM c
    WHERE c.category = @category
  `,
  parameters: [{ name: "@category", value: "electronics" }]
};

interface ProductStats {
  totalProducts: number;
  totalValue: number;
  averagePrice: number;
  minPrice: number;
  maxPrice: number;
}

const { resources } = await container.items
  .query<ProductStats>(statsQuery, { enableCrossPartitionQuery: true })
  .fetchAll();
```

## ORDER BY and TOP

```typescript
// Order by with TOP
const topQuery: SqlQuerySpec = {
  query: `
    SELECT TOP 10 *
    FROM c
    WHERE c.category = @category
    ORDER BY c.price DESC
  `,
  parameters: [{ name: "@category", value: "electronics" }]
};

// Multiple ORDER BY
const multiOrderQuery: SqlQuerySpec = {
  query: `
    SELECT * FROM c
    ORDER BY c.category ASC, c.price DESC
  `
};

// OFFSET and LIMIT (alternative to continuation tokens)
const offsetQuery: SqlQuerySpec = {
  query: `
    SELECT * FROM c
    ORDER BY c.createdAt DESC
    OFFSET @offset LIMIT @limit
  `,
  parameters: [
    { name: "@offset", value: 20 },
    { name: "@limit", value: 10 }
  ]
};
```

## String Functions

```typescript
const stringQuery: SqlQuerySpec = {
  query: `
    SELECT * FROM c
    WHERE CONTAINS(c.name, @searchTerm, true)
      OR STARTSWITH(c.name, @prefix)
  `,
  parameters: [
    { name: "@searchTerm", value: "phone" },
    { name: "@prefix", value: "Smart" }
  ]
};

// Case-insensitive search with LOWER
const caseInsensitiveQuery: SqlQuerySpec = {
  query: `
    SELECT * FROM c
    WHERE LOWER(c.name) LIKE @pattern
  `,
  parameters: [{ name: "@pattern", value: "%laptop%" }]
};
```

## FeedOptions Reference

```typescript
interface FeedOptions {
  /** Max items per page */
  maxItemCount?: number;
  
  /** Continuation token from previous page */
  continuationToken?: string;
  
  /** Enable cross-partition queries */
  enableCrossPartitionQuery?: boolean;
  
  /** Max parallelism for cross-partition queries */
  maxDegreeOfParallelism?: number;
  
  /** Partition key for scoped queries */
  partitionKey?: PartitionKey;
  
  /** Enable scan in queries (avoid if possible) */
  enableScanInQuery?: boolean;
  
  /** Populate index metrics in response */
  populateIndexMetrics?: boolean;
}
```

## Query Performance Tips

1. **Always specify partition key** — Avoids expensive cross-partition queries
2. **Use parameterized queries** — Enables query plan caching
3. **Project only needed fields** — Reduces response size and RU consumption
4. **Avoid cross-partition aggregates** — Very expensive; consider materialized views
5. **Use continuation tokens** — More efficient than OFFSET/LIMIT
6. **Check index metrics** — Use `populateIndexMetrics: true` to diagnose slow queries

## See Also

- [Bulk Operations Reference](./bulk-operations.md)
- [Azure Cosmos DB SQL Reference](https://learn.microsoft.com/azure/cosmos-db/nosql/query/)
- [Query Performance Tips](https://learn.microsoft.com/azure/cosmos-db/nosql/query-metrics)

````
