Skip to content

Partial document update with patch not working for vNext? #232

@murbanowicz

Description

@murbanowicz

PATCH Operations Don't Work in the Azure Cosmos DB Linux Emulator

Description

I ran into an issue where PATCH operations aren't supported in the Azure Cosmos DB Linux emulator. This is blocking my local tests, since PATCH works fine in the production Azure Cosmos DB environment but fails when running against the Linux emulator.

When I try to execute a partial document update using the PATCH API, the emulator returns an error saying "Invalid request format: missing patch operations array." This forces me to implement workarounds that differ from production code, which defeats the purpose of having a local emulator.

My Setup

  • Emulator image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview
  • Azure Cosmos SDK: @azure/cosmos version 4.5.1
  • Testcontainers: @testcontainers/azure-cosmosdb-emulator version 11.7.1
  • Platform: macOS/Linux (Docker-based development)
  • Connection mode: HTTP (Gateway mode)
  • Docker version: 28.3.2

What I Expected

PATCH should behave the same way it does in production, following the Azure Cosmos DB Partial Document Update docs. I should be able to use operations like:

  • set - update or add a field
  • add - add to an array or create a field
  • incr - increment a numeric field
  • remove - delete a field
  • replace - replace a value (strict)
  • move - move a value from one path to another

This is a GA feature that's been available since 2021, and it works perfectly in production Azure Cosmos DB. The emulator should support it too.

What Actually Happens

The PATCH operation fails immediately with this error:

Error: Invalid request format: missing patch operations array.
    at httpRequest (node_modules/@azure/cosmos/src/request/RequestHandler.ts:145:42)
    at node_modules/@azure/cosmos/src/retry/retryUtility.ts:124:26
    at addDiagnosticChild (node_modules/@azure/cosmos/src/utils/diagnostics.ts:68:22)
    at addDiagnosticChild (node_modules/@azure/cosmos/src/utils/diagnostics.ts:68:22)
    at ClientContext.patch (node_modules/@azure/cosmos/src/ClientContext.ts:433:24)
    at node_modules/@azure/cosmos/src/client/Item/Item.ts:575:20
    at withDiagnostics (node_modules/@azure/cosmos/src/utils/diagnostics.ts:140:27)

Full Reproduction Code

Here's a complete test that demonstrates the issue. This test passes against production Azure Cosmos DB but fails against the Linux emulator:

import { CosmosClient, PartitionKeyDefinitionVersion, PartitionKeyKind } from '@azure/cosmos';

describe('Cosmos DB PATCH Operations', () => {
  const endpoint = 'https://localhost:8081'; // or your emulator endpoint
  const key = 'C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==';
  const dbName = 'test-db';
  const containerId = 'test-container';

  const client = new CosmosClient({
    endpoint,
    key,
    connectionPolicy: {
      connectionMode: 0, // Gateway mode
      enableEndpointDiscovery: false,
    },
  });

  beforeAll(async () => {
    // Create database
    const { database } = await client.databases.createIfNotExists({ id: dbName });

    // Create container with partition key
    await database.containers.createIfNotExists({
      id: containerId,
      partitionKey: {
        paths: ['/pk'],
        kind: PartitionKeyKind.Hash,
        version: PartitionKeyDefinitionVersion.V2,
      },
    });
  });

  afterAll(async () => {
    // Cleanup
    try {
      await client.database(dbName).container(containerId).delete();
    } catch {}
  });

  it('should support basic PATCH operations', async () => {
    const container = client.database(dbName).container(containerId);

    // Create initial document
    const initialDoc = {
      id: 'doc-1',
      pk: 'partition1',
      value: 1,
      status: 'initial',
      arr: [1, 2],
      nested: { 
        count: 5,
        name: 'test' 
      },
    };

    await container.items.create(initialDoc);

    // THIS IS WHERE IT FAILS IN THE EMULATOR
    // Error: "Invalid request format: missing patch operations array."
    await container.item('doc-1', 'partition1').patch([
      { op: 'set', path: '/status', value: 'updated' },
      { op: 'incr', path: '/value', value: 10 },
      { op: 'add', path: '/arr/-', value: 3 },
      { op: 'set', path: '/nested/count', value: 10 },
      { op: 'add', path: '/newField', value: 'new value' },
    ]);

    // Read and verify
    const { resource: updated } = await container.item('doc-1', 'partition1').read();

    expect(updated.status).toBe('updated');
    expect(updated.value).toBe(11); // 1 + 10
    expect(updated.arr).toEqual([1, 2, 3]);
    expect(updated.nested.count).toBe(10);
    expect(updated.newField).toBe('new value');
  }, 30000);

  it('should support remove operations', async () => {
    const container = client.database(dbName).container(containerId);

    const doc = {
      id: 'doc-2',
      pk: 'partition1',
      fieldToRemove: 'will be deleted',
      fieldToKeep: 'will stay',
    };

    await container.items.create(doc);

    // ALSO FAILS IN EMULATOR
    await container.item('doc-2', 'partition1').patch([
      { op: 'remove', path: '/fieldToRemove' },
    ]);

    const { resource: updated } = await container.item('doc-2', 'partition1').read();

    expect(updated.fieldToRemove).toBeUndefined();
    expect(updated.fieldToKeep).toBe('will stay');
  }, 30000);

  it('should support complex nested path updates', async () => {
    const container = client.database(dbName).container(containerId);

    const doc = {
      id: 'doc-3',
      pk: 'partition1',
      metadata: {
        tags: ['tag1', 'tag2'],
        counts: {
          views: 0,
          likes: 0
        }
      }
    };

    await container.items.create(doc);

    // FAILS IN EMULATOR
    await container.item('doc-3', 'partition1').patch([
      { op: 'add', path: '/metadata/tags/-', value: 'tag3' },
      { op: 'incr', path: '/metadata/counts/views', value: 1 },
      { op: 'incr', path: '/metadata/counts/likes', value: 5 },
    ]);

    const { resource: updated } = await container.item('doc-3', 'partition1').read();

    expect(updated.metadata.tags).toEqual(['tag1', 'tag2', 'tag3']);
    expect(updated.metadata.counts.views).toBe(1);
    expect(updated.metadata.counts.likes).toBe(5);
  }, 30000);
});

How to Run

If you're using Docker:

# Pull and start the emulator
docker run -d -p 8081:8081 \
  --name cosmosdb-emulator \
  mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview

# Run the test
npm test

If you're using Testcontainers (like I am):

import { AzureCosmosDbEmulatorContainer } from '@testcontainers/azure-cosmosdb-emulator';

const cosmosContainer = await new AzureCosmosDbEmulatorContainer(
  'mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview'
)
  .withProtocol('http')
  .withTelemetryEnabled(false)
  .start();

const endpoint = cosmosContainer.getEndpoint();
const key = cosmosContainer.getKey();

Impact

This is causing several problems:

  1. Tests fail locally - All my E2E tests that use PATCH operations fail when running against the emulator
  2. Can't test PATCH logic - I can't verify that my partial update logic works correctly before deploying to production
  3. Need workarounds - I have to write fallback code that does read-modify-replace, which is different from production:
// I'm forced to do this in my code:
try {
  await container.item(id, pk).patch(operations);
} catch (error) {
  // Fallback for emulator - read entire document
  const { resource } = await container.item(id, pk).read();
  delete resource._rid;
  delete resource._self;
  delete resource._etag;
  delete resource._attachments;
  delete resource._ts;
  
  // Apply changes manually
  const updated = { ...resource, ...myChanges };
  await container.item(id, pk).replace(updated);
}
  1. Performance mismatch - The workaround uses 3 operations (read + delete metadata + replace) instead of 1 PATCH, so I can't accurately test performance
  2. Can't test concurrency - PATCH has path-level conflict resolution in multi-region writes, but my workaround doesn't, so I can't test that behavior
  3. CI/CD blocked - Our GitHub Actions workflows use the emulator, so we can't run integration tests there either

Related Issues

I found this similar issue for the .NET SDK: azure-cosmos-dotnet-v3#2976

It seems like PATCH support has been missing from the Linux emulator for a while now, even though it's been GA in production since 2021.

What I'm Asking For

Please add PATCH API support to the Linux emulator image. It would be great if mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview (or a newer version) could support the same PATCH operations that work in production Azure Cosmos DB.

This would allow developers using macOS, Linux, or Docker-based workflows to properly test their PATCH logic locally before deploying.

Additional Details

Thanks for looking into this!


This version is more conversational, includes complete reproduction code, and explains the real-world impact from a developer's perspective. Ready to post to GitHub!

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions