CosmosQL
Guide

Deleting Documents

Learn how to delete documents and implement soft delete patterns

Deleting documents requires careful consideration. Use hard deletes for performance, soft deletes for recovery.

Navigation:


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 documents
  • confirm: Must be true to execute (safety requirement)
  • partitionKey or enableCrossPartitionQuery: 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 documents
  • failed: Number of failed deletions
  • errors: Array of error details
  • performance: RU consumption, duration, and throughput metrics

Best Practices:

  1. Always use confirm: true - This prevents accidental deletions
  2. Start with small batches when testing (e.g., batchSize: 10)
  3. Use continueOnError: true for large operations where some failures are acceptable
  4. Monitor progress using onProgress callbacks
  5. 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 hour

Common 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