Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk/cosmos/azure-cosmos/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### 4.5.2b4 (Unreleased)

#### Features Added
* Added **preview** support for Computed Properties on Python SDK (Must be enabled on the account level before it can be used). See [PR 33626](https://github.com/Azure/azure-sdk-for-python/pull/33626).

#### Breaking Changes

Expand Down
13 changes: 12 additions & 1 deletion sdk/cosmos/azure-cosmos/azure/cosmos/aio/_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ 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: 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/en-us/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 int analytical_storage_ttl: Analytical store time to live (TTL) for items in the container. A value of
Expand Down Expand Up @@ -225,6 +228,9 @@ async def create_container(
analytical_storage_ttl = kwargs.pop("analytical_storage_ttl", None)
if analytical_storage_ttl is not None:
definition["analyticalStorageTtl"] = analytical_storage_ttl
computed_properties = kwargs.pop('computed_properties', None)
if computed_properties:
definition["computedProperties"] = computed_properties

request_options = _build_options(kwargs)
response_hook = kwargs.pop('response_hook', None)
Expand Down Expand Up @@ -269,6 +275,9 @@ 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: 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/en-us/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 int analytical_storage_ttl: Analytical store time to live (TTL) for items in the container. A value of
Expand All @@ -284,6 +293,7 @@ async def create_container_if_not_exists(
conflict_resolution_policy = kwargs.pop('conflict_resolution_policy', None)
analytical_storage_ttl = kwargs.pop("analytical_storage_ttl", None)
offer_throughput = kwargs.pop('offer_throughput', None)
computed_properties = kwargs.pop("computed_properties", None)
try:
container_proxy = self.get_container_client(id)
await container_proxy.read(**kwargs)
Expand All @@ -297,7 +307,8 @@ async def create_container_if_not_exists(
offer_throughput=offer_throughput,
unique_key_policy=unique_key_policy,
conflict_resolution_policy=conflict_resolution_policy,
analytical_storage_ttl=analytical_storage_ttl
analytical_storage_ttl=analytical_storage_ttl,
computed_properties=computed_properties
)

def get_container_client(self, container: Union[str, ContainerProxy, Dict[str, Any]]) -> ContainerProxy:
Expand Down
12 changes: 11 additions & 1 deletion sdk/cosmos/azure-cosmos/azure/cosmos/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ def create_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/en-us/azure/cosmos-db/nosql/query/computed-properties?tabs=dotnet`
:returns: A `ContainerProxy` instance representing the new container.
:raises ~azure.cosmos.exceptions.CosmosHttpResponseError: The container creation failed.
:rtype: ~azure.cosmos.ContainerProxy
Expand Down Expand Up @@ -223,7 +226,9 @@ def create_container(
analytical_storage_ttl = kwargs.pop("analytical_storage_ttl", None)
if analytical_storage_ttl is not None:
definition["analyticalStorageTtl"] = analytical_storage_ttl

computed_properties = kwargs.pop('computed_properties', None)
if computed_properties:
definition["computedProperties"] = computed_properties
request_options = build_options(kwargs)
response_hook = kwargs.pop('response_hook', None)
if populate_query_metrics is not None:
Expand Down Expand Up @@ -279,11 +284,15 @@ def create_container_if_not_exists(
: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/en-us/azure/cosmos-db/nosql/query/computed-properties?tabs=dotnet`
:returns: A `ContainerProxy` instance representing the container.
:raises ~azure.cosmos.exceptions.CosmosHttpResponseError: The container read or creation failed.
:rtype: ~azure.cosmos.ContainerProxy
"""
analytical_storage_ttl = kwargs.pop("analytical_storage_ttl", None)
computed_properties = kwargs.pop("computed_properties", None)
try:
container_proxy = self.get_container_client(id)
container_proxy.read(
Expand All @@ -302,6 +311,7 @@ def create_container_if_not_exists(
unique_key_policy=unique_key_policy,
conflict_resolution_policy=conflict_resolution_policy,
analytical_storage_ttl=analytical_storage_ttl,
computed_properties=computed_properties
)

@distributed_trace
Expand Down
67 changes: 67 additions & 0 deletions sdk/cosmos/azure-cosmos/test/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,73 @@ def test_continuation_token_size_limit_query(self):
self.assertLessEqual(len(token.encode('utf-8')), 1024)
self.created_db.delete_container(container)

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_database = self.config.create_database_if_not_exist(self.client)
created_collection = self.created_db.create_container_if_not_exists(
"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.client.delete_database(created_database)


def _MockNextFunction(self):
if self.count < len(self.payloads):
item, result = self.get_mock_result(self.payloads, self.count)
Expand Down
68 changes: 68 additions & 0 deletions sdk/cosmos/azure-cosmos/test/test_query_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,74 @@ async def test_continuation_token_size_limit_query_async(self):
assert len(token.encode('utf-8')) <= 1024
await self.created_db.delete_container(container)

@pytest.mark.asyncio
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_database = await self.config.create_database_if_not_exist(self.client)
created_collection = await self.created_db.create_container_if_not_exists(
"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

await self.client.delete_database(created_database)

def _MockNextFunction(self):
if self.count < len(self.payloads):
item, result = self.get_mock_result(self.payloads, self.count)
Expand Down