Skip to content

Commit 5665c4a

Browse files
authored
[Bug Fix] None and Null Partititon Key Improvements (#42747)
1 parent 5851cc3 commit 5665c4a

12 files changed

+727
-123
lines changed

sdk/cosmos/azure-cosmos/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* Fixed bug where `excluded_locations` was not being honored for some metadata calls. See [PR 42266](https://github.com/Azure/azure-sdk-for-python/pull/42266).
1616
* Fixed bug where Hybrid Search queries using parameters were not working. See [PR 42787](https://github.com/Azure/azure-sdk-for-python/pull/42787)
1717
* Fixed partition scoping for per partition circuit breaker. See [PR 42751](https://github.com/Azure/azure-sdk-for-python/pull/42751)
18+
* Fixed bug where `partition_key` set to None was not properly handled for some operations. See [PR 42747](https://github.com/Azure/azure-sdk-for-python/pull/42747)
1819

1920
#### Other Changes
2021
* Added session token false progress merge logic. See [42393](https://github.com/Azure/azure-sdk-for-python/pull/42393)

sdk/cosmos/azure-cosmos/azure/cosmos/aio/_container.py

Lines changed: 84 additions & 41 deletions
Large diffs are not rendered by default.

sdk/cosmos/azure-cosmos/azure/cosmos/aio/_cosmos_client_connection_async.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import uuid
2929
from typing import (
3030
Callable, Dict, Any, Iterable, Mapping, Optional, List,
31-
Sequence, Tuple, Type, Union, cast
31+
Sequence, Tuple, Union, cast
3232
)
3333

3434
from typing_extensions import TypedDict
@@ -76,15 +76,14 @@
7676
_PartitionKeyKind,
7777
_SequentialPartitionKeyType,
7878
_return_undefined_or_empty_partition_key,
79-
NonePartitionKeyValue, _Empty,
80-
_build_partition_key_from_properties,
79+
_Empty,
80+
_build_partition_key_from_properties, _PartitionKeyType
8181
)
8282
from ._auth_policy_async import AsyncCosmosBearerTokenCredentialPolicy
8383
from .._cosmos_http_logging_policy import CosmosHttpLoggingPolicy
8484
from .._range_partition_resolver import RangePartitionResolver
8585

8686

87-
PartitionKeyType = Union[str, int, float, bool, Sequence[Union[str, int, float, bool, None]], Type[NonePartitionKeyValue]] # pylint: disable=line-too-long
8887

8988

9089
class CredentialDict(TypedDict, total=False):
@@ -2260,15 +2259,15 @@ async def fetch_fn(options: Mapping[str, Any]) -> Tuple[List[Dict[str, Any]], Ca
22602259
async def read_items(
22612260
self,
22622261
collection_link: str,
2263-
items: Sequence[Tuple[str, PartitionKeyType]],
2262+
items: Sequence[Tuple[str, _PartitionKeyType]],
22642263
options: Optional[Mapping[str, Any]] = None,
22652264
**kwargs: Any
22662265
) -> CosmosList:
22672266
"""Reads many items.
22682267
22692268
:param str collection_link: The link to the document collection.
22702269
:param items: A list of tuples, where each tuple contains an item's ID and partition key.
2271-
:type items: Sequence[Tuple[str, PartitionKeyType]]
2270+
:type items: Sequence[Tuple[str, _PartitionKeyType]]
22722271
:param dict options: The request options for the request.
22732272
:return: The list of read items.
22742273
:rtype: CosmosList
@@ -2323,7 +2322,7 @@ def QueryItems(
23232322
database_or_container_link: str,
23242323
query: Optional[Union[str, Dict[str, Any]]],
23252324
options: Optional[Mapping[str, Any]] = None,
2326-
partition_key: Optional[PartitionKeyType] = None,
2325+
partition_key: Optional[_PartitionKeyType] = None,
23272326
response_hook: Optional[Callable[[Mapping[str, Any], Dict[str, Any]], None]] = None,
23282327
**kwargs: Any
23292328
) -> AsyncItemPaged[Dict[str, Any]]:

sdk/cosmos/azure-cosmos/azure/cosmos/aio/_read_items_helper_async.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,15 @@
3535
from azure.core.utils import CaseInsensitiveDict
3636
from azure.cosmos._query_builder import _QueryBuilder
3737
from azure.cosmos.partition_key import _get_partition_key_from_partition_key_definition, \
38-
NonePartitionKeyValue, _Empty, _Undefined
38+
NonePartitionKeyValue, _Empty, _Undefined, NullPartitionKeyValue
3939
from azure.cosmos import CosmosList
4040

4141
if TYPE_CHECKING:
4242
from azure.cosmos.aio._cosmos_client_connection_async import CosmosClientConnection
4343
PartitionKeyType = Union[
44-
bool, float, int, str, type[NonePartitionKeyValue], _Empty, _Undefined, None,
45-
Sequence[Union[bool, float, int, str, type[NonePartitionKeyValue], _Empty, _Undefined, None]]
44+
bool, float, int, str, type[NonePartitionKeyValue], type[NullPartitionKeyValue], _Empty, _Undefined, None,
45+
Sequence[Union[bool, float, int, str, type[NonePartitionKeyValue], type[NullPartitionKeyValue],
46+
_Empty, _Undefined, None]]
4647
]
4748

4849
class ReadItemsHelperAsync:

sdk/cosmos/azure-cosmos/azure/cosmos/container.py

Lines changed: 85 additions & 37 deletions
Large diffs are not rendered by default.

sdk/cosmos/azure-cosmos/azure/cosmos/partition_key.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,12 @@ class _PartitionKeyVersion:
7777
V2: int = 2
7878

7979
class NonePartitionKeyValue:
80-
"""Represents None value for partitionKey when it's missing in a container.
80+
"""Represents partition key missing from the document.
8181
"""
8282

83+
class NullPartitionKeyValue:
84+
"""Represents null value for a partition key.
85+
"""
8386

8487
class _Empty:
8588
"""Represents empty value for partitionKey when it's missing in an item belonging
@@ -96,7 +99,7 @@ class _Undefined:
9699
class _Infinity:
97100
"""Represents infinity value for partitionKey."""
98101

99-
_SingularPartitionKeyType = Union[None, bool, float, int, str, Type[NonePartitionKeyValue], _Empty, _Undefined]
102+
_SingularPartitionKeyType = Union[None, bool, float, int, str, Type[NonePartitionKeyValue], Type[NullPartitionKeyValue], _Empty, _Undefined] # pylint: disable=line-too-long
100103
_SequentialPartitionKeyType = Sequence[_SingularPartitionKeyType]
101104
_PartitionKeyType = Union[_SingularPartitionKeyType, _SequentialPartitionKeyType]
102105

@@ -405,7 +408,8 @@ def _to_hex_encoded_binary_string(components: Sequence[object]) -> str:
405408
def _to_hex_encoded_binary_string_v1(components: Sequence[object]) -> str:
406409
ms = BytesIO()
407410
for component in components:
408-
if isinstance(component, (bool, int, float, str, _Infinity, _Undefined)):
411+
if (isinstance(component, (bool, int, float, str, _Infinity, _Undefined, type))
412+
or component is None):
409413
component = cast(_SingularPartitionKeyType, component)
410414
_write_for_binary_encoding_v1(component, ms)
411415
else:
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Partition Keys in the Python SDK for Azure Cosmos DB
2+
3+
Partition keys determine how your data is logically distributed across physical partitions for scalability and performance in Azure Cosmos DB. Selecting an appropriate partition key is one of the most important design decisions you will make when modeling data.
4+
5+
For general best practices, see:
6+
- [Choosing a partition key](https://learn.microsoft.com/azure/cosmos-db/partitioning-overview#choose-a-partition-key)
7+
8+
---
9+
## Defining a Partition Key on Container Creation
10+
When you create a container you must define its partition key path (e.g. `/partition_key`). You may also specify the hashing algorithm kind and version (e.g. Hash v2). Use the `PartitionKey` class:
11+
12+
```python
13+
from azure.cosmos import PartitionKey
14+
15+
container = database.create_container_if_not_exists(
16+
id="items",
17+
partition_key=PartitionKey(path="/partition_key", kind="Hash", version=2)
18+
)
19+
```
20+
21+
---
22+
## Supplying Partition Key Values for Container Operations
23+
You can provide the partition key value in two ways for container APIs depending on the operation:
24+
25+
1. Pass the value explicitly via the `partition_key` parameter:
26+
```python
27+
document = {
28+
'id': 'item_id',
29+
'partition_key': 'partition_key_value',
30+
'otherProperty': 'value'
31+
}
32+
container.read_item(item=document['id'], partition_key=document['partition_key'])
33+
```
34+
2. Embed the partition key property in the document you supply via `body` (the SDK extracts it):
35+
```python
36+
doc = {"id": "item_id", "partition_key": "partition_key_value", "otherProperty": "value"}
37+
container.create_item(body=doc)
38+
```
39+
40+
---
41+
## Valid Partition Key Value Types
42+
The SDK accepts a broad set of types for a single (logical) partition key component:
43+
```python
44+
Union[None, bool, float, int, str, Type[NonePartitionKeyValue], Type[NullPartitionKeyValue], Sequence[Union[str, int, float, bool, None]]
45+
```
46+
These include two special sentinel values that disambiguate absence vs explicit null:
47+
48+
- **`NonePartitionKeyValue`**: Indicates the partition key property is **absent** in the stored item (the property was omitted).
49+
- **`NullPartitionKeyValue`**: Indicates the partition key property exists and its JSON value is **null**.
50+
51+
> Why two sentinels? Python `None` can mean either "explicitly null" *or* "perform a cross-partition operation" depending on the API. These sentinels remove ambiguity.
52+
53+
---
54+
## Creating Items with Special Partition Key States
55+
56+
### Item with NO partition key property (use `NonePartitionKeyValue` for reads)
57+
Omit the property when creating the item:
58+
```python
59+
import azure.cosmos.partition_key as pk
60+
61+
doc = {"id": "item_without_pk"} # no partition_key field
62+
container.create_item(body=doc)
63+
64+
retrieved = container.read_item(item="item_without_pk", partition_key=pk.NonePartitionKeyValue)
65+
```
66+
67+
### Item with an explicit JSON null partition key (use `NullPartitionKeyValue` OR `None` for most point ops)
68+
Set the property to `None` in the Python dict:
69+
```python
70+
doc = {"id": "item_with_null_pk", "partition_key": None}
71+
container.create_item(body=doc)
72+
73+
# Reads (point operations) – either works
74+
a = container.read_item(item="item_with_null_pk", partition_key=None) # treated as null value
75+
b = container.read_item(item="item_with_null_pk", partition_key=pk.NullPartitionKeyValue)
76+
```
77+
78+
---
79+
## Choosing the Correct Value When Calling APIs
80+
| Scenario | Use This Partition Key Argument |
81+
|----------|---------------------------------|
82+
| Item lacked the partition key property | `NonePartitionKeyValue` |
83+
| Item has partition key explicitly null (JSON `null`) – point read / replace / delete | `None` OR `NullPartitionKeyValue` |
84+
| Query constrained to the null value | `NullPartitionKeyValue` (NOT plain `None`) |
85+
| Cross-partition query (scan all logical partitions) | `None` |
86+
87+
---
88+
## Queries with None Caveat
89+
For `query_items` and `query_conflicts`:
90+
- Passing `partition_key=None` signals a **cross-partition** query. It does **not** filter to items whose key is null.
91+
- To restrict to items whose partition key value is JSON null, you must pass `partition_key=pk.NullPartitionKeyValue`.
92+
93+
Example (query only null-key items):
94+
```python
95+
import azure.cosmos.partition_key as pk
96+
97+
results = list(container.query_items(
98+
query="SELECT * FROM c WHERE c.type = 'Example'",
99+
partition_key=pk.NullPartitionKeyValue
100+
))
101+
```
102+
103+
---
104+
## Summary Decision Guide
105+
- Omit property to create an item with *no* partition key value; later read with `NonePartitionKeyValue`.
106+
- Set property to `None` to create an item whose partition key value is JSON null; read with `None` or `NullPartitionKeyValue`.
107+
- Use `NullPartitionKeyValue` for queries targeting the null value.
108+
- Use plain `None` for cross-partition queries.
109+
110+
---
111+
## Related Links
112+
- [Azure Cosmos DB partitioning overview](https://learn.microsoft.com/azure/cosmos-db/partitioning-overview)
113+
- [SDK samples](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/cosmos/azure-cosmos/samples/README.md)
114+
115+
---

0 commit comments

Comments
 (0)