diff --git a/sdk/data/azcosmos/CHANGELOG.md b/sdk/data/azcosmos/CHANGELOG.md index e110a9000af9..b95809c018a6 100644 --- a/sdk/data/azcosmos/CHANGELOG.md +++ b/sdk/data/azcosmos/CHANGELOG.md @@ -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 diff --git a/sdk/data/azcosmos/emulator_cosmos_item_test.go b/sdk/data/azcosmos/emulator_cosmos_item_test.go index 3a402efc0cf6..1086f62d7366 100644 --- a/sdk/data/azcosmos/emulator_cosmos_item_test.go +++ b/sdk/data/azcosmos/emulator_cosmos_item_test.go @@ -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) diff --git a/sdk/data/azcosmos/partition_key.go b/sdk/data/azcosmos/partition_key.go index da0ac27763d0..ca61aec87ae2 100644 --- a/sdk/data/azcosmos/partition_key.go +++ b/sdk/data/azcosmos/partition_key.go @@ -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} diff --git a/sdk/data/azcosmos/partition_key_test.go b/sdk/data/azcosmos/partition_key_test.go index 44096f5444c5..bd7e651aba29 100644 --- a/sdk/data/azcosmos/partition_key_test.go +++ b/sdk/data/azcosmos/partition_key_test.go @@ -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 { @@ -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) + } }