From c68c6767b082f98c6055f03acbdaddbd5e3ca5b4 Mon Sep 17 00:00:00 2001 From: Arooshi Avasthy Date: Mon, 15 Dec 2025 12:23:31 -0800 Subject: [PATCH 1/3] Add support for store procedure in thinclient mode. --- .../src/GatewayStoreModel.cs | 33 +++-- .../CosmosItemThinClientTests.cs | 113 ++++++++++++++++++ 2 files changed, 136 insertions(+), 10 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/GatewayStoreModel.cs b/Microsoft.Azure.Cosmos/src/GatewayStoreModel.cs index 2274fdeac4..4c18b4a937 100644 --- a/Microsoft.Azure.Cosmos/src/GatewayStoreModel.cs +++ b/Microsoft.Azure.Cosmos/src/GatewayStoreModel.cs @@ -105,7 +105,7 @@ await GatewayStoreModel.ApplySessionTokenAsync( // This is applicable for both per partition automatic failover and per partition circuit breaker. if ((isPPAFEnabled || this.isThinClientEnabled) && !ReplicatedResourceClient.IsMasterResource(request.ResourceType) - && request.ResourceType.IsPartitioned()) + && (request.ResourceType.IsPartitioned() || request.ResourceType == ResourceType.StoredProcedure)) { (bool isSuccess, PartitionKeyRange partitionKeyRange) = await TryResolvePartitionKeyRangeAsync( request: request, @@ -553,15 +553,28 @@ internal static bool IsStoredProcedureCrudOperation( internal static bool IsOperationSupportedByThinClient(DocumentServiceRequest request) { - return request.ResourceType == ResourceType.Document - && (request.OperationType == OperationType.Batch - || request.OperationType == OperationType.Patch - || request.OperationType == OperationType.Create - || request.OperationType == OperationType.Read - || request.OperationType == OperationType.Upsert - || request.OperationType == OperationType.Replace - || request.OperationType == OperationType.Delete - || request.OperationType == OperationType.Query); + // Document operations + if (request.ResourceType == ResourceType.Document + && (request.OperationType == OperationType.Batch + || request.OperationType == OperationType.Patch + || request.OperationType == OperationType.Create + || request.OperationType == OperationType.Read + || request.OperationType == OperationType.Upsert + || request.OperationType == OperationType.Replace + || request.OperationType == OperationType.Delete + || request.OperationType == OperationType.Query)) + { + return true; + } + + // Stored Procedure execution + if (request.ResourceType == ResourceType.StoredProcedure + && request.OperationType == OperationType.ExecuteJavaScript) + { + return true; + } + + return false; } private async Task GetDatabaseAccountPropertiesAsync() { diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemThinClientTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemThinClientTests.cs index 56aedfa1d7..f20e826b24 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemThinClientTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemThinClientTests.cs @@ -184,6 +184,119 @@ public async Task RegionalDatabaseAccountNameIsEmptyInPayload() await database.DeleteAsync(); } + [TestMethod] + [TestCategory("ThinClient")] + public async Task StoredProcedureEndToEndTest() + { + try + { + Environment.SetEnvironmentVariable(ConfigurationManager.ThinClientModeEnabled, "true"); + this.connectionString = ""; + + if (string.IsNullOrEmpty(this.connectionString)) + { + Assert.Fail("Set environment variable COSMOSDB_THINCLIENT to run the tests"); + } + + JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = null, + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + this.cosmosSystemTextJsonSerializer = new MultiRegionSetupHelpers.CosmosSystemTextJsonSerializer(jsonSerializerOptions); + + this.client = new CosmosClient( + this.connectionString, + new CosmosClientOptions() + { + ConnectionMode = ConnectionMode.Gateway, + Serializer = this.cosmosSystemTextJsonSerializer, + }); + + string uniqueDbName = "TestDbStoreProc_" + Guid.NewGuid().ToString(); + this.database = await this.client.CreateDatabaseIfNotExistsAsync(uniqueDbName); + string uniqueContainerName = "TestDbStoreProcContainer_" + Guid.NewGuid().ToString(); + this.container = await this.database.CreateContainerIfNotExistsAsync(uniqueContainerName, "/pk"); + + + string sprocId = "testSproc_" + Guid.NewGuid().ToString(); + string sprocBody = @"function(itemToCreate) { + var context = getContext(); + var collection = context.getCollection(); + var response = context.getResponse(); + + if (!itemToCreate) throw new Error('Item is undefined or null.'); + + // Create a document + var accepted = collection.createDocument( + collection.getSelfLink(), + itemToCreate, + function(err, newItem) { + if (err) throw err; + + // Query the created document + var query = 'SELECT * FROM c WHERE c.id = ""' + newItem.id + '""'; + var isAccepted = collection.queryDocuments( + collection.getSelfLink(), + query, + function(queryErr, documents) { + if (queryErr) throw queryErr; + response.setBody({ + created: newItem, + queried: documents[0] + }); + } + ); + if (!isAccepted) throw 'Query not accepted'; + }); + + if (!accepted) throw new Error('Create was not accepted.'); + }"; + + // Create stored procedure + Scripts.StoredProcedureResponse createResponse = await this.container.Scripts.CreateStoredProcedureAsync( + new Scripts.StoredProcedureProperties(sprocId, sprocBody)); + Assert.AreEqual(HttpStatusCode.Created, createResponse.StatusCode); + Assert.IsTrue(createResponse.RequestCharge > 0); + + // Execute stored procedure + string testPartitionId = Guid.NewGuid().ToString(); + TestObject testItem = new TestObject + { + Id = Guid.NewGuid().ToString(), + Pk = testPartitionId, + Other = "Created by Stored Procedure" + }; + + Scripts.StoredProcedureExecuteResponse executeResponse = + await this.container.Scripts.ExecuteStoredProcedureAsync( + sprocId, + new PartitionKey(testPartitionId), + new dynamic[] { testItem }); + + Assert.AreEqual(HttpStatusCode.OK, executeResponse.StatusCode); + Assert.IsTrue(executeResponse.RequestCharge > 0); + Assert.IsNotNull(executeResponse.Resource); + string diagnostics = executeResponse.Diagnostics.ToString(); + Assert.IsTrue(diagnostics.Contains("|F4"), "Diagnostics User Agent should contain '|F4' for ThinClient"); + + // Verify the result contains both created and queried documents + dynamic result = executeResponse.Resource; + Assert.IsNotNull(result.created); + Assert.IsNotNull(result.queried); + Assert.AreEqual(testItem.Id, (string)result.created.id); + Assert.AreEqual(testItem.Id, (string)result.queried.id); + + // Delete stored procedure + await this.container.Scripts.DeleteStoredProcedureAsync(sprocId); + } + catch (CosmosException ex) + { + Assert.Fail($"StoredProcedureEndToEndTest failed with exception: {ex}"); + } + } + [TestMethod] [TestCategory("ThinClient")] public async Task HttpRequestVersionIsTwoPointZeroWhenUsingThinClientMode() From 5649b08603fc4ee8ddd826cfb7699463275198f9 Mon Sep 17 00:00:00 2001 From: Arooshi Avasthy Date: Thu, 18 Dec 2025 10:04:23 -0800 Subject: [PATCH 2/3] Update store proc test. --- .../CosmosItemThinClientTests.cs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemThinClientTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemThinClientTests.cs index f20e826b24..80f8206f6e 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemThinClientTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemThinClientTests.cs @@ -186,17 +186,11 @@ public async Task RegionalDatabaseAccountNameIsEmptyInPayload() [TestMethod] [TestCategory("ThinClient")] - public async Task StoredProcedureEndToEndTest() + public async Task TestThinClientWithExecuteStoredProcedureAsync() { try { Environment.SetEnvironmentVariable(ConfigurationManager.ThinClientModeEnabled, "true"); - this.connectionString = ""; - - if (string.IsNullOrEmpty(this.connectionString)) - { - Assert.Fail("Set environment variable COSMOSDB_THINCLIENT to run the tests"); - } JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions { @@ -258,7 +252,6 @@ public async Task StoredProcedureEndToEndTest() Scripts.StoredProcedureResponse createResponse = await this.container.Scripts.CreateStoredProcedureAsync( new Scripts.StoredProcedureProperties(sprocId, sprocBody)); Assert.AreEqual(HttpStatusCode.Created, createResponse.StatusCode); - Assert.IsTrue(createResponse.RequestCharge > 0); // Execute stored procedure string testPartitionId = Guid.NewGuid().ToString(); @@ -276,18 +269,10 @@ await this.container.Scripts.ExecuteStoredProcedureAsync( new dynamic[] { testItem }); Assert.AreEqual(HttpStatusCode.OK, executeResponse.StatusCode); - Assert.IsTrue(executeResponse.RequestCharge > 0); Assert.IsNotNull(executeResponse.Resource); string diagnostics = executeResponse.Diagnostics.ToString(); Assert.IsTrue(diagnostics.Contains("|F4"), "Diagnostics User Agent should contain '|F4' for ThinClient"); - // Verify the result contains both created and queried documents - dynamic result = executeResponse.Resource; - Assert.IsNotNull(result.created); - Assert.IsNotNull(result.queried); - Assert.AreEqual(testItem.Id, (string)result.created.id); - Assert.AreEqual(testItem.Id, (string)result.queried.id); - // Delete stored procedure await this.container.Scripts.DeleteStoredProcedureAsync(sprocId); } From 87ee910d216dbf28e51c90fbb2a12ca11350e7ef Mon Sep 17 00:00:00 2001 From: Arooshi Avasthy Date: Thu, 18 Dec 2025 10:55:01 -0800 Subject: [PATCH 3/3] Add test support execute store proc stream --- .../CosmosItemThinClientTests.cs | 247 ++++++++++++------ 1 file changed, 166 insertions(+), 81 deletions(-) diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemThinClientTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemThinClientTests.cs index 80f8206f6e..5671163977 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemThinClientTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemThinClientTests.cs @@ -188,98 +188,183 @@ public async Task RegionalDatabaseAccountNameIsEmptyInPayload() [TestCategory("ThinClient")] public async Task TestThinClientWithExecuteStoredProcedureAsync() { - try + Environment.SetEnvironmentVariable(ConfigurationManager.ThinClientModeEnabled, "true"); + + JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions { - Environment.SetEnvironmentVariable(ConfigurationManager.ThinClientModeEnabled, "true"); + PropertyNamingPolicy = null, + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + this.cosmosSystemTextJsonSerializer = new MultiRegionSetupHelpers.CosmosSystemTextJsonSerializer(jsonSerializerOptions); - JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = null, - PropertyNameCaseInsensitive = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - this.cosmosSystemTextJsonSerializer = new MultiRegionSetupHelpers.CosmosSystemTextJsonSerializer(jsonSerializerOptions); - - this.client = new CosmosClient( - this.connectionString, - new CosmosClientOptions() - { - ConnectionMode = ConnectionMode.Gateway, - Serializer = this.cosmosSystemTextJsonSerializer, - }); - - string uniqueDbName = "TestDbStoreProc_" + Guid.NewGuid().ToString(); - this.database = await this.client.CreateDatabaseIfNotExistsAsync(uniqueDbName); - string uniqueContainerName = "TestDbStoreProcContainer_" + Guid.NewGuid().ToString(); - this.container = await this.database.CreateContainerIfNotExistsAsync(uniqueContainerName, "/pk"); - - - string sprocId = "testSproc_" + Guid.NewGuid().ToString(); - string sprocBody = @"function(itemToCreate) { - var context = getContext(); - var collection = context.getCollection(); - var response = context.getResponse(); + this.client = new CosmosClient( + this.connectionString, + new CosmosClientOptions() + { + ConnectionMode = ConnectionMode.Gateway, + Serializer = this.cosmosSystemTextJsonSerializer, + }); + + string uniqueDbName = "TestDbStoreProc_" + Guid.NewGuid().ToString(); + this.database = await this.client.CreateDatabaseIfNotExistsAsync(uniqueDbName); + string uniqueContainerName = "TestDbStoreProcContainer_" + Guid.NewGuid().ToString(); + this.container = await this.database.CreateContainerIfNotExistsAsync(uniqueContainerName, "/pk"); + + + string sprocId = "testSproc_" + Guid.NewGuid().ToString(); + string sprocBody = @"function(itemToCreate) { + var context = getContext(); + var collection = context.getCollection(); + var response = context.getResponse(); - if (!itemToCreate) throw new Error('Item is undefined or null.'); + if (!itemToCreate) throw new Error('Item is undefined or null.'); - // Create a document - var accepted = collection.createDocument( - collection.getSelfLink(), - itemToCreate, - function(err, newItem) { - if (err) throw err; + // Create a document + var accepted = collection.createDocument( + collection.getSelfLink(), + itemToCreate, + function(err, newItem) { + if (err) throw err; - // Query the created document - var query = 'SELECT * FROM c WHERE c.id = ""' + newItem.id + '""'; - var isAccepted = collection.queryDocuments( - collection.getSelfLink(), - query, - function(queryErr, documents) { - if (queryErr) throw queryErr; - response.setBody({ - created: newItem, - queried: documents[0] - }); - } - ); - if (!isAccepted) throw 'Query not accepted'; - }); + // Query the created document + var query = 'SELECT * FROM c WHERE c.id = ""' + newItem.id + '""'; + var isAccepted = collection.queryDocuments( + collection.getSelfLink(), + query, + function(queryErr, documents) { + if (queryErr) throw queryErr; + response.setBody({ + created: newItem, + queried: documents[0] + }); + } + ); + if (!isAccepted) throw 'Query not accepted'; + }); - if (!accepted) throw new Error('Create was not accepted.'); - }"; + if (!accepted) throw new Error('Create was not accepted.'); + }"; - // Create stored procedure - Scripts.StoredProcedureResponse createResponse = await this.container.Scripts.CreateStoredProcedureAsync( - new Scripts.StoredProcedureProperties(sprocId, sprocBody)); - Assert.AreEqual(HttpStatusCode.Created, createResponse.StatusCode); + // Create stored procedure + Scripts.StoredProcedureResponse createResponse = await this.container.Scripts.CreateStoredProcedureAsync( + new Scripts.StoredProcedureProperties(sprocId, sprocBody)); + Assert.AreEqual(HttpStatusCode.Created, createResponse.StatusCode); - // Execute stored procedure - string testPartitionId = Guid.NewGuid().ToString(); - TestObject testItem = new TestObject - { - Id = Guid.NewGuid().ToString(), - Pk = testPartitionId, - Other = "Created by Stored Procedure" - }; + // Execute stored procedure + string testPartitionId = Guid.NewGuid().ToString(); + TestObject testItem = new TestObject + { + Id = Guid.NewGuid().ToString(), + Pk = testPartitionId, + Other = "Created by Stored Procedure" + }; - Scripts.StoredProcedureExecuteResponse executeResponse = - await this.container.Scripts.ExecuteStoredProcedureAsync( - sprocId, - new PartitionKey(testPartitionId), - new dynamic[] { testItem }); + Scripts.StoredProcedureExecuteResponse executeResponse = + await this.container.Scripts.ExecuteStoredProcedureAsync( + sprocId, + new PartitionKey(testPartitionId), + new dynamic[] { testItem }); - Assert.AreEqual(HttpStatusCode.OK, executeResponse.StatusCode); - Assert.IsNotNull(executeResponse.Resource); - string diagnostics = executeResponse.Diagnostics.ToString(); - Assert.IsTrue(diagnostics.Contains("|F4"), "Diagnostics User Agent should contain '|F4' for ThinClient"); + Assert.AreEqual(HttpStatusCode.OK, executeResponse.StatusCode); + Assert.IsNotNull(executeResponse.Resource); + string diagnostics = executeResponse.Diagnostics.ToString(); + Assert.IsTrue(diagnostics.Contains("|F4"), "Diagnostics User Agent should contain '|F4' for ThinClient"); - // Delete stored procedure - await this.container.Scripts.DeleteStoredProcedureAsync(sprocId); - } - catch (CosmosException ex) - { - Assert.Fail($"StoredProcedureEndToEndTest failed with exception: {ex}"); - } + // Delete stored procedure + await this.container.Scripts.DeleteStoredProcedureAsync(sprocId); + } + + [TestMethod] + [TestCategory("ThinClient")] + public async Task TestThinClientWithExecuteStoredProcedureStreamAsync() + { + Environment.SetEnvironmentVariable(ConfigurationManager.ThinClientModeEnabled, "true"); + + JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = null, + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + this.cosmosSystemTextJsonSerializer = new MultiRegionSetupHelpers.CosmosSystemTextJsonSerializer(jsonSerializerOptions); + + this.client = new CosmosClient( + this.connectionString, + new CosmosClientOptions() + { + ConnectionMode = ConnectionMode.Gateway, + Serializer = this.cosmosSystemTextJsonSerializer, + }); + + string uniqueDbName = "TestDbStoreProc_" + Guid.NewGuid().ToString(); + this.database = await this.client.CreateDatabaseIfNotExistsAsync(uniqueDbName); + string uniqueContainerName = "TestDbStoreProcContainer_" + Guid.NewGuid().ToString(); + this.container = await this.database.CreateContainerIfNotExistsAsync(uniqueContainerName, "/pk"); + + + string sprocId = "testSproc_" + Guid.NewGuid().ToString(); + string sprocBody = @"function(itemToCreate) { + var context = getContext(); + var collection = context.getCollection(); + var response = context.getResponse(); + + if (!itemToCreate) throw new Error('Item is undefined or null.'); + + // Create a document + var accepted = collection.createDocument( + collection.getSelfLink(), + itemToCreate, + function(err, newItem) { + if (err) throw err; + + // Query the created document + var query = 'SELECT * FROM c WHERE c.id = ""' + newItem.id + '""'; + var isAccepted = collection.queryDocuments( + collection.getSelfLink(), + query, + function(queryErr, documents) { + if (queryErr) throw queryErr; + response.setBody({ + created: newItem, + queried: documents[0] + }); + } + ); + if (!isAccepted) throw 'Query not accepted'; + }); + + if (!accepted) throw new Error('Create was not accepted.'); + }"; + + // Create stored procedure + Scripts.StoredProcedureResponse createResponse = await this.container.Scripts.CreateStoredProcedureAsync( + new Scripts.StoredProcedureProperties(sprocId, sprocBody)); + Assert.AreEqual(HttpStatusCode.Created, createResponse.StatusCode); + + // Execute stored procedure + string testPartitionId = Guid.NewGuid().ToString(); + TestObject testItem = new TestObject + { + Id = Guid.NewGuid().ToString(), + Pk = testPartitionId, + Other = "Created by Stored Procedure" + }; + + using (ResponseMessage executeResponse = + await this.container.Scripts.ExecuteStoredProcedureStreamAsync( + sprocId, + new PartitionKey(testPartitionId), + new dynamic[] { testItem })) + { + Assert.AreEqual(HttpStatusCode.OK, executeResponse.StatusCode); + Assert.IsNotNull(executeResponse.Content); + string diagnostics = executeResponse.Diagnostics.ToString(); + Assert.IsTrue(diagnostics.Contains("|F4"), "Diagnostics User Agent should contain '|F4' for ThinClient"); + } + + // Delete stored procedure + await this.container.Scripts.DeleteStoredProcedureAsync(sprocId); } [TestMethod]