Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 conditional support for preview feature of Computed Properties on Python SDK. See [PR 33626](https://github.com/Azure/azure-sdk-for-python/pull/33626)
Comment thread
bambriz marked this conversation as resolved.
Outdated

#### Breaking Changes

Expand Down
19 changes: 18 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,12 @@ 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: Computed properties must be at the top level in the item and
can't have a nested path. Each computed property definition has two components: a name and a query.
The name is the computed property name, and the query defines logic to calculate the property value
for each item. Computed properties are scoped to an individual item and therefore can't use values
from multiple items or rely on other computed properties. Every container can have
a maximum of 20 computed properties.
Comment thread
bambriz marked this conversation as resolved.
Outdated
: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 +231,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 +278,12 @@ 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: Computed properties must be at the top level in the item and
can't have a nested path. Each computed property definition has two components: a name and a query.
The name is the computed property name, and the query defines logic to calculate the property value
for each item. Computed properties are scoped to an individual item and therefore can't use values
from multiple items or rely on other computed properties. Every container can have
a maximum of 20 computed properties.
: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 +299,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 +313,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
18 changes: 17 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,12 @@ 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: Computed properties must be at the top level in the item and
can't have a nested path. Each computed property definition has two components: a name and a query.
The name is the computed property name, and the query defines logic to calculate the property value
for each item. Computed properties are scoped to an individual item and therefore can't use values
from multiple items or rely on other computed properties. Every container can have
a maximum of 20 computed properties.
: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 +229,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 +287,18 @@ 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: Computed properties must be at the top level in the item and
can't have a nested path. Each computed property definition has two components: a name and a query.
The name is the computed property name, and the query defines logic to calculate the property value
for each item. Computed properties are scoped to an individual item and therefore can't use values
from multiple items or rely on other computed properties. Every container can have
a maximum of 20 computed properties.
: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 +317,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