From 9e9207c8097458d4d4f7daa577fe01d627c7a138 Mon Sep 17 00:00:00 2001 From: ramnnnn2006 Date: Wed, 10 Jun 2026 22:18:07 +0530 Subject: [PATCH 1/3] feat: add rule to limit stored procedures to single-partition atomic operations --- skills/cosmosdb-best-practices/AGENTS.md | 108 +++++++++++++++++ .../rules/pattern-stored-procedure-scope.md | 109 ++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 skills/cosmosdb-best-practices/rules/pattern-stored-procedure-scope.md diff --git a/skills/cosmosdb-best-practices/AGENTS.md b/skills/cosmosdb-best-practices/AGENTS.md index 010b0ba..96227c5 100644 --- a/skills/cosmosdb-best-practices/AGENTS.md +++ b/skills/cosmosdb-best-practices/AGENTS.md @@ -125,6 +125,7 @@ Performance optimization and best practices guide for Azure Cosmos DB applicatio - 9.12 [Use StateGraph with Conditional Edges for Multi-Agent Routing](#912-use-stategraph-with-conditional-edges-for-multi-agent-routing) - 9.13 [Resume LangGraph from Checkpoint After Interrupt](#913-resume-langgraph-from-checkpoint-after-interrupt) - 9.14 [Use a service layer to hydrate document references before rendering](#914-use-a-service-layer-to-hydrate-document-references-before-rendering) + - 9.15 [Use stored procedures only for multi-document atomic operations within a single partition](#915-use-stored-procedures-only-for-multi-document-atomic-operations-within-a-single-partition) 10. [Developer Tooling](#10-developer-tooling) — **MEDIUM** - 10.1 [Use Azure Cosmos DB Emulator for local development and testing](#101-use-azure-cosmos-db-emulator-for-local-development-and-testing) - 10.2 [Use Azure Cosmos DB VS Code extension for routine inspection and management](#102-use-azure-cosmos-db-vs-code-extension-for-routine-inspection-and-management) @@ -11450,6 +11451,113 @@ For truly high-volume scenarios, consider denormalizing the data instead (see `m Reference: [Data modeling in Azure Cosmos DB](https://learn.microsoft.com/azure/cosmos-db/nosql/modeling-data) +### 9.15 Use stored procedures only for multi-document atomic operations within a single partition + +**Impact: MEDIUM** (avoids server-side JavaScript that is slow to execute, hard to debug, and capped at 5 seconds) + +## Use Stored Procedures Only for Multi-Document Atomic Operations Within a Single Partition + +**Impact: MEDIUM (avoids server-side JavaScript that is slow to execute, hard to debug, and capped at 5 seconds)** + +Stored procedures run JavaScript inside the database engine and are scoped to a single logical partition. Their one real strength is transactional execution: all operations in a stored procedure either commit together or roll back together. They are not a general server-side compute layer, and using them that way produces logic that is hard to test, hard to debug, and subject to strict execution limits. + +Reach for a stored procedure only when you need ACID guarantees across multiple items in the same partition and transactional batch cannot express the operation, for example when a write depends on data read inside the transaction, or when you exceed the batch limits of 100 operations or 2 MB per request. + +**Limitations to be aware of:** + +- Bounded execution of roughly 5 seconds; long-running scripts must implement continuation logic or they are rolled back +- Scoped to one logical partition; a stored procedure can never read or write items across partitions +- JavaScript only, with no breakpoints, no SDK-level diagnostics, and weak logging, so failures are hard to investigate +- Script source lives in the database rather than your codebase, which complicates versioning, code review, and deployment + +**Incorrect (stored procedure for single-item CRUD):** + +```csharp +// A script invocation, serialization, and JS execution just to create one document +var scripts = container.Scripts; +await scripts.ExecuteStoredProcedureAsync( + "createOrder", + new PartitionKey(order.CustomerId), + new dynamic[] { order }); +``` + +**Correct (plain SDK call):** + +```csharp +// One point write, fully typed, retriable, and easy to debug +await container.CreateItemAsync(order, new PartitionKey(order.CustomerId)); +``` + +**Incorrect (heavy computation or cross-partition logic in a script):** + +```javascript +// Aggregating "all" orders server-side: only sees one partition, +// and hits the bounded execution limit as data grows +function monthlyRevenue() { + var collection = getContext().getCollection(); + collection.queryDocuments(collection.getSelfLink(), + "SELECT * FROM c WHERE c.type = 'order'", + function (err, docs) { + var total = 0; + for (var i = 0; i < docs.length; i++) { + total += docs[i].amount; // unbounded loop inside the engine + } + getContext().getResponse().setBody(total); + }); +} +``` + +**Correct (transactional batch for multi-item atomicity in one partition):** + +```csharp +// Atomic create + update + audit entry, same partition key, no server-side code +var batch = container.CreateTransactionalBatch(new PartitionKey(order.CustomerId)) + .CreateItem(order) + .ReplaceItem(customer.Id, customer) + .CreateItem(auditEntry); + +var response = await batch.ExecuteAsync(); +if (!response.IsSuccessStatusCode) +{ + // The whole batch rolled back; inspect per-operation status codes +} +``` + +Aggregations and other computation belong in your application code, in queries using `SUM`/`COUNT`/`GROUP BY`, or in materialized views maintained through the change feed. + +**When a stored procedure is still the right tool:** + +```javascript +// Conditional read-modify-write across two items that must commit atomically: +// debit one account item and credit another, but only if the balance allows it. +// Transactional batch cannot make a write depend on a read inside the transaction. +function transfer(fromId, toId, amount) { + var collection = getContext().getCollection(); + collection.readDocument(documentLink(fromId), function (err, from) { + if (err) throw err; + if (from.balance < amount) throw new Error("Insufficient funds"); + from.balance -= amount; + collection.replaceDocument(from._self, from, function (err) { + if (err) throw err; // any failure rolls back the whole transfer + // ...read and credit the destination account next + }); + }); +} +``` + +Both account items must share the same partition key for this to work. + +**Key points:** + +- Default to SDK-side logic; use transactional batch for multi-item atomic writes within a partition +- Use a stored procedure only when the transaction needs server-side reads to decide its writes, or exceeds batch limits +- Never use stored procedures for single-item CRUD, cross-partition operations, or general computation +- Keep scripts small and idempotent so they finish well within the bounded execution window + +Reference(s): +[Stored procedures, triggers, and UDFs in Azure Cosmos DB](https://learn.microsoft.com/azure/cosmos-db/nosql/stored-procedures-triggers-udfs) +[Transactional batch operations in Azure Cosmos DB](https://learn.microsoft.com/azure/cosmos-db/nosql/transactional-batch) + --- ## 10. Developer Tooling diff --git a/skills/cosmosdb-best-practices/rules/pattern-stored-procedure-scope.md b/skills/cosmosdb-best-practices/rules/pattern-stored-procedure-scope.md new file mode 100644 index 0000000..54bed3e --- /dev/null +++ b/skills/cosmosdb-best-practices/rules/pattern-stored-procedure-scope.md @@ -0,0 +1,109 @@ +--- +title: Use stored procedures only for multi-document atomic operations within a single partition +impact: MEDIUM +impactDescription: avoids server-side JavaScript that is slow to execute, hard to debug, and capped at 5 seconds +tags: pattern, stored-procedures, transactional-batch, atomicity, single-partition +--- + +## Use Stored Procedures Only for Multi-Document Atomic Operations Within a Single Partition + +**Impact: MEDIUM (avoids server-side JavaScript that is slow to execute, hard to debug, and capped at 5 seconds)** + +Stored procedures run JavaScript inside the database engine and are scoped to a single logical partition. Their one real strength is transactional execution: all operations in a stored procedure either commit together or roll back together. They are not a general server-side compute layer, and using them that way produces logic that is hard to test, hard to debug, and subject to strict execution limits. + +Reach for a stored procedure only when you need ACID guarantees across multiple items in the same partition and transactional batch cannot express the operation, for example when a write depends on data read inside the transaction, or when you exceed the batch limits of 100 operations or 2 MB per request. + +**Limitations to be aware of:** + +- Bounded execution of roughly 5 seconds; long-running scripts must implement continuation logic or they are rolled back +- Scoped to one logical partition; a stored procedure can never read or write items across partitions +- JavaScript only, with no breakpoints, no SDK-level diagnostics, and weak logging, so failures are hard to investigate +- Script source lives in the database rather than your codebase, which complicates versioning, code review, and deployment + +**Incorrect (stored procedure for single-item CRUD):** + +```csharp +// A script invocation, serialization, and JS execution just to create one document +var scripts = container.Scripts; +await scripts.ExecuteStoredProcedureAsync( + "createOrder", + new PartitionKey(order.CustomerId), + new dynamic[] { order }); +``` + +**Correct (plain SDK call):** + +```csharp +// One point write, fully typed, retriable, and easy to debug +await container.CreateItemAsync(order, new PartitionKey(order.CustomerId)); +``` + +**Incorrect (heavy computation or cross-partition logic in a script):** + +```javascript +// Aggregating "all" orders server-side: only sees one partition, +// and hits the bounded execution limit as data grows +function monthlyRevenue() { + var collection = getContext().getCollection(); + collection.queryDocuments(collection.getSelfLink(), + "SELECT * FROM c WHERE c.type = 'order'", + function (err, docs) { + var total = 0; + for (var i = 0; i < docs.length; i++) { + total += docs[i].amount; // unbounded loop inside the engine + } + getContext().getResponse().setBody(total); + }); +} +``` + +**Correct (transactional batch for multi-item atomicity in one partition):** + +```csharp +// Atomic create + update + audit entry, same partition key, no server-side code +var batch = container.CreateTransactionalBatch(new PartitionKey(order.CustomerId)) + .CreateItem(order) + .ReplaceItem(customer.Id, customer) + .CreateItem(auditEntry); + +var response = await batch.ExecuteAsync(); +if (!response.IsSuccessStatusCode) +{ + // The whole batch rolled back; inspect per-operation status codes +} +``` + +Aggregations and other computation belong in your application code, in queries using `SUM`/`COUNT`/`GROUP BY`, or in materialized views maintained through the change feed. + +**When a stored procedure is still the right tool:** + +```javascript +// Conditional read-modify-write across two items that must commit atomically: +// debit one account item and credit another, but only if the balance allows it. +// Transactional batch cannot make a write depend on a read inside the transaction. +function transfer(fromId, toId, amount) { + var collection = getContext().getCollection(); + collection.readDocument(documentLink(fromId), function (err, from) { + if (err) throw err; + if (from.balance < amount) throw new Error("Insufficient funds"); + from.balance -= amount; + collection.replaceDocument(from._self, from, function (err) { + if (err) throw err; // any failure rolls back the whole transfer + // ...read and credit the destination account next + }); + }); +} +``` + +Both account items must share the same partition key for this to work. + +**Key points:** + +- Default to SDK-side logic; use transactional batch for multi-item atomic writes within a partition +- Use a stored procedure only when the transaction needs server-side reads to decide its writes, or exceeds batch limits +- Never use stored procedures for single-item CRUD, cross-partition operations, or general computation +- Keep scripts small and idempotent so they finish well within the bounded execution window + +Reference(s): +[Stored procedures, triggers, and UDFs in Azure Cosmos DB](https://learn.microsoft.com/azure/cosmos-db/nosql/stored-procedures-triggers-udfs) +[Transactional batch operations in Azure Cosmos DB](https://learn.microsoft.com/azure/cosmos-db/nosql/transactional-batch) From 77d0c22cee3ac82679edbe646b930d08b20536f8 Mon Sep 17 00:00:00 2001 From: ramnnnn2006 Date: Thu, 11 Jun 2026 00:00:06 +0530 Subject: [PATCH 2/3] test: add eval task for stored procedure scope rule --- .../tasks/pattern-sproc-scope.yaml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 evals/cosmosdb-best-practices/tasks/pattern-sproc-scope.yaml diff --git a/evals/cosmosdb-best-practices/tasks/pattern-sproc-scope.yaml b/evals/cosmosdb-best-practices/tasks/pattern-sproc-scope.yaml new file mode 100644 index 0000000..16f6edb --- /dev/null +++ b/evals/cosmosdb-best-practices/tasks/pattern-sproc-scope.yaml @@ -0,0 +1,20 @@ +id: pattern-sproc-scope-007 +name: Design Patterns - Stored Procedure Scope +description: | + Test that the skill steers away from stored procedures for single-item + CRUD and recommends transactional batch for multi-item atomic writes + within a single partition. +tags: + - pattern + - happy-path + - stored-procedures +inputs: + prompt: | + I'm building an order system on Cosmos DB. When an order is placed I need to + create the order document, update the customer's order count, and write an + audit entry. All three share the customer id as partition key. I was planning + to write a stored procedure for this, and another stored procedure for plain + order lookups by id. Is that the right approach? +expected: + outcomes: + - type: task_completed From 87c7fb68d645409add93b06c92b2e9853f205cfe Mon Sep 17 00:00:00 2001 From: ramnnnn2006 Date: Sun, 14 Jun 2026 22:22:54 +0530 Subject: [PATCH 3/3] fix: drop duplicate impact line from stored procedure rule body --- skills/cosmosdb-best-practices/AGENTS.md | 2 -- .../rules/pattern-stored-procedure-scope.md | 2 -- 2 files changed, 4 deletions(-) diff --git a/skills/cosmosdb-best-practices/AGENTS.md b/skills/cosmosdb-best-practices/AGENTS.md index 96227c5..dea9906 100644 --- a/skills/cosmosdb-best-practices/AGENTS.md +++ b/skills/cosmosdb-best-practices/AGENTS.md @@ -11457,8 +11457,6 @@ Reference: [Data modeling in Azure Cosmos DB](https://learn.microsoft.com/azure/ ## Use Stored Procedures Only for Multi-Document Atomic Operations Within a Single Partition -**Impact: MEDIUM (avoids server-side JavaScript that is slow to execute, hard to debug, and capped at 5 seconds)** - Stored procedures run JavaScript inside the database engine and are scoped to a single logical partition. Their one real strength is transactional execution: all operations in a stored procedure either commit together or roll back together. They are not a general server-side compute layer, and using them that way produces logic that is hard to test, hard to debug, and subject to strict execution limits. Reach for a stored procedure only when you need ACID guarantees across multiple items in the same partition and transactional batch cannot express the operation, for example when a write depends on data read inside the transaction, or when you exceed the batch limits of 100 operations or 2 MB per request. diff --git a/skills/cosmosdb-best-practices/rules/pattern-stored-procedure-scope.md b/skills/cosmosdb-best-practices/rules/pattern-stored-procedure-scope.md index 54bed3e..c3c0bf8 100644 --- a/skills/cosmosdb-best-practices/rules/pattern-stored-procedure-scope.md +++ b/skills/cosmosdb-best-practices/rules/pattern-stored-procedure-scope.md @@ -7,8 +7,6 @@ tags: pattern, stored-procedures, transactional-batch, atomicity, single-partiti ## Use Stored Procedures Only for Multi-Document Atomic Operations Within a Single Partition -**Impact: MEDIUM (avoids server-side JavaScript that is slow to execute, hard to debug, and capped at 5 seconds)** - Stored procedures run JavaScript inside the database engine and are scoped to a single logical partition. Their one real strength is transactional execution: all operations in a stored procedure either commit together or roll back together. They are not a general server-side compute layer, and using them that way produces logic that is hard to test, hard to debug, and subject to strict execution limits. Reach for a stored procedure only when you need ACID guarantees across multiple items in the same partition and transactional batch cannot express the operation, for example when a write depends on data read inside the transaction, or when you exceed the batch limits of 100 operations or 2 MB per request.