Skip to content

Commit

Permalink
Null Partition Key for Cosmos (Azure#20002)
Browse files Browse the repository at this point in the history
* Nil partition key

* Changelog

* Update CHANGELOG.md

* Tests for Null Partition Key

* Unit Tests for Null Partition Key CRUD Operations on Emulator

* NullPartitionKey variable instead of function

* Typo Edit

* Docstring
  • Loading branch information
karunmotorq authored Feb 15, 2023
1 parent fab2a5a commit 3909a45
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 0 deletions.
1 change: 1 addition & 0 deletions sdk/data/azcosmos/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.3.4 (Unreleased)

### Features Added
* Added `NullPartitionKey` variable to create and query documents with null partition key in CosmosDB

### Breaking Changes

Expand Down
175 changes: 175 additions & 0 deletions sdk/data/azcosmos/emulator_cosmos_item_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,181 @@ func TestItemCRUD(t *testing.T) {
}
}

func TestItemCRUDforNullPartitionKey(t *testing.T) {
emulatorTests := newEmulatorTests(t)
client := emulatorTests.getClient(t)

database := emulatorTests.createDatabase(t, context.TODO(), client, "itemCRUD")
defer emulatorTests.deleteDatabase(t, context.TODO(), database)
properties := ContainerProperties{
ID: "aContainer",
PartitionKeyDefinition: PartitionKeyDefinition{
Paths: []string{"/partitionKey"},
},
}

_, err := database.CreateContainer(context.TODO(), properties, nil)
if err != nil {
t.Fatalf("Failed to create container: %v", err)
}

item := map[string]interface{}{
"partitionKey": nil,
"id": "1",
"value": "2",
"count": 3,
"description": "4",
}

container, _ := database.NewContainer("aContainer")
pk := NullPartitionKey

marshalled, err := json.Marshal(item)
if err != nil {
t.Fatal(err)
}

itemResponse, err := container.CreateItem(context.TODO(), pk, marshalled, nil)
if err != nil {
t.Fatalf("Failed to create item: %v", err)
}

if itemResponse.SessionToken == "" {
t.Fatalf("Session token is empty")
}

// No content on write by default
if len(itemResponse.Value) != 0 {
t.Fatalf("Expected empty response, got %v", itemResponse.Value)
}

itemResponse, err = container.ReadItem(context.TODO(), pk, "1", nil)
if err != nil {
t.Fatalf("Failed to read item: %v", err)
}

if len(itemResponse.Value) == 0 {
t.Fatalf("Expected non-empty response, got %v", itemResponse.Value)
}

var itemResponseBody map[string]interface{}
err = json.Unmarshal(itemResponse.Value, &itemResponseBody)
if err != nil {
t.Fatalf("Failed to unmarshal item response: %v", err)
}
if itemResponseBody["id"] != "1" {
t.Fatalf("Expected id to be 1, got %v", itemResponseBody["id"])
}
if itemResponseBody["value"] != "2" {
t.Fatalf("Expected value to be 2, got %v", itemResponseBody["value"])
}

item["value"] = "3"
marshalled, err = json.Marshal(item)
if err != nil {
t.Fatal(err)
}
itemResponse, err = container.ReplaceItem(context.TODO(), pk, "1", marshalled, &ItemOptions{EnableContentResponseOnWrite: true})
if err != nil {
t.Fatalf("Failed to replace item: %v", err)
}

// Explicitly requesting body on write
if len(itemResponse.Value) == 0 {
t.Fatalf("Expected non-empty response, got %v", itemResponse.Value)
}

err = json.Unmarshal(itemResponse.Value, &itemResponseBody)
if err != nil {
t.Fatalf("Failed to unmarshal item response: %v", err)
}
if itemResponseBody["id"] != "1" {
t.Fatalf("Expected id to be 1, got %v", itemResponseBody["id"])
}
if itemResponseBody["value"] != "3" {
t.Fatalf("Expected value to be 3, got %v", itemResponseBody["value"])
}

item["value"] = "4"
marshalled, err = json.Marshal(item)
if err != nil {
t.Fatal(err)
}
itemResponse, err = container.UpsertItem(context.TODO(), pk, marshalled, &ItemOptions{EnableContentResponseOnWrite: true})
if err != nil {
t.Fatalf("Failed to upsert item: %v", err)
}

// Explicitly requesting body on write
if len(itemResponse.Value) == 0 {
t.Fatalf("Expected non-empty response, got %v", itemResponse.Value)
}

err = json.Unmarshal(itemResponse.Value, &itemResponseBody)
if err != nil {
t.Fatalf("Failed to unmarshal item response: %v", err)
}
if itemResponseBody["id"] != "1" {
t.Fatalf("Expected id to be 1, got %v", itemResponseBody["id"])
}
if itemResponseBody["value"] != "4" {
t.Fatalf("Expected value to be 4, got %v", itemResponseBody["value"])
}

patchItem := PatchOperations{}
patchItem.AppendReplace("/value", "5")
patchItem.AppendSet("/hello", "world")
patchItem.AppendAdd("/foo", "bar")
patchItem.AppendRemove("/description")
patchItem.AppendIncrement("/count", 1)

itemResponse, err = container.PatchItem(context.TODO(), pk, "1", patchItem, nil)
if err != nil {
t.Fatalf("Failed to patch item: %v", err)
}

// No content on write by default
if len(itemResponse.Value) != 0 {
t.Fatalf("Expected empty response, got %v", itemResponse.Value)
}

itemResponse, _ = container.ReadItem(context.TODO(), pk, "1", nil)

err = json.Unmarshal(itemResponse.Value, &itemResponseBody)
if err != nil {
t.Fatalf("Failed to unmarshal item response: %v", err)
}

if itemResponseBody["value"] != "5" {
t.Fatalf("Expected value to be 5, got %v", itemResponseBody["id"])
}

if itemResponseBody["hello"] != "world" {
t.Fatalf("Expected hello to be world, got %v", itemResponseBody["hello"])
}

if itemResponseBody["foo"] != "bar" {
t.Fatalf("Expected foo to be bar, got %v", itemResponseBody["foo"])
}

if itemResponseBody["count"].(float64) != float64(4) {
t.Fatalf("Expected count to be 4, got %v", itemResponseBody["count"])
}

if itemResponseBody["toremove"] != nil {
t.Fatalf("Expected toremove to be nil, got %v", itemResponseBody)
}

itemResponse, err = container.DeleteItem(context.TODO(), pk, "1", nil)
if err != nil {
t.Fatalf("Failed to replace item: %v", err)
}

if len(itemResponse.Value) != 0 {
t.Fatalf("Expected empty response, got %v", itemResponse.Value)
}
}

func TestItemIdEncodingRoutingGW(t *testing.T) {
emulatorTests := newEmulatorTests(t)
client := emulatorTests.getClient(t)
Expand Down
5 changes: 5 additions & 0 deletions sdk/data/azcosmos/partition_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ type PartitionKey struct {
values []interface{}
}

// NullPartitionKey represents a partition key with a null value.
var NullPartitionKey PartitionKey = PartitionKey{
values: []interface{}{nil},
}

// NewPartitionKeyString creates a partition key with a string value.
func NewPartitionKeyString(value string) PartitionKey {
components := []interface{}{value}
Expand Down
7 changes: 7 additions & 0 deletions sdk/data/azcosmos/partition_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func TestSerialization(t *testing.T) {
"[\"some string\"]": NewPartitionKeyString("some string"),
"[true]": NewPartitionKeyBool(true),
"[false]": NewPartitionKeyBool(false),
"[null]": NullPartitionKey,
}

for expectedSerialization, pk := range validTypes {
Expand Down Expand Up @@ -68,4 +69,10 @@ func TestPartitionKeyEquality(t *testing.T) {
if !reflect.DeepEqual(pk, pk2) {
t.Errorf("Expected %v to equal %v", pk, pk2)
}

pk = NullPartitionKey
pk2 = NullPartitionKey
if !reflect.DeepEqual(pk, pk2) {
t.Errorf("Expected %v to equal %v", pk, pk2)
}
}

0 comments on commit 3909a45

Please sign in to comment.