From 8a2b2354e45f312a85a9905cd63fbd82e81aeb6d Mon Sep 17 00:00:00 2001 From: Andrew Mathew Date: Mon, 3 Feb 2025 18:08:06 -0500 Subject: [PATCH 1/7] removed **provisional** from computed_properties --- sdk/cosmos/azure-cosmos/azure/cosmos/aio/_database.py | 4 ++-- sdk/cosmos/azure-cosmos/azure/cosmos/database.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_database.py b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_database.py index a41d9812bfdf..2c9a8bdb68e8 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_database.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_database.py @@ -199,7 +199,7 @@ async def create_container( has changed, and act according to the condition specified by the `match_condition` parameter. :keyword match_condition: The match condition to use upon the etag. :paramtype match_condition: ~azure.core.MatchConditions - :keyword List[Dict[str, str]] computed_properties: **provisional** Sets The computed properties for this + :keyword List[Dict[str, str]] computed_properties: Sets The computed properties for this container in the Azure Cosmos DB Service. For more Information on how to use computed properties visit `here: https://learn.microsoft.com/azure/cosmos-db/nosql/query/computed-properties?tabs=dotnet` :keyword response_hook: A callable invoked with the response metadata. @@ -324,7 +324,7 @@ async def create_container_if_not_exists( has changed, and act according to the condition specified by the `match_condition` parameter. :keyword match_condition: The match condition to use upon the etag. :paramtype match_condition: ~azure.core.MatchConditions - :keyword List[Dict[str, str]] computed_properties: **provisional** Sets The computed properties for this + :keyword List[Dict[str, str]] computed_properties: Sets The computed properties for this container in the Azure Cosmos DB Service. For more Information on how to use computed properties visit `here: https://learn.microsoft.com/azure/cosmos-db/nosql/query/computed-properties?tabs=dotnet` :keyword response_hook: A callable invoked with the response metadata. diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/database.py b/sdk/cosmos/azure-cosmos/azure/cosmos/database.py index 17890cfa3904..d800cd6d37e2 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/database.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/database.py @@ -200,7 +200,7 @@ def create_container( # pylint:disable=docstring-missing-param :keyword int analytical_storage_ttl: Analytical store time to live (TTL) for items in the container. A value of None leaves analytical storage off and a value of -1 turns analytical storage on with no TTL. Please note that analytical storage can only be enabled on Synapse Link enabled accounts. - :keyword List[Dict[str, str]] computed_properties: **provisional** Sets The computed properties for this + :keyword List[Dict[str, str]] computed_properties: Sets The computed properties for this container in the Azure Cosmos DB Service. For more Information on how to use computed properties visit `here: https://learn.microsoft.com/azure/cosmos-db/nosql/query/computed-properties?tabs=dotnet` :keyword Dict[str, Any] vector_embedding_policy: **provisional** The vector embedding policy for the container. @@ -326,7 +326,7 @@ def create_container_if_not_exists( # pylint:disable=docstring-missing-param :keyword int analytical_storage_ttl: Analytical store time to live (TTL) for items in the container. A value of None leaves analytical storage off and a value of -1 turns analytical storage on with no TTL. Please note that analytical storage can only be enabled on Synapse Link enabled accounts. - :keyword List[Dict[str, str]] computed_properties: **provisional** Sets The computed properties for this + :keyword List[Dict[str, str]] computed_properties: Sets The computed properties for this container in the Azure Cosmos DB Service. For more Information on how to use computed properties visit `here: https://learn.microsoft.com/azure/cosmos-db/nosql/query/computed-properties?tabs=dotnet` :keyword Dict[str, Any] vector_embedding_policy: The vector embedding policy for the container. Each vector From cf8b4b5f34f3db592922abf710f1dd1d9d86b25e Mon Sep 17 00:00:00 2001 From: Andrew Mathew Date: Tue, 4 Feb 2025 11:54:39 -0500 Subject: [PATCH 2/7] updated changelog --- sdk/cosmos/azure-cosmos/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/cosmos/azure-cosmos/CHANGELOG.md b/sdk/cosmos/azure-cosmos/CHANGELOG.md index 58cadd99d8e3..1d8526594aae 100644 --- a/sdk/cosmos/azure-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos/CHANGELOG.md @@ -11,6 +11,7 @@ #### Other Changes * This release requires aiohttp version 3.10.11 and above. See [PR 39396](https://github.com/Azure/azure-sdk-for-python/pull/39396) +* Un-marked `computed_properties` keyword as **provisional**. See [PR 39543](https://github.com/Azure/azure-sdk-for-python/pull/39543) ### 4.9.1b1 (2024-12-13) From 28818340ce951280fae7d05f3e802df0db7178b1 Mon Sep 17 00:00:00 2001 From: Andrew Mathew Date: Mon, 10 Feb 2025 19:28:33 -0500 Subject: [PATCH 3/7] added computed_properties keyword argument to function def --- sdk/cosmos/azure-cosmos/azure/cosmos/aio/_database.py | 2 ++ sdk/cosmos/azure-cosmos/azure/cosmos/database.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_database.py b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_database.py index 2c9a8bdb68e8..90f7c14eff27 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_database.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_database.py @@ -173,6 +173,7 @@ async def create_container( initial_headers: Optional[Dict[str, str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, + computed_properties: Optional[List[Dict[str, str]]] = None, analytical_storage_ttl: Optional[int] = None, vector_embedding_policy: Optional[Dict[str, Any]] = None, change_feed_policy: Optional[Dict[str, Any]] = None, @@ -296,6 +297,7 @@ async def create_container_if_not_exists( initial_headers: Optional[Dict[str, str]] = None, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, + computed_properties: Optional[List[Dict[str, str]]] = None, analytical_storage_ttl: Optional[int] = None, vector_embedding_policy: Optional[Dict[str, Any]] = None, change_feed_policy: Optional[Dict[str, Any]] = None, diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/database.py b/sdk/cosmos/azure-cosmos/azure/cosmos/database.py index d800cd6d37e2..74597b206d6c 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/database.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/database.py @@ -174,6 +174,7 @@ def create_container( # pylint:disable=docstring-missing-param etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, analytical_storage_ttl: Optional[int] = None, + computed_properties: Optional[List[Dict[str, str]]] = None, vector_embedding_policy: Optional[Dict[str, Any]] = None, change_feed_policy: Optional[Dict[str, Any]] = None, full_text_policy: Optional[Dict[str, Any]] = None, @@ -298,6 +299,7 @@ def create_container_if_not_exists( # pylint:disable=docstring-missing-param etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, analytical_storage_ttl: Optional[int] = None, + computed_properties: Optional[List[Dict[str, str]]] = None, vector_embedding_policy: Optional[Dict[str, Any]] = None, change_feed_policy: Optional[Dict[str, Any]] = None, full_text_policy: Optional[Dict[str, Any]] = None, From af5976bc22139b30ff1a0c1e2c3e9215f8ebf4c7 Mon Sep 17 00:00:00 2001 From: Andrew Mathew Date: Tue, 11 Feb 2025 12:54:28 -0500 Subject: [PATCH 4/7] annotated computed_properties tests --- sdk/cosmos/azure-cosmos/test/test_query.py | 1 - sdk/cosmos/azure-cosmos/test/test_query_async.py | 1 - sdk/cosmos/azure-cosmos/test/test_query_computed_properties.py | 2 ++ .../azure-cosmos/test/test_query_computed_properties_async.py | 2 ++ 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/test/test_query.py b/sdk/cosmos/azure-cosmos/test/test_query.py index cf847ac02d21..28fbcd3e0a51 100644 --- a/sdk/cosmos/azure-cosmos/test/test_query.py +++ b/sdk/cosmos/azure-cosmos/test/test_query.py @@ -508,7 +508,6 @@ def test_continuation_token_size_limit_query(self): self.assertLessEqual(len(token.encode('utf-8')), 1024) @pytest.mark.cosmosQuery - @pytest.mark.skip def test_computed_properties_query(self): computed_properties = [{'name': "cp_lower", 'query': "SELECT VALUE LOWER(c.db_group) FROM c"}, {'name': "cp_power", diff --git a/sdk/cosmos/azure-cosmos/test/test_query_async.py b/sdk/cosmos/azure-cosmos/test/test_query_async.py index ed35e0d6285e..eca772a31ed6 100644 --- a/sdk/cosmos/azure-cosmos/test/test_query_async.py +++ b/sdk/cosmos/azure-cosmos/test/test_query_async.py @@ -490,7 +490,6 @@ async def test_continuation_token_size_limit_query_async(self): assert len(token.encode('utf-8')) <= 1024 @pytest.mark.cosmosQuery - @pytest.mark.skip async def test_computed_properties_query(self): computed_properties = [{'name': "cp_lower", 'query': "SELECT VALUE LOWER(c.db_group) FROM c"}, {'name': "cp_power", diff --git a/sdk/cosmos/azure-cosmos/test/test_query_computed_properties.py b/sdk/cosmos/azure-cosmos/test/test_query_computed_properties.py index ea5c9d350da3..359bd544fc57 100644 --- a/sdk/cosmos/azure-cosmos/test/test_query_computed_properties.py +++ b/sdk/cosmos/azure-cosmos/test/test_query_computed_properties.py @@ -3,6 +3,7 @@ import unittest import uuid +import pytest import azure.cosmos.cosmos_client as cosmos_client import test_config @@ -33,6 +34,7 @@ def setUpClass(cls): cls.client = cosmos_client.CosmosClient(cls.host, cls.masterKey) cls.created_db = cls.client.get_database_client(cls.TEST_DATABASE_ID) + @pytest.mark.cosmosQuery def test_computed_properties_query(self): computed_properties = [{'name': "cp_lower", 'query': "SELECT VALUE LOWER(c.db_group) FROM c"}, {'name': "cp_power", diff --git a/sdk/cosmos/azure-cosmos/test/test_query_computed_properties_async.py b/sdk/cosmos/azure-cosmos/test/test_query_computed_properties_async.py index a382c90dfb33..88a32352223e 100644 --- a/sdk/cosmos/azure-cosmos/test/test_query_computed_properties_async.py +++ b/sdk/cosmos/azure-cosmos/test/test_query_computed_properties_async.py @@ -3,6 +3,7 @@ import unittest import uuid +import pytest import test_config from azure.cosmos.aio import CosmosClient, DatabaseProxy, ContainerProxy @@ -38,6 +39,7 @@ async def asyncSetUp(self): async def asyncTearDown(self): await self.client.close() + @pytest.mark.cosmosQuery async def test_computed_properties_query_async(self): computed_properties = [{'name': "cp_lower", 'query': "SELECT VALUE LOWER(c.db_group) FROM c"}, {'name': "cp_power", From 83f1aadaae688049a682ecb8d2628416cb6421de Mon Sep 17 00:00:00 2001 From: Andrew Mathew Date: Wed, 12 Feb 2025 11:46:05 -0500 Subject: [PATCH 5/7] added replace computed_properties feature with tests --- .../test/test_computed_properties.py | 249 ++++++++++++++++ .../test/test_computed_properties_async.py | 271 ++++++++++++++++++ .../test/test_query_computed_properties.py | 104 ------- .../test_query_computed_properties_async.py | 113 -------- 4 files changed, 520 insertions(+), 217 deletions(-) create mode 100644 sdk/cosmos/azure-cosmos/test/test_computed_properties.py create mode 100644 sdk/cosmos/azure-cosmos/test/test_computed_properties_async.py delete mode 100644 sdk/cosmos/azure-cosmos/test/test_query_computed_properties.py delete mode 100644 sdk/cosmos/azure-cosmos/test/test_query_computed_properties_async.py diff --git a/sdk/cosmos/azure-cosmos/test/test_computed_properties.py b/sdk/cosmos/azure-cosmos/test/test_computed_properties.py new file mode 100644 index 000000000000..545d02c36bd3 --- /dev/null +++ b/sdk/cosmos/azure-cosmos/test/test_computed_properties.py @@ -0,0 +1,249 @@ +# The MIT License (MIT) +# Copyright (c) Microsoft Corporation. All rights reserved. + +import unittest +import uuid +import pytest + +import azure.cosmos.cosmos_client as cosmos_client +import test_config +from azure.cosmos import DatabaseProxy +from azure.cosmos.partition_key import PartitionKey +import azure.cosmos.exceptions as exceptions + +@pytest.mark.cosmosSplit +class TestComputedPropertiesQuery(unittest.TestCase): + """Test to ensure escaping of non-ascii characters from partition key""" + + created_db: DatabaseProxy = None + client: cosmos_client.CosmosClient = None + config = test_config.TestConfig + host = config.host + masterKey = config.masterKey + connectionPolicy = config.connectionPolicy + TEST_DATABASE_ID = config.TEST_DATABASE_ID + + @classmethod + def setUpClass(cls): + if (cls.masterKey == '[YOUR_KEY_HERE]' or + cls.host == '[YOUR_ENDPOINT_HERE]'): + raise Exception( + "You must specify your Azure Cosmos account values for " + "'masterKey' and 'host' at the top of this class to run the " + "tests.") + + cls.client = cosmos_client.CosmosClient(cls.host, cls.masterKey) + cls.client.create_database_if_not_exists(cls.TEST_DATABASE_ID) + cls.created_db = cls.client.get_database_client(cls.TEST_DATABASE_ID) + cls.items = [ + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'prefixOne', 'db_group': 'GroUp1'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'prefixTwo', 'db_group': 'GrOUp1'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord1', 'db_group': 'GroUp2'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord2', 'db_group': 'groUp1'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord3', 'db_group': 'GroUp3'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord4', 'db_group': 'GrOUP1'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord5', 'db_group': 'GroUp2'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 0, 'stringProperty': 'randomWord6', 'db_group': 'group1'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 3, 'stringProperty': 'randomWord7', 'db_group': 'group2'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 2, 'stringProperty': 'randomWord8', 'db_group': 'GroUp3'} + ] + cls.computed_properties = [{'name': "cp_lower", 'query': "SELECT VALUE LOWER(c.db_group) FROM c"}, + {'name': "cp_power", 'query': "SELECT VALUE POWER(c.val, 2) FROM c"}, + {'name': "cp_str_len", 'query': "SELECT VALUE LENGTH(c.stringProperty) FROM c"}] + + def computedPropertiesTestCases(self, created_collection): + # Check that computed properties were properly sent + self.assertListEqual(self.computed_properties, created_collection.read()["computedProperties"]) + + # Test 0: Negative test, test if using non-existent computed property + queried_items = list( + created_collection.query_items(query='Select * from c Where c.cp_upper = "GROUP2"', + partition_key="test")) + self.assertEqual(len(queried_items), 0) + + # Test 1: Test first computed property + queried_items = list( + created_collection.query_items(query='Select * from c Where c.cp_lower = "group1"', partition_key="test")) + self.assertEqual(len(queried_items), 5) + + # Test 1 Negative: Test if using non-existent string in group property returns nothing + queried_items = list( + created_collection.query_items(query='Select * from c Where c.cp_lower = "group4"', partition_key="test")) + self.assertEqual(len(queried_items), 0) + + # Test 2: Test second computed property + queried_items = list( + created_collection.query_items(query='Select * from c Where c.cp_power = 25', partition_key="test")) + self.assertEqual(len(queried_items), 7) + + # Test 2 Negative: Test Non-Existent POWER + queried_items = list( + created_collection.query_items(query='Select * from c Where c.cp_power = 16', partition_key="test")) + self.assertEqual(len(queried_items), 0) + + # Test 3: Test Third Computed Property + queried_items = list( + created_collection.query_items(query='Select * from c Where c.cp_str_len = 9', partition_key="test")) + self.assertEqual(len(queried_items), 2) + + # Test 3 Negative: Test Str length that isn't there + queried_items = list( + created_collection.query_items(query='Select * from c Where c.cp_str_len = 3', partition_key="test")) + self.assertEqual(len(queried_items), 0) + + @pytest.mark.cosmosQuery + def test_computed_properties_query(self): + + created_collection = self.created_db.create_container( + "computed_properties_query_test_" + str(uuid.uuid4()), + PartitionKey(path="/pk"), + computed_properties=self.computed_properties) + + # Create Items + for item in self.items: + created_collection.create_item(body=item) + + self.computedPropertiesTestCases(created_collection) + self.created_db.delete_container(created_collection.id) + + @pytest.mark.cosmosQuery + def test_replace_with_same_computed_properties(self): + created_collection = self.created_db.create_container( + id="computed_properties_query_test_" + str(uuid.uuid4()), + partition_key=PartitionKey(path="/pk"), + computed_properties=self.computed_properties) + + # Create Items + for item in self.items: + created_collection.create_item(body=item) + # Check that computed properties were properly sent + self.assertListEqual(self.computed_properties, created_collection.read()["computedProperties"]) + + replaced_collection= self.created_db.replace_container( + container=created_collection.id, + partition_key=PartitionKey(path="/pk"), + computed_properties= self.computed_properties) + + self.computedPropertiesTestCases(replaced_collection) + self.created_db.delete_container(replaced_collection.id) + + @pytest.mark.cosmosQuery + def test_replace_without_computed_properties(self): + created_collection = self.created_db.create_container( + id="computed_properties_query_test_" + str(uuid.uuid4()), + partition_key=PartitionKey(path="/pk")) + + # Create Items + for item in self.items: + created_collection.create_item(body=item) + + # Replace Container + replaced_collection= self.created_db.replace_container( + container=created_collection.id, + partition_key=PartitionKey(path="/pk"), + computed_properties= self.computed_properties + ) + self.computedPropertiesTestCases(replaced_collection) + self.created_db.delete_container(replaced_collection.id) + + @pytest.mark.cosmosQuery + def test_replace_with_new_computed_properties(self): + created_collection = self.created_db.create_container( + id="computed_properties_query_test_" + str(uuid.uuid4()), + partition_key=PartitionKey(path="/pk"), + computed_properties=self.computed_properties) + + # Create Items + for item in self.items: + created_collection.create_item(body=item) + + # Check that computed properties were properly sent + self.assertListEqual(self.computed_properties, created_collection.read()["computedProperties"]) + + new_computed_properties = [{'name': "cp_upper", 'query': "SELECT VALUE UPPER(c.db_group) FROM c"}, + {'name': "cp_len", 'query': "SELECT VALUE LENGTH(c.stringProperty) FROM c"}] + # Replace Container + replaced_collection = self.created_db.replace_container( + container=created_collection.id, + partition_key=PartitionKey(path="/pk"), + computed_properties=new_computed_properties + ) + # Check that computed properties were properly sent to replaced container + self.assertListEqual(new_computed_properties, replaced_collection.read()["computedProperties"]) + + # Test 1: Test first computed property + queried_items = list( + replaced_collection.query_items(query='Select * from c Where c.cp_upper = "GROUP2"', + partition_key="test")) + self.assertEqual(len(queried_items), 3) + + # Test 1 Negative: Test if using non-existent computed property name returns nothing + queried_items = list( + replaced_collection.query_items(query='Select * from c Where c.cp_lower = "group1"', partition_key="test")) + self.assertEqual(len(queried_items), 0) + + # Test 2: Test Second Computed Property + queried_items = list( + replaced_collection.query_items(query='Select * from c Where c.cp_len = 9', partition_key="test")) + self.assertEqual(len(queried_items), 2) + + # Test 2 Negative: Test Str length using old computed properties name + queried_items = list( + replaced_collection.query_items(query='Select * from c Where c.cp_str_len = 9', partition_key="test")) + self.assertEqual(len(queried_items), 0) + self.created_db.delete_container(created_collection.id) + + @pytest.mark.cosmosQuery + def test_replace_with_incorrect_computed_properties(self): + created_collection = self.created_db.create_container( + id="computed_properties_query_test_" + str(uuid.uuid4()), + partition_key=PartitionKey(path="/pk")) + + # Create Items + for item in self.items: + created_collection.create_item(body=item) + + computed_properties = {'name': "cp_lower", 'query': "SELECT VALUE LOWER(c.db_group) FROM c"} + + try: + # Replace Container with wrong type for computed_properties + self.created_db.replace_container( + container=created_collection.id, + partition_key=PartitionKey(path="/pk"), + computed_properties= computed_properties + ) + pytest.fail("Container creation should have failed for value mismatch.") + except exceptions.CosmosHttpResponseError as e: + assert e.status_code == 400 + assert "One of the specified inputs is invalid" in e.http_error_message + + @pytest.mark.cosmosQuery + async def test_replace_with_remove_computed_properties_(self): + created_collection = self.created_db.create_container( + "computed_properties_query_test_" + str(uuid.uuid4()), + PartitionKey(path="/pk"), + computed_properties=self.computed_properties) + + # Create Items + for item in self.items: + created_collection.create_item(body=item) + + # Check if computed properties were set + container = created_collection.read() + assert self.computed_properties == container["computedProperties"] + + # Replace Container + replaced_collection = self.created_db.replace_container( + container=created_collection.id, + partition_key=PartitionKey(path="/pk")) + + # Check if computed properties were not set + container = replaced_collection.read() + + # If keyError is not raised the test will fail + with pytest.raises(KeyError): + computed_properties = container["computedProperties"] + + +if __name__ == "__main__": + unittest.main() diff --git a/sdk/cosmos/azure-cosmos/test/test_computed_properties_async.py b/sdk/cosmos/azure-cosmos/test/test_computed_properties_async.py new file mode 100644 index 000000000000..0b84c2b669dc --- /dev/null +++ b/sdk/cosmos/azure-cosmos/test/test_computed_properties_async.py @@ -0,0 +1,271 @@ +# The MIT License (MIT) +# Copyright (c) Microsoft Corporation. All rights reserved. + +import unittest +import uuid +import pytest + +import test_config +from azure.cosmos.aio import CosmosClient, DatabaseProxy, ContainerProxy +from azure.cosmos.partition_key import PartitionKey +import azure.cosmos.exceptions as exceptions + +@pytest.mark.cosmosSplit +class TestComputedPropertiesQueryAsync(unittest.IsolatedAsyncioTestCase): + """Test to ensure escaping of non-ascii characters from partition key""" + + created_db: DatabaseProxy = None + created_container: ContainerProxy = None + client: CosmosClient = None + config = test_config.TestConfig + TEST_CONTAINER_ID = config.TEST_MULTI_PARTITION_CONTAINER_ID + TEST_DATABASE_ID = config.TEST_DATABASE_ID + host = config.host + masterKey = config.masterKey + connectionPolicy = config.connectionPolicy + + @classmethod + def setUpClass(cls): + if (cls.masterKey == '[YOUR_KEY_HERE]' or + cls.host == '[YOUR_ENDPOINT_HERE]'): + raise Exception( + "You must specify your Azure Cosmos account values for " + "'masterKey' and 'host' at the top of this class to run the " + "tests.") + + async def asyncSetUp(self): + self.client = CosmosClient(self.host, self.masterKey) + self.created_db = self.client.get_database_client(self.TEST_DATABASE_ID) + self.items = [ + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'prefixOne', 'db_group': 'GroUp1'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'prefixTwo', 'db_group': 'GrOUp1'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord1', 'db_group': 'GroUp2'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord2', 'db_group': 'groUp1'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord3', 'db_group': 'GroUp3'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord4', 'db_group': 'GrOUP1'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord5', 'db_group': 'GroUp2'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 0, 'stringProperty': 'randomWord6', 'db_group': 'group1'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 3, 'stringProperty': 'randomWord7', 'db_group': 'group2'}, + {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 2, 'stringProperty': 'randomWord8', 'db_group': 'GroUp3'} + ] + self.computed_properties = [{'name': "cp_lower", 'query': "SELECT VALUE LOWER(c.db_group) FROM c"}, + {'name': "cp_power", 'query': "SELECT VALUE POWER(c.val, 2) FROM c"}, + {'name': "cp_str_len", 'query': "SELECT VALUE LENGTH(c.stringProperty) FROM c"}] + + async def asyncTearDown(self): + await self.client.close() + + async def computedPropertiesTestCases(self, created_collection): + # Check if computed properties were set + container = await created_collection.read() + assert self.computed_properties == container["computedProperties"] + + # Test 0: Negative test, test if using non-existent computed property + queried_items = [q async for q in + created_collection.query_items(query='Select * from c Where c.cp_upper = "GROUP2"', + partition_key="test")] + assert len(queried_items) == 0 + + # Test 1: Test first computed property + queried_items = [q async for q in + created_collection.query_items(query='Select * from c Where c.cp_lower = "group1"', + partition_key="test")] + assert len(queried_items) == 5 + + # Test 1 Negative: Test if using non-existent string in group property returns nothing + queried_items = [q async for q in + created_collection.query_items(query='Select * from c Where c.cp_lower = "group4"', + partition_key="test")] + assert len(queried_items) == 0 + + # Test 2: Test second computed property + queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_power = 25', + partition_key="test")] + assert len(queried_items) == 7 + + # Test 2 Negative: Test Non-Existent POWER + queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_power = 16', + partition_key="test")] + assert len(queried_items) == 0 + + # Test 3: Test Third Computed Property + queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_str_len = 9', + partition_key="test")] + assert len(queried_items) == 2 + + # Test 3 Negative: Test Str length that isn't there + queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_str_len = 3', + partition_key="test")] + assert len(queried_items) == 0 + + + @pytest.mark.cosmosQuery + async def test_computed_properties_query_async(self): + created_collection = await self.created_db.create_container( + "computed_properties_query_test_" + str(uuid.uuid4()), + PartitionKey(path="/pk"), + computed_properties=self.computed_properties) + + # Create Items + for item in self.items: + await created_collection.create_item(body=item) + + await self.computedPropertiesTestCases(created_collection) + self.created_db.delete_container(created_collection.id) + + + @pytest.mark.cosmosQuery + async def test_replace_with_same_computed_properties_async(self): + created_collection = await self.created_db.create_container( + "computed_properties_query_test_" + str(uuid.uuid4()), + PartitionKey(path="/pk"), + computed_properties=self.computed_properties) + + # Create Items + for item in self.items: + await created_collection.create_item(body=item) + + # Replace Container + replaced_collection = await self.created_db.replace_container( + container=created_collection.id, + partition_key=PartitionKey(path="/pk"), + computed_properties=self.computed_properties + ) + + await self.computedPropertiesTestCases(replaced_collection) + self.created_db.delete_container(created_collection.id) + + + @pytest.mark.cosmosQuery + async def test_replace_without_computed_properties_async(self): + created_collection = await self.created_db.create_container( + "computed_properties_query_test_" + str(uuid.uuid4()), + PartitionKey(path="/pk")) + + # Create Items + for item in self.items: + await created_collection.create_item(body=item) + + # Replace Container + replaced_collection = await self.created_db.replace_container( + container=created_collection.id, + partition_key=PartitionKey(path="/pk"), + computed_properties=self.computed_properties + ) + + await self.computedPropertiesTestCases(replaced_collection) + self.created_db.delete_container(created_collection.id) + + @pytest.mark.cosmosQuery + async def test_replace_with_new_computed_properties_async(self): + created_collection = await self.created_db.create_container( + "computed_properties_query_test_" + str(uuid.uuid4()), + PartitionKey(path="/pk"), + computed_properties=self.computed_properties) + + # Create Items + for item in self.items: + await created_collection.create_item(body=item) + + # Check if computed properties were set + container = await created_collection.read() + assert self.computed_properties == container["computedProperties"] + + new_computed_properties = [{'name': "cp_upper", 'query': "SELECT VALUE UPPER(c.db_group) FROM c"}, + {'name': "cp_len", 'query': "SELECT VALUE LENGTH(c.stringProperty) FROM c"}] + + # Replace Container + replaced_collection = await self.created_db.replace_container( + container=created_collection.id, + partition_key=PartitionKey(path="/pk"), + computed_properties=new_computed_properties + ) + + # Check if computed properties were set + container = await replaced_collection.read() + assert new_computed_properties == container["computedProperties"] + + # Test 1: Test first computed property + queried_items = [q async for q in + replaced_collection.query_items(query='Select * from c Where c.cp_upper = "GROUP2"', + partition_key="test")] + self.assertEqual(len(queried_items), 3) + + # Test 1 Negative: Test if using non-existent computed property name returns nothing + queried_items = [q async for q in + replaced_collection.query_items(query='Select * from c Where c.cp_lower = "group1"', + partition_key="test")] + self.assertEqual(len(queried_items), 0) + + # Test 2: Test Second Computed Property + queried_items = [q async for q in + replaced_collection.query_items(query='Select * from c Where c.cp_len = 9', + partition_key="test")] + self.assertEqual(len(queried_items), 2) + + # Test 2 Negative: Test Str length using old computed properties name + queried_items = [q async for q in + replaced_collection.query_items(query='Select * from c Where c.cp_str_len = 9', + partition_key="test")] + self.assertEqual(len(queried_items), 0) + self.created_db.delete_container(created_collection.id) + + @pytest.mark.cosmosQuery + async def test_replace_with_incorrect_computed_properties_async(self): + created_collection = await self.created_db.create_container( + "computed_properties_query_test_" + str(uuid.uuid4()), + PartitionKey(path="/pk"), + computed_properties=self.computed_properties) + + # Create Items + for item in self.items: + await created_collection.create_item(body=item) + + # Check if computed properties were set + container = await created_collection.read() + assert self.computed_properties == container["computedProperties"] + + new_computed_properties = {'name': "cp_lower", 'query': "SELECT VALUE LOWER(c.db_group) FROM c"} + + try: + # Replace Container with wrong type for computed_properties + await self.created_db.replace_container( + container=created_collection.id, + partition_key=PartitionKey(path="/pk"), + computed_properties=new_computed_properties + ) + pytest.fail("Container creation should have failed for value mismatch.") + except exceptions.CosmosHttpResponseError as e: + assert e.status_code == 400 + assert "One of the specified inputs is invalid" in e.http_error_message + + @pytest.mark.cosmosQuery + async def test_replace_with_remove_computed_properties_async(self): + created_collection = await self.created_db.create_container( + "computed_properties_query_test_" + str(uuid.uuid4()), + PartitionKey(path="/pk"), + computed_properties=self.computed_properties) + + # Create Items + for item in self.items: + await created_collection.create_item(body=item) + + # Check if computed properties were set + container = await created_collection.read() + assert self.computed_properties == container["computedProperties"] + + # Replace Container + replaced_collection = await self.created_db.replace_container( + container=created_collection.id, + partition_key=PartitionKey(path="/pk")) + + # Check if computed properties were not set + container = await replaced_collection.read() + + # If keyError is not raised the test will fail + with pytest.raises(KeyError): + computed_properties = container["computedProperties"] + self.created_db.delete_container(created_collection.id) + +if __name__ == '__main__': + unittest.main() diff --git a/sdk/cosmos/azure-cosmos/test/test_query_computed_properties.py b/sdk/cosmos/azure-cosmos/test/test_query_computed_properties.py deleted file mode 100644 index 359bd544fc57..000000000000 --- a/sdk/cosmos/azure-cosmos/test/test_query_computed_properties.py +++ /dev/null @@ -1,104 +0,0 @@ -# The MIT License (MIT) -# Copyright (c) Microsoft Corporation. All rights reserved. - -import unittest -import uuid -import pytest - -import azure.cosmos.cosmos_client as cosmos_client -import test_config -from azure.cosmos import DatabaseProxy -from azure.cosmos.partition_key import PartitionKey - - -class TestComputedPropertiesQuery(unittest.TestCase): - """Test to ensure escaping of non-ascii characters from partition key""" - - created_db: DatabaseProxy = None - client: cosmos_client.CosmosClient = None - config = test_config.TestConfig - host = config.host - masterKey = config.masterKey - connectionPolicy = config.connectionPolicy - TEST_DATABASE_ID = config.TEST_DATABASE_ID - - @classmethod - def setUpClass(cls): - if (cls.masterKey == '[YOUR_KEY_HERE]' or - cls.host == '[YOUR_ENDPOINT_HERE]'): - raise Exception( - "You must specify your Azure Cosmos account values for " - "'masterKey' and 'host' at the top of this class to run the " - "tests.") - - cls.client = cosmos_client.CosmosClient(cls.host, cls.masterKey) - cls.created_db = cls.client.get_database_client(cls.TEST_DATABASE_ID) - - @pytest.mark.cosmosQuery - def test_computed_properties_query(self): - computed_properties = [{'name': "cp_lower", 'query': "SELECT VALUE LOWER(c.db_group) FROM c"}, - {'name': "cp_power", - 'query': "SELECT VALUE POWER(c.val, 2) FROM c"}, - {'name': "cp_str_len", 'query': "SELECT VALUE LENGTH(c.stringProperty) FROM c"}] - items = [ - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'prefixOne', 'db_group': 'GroUp1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'prefixTwo', 'db_group': 'GrOUp1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord1', 'db_group': 'GroUp2'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord2', 'db_group': 'groUp1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord3', 'db_group': 'GroUp3'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord4', 'db_group': 'GrOUP1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord5', 'db_group': 'GroUp2'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 0, 'stringProperty': 'randomWord6', 'db_group': 'group1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 3, 'stringProperty': 'randomWord7', 'db_group': 'group2'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 2, 'stringProperty': 'randomWord8', 'db_group': 'GroUp3'} - ] - created_collection = self.created_db.create_container( - "computed_properties_query_test_" + str(uuid.uuid4()), PartitionKey(path="/pk") - , computed_properties=computed_properties) - - # Create Items - for item in items: - created_collection.create_item(body=item) - # Check that computed properties were properly sent - self.assertListEqual(computed_properties, created_collection.read()["computedProperties"]) - - # Test 0: Negative test, test if using non-existent computed property - queried_items = list( - created_collection.query_items(query='Select * from c Where c.cp_upper = "GROUP2"', - partition_key="test")) - self.assertEqual(len(queried_items), 0) - - # Test 1: Test first computed property - queried_items = list( - created_collection.query_items(query='Select * from c Where c.cp_lower = "group1"', partition_key="test")) - self.assertEqual(len(queried_items), 5) - - # Test 1 Negative: Test if using non-existent string in group property returns nothing - queried_items = list( - created_collection.query_items(query='Select * from c Where c.cp_lower = "group4"', partition_key="test")) - self.assertEqual(len(queried_items), 0) - - # Test 2: Test second computed property - queried_items = list( - created_collection.query_items(query='Select * from c Where c.cp_power = 25', partition_key="test")) - self.assertEqual(len(queried_items), 7) - - # Test 2 Negative: Test Non-Existent POWER - queried_items = list( - created_collection.query_items(query='Select * from c Where c.cp_power = 16', partition_key="test")) - self.assertEqual(len(queried_items), 0) - - # Test 3: Test Third Computed Property - queried_items = list( - created_collection.query_items(query='Select * from c Where c.cp_str_len = 9', partition_key="test")) - self.assertEqual(len(queried_items), 2) - - # Test 3 Negative: Test Str length that isn't there - queried_items = list( - created_collection.query_items(query='Select * from c Where c.cp_str_len = 3', partition_key="test")) - self.assertEqual(len(queried_items), 0) - self.created_db.delete_container(created_collection.id) - - -if __name__ == "__main__": - unittest.main() diff --git a/sdk/cosmos/azure-cosmos/test/test_query_computed_properties_async.py b/sdk/cosmos/azure-cosmos/test/test_query_computed_properties_async.py deleted file mode 100644 index 88a32352223e..000000000000 --- a/sdk/cosmos/azure-cosmos/test/test_query_computed_properties_async.py +++ /dev/null @@ -1,113 +0,0 @@ -# The MIT License (MIT) -# Copyright (c) Microsoft Corporation. All rights reserved. - -import unittest -import uuid -import pytest - -import test_config -from azure.cosmos.aio import CosmosClient, DatabaseProxy, ContainerProxy -from azure.cosmos.partition_key import PartitionKey - - -class TestComputedPropertiesQueryAsync(unittest.IsolatedAsyncioTestCase): - """Test to ensure escaping of non-ascii characters from partition key""" - - created_db: DatabaseProxy = None - created_container: ContainerProxy = None - client: CosmosClient = None - config = test_config.TestConfig - TEST_CONTAINER_ID = config.TEST_MULTI_PARTITION_CONTAINER_ID - TEST_DATABASE_ID = config.TEST_DATABASE_ID - host = config.host - masterKey = config.masterKey - connectionPolicy = config.connectionPolicy - - @classmethod - def setUpClass(cls): - if (cls.masterKey == '[YOUR_KEY_HERE]' or - cls.host == '[YOUR_ENDPOINT_HERE]'): - raise Exception( - "You must specify your Azure Cosmos account values for " - "'masterKey' and 'host' at the top of this class to run the " - "tests.") - - async def asyncSetUp(self): - self.client = CosmosClient(self.host, self.masterKey) - self.created_db = self.client.get_database_client(self.TEST_DATABASE_ID) - - async def asyncTearDown(self): - await self.client.close() - - @pytest.mark.cosmosQuery - async def test_computed_properties_query_async(self): - computed_properties = [{'name': "cp_lower", 'query': "SELECT VALUE LOWER(c.db_group) FROM c"}, - {'name': "cp_power", - 'query': "SELECT VALUE POWER(c.val, 2) FROM c"}, - {'name': "cp_str_len", 'query': "SELECT VALUE LENGTH(c.stringProperty) FROM c"}] - items = [ - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'prefixOne', 'db_group': 'GroUp1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'prefixTwo', 'db_group': 'GrOUp1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord1', 'db_group': 'GroUp2'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord2', 'db_group': 'groUp1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord3', 'db_group': 'GroUp3'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord4', 'db_group': 'GrOUP1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord5', 'db_group': 'GroUp2'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 0, 'stringProperty': 'randomWord6', 'db_group': 'group1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 3, 'stringProperty': 'randomWord7', 'db_group': 'group2'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 2, 'stringProperty': 'randomWord8', 'db_group': 'GroUp3'} - ] - created_collection = await self.created_db.create_container( - "computed_properties_query_test_" + str(uuid.uuid4()), - PartitionKey(path="/pk"), - computed_properties=computed_properties) - - # Create Items - for item in items: - await created_collection.create_item(body=item) - - # Check if computed properties were set - container = await created_collection.read() - assert computed_properties == container["computedProperties"] - - # Test 0: Negative test, test if using non-existent computed property - queried_items = [q async for q in - created_collection.query_items(query='Select * from c Where c.cp_upper = "GROUP2"', - partition_key="test")] - assert len(queried_items) == 0 - - # Test 1: Test first computed property - queried_items = [q async for q in - created_collection.query_items(query='Select * from c Where c.cp_lower = "group1"', - partition_key="test")] - assert len(queried_items) == 5 - - # Test 1 Negative: Test if using non-existent string in group property returns nothing - queried_items = [q async for q in - created_collection.query_items(query='Select * from c Where c.cp_lower = "group4"', - partition_key="test")] - assert len(queried_items) == 0 - - # Test 2: Test second computed property - queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_power = 25', - partition_key="test")] - assert len(queried_items) == 7 - - # Test 2 Negative: Test Non-Existent POWER - queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_power = 16', - partition_key="test")] - assert len(queried_items) == 0 - - # Test 3: Test Third Computed Property - queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_str_len = 9', - partition_key="test")] - assert len(queried_items) == 2 - - # Test 3 Negative: Test Str length that isn't there - queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_str_len = 3', - partition_key="test")] - assert len(queried_items) == 0 - - -if __name__ == '__main__': - unittest.main() From 76340305186a42719732a4adaf85edb1f2f421ef Mon Sep 17 00:00:00 2001 From: Andrew Mathew Date: Wed, 12 Feb 2025 12:13:06 -0500 Subject: [PATCH 6/7] added replace computed_properties feature with tests --- sdk/cosmos/azure-cosmos/CHANGELOG.md | 3 +- .../azure/cosmos/aio/_database.py | 7 +- .../azure-cosmos/azure/cosmos/database.py | 7 +- sdk/cosmos/azure-cosmos/test/test_query.py | 65 ----------------- .../azure-cosmos/test/test_query_async.py | 69 ------------------- 5 files changed, 12 insertions(+), 139 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/CHANGELOG.md b/sdk/cosmos/azure-cosmos/CHANGELOG.md index 06f2317f1e46..5bea792414d6 100644 --- a/sdk/cosmos/azure-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos/CHANGELOG.md @@ -3,12 +3,14 @@ ### 4.9.1b5 (Unreleased) #### Features Added +* Added ability to replace `computed_properties` through `replace_container` method. See [PR 39543](https://github.com/Azure/azure-sdk-for-python/pull/39543) #### Breaking Changes #### Bugs Fixed #### Other Changes +* Un-marked `computed_properties` keyword as **provisional**. See [PR 39543](https://github.com/Azure/azure-sdk-for-python/pull/39543) ### 4.9.1b4 (2025-02-06) @@ -45,7 +47,6 @@ #### Other Changes * This release requires aiohttp version 3.10.11 and above. See [PR 39396](https://github.com/Azure/azure-sdk-for-python/pull/39396) -* Un-marked `computed_properties` keyword as **provisional**. See [PR 39543](https://github.com/Azure/azure-sdk-for-python/pull/39543) ### 4.9.1b1 (2024-12-13) diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_database.py b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_database.py index 90f7c14eff27..a8ef44150525 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_database.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/aio/_database.py @@ -256,7 +256,6 @@ async def create_container( definition["conflictResolutionPolicy"] = conflict_resolution_policy if analytical_storage_ttl is not None: definition["analyticalStorageTtl"] = analytical_storage_ttl - computed_properties = kwargs.pop('computed_properties', None) if computed_properties is not None: definition["computedProperties"] = computed_properties if vector_embedding_policy is not None: @@ -346,7 +345,6 @@ async def create_container_if_not_exists( :returns: A `ContainerProxy` instance representing the new container. :rtype: ~azure.cosmos.aio.ContainerProxy """ - computed_properties = kwargs.pop("computed_properties", None) try: container_proxy = self.get_container_client(id) await container_proxy.read( @@ -506,6 +504,7 @@ async def replace_container( etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, analytical_storage_ttl: Optional[int] = None, + computed_properties: Optional[List[Dict[str, str]]] = None, full_text_policy: Optional[Dict[str, Any]] = None, **kwargs: Any ) -> ContainerProxy: @@ -532,6 +531,9 @@ async def replace_container( :keyword int analytical_storage_ttl: Analytical store time to live (TTL) for items in the container. A value of None leaves analytical storage off and a value of -1 turns analytical storage on with no TTL. Please note that analytical storage can only be enabled on Synapse Link enabled accounts. + :keyword List[Dict[str, str]] computed_properties: Sets The computed properties for this + container in the Azure Cosmos DB Service. For more Information on how to use computed properties visit + `here: https://learn.microsoft.com/azure/cosmos-db/nosql/query/computed-properties?tabs=dotnet` :keyword response_hook: A callable invoked with the response metadata. :paramtype response_hook: Callable[[Dict[str, str], Dict[str, Any]], None] :keyword Dict[str, Any] full_text_policy: **provisional** The full text policy for the container. @@ -573,6 +575,7 @@ async def replace_container( "defaultTtl": default_ttl, "conflictResolutionPolicy": conflict_resolution_policy, "analyticalStorageTtl": analytical_storage_ttl, + "computedProperties": computed_properties, "fullTextPolicy": full_text_policy }.items() if value is not None diff --git a/sdk/cosmos/azure-cosmos/azure/cosmos/database.py b/sdk/cosmos/azure-cosmos/azure/cosmos/database.py index 74597b206d6c..dfc7e8b61d65 100644 --- a/sdk/cosmos/azure-cosmos/azure/cosmos/database.py +++ b/sdk/cosmos/azure-cosmos/azure/cosmos/database.py @@ -250,7 +250,6 @@ def create_container( # pylint:disable=docstring-missing-param definition["conflictResolutionPolicy"] = conflict_resolution_policy if analytical_storage_ttl is not None: definition["analyticalStorageTtl"] = analytical_storage_ttl - computed_properties = kwargs.pop('computed_properties', None) if computed_properties is not None: definition["computedProperties"] = computed_properties if vector_embedding_policy is not None: @@ -343,7 +342,6 @@ def create_container_if_not_exists( # pylint:disable=docstring-missing-param :raises ~azure.cosmos.exceptions.CosmosHttpResponseError: The container read or creation failed. :rtype: ~azure.cosmos.ContainerProxy """ - computed_properties = kwargs.pop("computed_properties", None) try: container_proxy = self.get_container_client(id) container_proxy.read( @@ -561,6 +559,7 @@ def replace_container( # pylint:disable=docstring-missing-param etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None, analytical_storage_ttl: Optional[int] = None, + computed_properties: Optional[List[Dict[str, str]]] = None, full_text_policy: Optional[Dict[str, Any]] = None, **kwargs: Any ) -> ContainerProxy: @@ -585,6 +584,9 @@ def replace_container( # pylint:disable=docstring-missing-param :keyword int analytical_storage_ttl: Analytical store time to live (TTL) for items in the container. A value of None leaves analytical storage off and a value of -1 turns analytical storage on with no TTL. Please note that analytical storage can only be enabled on Synapse Link enabled accounts. + :keyword List[Dict[str, str]] computed_properties: Sets The computed properties for this + container in the Azure Cosmos DB Service. For more Information on how to use computed properties visit + `here: https://learn.microsoft.com/azure/cosmos-db/nosql/query/computed-properties?tabs=dotnet` :keyword Callable response_hook: A callable invoked with the response metadata. :keyword Dict[str, Any] full_text_policy: **provisional** The full text policy for the container. Used to denote the default language to be used for all full text indexes, or to individually @@ -630,6 +632,7 @@ def replace_container( # pylint:disable=docstring-missing-param "defaultTtl": default_ttl, "conflictResolutionPolicy": conflict_resolution_policy, "analyticalStorageTtl": analytical_storage_ttl, + "computedProperties": computed_properties, "fullTextPolicy": full_text_policy, }.items() if value is not None diff --git a/sdk/cosmos/azure-cosmos/test/test_query.py b/sdk/cosmos/azure-cosmos/test/test_query.py index 28fbcd3e0a51..28262aa0f7e3 100644 --- a/sdk/cosmos/azure-cosmos/test/test_query.py +++ b/sdk/cosmos/azure-cosmos/test/test_query.py @@ -507,71 +507,6 @@ def test_continuation_token_size_limit_query(self): # verify a second time self.assertLessEqual(len(token.encode('utf-8')), 1024) - @pytest.mark.cosmosQuery - def test_computed_properties_query(self): - computed_properties = [{'name': "cp_lower", 'query': "SELECT VALUE LOWER(c.db_group) FROM c"}, - {'name': "cp_power", - 'query': "SELECT VALUE POWER(c.val, 2) FROM c"}, - {'name': "cp_str_len", 'query': "SELECT VALUE LENGTH(c.stringProperty) FROM c"}] - items = [ - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'prefixOne', 'db_group': 'GroUp1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'prefixTwo', 'db_group': 'GrOUp1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord1', 'db_group': 'GroUp2'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord2', 'db_group': 'groUp1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord3', 'db_group': 'GroUp3'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord4', 'db_group': 'GrOUP1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord5', 'db_group': 'GroUp2'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 0, 'stringProperty': 'randomWord6', 'db_group': 'group1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 3, 'stringProperty': 'randomWord7', 'db_group': 'group2'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 2, 'stringProperty': 'randomWord8', 'db_group': 'GroUp3'} - ] - created_collection = self.created_db.create_container( - "computed_properties_query_test_" + str(uuid.uuid4()), PartitionKey(path="/pk") - , computed_properties=computed_properties) - - # Create Items - for item in items: - created_collection.create_item(body=item) - # Check that computed properties were properly sent - self.assertListEqual(computed_properties, created_collection._get_properties()["computedProperties"]) - - # Test 0: Negative test, test if using non-existent computed property - queried_items = list( - created_collection.query_items(query='Select * from c Where c.cp_upper = "GROUP2"', - partition_key="test")) - self.assertEqual(len(queried_items), 0) - - # Test 1: Test first computed property - queried_items = list( - created_collection.query_items(query='Select * from c Where c.cp_lower = "group1"', partition_key="test")) - self.assertEqual(len(queried_items), 5) - - # Test 1 Negative: Test if using non-existent string in group property returns nothing - queried_items = list( - created_collection.query_items(query='Select * from c Where c.cp_lower = "group4"', partition_key="test")) - self.assertEqual(len(queried_items), 0) - - # Test 2: Test second computed property - queried_items = list( - created_collection.query_items(query='Select * from c Where c.cp_power = 25', partition_key="test")) - self.assertEqual(len(queried_items), 7) - - # Test 2 Negative: Test Non-Existent POWER - queried_items = list( - created_collection.query_items(query='Select * from c Where c.cp_power = 16', partition_key="test")) - self.assertEqual(len(queried_items), 0) - - # Test 3: Test Third Computed Property - queried_items = list( - created_collection.query_items(query='Select * from c Where c.cp_str_len = 9', partition_key="test")) - self.assertEqual(len(queried_items), 2) - - # Test 3 Negative: Test Str length that isn't there - queried_items = list( - created_collection.query_items(query='Select * from c Where c.cp_str_len = 3', partition_key="test")) - self.assertEqual(len(queried_items), 0) - self.created_db.delete_container(created_collection.id) - def test_query_request_params_none_retry_policy(self): created_collection = self.created_db.create_container( "query_request_params_none_retry_policy_" + str(uuid.uuid4()), PartitionKey(path="/pk")) diff --git a/sdk/cosmos/azure-cosmos/test/test_query_async.py b/sdk/cosmos/azure-cosmos/test/test_query_async.py index eca772a31ed6..19e01d8149b4 100644 --- a/sdk/cosmos/azure-cosmos/test/test_query_async.py +++ b/sdk/cosmos/azure-cosmos/test/test_query_async.py @@ -489,75 +489,6 @@ async def test_continuation_token_size_limit_query_async(self): # verify a second time assert len(token.encode('utf-8')) <= 1024 - @pytest.mark.cosmosQuery - async def test_computed_properties_query(self): - computed_properties = [{'name': "cp_lower", 'query': "SELECT VALUE LOWER(c.db_group) FROM c"}, - {'name': "cp_power", - 'query': "SELECT VALUE POWER(c.val, 2) FROM c"}, - {'name': "cp_str_len", 'query': "SELECT VALUE LENGTH(c.stringProperty) FROM c"}] - items = [ - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'prefixOne', 'db_group': 'GroUp1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'prefixTwo', 'db_group': 'GrOUp1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord1', 'db_group': 'GroUp2'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord2', 'db_group': 'groUp1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord3', 'db_group': 'GroUp3'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord4', 'db_group': 'GrOUP1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 5, 'stringProperty': 'randomWord5', 'db_group': 'GroUp2'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 0, 'stringProperty': 'randomWord6', 'db_group': 'group1'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 3, 'stringProperty': 'randomWord7', 'db_group': 'group2'}, - {'id': str(uuid.uuid4()), 'pk': 'test', 'val': 2, 'stringProperty': 'randomWord8', 'db_group': 'GroUp3'} - ] - created_collection = await self.created_db.create_container( - "computed_properties_query_test_" + str(uuid.uuid4()), - PartitionKey(path="/pk"), - computed_properties=computed_properties) - - # Create Items - for item in items: - await created_collection.create_item(body=item) - - # Check if computed properties were set - container_properties = await created_collection._get_properties() - assert computed_properties == container_properties["computedProperties"] - - # Test 0: Negative test, test if using non-existent computed property - queried_items = [q async for q in - created_collection.query_items(query='Select * from c Where c.cp_upper = "GROUP2"', - partition_key="test")] - assert len(queried_items) == 0 - - # Test 1: Test first computed property - queried_items = [q async for q in - created_collection.query_items(query='Select * from c Where c.cp_lower = "group1"', - partition_key="test")] - assert len(queried_items) == 5 - - # Test 1 Negative: Test if using non-existent string in group property returns nothing - queried_items = [q async for q in - created_collection.query_items(query='Select * from c Where c.cp_lower = "group4"', - partition_key="test")] - assert len(queried_items) == 0 - - # Test 2: Test second computed property - queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_power = 25', - partition_key="test")] - assert len(queried_items) == 7 - - # Test 2 Negative: Test Non-Existent POWER - queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_power = 16', - partition_key="test")] - assert len(queried_items) == 0 - - # Test 3: Test Third Computed Property - queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_str_len = 9', - partition_key="test")] - assert len(queried_items) == 2 - - # Test 3 Negative: Test Str length that isn't there - queried_items = [q async for q in created_collection.query_items(query='Select * from c Where c.cp_str_len = 3', - partition_key="test")] - assert len(queried_items) == 0 - async def test_cosmos_query_retryable_error_async(self): async def query_items(database): # Tests to make sure 429 exception is surfaced when retries run out in the first page of a query. From 4b1966a6ed1588886925ccb5ad2728ee20e29bce Mon Sep 17 00:00:00 2001 From: Andrew Mathew Date: Wed, 12 Feb 2025 14:51:40 -0500 Subject: [PATCH 7/7] changed test class annotation to cosmosQuery, removed extra annotations --- .../azure-cosmos/test/test_computed_properties.py | 13 +++---------- .../test/test_computed_properties_async.py | 11 ++--------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/sdk/cosmos/azure-cosmos/test/test_computed_properties.py b/sdk/cosmos/azure-cosmos/test/test_computed_properties.py index 545d02c36bd3..c41fcd52567f 100644 --- a/sdk/cosmos/azure-cosmos/test/test_computed_properties.py +++ b/sdk/cosmos/azure-cosmos/test/test_computed_properties.py @@ -11,7 +11,7 @@ from azure.cosmos.partition_key import PartitionKey import azure.cosmos.exceptions as exceptions -@pytest.mark.cosmosSplit +@pytest.mark.cosmosQuery class TestComputedPropertiesQuery(unittest.TestCase): """Test to ensure escaping of non-ascii characters from partition key""" @@ -91,7 +91,6 @@ def computedPropertiesTestCases(self, created_collection): created_collection.query_items(query='Select * from c Where c.cp_str_len = 3', partition_key="test")) self.assertEqual(len(queried_items), 0) - @pytest.mark.cosmosQuery def test_computed_properties_query(self): created_collection = self.created_db.create_container( @@ -106,7 +105,6 @@ def test_computed_properties_query(self): self.computedPropertiesTestCases(created_collection) self.created_db.delete_container(created_collection.id) - @pytest.mark.cosmosQuery def test_replace_with_same_computed_properties(self): created_collection = self.created_db.create_container( id="computed_properties_query_test_" + str(uuid.uuid4()), @@ -127,7 +125,6 @@ def test_replace_with_same_computed_properties(self): self.computedPropertiesTestCases(replaced_collection) self.created_db.delete_container(replaced_collection.id) - @pytest.mark.cosmosQuery def test_replace_without_computed_properties(self): created_collection = self.created_db.create_container( id="computed_properties_query_test_" + str(uuid.uuid4()), @@ -146,7 +143,6 @@ def test_replace_without_computed_properties(self): self.computedPropertiesTestCases(replaced_collection) self.created_db.delete_container(replaced_collection.id) - @pytest.mark.cosmosQuery def test_replace_with_new_computed_properties(self): created_collection = self.created_db.create_container( id="computed_properties_query_test_" + str(uuid.uuid4()), @@ -193,7 +189,6 @@ def test_replace_with_new_computed_properties(self): self.assertEqual(len(queried_items), 0) self.created_db.delete_container(created_collection.id) - @pytest.mark.cosmosQuery def test_replace_with_incorrect_computed_properties(self): created_collection = self.created_db.create_container( id="computed_properties_query_test_" + str(uuid.uuid4()), @@ -212,13 +207,12 @@ def test_replace_with_incorrect_computed_properties(self): partition_key=PartitionKey(path="/pk"), computed_properties= computed_properties ) - pytest.fail("Container creation should have failed for value mismatch.") + pytest.fail("Container creation should have failed for invalid input.") except exceptions.CosmosHttpResponseError as e: assert e.status_code == 400 assert "One of the specified inputs is invalid" in e.http_error_message - @pytest.mark.cosmosQuery - async def test_replace_with_remove_computed_properties_(self): + def test_replace_with_remove_computed_properties_(self): created_collection = self.created_db.create_container( "computed_properties_query_test_" + str(uuid.uuid4()), PartitionKey(path="/pk"), @@ -244,6 +238,5 @@ async def test_replace_with_remove_computed_properties_(self): with pytest.raises(KeyError): computed_properties = container["computedProperties"] - if __name__ == "__main__": unittest.main() diff --git a/sdk/cosmos/azure-cosmos/test/test_computed_properties_async.py b/sdk/cosmos/azure-cosmos/test/test_computed_properties_async.py index 0b84c2b669dc..ff4ab0948fbe 100644 --- a/sdk/cosmos/azure-cosmos/test/test_computed_properties_async.py +++ b/sdk/cosmos/azure-cosmos/test/test_computed_properties_async.py @@ -10,7 +10,7 @@ from azure.cosmos.partition_key import PartitionKey import azure.cosmos.exceptions as exceptions -@pytest.mark.cosmosSplit +@pytest.mark.cosmosQuery class TestComputedPropertiesQueryAsync(unittest.IsolatedAsyncioTestCase): """Test to ensure escaping of non-ascii characters from partition key""" @@ -99,7 +99,6 @@ async def computedPropertiesTestCases(self, created_collection): assert len(queried_items) == 0 - @pytest.mark.cosmosQuery async def test_computed_properties_query_async(self): created_collection = await self.created_db.create_container( "computed_properties_query_test_" + str(uuid.uuid4()), @@ -114,7 +113,6 @@ async def test_computed_properties_query_async(self): self.created_db.delete_container(created_collection.id) - @pytest.mark.cosmosQuery async def test_replace_with_same_computed_properties_async(self): created_collection = await self.created_db.create_container( "computed_properties_query_test_" + str(uuid.uuid4()), @@ -135,8 +133,6 @@ async def test_replace_with_same_computed_properties_async(self): await self.computedPropertiesTestCases(replaced_collection) self.created_db.delete_container(created_collection.id) - - @pytest.mark.cosmosQuery async def test_replace_without_computed_properties_async(self): created_collection = await self.created_db.create_container( "computed_properties_query_test_" + str(uuid.uuid4()), @@ -156,7 +152,6 @@ async def test_replace_without_computed_properties_async(self): await self.computedPropertiesTestCases(replaced_collection) self.created_db.delete_container(created_collection.id) - @pytest.mark.cosmosQuery async def test_replace_with_new_computed_properties_async(self): created_collection = await self.created_db.create_container( "computed_properties_query_test_" + str(uuid.uuid4()), @@ -210,7 +205,6 @@ async def test_replace_with_new_computed_properties_async(self): self.assertEqual(len(queried_items), 0) self.created_db.delete_container(created_collection.id) - @pytest.mark.cosmosQuery async def test_replace_with_incorrect_computed_properties_async(self): created_collection = await self.created_db.create_container( "computed_properties_query_test_" + str(uuid.uuid4()), @@ -234,12 +228,11 @@ async def test_replace_with_incorrect_computed_properties_async(self): partition_key=PartitionKey(path="/pk"), computed_properties=new_computed_properties ) - pytest.fail("Container creation should have failed for value mismatch.") + pytest.fail("Container creation should have failed for invalid input.") except exceptions.CosmosHttpResponseError as e: assert e.status_code == 400 assert "One of the specified inputs is invalid" in e.http_error_message - @pytest.mark.cosmosQuery async def test_replace_with_remove_computed_properties_async(self): created_collection = await self.created_db.create_container( "computed_properties_query_test_" + str(uuid.uuid4()),