From 5ea3e631385b277f2087d2c28ac15173c227869b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Kr=C3=BCger?= Date: Wed, 28 Oct 2026 10:30:08 +0100 Subject: [PATCH 1/4] Report actual outcome of database/container creation (#261) mkdb, mkcon, create database, and create container always reported created: true, even when an existing resource was reused. Carry an explicit creation outcome through the resource-operations abstraction: the data plane uses the SDK response status (201 created, 200 existing), and ARM reports created: null with outcome createdOrUpdated because its create-or-update API cannot distinguish the two. Human-readable messages match the structured result. Fixes #261 --- CHANGELOG.md | 4 + .../ResourceCreationResultTests.cs | 207 ++++++++++++++++++ .../ServerlessCreationThroughputTests.cs | 7 +- .../Integration/ResourceCreationTests.cs | 84 +++++++ .../MakeContainerCommand.cs | 17 +- .../MakeDbCommand.cs | 17 +- .../ArmCosmosResourceOperations.cs | 8 +- .../CosmosResourceFacade.cs | 4 +- .../DataPlaneCosmosResourceOperations.cs | 8 +- .../ICosmosResourceOperations.cs | 4 +- .../ResourceCreationResult.cs | 16 ++ CosmosDBShell/lang/en.ftl | 4 + docs/commands.md | 15 ++ docs/mcp.md | 7 + l10n/CosmosDBShell.json | 4 + 15 files changed, 381 insertions(+), 25 deletions(-) create mode 100644 CosmosDBShell.Tests/CommandTests/ResourceCreationResultTests.cs create mode 100644 CosmosDBShell.Tests/Integration/ResourceCreationTests.cs create mode 100644 CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ResourceCreationResult.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b9156fb..420ea568 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,14 @@ ### Breaking changes +- On Entra ID (Azure Resource Manager) connections, `mkdb`, `mkcon`, `create database`, and `create container` now return `created: null` with `outcome: "createdOrUpdated"` instead of `created: true`, because the ARM create-or-update API cannot distinguish a new resource from an existing one. Scripts that treat `created` as a boolean should check `outcome` instead. ([#261](https://github.com/Azure/CosmosDBShell/issues/261)) + ### New features ### Fixes +- Database and container creation on key, emulator, and static-token connections now report `created: false` and `outcome: "existing"` when an existing resource is reused, instead of always reporting `created: true`. Human-readable output says "Using existing database/container" in that case. ([#261](https://github.com/Azure/CosmosDBShell/issues/261)) + ### Improvements ## 1.1.291 — 2026-10-26 diff --git a/CosmosDBShell.Tests/CommandTests/ResourceCreationResultTests.cs b/CosmosDBShell.Tests/CommandTests/ResourceCreationResultTests.cs new file mode 100644 index 00000000..877d5499 --- /dev/null +++ b/CosmosDBShell.Tests/CommandTests/ResourceCreationResultTests.cs @@ -0,0 +1,207 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------ + +namespace CosmosShell.Tests.CommandTests; + +using System.Net; +using System.Text.Json; +using Azure; +using Azure.Core; +using Azure.Data.Cosmos.Shell.Commands; +using Azure.Data.Cosmos.Shell.Core; +using Azure.Data.Cosmos.Shell.Parser; +using Azure.Data.Cosmos.Shell.States; +using Azure.Data.Cosmos.Shell.Util; +using Azure.ResourceManager; +using Azure.ResourceManager.CosmosDB; +using Azure.ResourceManager.CosmosDB.Models; +using Microsoft.Azure.Cosmos; +using NSubstitute; +using Spectre.Console; + +[Collection(ConsoleOutputTestCollection.Name)] +public class ResourceCreationResultTests +{ + public static IEnumerable CreationCases() + { + foreach (var container in new[] { false, true }) + { + foreach (var longForm in new[] { false, true }) + { + foreach (bool? created in new bool?[] { true, false, null }) + { + foreach (var format in new[] { "user", "json" }) + { + yield return [container, longForm, created, format]; + } + } + } + } + } + + [Theory] + [MemberData(nameof(CreationCases))] + public async Task Commands_ReportBackendOutcomeInStructuredAndUserOutput(bool container, bool longForm, bool? created, string format) + { + using var shell = ShellInterpreter.CreateInstance(); + var client = Substitute.For(); + shell.Options = new Program.CosmosShellOptions { Output = format }; + if (created.HasValue) + { + ConfigureDataPlane(client, created.Value); + shell.State = new ConnectedState(client); + } + else + { + shell.State = new ConnectedState(client, ConfigureArm()); + } + + CosmosCommand command = (container, longForm) switch + { + (false, false) => new MakeDbCommand { Name = "db" }, + (false, true) => new CreateCommand { Item = "database", Name = "db" }, + (true, false) => new MakeContainerCommand { Name = "Items", PartitionKey = "/pk", Database = "db" }, + (true, true) => new CreateCommand { Item = "container", Name = "Items", PartitionKey = "/pk", Database = "db" }, + }; + var state = await command.ExecuteAsync(shell, new CommandState(), string.Empty, TestContext.Current.CancellationToken); + var result = Assert.IsType(state.Result).Value; + var outcome = created switch { true => "created", false => "existing", null => "createdOrUpdated" }; + Assert.Equal(container ? "container" : "database", result.GetProperty("type").GetString()); + Assert.Equal(container ? "Items" : "db", result.GetProperty("id").GetString()); + Assert.Equal(outcome, result.GetProperty("outcome").GetString()); + if (created.HasValue) + { + Assert.Equal(created.Value, result.GetProperty("created").GetBoolean()); + } + else + { + Assert.Equal(JsonValueKind.Null, result.GetProperty("created").ValueKind); + await client.DidNotReceiveWithAnyArgs().CreateDatabaseIfNotExistsAsync(default!, default(ThroughputProperties), default, TestContext.Current.CancellationToken); + } + + var originalOut = Console.Out; + var originalConsole = AnsiConsole.Console; + using var output = new StringWriter(); + try + { + Console.SetOut(output); + AnsiConsole.Console = AnsiConsole.Create(new AnsiConsoleSettings + { + Ansi = AnsiSupport.No, + ColorSystem = ColorSystemSupport.NoColors, + Out = new AnsiConsoleOutput(output), + }); + shell.PrintState(state); + if (format == "json") + { + using var document = JsonDocument.Parse(output.ToString()); + Assert.True(JsonElement.DeepEquals(result, document.RootElement)); + } + else + { + var key = (container, created) switch + { + (false, true) => "command-mkdb-database_created", + (false, false) => "command-mkdb-database_existing", + (false, null) => "command-mkdb-database_created_or_updated", + (true, true) => "command-mkcon-CreatedContainer", + (true, false) => "command-mkcon-container_existing", + (true, null) => "command-mkcon-container_created_or_updated", + }; + var expected = MessageService.GetArgsString(key, container ? "container" : "db", container ? "Items" : "db"); + Assert.Equal(expected + Environment.NewLine, output.ToString()); + } + } + finally + { + Console.SetOut(originalOut); + AnsiConsole.Console = originalConsole; + } + } + + [Theory] + [InlineData(false, 200)] + [InlineData(false, 202)] + [InlineData(true, 200)] + [InlineData(true, 202)] + public async Task ArmCreation_DoesNotInferCreationFromResponseOrExistence(bool container, int status) + { + var context = ConfigureArm(status); + var operations = new ArmCosmosResourceOperations(context); + for (var attempt = 0; attempt < 2; attempt++) + { + var result = container + ? await operations.CreateContainerAsync("db", "Items", ["/pk"], null, null, null, null, TestContext.Current.CancellationToken) + : await operations.CreateDatabaseAsync("db", null, null, TestContext.Current.CancellationToken); + Assert.Equal(container ? "Items" : "db", result.Id); + Assert.Null(result.Created); + Assert.Equal("createdOrUpdated", result.Outcome); + } + + var databases = context.Account.GetCosmosDBSqlDatabases(); + if (container) + { + var database = await databases.GetIfExistsAsync("db", TestContext.Current.CancellationToken); + await database.Value!.GetCosmosDBSqlContainers().DidNotReceiveWithAnyArgs() + .GetIfExistsAsync(default!, TestContext.Current.CancellationToken); + } + else + { + await databases.DidNotReceiveWithAnyArgs().GetIfExistsAsync(default!, TestContext.Current.CancellationToken); + } + } + + private static void ConfigureDataPlane(CosmosClient client, bool created) + { + var database = Substitute.For(); + database.Id.Returns("db"); + client.GetDatabase("db").Returns(database); + var existing = Substitute.For(); + existing.StatusCode.Returns(HttpStatusCode.OK); + database.ReadAsync(Arg.Any(), Arg.Any()).Returns(existing); + var databaseResponse = Substitute.For(); + databaseResponse.Database.Returns(database); + databaseResponse.StatusCode.Returns(created ? HttpStatusCode.Created : HttpStatusCode.OK); + client.CreateDatabaseIfNotExistsAsync("db", Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(databaseResponse); + var containerResponse = Substitute.For(); + containerResponse.Container.Id.Returns("Items"); + containerResponse.StatusCode.Returns(created ? HttpStatusCode.Created : HttpStatusCode.OK); + database.CreateContainerIfNotExistsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(containerResponse); + } + + private static ArmCosmosContext ConfigureArm(int status = 200) + { + var account = Substitute.For(); + account.Data.Returns(System.ClientModel.Primitives.ModelReaderWriter.Read( + BinaryData.FromString("""{"location":"westus"}"""))!); + var databases = Substitute.For(); + account.GetCosmosDBSqlDatabases().Returns(databases); + var database = Substitute.For(); + database.Data.Returns(System.ClientModel.Primitives.ModelReaderWriter.Read( + BinaryData.FromString("""{"properties":{"resource":{"id":"db"}}}"""))!); + var databaseOperation = Substitute.For>(); + databaseOperation.Value.Returns(database); + var response = Substitute.For(); + response.Status.Returns(status); + databaseOperation.GetRawResponse().Returns(response); + databases.CreateOrUpdateAsync(WaitUntil.Completed, "db", Arg.Any(), Arg.Any()) + .Returns(databaseOperation); + databases.GetIfExistsAsync("db", Arg.Any()) + .Returns(Task.FromResult>(Response.FromValue(database, Substitute.For()))); + var containers = Substitute.For(); + database.GetCosmosDBSqlContainers().Returns(containers); + var container = Substitute.For(); + container.Data.Returns(System.ClientModel.Primitives.ModelReaderWriter.Read( + BinaryData.FromString("""{"properties":{"resource":{"id":"Items"}}}"""))!); + var containerOperation = Substitute.For>(); + containerOperation.Value.Returns(container); + containerOperation.GetRawResponse().Returns(response); + containers.CreateOrUpdateAsync(WaitUntil.Completed, "Items", Arg.Any(), Arg.Any()) + .Returns(containerOperation); + var id = new ResourceIdentifier("/subscriptions/s/resourceGroups/rg/providers/Microsoft.DocumentDB/databaseAccounts/a"); + return new ArmCosmosContext(Substitute.For(), id, "s", "rg", "a", new Uri("https://a.documents.azure.com"), account); + } +} diff --git a/CosmosDBShell.Tests/CommandTests/ServerlessCreationThroughputTests.cs b/CosmosDBShell.Tests/CommandTests/ServerlessCreationThroughputTests.cs index 8c1ccb36..d44889b8 100644 --- a/CosmosDBShell.Tests/CommandTests/ServerlessCreationThroughputTests.cs +++ b/CosmosDBShell.Tests/CommandTests/ServerlessCreationThroughputTests.cs @@ -343,7 +343,8 @@ public async Task ExistingResource_ProvisionedWithExplicitThroughput_PreservesSu var result = await CreateResourceAsync(operations, isContainer, "manual", 400); - Assert.Equal(isContainer ? "Items" : "db", result); + Assert.Equal(isContainer ? "Items" : "db", result.Id); + Assert.False(result.Created); } [Theory] @@ -362,7 +363,7 @@ public async Task ExistingResource_OfferReadFailure_OnlyMissingOfferPreservesSuc if (status == HttpStatusCode.NotFound) { - Assert.Equal(isContainer ? "Items" : "db", await CreateResourceAsync(operations, isContainer, null, 400)); + Assert.Equal(isContainer ? "Items" : "db", (await CreateResourceAsync(operations, isContainer, null, 400)).Id); } else { @@ -372,7 +373,7 @@ public async Task ExistingResource_OfferReadFailure_OnlyMissingOfferPreservesSuc } } - private static Task CreateResourceAsync(DataPlaneCosmosResourceOperations operations, bool isContainer, string? scale, int? ru) + private static Task CreateResourceAsync(DataPlaneCosmosResourceOperations operations, bool isContainer, string? scale, int? ru) => isContainer ? operations.CreateContainerAsync("db", "Items", ["/pk"], null, null, scale, ru, TestContext.Current.CancellationToken) : operations.CreateDatabaseAsync("db", scale, ru, TestContext.Current.CancellationToken); diff --git a/CosmosDBShell.Tests/Integration/ResourceCreationTests.cs b/CosmosDBShell.Tests/Integration/ResourceCreationTests.cs new file mode 100644 index 00000000..f5f40176 --- /dev/null +++ b/CosmosDBShell.Tests/Integration/ResourceCreationTests.cs @@ -0,0 +1,84 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------ + +namespace CosmosShell.Tests.Integration; + +using Azure.Data.Cosmos.Shell.Core; +using Azure.Data.Cosmos.Shell.Parser; +using Microsoft.Azure.Cosmos; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +public class ResourceCreationTests : ConnectedEmulatorTestBase +{ + [Theory] + [InlineData("mkdb")] + [InlineData("create database")] + public async Task DatabaseCreation_ReportsCreatedThenExisting(string command) + { + var name = $"Creation_{Guid.NewGuid():N}"; + CreatedDatabases.Add(name); + await AssertCreationAsync($"{command} {name}", "database", name, true); + var before = await CosmosClient.GetDatabase(name).ReadAsync(cancellationToken: TestContext.Current.CancellationToken); + await AssertCreationAsync($"{command} {name}", "database", name, false); + var after = await CosmosClient.GetDatabase(name).ReadAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Equal(JsonConvert.SerializeObject(before.Resource), JsonConvert.SerializeObject(after.Resource)); + } + + [Theory] + [InlineData("mkcon", false)] + [InlineData("create container", false)] + [InlineData("mkcon", true)] + [InlineData("create container", true)] + public async Task ContainerCreation_PreservesExistingResourceAndData(string command, bool sdkCreated) + { + var name = $"Creation_{Guid.NewGuid():N}"; + CreatedDatabases.Add(name); + var database = await CosmosClient.CreateDatabaseAsync(name, cancellationToken: TestContext.Current.CancellationToken); + if (sdkCreated) + { + var properties = new ContainerProperties("Items", "/pk") + { + PartitionKeyDefinitionVersion = PartitionKeyDefinitionVersion.V2, + DefaultTimeToLive = 3600, + }; + properties.UniqueKeyPolicy.UniqueKeys.Add(new UniqueKey { Paths = { "/email" } }); + await database.Database.CreateContainerAsync(properties, cancellationToken: TestContext.Current.CancellationToken); + } + + var text = $"{command} Items /pk --database={name}"; + await AssertCreationAsync(text, "container", "Items", !sdkCreated); + var container = CosmosClient.GetContainer(name, "Items"); + var before = await container.ReadContainerAsync(cancellationToken: TestContext.Current.CancellationToken); + var item = new { id = "retained", pk = "test", email = "test@example.test" }; + await container.CreateItemAsync(item, new PartitionKey("test"), cancellationToken: TestContext.Current.CancellationToken); + await AssertCreationAsync(text, "container", "Items", false); + var after = await container.ReadContainerAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.True(JToken.DeepEquals(JObject.Parse(JsonConvert.SerializeObject(before.Resource)), JObject.Parse(JsonConvert.SerializeObject(after.Resource)))); + if (sdkCreated) + { + Assert.Equal(PartitionKeyDefinitionVersion.V2, after.Resource.PartitionKeyDefinitionVersion); + } + + var retained = await container.ReadItemAsync("retained", new PartitionKey("test"), cancellationToken: TestContext.Current.CancellationToken); + Assert.Equal("test@example.test", (string?)retained.Resource["email"]); + + var mismatch = await Assert.ThrowsAsync(() => + Shell.RunCommandAsync(new CommandState(), $"{command} Items /different --database={name}", TestContext.Current.CancellationToken)); + Assert.Equal("PartitionKey", mismatch.ParamName); + var unchanged = await container.ReadContainerAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Equal(JsonConvert.SerializeObject(after.Resource), JsonConvert.SerializeObject(unchanged.Resource)); + } + + private async Task AssertCreationAsync(string command, string type, string id, bool created) + { + var state = await Shell.RunCommandAsync(new CommandState(), command, TestContext.Current.CancellationToken); + Assert.False(state.IsError, FormatError(state)); + var result = Assert.IsType(state.Result).Value; + Assert.Equal(type, result.GetProperty("type").GetString()); + Assert.Equal(id, result.GetProperty("id").GetString()); + Assert.Equal(created, result.GetProperty("created").GetBoolean()); + Assert.Equal(created ? "created" : "existing", result.GetProperty("outcome").GetString()); + } +} diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeContainerCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeContainerCommand.cs index ef6a0074..d28dd47d 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeContainerCommand.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeContainerCommand.cs @@ -149,10 +149,10 @@ private async Task CreateContainerInDatabaseAsync(ConnectedState s await ValidateDatabaseExistsAsync(state, databaseName, "mkcon", token); var partitionKeyPaths = this.GetPartitionKeyPaths(); - string containerName; + ResourceCreationResult creation; try { - containerName = await CosmosResourceFacade.CreateContainerAsync( + creation = await CosmosResourceFacade.CreateContainerAsync( state, databaseName, this.Name ?? string.Empty, @@ -177,10 +177,17 @@ private async Task CreateContainerInDatabaseAsync(ConnectedState s commandState.Result = new ShellJson(JsonSerializer.SerializeToElement(new { type = "container", - id = containerName, - created = true, + id = creation.Id, + created = creation.Created, + outcome = creation.Outcome, })); - commandState.RenderUser = () => ShellInterpreter.WriteLine(MessageService.GetString("command-mkcon-CreatedContainer", new Dictionary { { "container", containerName } })); + var messageKey = creation.Created switch + { + true => "command-mkcon-CreatedContainer", + false => "command-mkcon-container_existing", + null => "command-mkcon-container_created_or_updated", + }; + commandState.RenderUser = () => ShellInterpreter.WriteLine(MessageService.GetString(messageKey, new Dictionary { { "container", creation.Id } })); return commandState; } diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeDbCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeDbCommand.cs index c972f376..d5458c96 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeDbCommand.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeDbCommand.cs @@ -42,10 +42,10 @@ Task IStateVisitor.VisitDisconnect async Task IStateVisitor.VisitConnectedStateAsync(ConnectedState state, ShellInterpreter shell, CancellationToken token) { - string databaseName; + ResourceCreationResult creation; try { - databaseName = await CosmosResourceFacade.CreateDatabaseAsync(state, this.Name ?? string.Empty, this.Scale, this.MaxRU, token); + creation = await CosmosResourceFacade.CreateDatabaseAsync(state, this.Name ?? string.Empty, this.Scale, this.MaxRU, token); } catch (ServerlessThroughputNotSupportedException ex) { @@ -58,10 +58,17 @@ async Task IStateVisitor.VisitConn commandState.Result = new ShellJson(JsonSerializer.SerializeToElement(new { type = "database", - id = databaseName, - created = true, + id = creation.Id, + created = creation.Created, + outcome = creation.Outcome, })); - commandState.RenderUser = () => ShellInterpreter.WriteLine(MessageService.GetString("command-mkdb-database_created", new Dictionary { { "db", databaseName } })); + var messageKey = creation.Created switch + { + true => "command-mkdb-database_created", + false => "command-mkdb-database_existing", + null => "command-mkdb-database_created_or_updated", + }; + commandState.RenderUser = () => ShellInterpreter.WriteLine(MessageService.GetString(messageKey, new Dictionary { { "db", creation.Id } })); return commandState; } diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ArmCosmosResourceOperations.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ArmCosmosResourceOperations.cs index 6bd48f6b..3089b77c 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ArmCosmosResourceOperations.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ArmCosmosResourceOperations.cs @@ -56,13 +56,13 @@ public async Task ContainerExistsAsync(string databaseName, string contain } } - public async Task CreateDatabaseAsync(string databaseName, string? scale, int? maxRu, CancellationToken token) + public async Task CreateDatabaseAsync(string databaseName, string? scale, int? maxRu, CancellationToken token) { var resource = await CosmosArmResourceProvider.CreateDatabaseAsync(context, databaseName, scale, maxRu, token); - return resource.Data.Resource.DatabaseName; + return new ResourceCreationResult(resource.Data.Resource.DatabaseName, null); } - public async Task CreateContainerAsync( + public async Task CreateContainerAsync( string databaseName, string containerName, IReadOnlyList partitionKeyPaths, @@ -86,7 +86,7 @@ public async Task CreateContainerAsync( } var resource = await CosmosArmResourceProvider.CreateContainerAsync(context, databaseName, containerName, partitionKeyPaths, uniqueKey, parsedIndex, scale, maxRu, token); - return resource.Data.Resource.ContainerName; + return new ResourceCreationResult(resource.Data.Resource.ContainerName, null); } public Task DeleteDatabaseAsync(string databaseName, CancellationToken token) diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CosmosResourceFacade.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CosmosResourceFacade.cs index 3758f266..14a5d4eb 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CosmosResourceFacade.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CosmosResourceFacade.cs @@ -33,12 +33,12 @@ public static Task ContainerExistsAsync(ConnectedState state, string datab return For(state).ContainerExistsAsync(databaseName, containerName, token); } - public static Task CreateDatabaseAsync(ConnectedState state, string databaseName, string? scale, int? maxRu, CancellationToken token) + public static Task CreateDatabaseAsync(ConnectedState state, string databaseName, string? scale, int? maxRu, CancellationToken token) { return For(state).CreateDatabaseAsync(databaseName, scale, maxRu, token); } - public static Task CreateContainerAsync( + public static Task CreateContainerAsync( ConnectedState state, string databaseName, string containerName, diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/DataPlaneCosmosResourceOperations.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/DataPlaneCosmosResourceOperations.cs index 6f4840a4..4cd5c255 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/DataPlaneCosmosResourceOperations.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/DataPlaneCosmosResourceOperations.cs @@ -71,7 +71,7 @@ public async Task ContainerExistsAsync(string databaseName, string contain } } - public async Task CreateDatabaseAsync(string databaseName, string? scale, int? maxRu, CancellationToken token) + public async Task CreateDatabaseAsync(string databaseName, string? scale, int? maxRu, CancellationToken token) { var response = await CreateWithServerlessFallbackAsync( scale, @@ -84,10 +84,10 @@ await ValidateExistingResourceThroughputAsync( () => response.Database.ReadThroughputAsync(new RequestOptions(), token)); } - return response.Database.Id; + return new ResourceCreationResult(response.Database.Id, response.StatusCode == HttpStatusCode.Created); } - public async Task CreateContainerAsync( + public async Task CreateContainerAsync( string databaseName, string containerName, IReadOnlyList partitionKeyPaths, @@ -129,7 +129,7 @@ await ValidateExistingResourceThroughputAsync( () => response.Container.ReadThroughputAsync(new RequestOptions(), token)); } - return response.Container.Id; + return new ResourceCreationResult(response.Container.Id, response.StatusCode == HttpStatusCode.Created); } public async Task DeleteDatabaseAsync(string databaseName, CancellationToken token) diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ICosmosResourceOperations.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ICosmosResourceOperations.cs index ff6ea44d..bd632d28 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ICosmosResourceOperations.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ICosmosResourceOperations.cs @@ -14,9 +14,9 @@ internal interface ICosmosResourceOperations Task ContainerExistsAsync(string databaseName, string containerName, CancellationToken token); - Task CreateDatabaseAsync(string databaseName, string? scale, int? maxRu, CancellationToken token); + Task CreateDatabaseAsync(string databaseName, string? scale, int? maxRu, CancellationToken token); - Task CreateContainerAsync( + Task CreateContainerAsync( string databaseName, string containerName, IReadOnlyList partitionKeyPaths, diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ResourceCreationResult.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ResourceCreationResult.cs new file mode 100644 index 00000000..267cd7c1 --- /dev/null +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ResourceCreationResult.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------ + +namespace Azure.Data.Cosmos.Shell.Core; + +internal sealed record ResourceCreationResult(string Id, bool? Created) +{ + // ARM create-or-update responses do not distinguish creation from update. + public string Outcome => this.Created switch + { + true => "created", + false => "existing", + null => "createdOrUpdated", + }; +} diff --git a/CosmosDBShell/lang/en.ftl b/CosmosDBShell/lang/en.ftl index 24647f75..1c0ca28c 100644 --- a/CosmosDBShell/lang/en.ftl +++ b/CosmosDBShell/lang/en.ftl @@ -621,6 +621,8 @@ command-mkdb-description-name = The database name to create command-mkdb-description-scale = Database throughput mode (manual or auto). Not supported on serverless accounts. command-mkdb-description-ru = Database max RU/s (default: 1000; omitted on serverless accounts) command-mkdb-database_created = Created database { $db } +command-mkdb-database_existing = Using existing database { $db } +command-mkdb-database_created_or_updated = Created or updated database { $db } command-mkdb-error-only_auto_or_manual_allowed = Only manual or autoscale are allowed. Not both. command-mkcon-description = Creates a new container in the current database. @@ -631,6 +633,8 @@ command-mkcon-description-scale = Container throughput mode (manual or auto). No command-mkcon-description-ru = Container max RU/s (default: 1000; omitted on serverless accounts) command-mkcon-description-database = The database where the container should be created command-mkcon-CreatedContainer = Created container { $container } +command-mkcon-container_existing = Using existing container { $container } +command-mkcon-container_created_or_updated = Created or updated container { $container } command-mkcon-error_partition_key_empty = Partition key path cannot be empty. Provide a path that starts with '/', for example: mkcon name /pk. command-mkcon-error_partition_key_slash = Partition key path must start with a forward slash (/), for example: mkcon name /pk. command-mkcon-error_invalid_index_policy = Invalid indexing policy JSON. Please provide a valid Cosmos DB indexing policy. diff --git a/docs/commands.md b/docs/commands.md index 39037045..218d1472 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1029,6 +1029,21 @@ The editor is resolved from `$VISUAL`, then `$EDITOR`, then a platform default ( Database and container management commands prefer Azure Resource Manager when an ARM context is attached (Entra ID connections, optionally specifying `--subscription` and `--resource-group` for explicit account targeting). The account name is inferred from the endpoint. Account-key, emulator, and static-token connections do not attach ARM context, so these commands automatically fall back to the Cosmos DB data plane and use the connection's existing credentials. +`mkdb` / `create database` and `mkcon` / `create container` return `type`, `id`, +`created`, and `outcome` in structured results: + +| Backend result | `created` | `outcome` | Human-readable status | +| --- | --- | --- | --- | +| Data plane: newly created (HTTP 201) | `true` | `created` | Created database/container | +| Data plane: existing resource reused (HTTP 200) | `false` | `existing` | Using existing database/container | +| ARM: successful create-or-update | `null` | `createdOrUpdated` | Created or updated database/container | + +Data-plane replay preserves the existing resource rather than recreating it; incompatible +partition keys and other service errors still fail. ARM retains its create-or-update +behavior and can update an existing resource. Its API does not distinguish creation +from update, so `null` means unknown, not `false`. The shell does not infer creation +from a preflight existence check. + ### mkdb Create database. diff --git a/docs/mcp.md b/docs/mcp.md index 088acbd4..816f7a79 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2,6 +2,13 @@ Model Context Protocol (MCP) allows AI clients to control CosmosDBShell programmatically. +Database/container creation tools expose `created: true` and `outcome: "created"` +for a new data-plane resource, or `created: false` and `outcome: "existing"` when +reusing one. ARM create-or-update returns `created: null` and +`outcome: "createdOrUpdated"` because the API cannot distinguish the two. +Do not count that unknown outcome as a confirmed creation. +See [management result semantics](commands.md#management). + ## Start MCP Server ```bash diff --git a/l10n/CosmosDBShell.json b/l10n/CosmosDBShell.json index 10d60fd8..901f2d3e 100644 --- a/l10n/CosmosDBShell.json +++ b/l10n/CosmosDBShell.json @@ -609,6 +609,8 @@ "command-ls-table-header-container": "Container", "command-ls-table-header-database": "Database", "command-mkcon-CreatedContainer": "Created container {0}", + "command-mkcon-container_created_or_updated": "Created or updated container {0}", + "command-mkcon-container_existing": "Using existing container {0}", "command-mkcon-description": "Creates a new container in the current database.", "command-mkcon-description-database": "The database where the container should be created", "command-mkcon-description-index_policy": "The indexing policy as a JSON string. Follows the Cosmos DB indexing policy schema.", @@ -626,6 +628,8 @@ "command-mkcon-example-4": "Create container in specific database", "command-mkcon-example-5": "Create container with custom indexing policy", "command-mkdb-database_created": "Created database {0}", + "command-mkdb-database_created_or_updated": "Created or updated database {0}", + "command-mkdb-database_existing": "Using existing database {0}", "command-mkdb-description": "Creates new database", "command-mkdb-description-name": "The database name to create", "command-mkdb-description-ru": "Database max RU/s (default: 1000; omitted on serverless accounts)", From 6d693814a00e51faf0d20114b3168b5e594cc433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Kr=C3=BCger?= Date: Wed, 28 Oct 2026 10:37:22 +0100 Subject: [PATCH 2/4] Replace nullable-bool switches with explicit HasValue selection Address CodeQL constant-condition findings on PR #263. The null arm is reachable (ARM create-or-update), so keep all three outcomes and centralize selection in ResourceCreationResult.Select. --- .../CommandTests/ResourceCreationResultTests.cs | 2 +- .../MakeContainerCommand.cs | 10 ++++------ .../MakeDbCommand.cs | 10 ++++------ .../ResourceCreationResult.cs | 12 ++++++------ 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/CosmosDBShell.Tests/CommandTests/ResourceCreationResultTests.cs b/CosmosDBShell.Tests/CommandTests/ResourceCreationResultTests.cs index 877d5499..19ab020e 100644 --- a/CosmosDBShell.Tests/CommandTests/ResourceCreationResultTests.cs +++ b/CosmosDBShell.Tests/CommandTests/ResourceCreationResultTests.cs @@ -66,7 +66,7 @@ public async Task Commands_ReportBackendOutcomeInStructuredAndUserOutput(bool co }; var state = await command.ExecuteAsync(shell, new CommandState(), string.Empty, TestContext.Current.CancellationToken); var result = Assert.IsType(state.Result).Value; - var outcome = created switch { true => "created", false => "existing", null => "createdOrUpdated" }; + var outcome = !created.HasValue ? "createdOrUpdated" : created.Value ? "created" : "existing"; Assert.Equal(container ? "container" : "database", result.GetProperty("type").GetString()); Assert.Equal(container ? "Items" : "db", result.GetProperty("id").GetString()); Assert.Equal(outcome, result.GetProperty("outcome").GetString()); diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeContainerCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeContainerCommand.cs index d28dd47d..2dbb351c 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeContainerCommand.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeContainerCommand.cs @@ -181,12 +181,10 @@ private async Task CreateContainerInDatabaseAsync(ConnectedState s created = creation.Created, outcome = creation.Outcome, })); - var messageKey = creation.Created switch - { - true => "command-mkcon-CreatedContainer", - false => "command-mkcon-container_existing", - null => "command-mkcon-container_created_or_updated", - }; + var messageKey = creation.Select( + "command-mkcon-CreatedContainer", + "command-mkcon-container_existing", + "command-mkcon-container_created_or_updated"); commandState.RenderUser = () => ShellInterpreter.WriteLine(MessageService.GetString(messageKey, new Dictionary { { "container", creation.Id } })); return commandState; } diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeDbCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeDbCommand.cs index d5458c96..32dc4b05 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeDbCommand.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeDbCommand.cs @@ -62,12 +62,10 @@ async Task IStateVisitor.VisitConn created = creation.Created, outcome = creation.Outcome, })); - var messageKey = creation.Created switch - { - true => "command-mkdb-database_created", - false => "command-mkdb-database_existing", - null => "command-mkdb-database_created_or_updated", - }; + var messageKey = creation.Select( + "command-mkdb-database_created", + "command-mkdb-database_existing", + "command-mkdb-database_created_or_updated"); commandState.RenderUser = () => ShellInterpreter.WriteLine(MessageService.GetString(messageKey, new Dictionary { { "db", creation.Id } })); return commandState; } diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ResourceCreationResult.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ResourceCreationResult.cs index 267cd7c1..8cefc78c 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ResourceCreationResult.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ResourceCreationResult.cs @@ -7,10 +7,10 @@ namespace Azure.Data.Cosmos.Shell.Core; internal sealed record ResourceCreationResult(string Id, bool? Created) { // ARM create-or-update responses do not distinguish creation from update. - public string Outcome => this.Created switch - { - true => "created", - false => "existing", - null => "createdOrUpdated", - }; + public string Outcome => this.Select("created", "existing", "createdOrUpdated"); + + public T Select(T created, T existing, T createdOrUpdated) => + this.Created.HasValue + ? (this.Created.Value ? created : existing) + : createdOrUpdated; } From 79342d390c91ee612749096df477d1ce84d26e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Kr=C3=BCger?= Date: Wed, 28 Oct 2026 10:38:51 +0100 Subject: [PATCH 3/4] Keep existing changelog entry order after merging main --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48d0c065..0421584f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,8 @@ ### Fixes -- Database and container creation on key, emulator, and static-token connections now report `created: false` and `outcome: "existing"` when an existing resource is reused, instead of always reporting `created: true`. Human-readable output says "Using existing database/container" in that case. ([#261](https://github.com/Azure/CosmosDBShell/issues/261)) - Preserve `?` in unquoted command arguments so scalar indexing paths such as `index add /city/?` and `index remove /city/?` reach Cosmos DB unchanged. ([#260](https://github.com/Azure/CosmosDBShell/issues/260)) +- Database and container creation on key, emulator, and static-token connections now report `created: false` and `outcome: "existing"` when an existing resource is reused, instead of always reporting `created: true`. Human-readable output says "Using existing database/container" in that case. ([#261](https://github.com/Azure/CosmosDBShell/issues/261)) ### Improvements From a172e7e73d7c60a822547753f750ff659e5bc48e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Kr=C3=BCger?= Date: Wed, 28 Oct 2026 10:48:38 +0100 Subject: [PATCH 4/4] Model creation outcome as an enum Replace the nullable Created flag with ResourceCreationOutcome (Created, Existing, CreatedOrUpdated) so the unknown ARM outcome is explicit rather than encoded as null. The structured result keeps its created/outcome fields; outcome is derived from the enum name in camelCase. --- .../ResourceCreationResultTests.cs | 3 ++- .../ServerlessCreationThroughputTests.cs | 2 +- .../MakeContainerCommand.cs | 14 +++++++++----- .../MakeDbCommand.cs | 14 +++++++++----- .../ArmCosmosResourceOperations.cs | 4 ++-- .../DataPlaneCosmosResourceOperations.cs | 7 +++++-- .../ResourceCreationOutcome.cs | 14 ++++++++++++++ .../ResourceCreationResult.cs | 19 ++++++++++++------- 8 files changed, 54 insertions(+), 23 deletions(-) create mode 100644 CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ResourceCreationOutcome.cs diff --git a/CosmosDBShell.Tests/CommandTests/ResourceCreationResultTests.cs b/CosmosDBShell.Tests/CommandTests/ResourceCreationResultTests.cs index 19ab020e..009481e6 100644 --- a/CosmosDBShell.Tests/CommandTests/ResourceCreationResultTests.cs +++ b/CosmosDBShell.Tests/CommandTests/ResourceCreationResultTests.cs @@ -135,8 +135,9 @@ public async Task ArmCreation_DoesNotInferCreationFromResponseOrExistence(bool c ? await operations.CreateContainerAsync("db", "Items", ["/pk"], null, null, null, null, TestContext.Current.CancellationToken) : await operations.CreateDatabaseAsync("db", null, null, TestContext.Current.CancellationToken); Assert.Equal(container ? "Items" : "db", result.Id); + Assert.Equal(ResourceCreationOutcome.CreatedOrUpdated, result.Outcome); Assert.Null(result.Created); - Assert.Equal("createdOrUpdated", result.Outcome); + Assert.Equal("createdOrUpdated", result.OutcomeName); } var databases = context.Account.GetCosmosDBSqlDatabases(); diff --git a/CosmosDBShell.Tests/CommandTests/ServerlessCreationThroughputTests.cs b/CosmosDBShell.Tests/CommandTests/ServerlessCreationThroughputTests.cs index d44889b8..66862b6d 100644 --- a/CosmosDBShell.Tests/CommandTests/ServerlessCreationThroughputTests.cs +++ b/CosmosDBShell.Tests/CommandTests/ServerlessCreationThroughputTests.cs @@ -344,7 +344,7 @@ public async Task ExistingResource_ProvisionedWithExplicitThroughput_PreservesSu var result = await CreateResourceAsync(operations, isContainer, "manual", 400); Assert.Equal(isContainer ? "Items" : "db", result.Id); - Assert.False(result.Created); + Assert.Equal(ResourceCreationOutcome.Existing, result.Outcome); } [Theory] diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeContainerCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeContainerCommand.cs index 2dbb351c..b0ee4e0b 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeContainerCommand.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeContainerCommand.cs @@ -4,6 +4,7 @@ namespace Azure.Data.Cosmos.Shell.Commands; +using System.Diagnostics; using System.Text.Json; using Azure.Data.Cosmos.Shell.Mcp; using Azure.Data.Cosmos.Shell.Parser; @@ -179,12 +180,15 @@ private async Task CreateContainerInDatabaseAsync(ConnectedState s type = "container", id = creation.Id, created = creation.Created, - outcome = creation.Outcome, + outcome = creation.OutcomeName, })); - var messageKey = creation.Select( - "command-mkcon-CreatedContainer", - "command-mkcon-container_existing", - "command-mkcon-container_created_or_updated"); + var messageKey = creation.Outcome switch + { + ResourceCreationOutcome.Created => "command-mkcon-CreatedContainer", + ResourceCreationOutcome.Existing => "command-mkcon-container_existing", + ResourceCreationOutcome.CreatedOrUpdated => "command-mkcon-container_created_or_updated", + _ => throw new UnreachableException(), + }; commandState.RenderUser = () => ShellInterpreter.WriteLine(MessageService.GetString(messageKey, new Dictionary { { "container", creation.Id } })); return commandState; } diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeDbCommand.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeDbCommand.cs index 32dc4b05..a03da155 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeDbCommand.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeDbCommand.cs @@ -4,6 +4,7 @@ namespace Azure.Data.Cosmos.Shell.Commands; +using System.Diagnostics; using System.Text.Json; using Azure.Data.Cosmos.Shell.Parser; using Azure.Data.Cosmos.Shell.Util; @@ -60,12 +61,15 @@ async Task IStateVisitor.VisitConn type = "database", id = creation.Id, created = creation.Created, - outcome = creation.Outcome, + outcome = creation.OutcomeName, })); - var messageKey = creation.Select( - "command-mkdb-database_created", - "command-mkdb-database_existing", - "command-mkdb-database_created_or_updated"); + var messageKey = creation.Outcome switch + { + ResourceCreationOutcome.Created => "command-mkdb-database_created", + ResourceCreationOutcome.Existing => "command-mkdb-database_existing", + ResourceCreationOutcome.CreatedOrUpdated => "command-mkdb-database_created_or_updated", + _ => throw new UnreachableException(), + }; commandState.RenderUser = () => ShellInterpreter.WriteLine(MessageService.GetString(messageKey, new Dictionary { { "db", creation.Id } })); return commandState; } diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ArmCosmosResourceOperations.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ArmCosmosResourceOperations.cs index 3089b77c..988125cf 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ArmCosmosResourceOperations.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ArmCosmosResourceOperations.cs @@ -59,7 +59,7 @@ public async Task ContainerExistsAsync(string databaseName, string contain public async Task CreateDatabaseAsync(string databaseName, string? scale, int? maxRu, CancellationToken token) { var resource = await CosmosArmResourceProvider.CreateDatabaseAsync(context, databaseName, scale, maxRu, token); - return new ResourceCreationResult(resource.Data.Resource.DatabaseName, null); + return new ResourceCreationResult(resource.Data.Resource.DatabaseName, ResourceCreationOutcome.CreatedOrUpdated); } public async Task CreateContainerAsync( @@ -86,7 +86,7 @@ public async Task CreateContainerAsync( } var resource = await CosmosArmResourceProvider.CreateContainerAsync(context, databaseName, containerName, partitionKeyPaths, uniqueKey, parsedIndex, scale, maxRu, token); - return new ResourceCreationResult(resource.Data.Resource.ContainerName, null); + return new ResourceCreationResult(resource.Data.Resource.ContainerName, ResourceCreationOutcome.CreatedOrUpdated); } public Task DeleteDatabaseAsync(string databaseName, CancellationToken token) diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/DataPlaneCosmosResourceOperations.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/DataPlaneCosmosResourceOperations.cs index 4cd5c255..eb5c8d78 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/DataPlaneCosmosResourceOperations.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/DataPlaneCosmosResourceOperations.cs @@ -84,7 +84,7 @@ await ValidateExistingResourceThroughputAsync( () => response.Database.ReadThroughputAsync(new RequestOptions(), token)); } - return new ResourceCreationResult(response.Database.Id, response.StatusCode == HttpStatusCode.Created); + return new ResourceCreationResult(response.Database.Id, ToCreationOutcome(response.StatusCode)); } public async Task CreateContainerAsync( @@ -129,7 +129,7 @@ await ValidateExistingResourceThroughputAsync( () => response.Container.ReadThroughputAsync(new RequestOptions(), token)); } - return new ResourceCreationResult(response.Container.Id, response.StatusCode == HttpStatusCode.Created); + return new ResourceCreationResult(response.Container.Id, ToCreationOutcome(response.StatusCode)); } public async Task DeleteDatabaseAsync(string databaseName, CancellationToken token) @@ -418,6 +418,9 @@ private static ContainerProperties GetContainerPropertiesOrThrow(ContainerRespon return response.Resource ?? throw new ShellException(MessageService.GetString("error-unable_to_read_container")); } + private static ResourceCreationOutcome ToCreationOutcome(HttpStatusCode status) => + status == HttpStatusCode.Created ? ResourceCreationOutcome.Created : ResourceCreationOutcome.Existing; + internal static IndexingPolicy ParseIndexingPolicy(string indexPolicyJson) { try diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ResourceCreationOutcome.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ResourceCreationOutcome.cs new file mode 100644 index 00000000..d7b3663f --- /dev/null +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ResourceCreationOutcome.cs @@ -0,0 +1,14 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ------------------------------------------------------------ + +namespace Azure.Data.Cosmos.Shell.Core; + +internal enum ResourceCreationOutcome +{ + Created, + Existing, + + // ARM create-or-update responses do not distinguish creation from update. + CreatedOrUpdated, +} diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ResourceCreationResult.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ResourceCreationResult.cs index 8cefc78c..fdc9fec5 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ResourceCreationResult.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Core/ResourceCreationResult.cs @@ -4,13 +4,18 @@ namespace Azure.Data.Cosmos.Shell.Core; -internal sealed record ResourceCreationResult(string Id, bool? Created) +using System.Diagnostics; +using System.Text.Json; + +internal sealed record ResourceCreationResult(string Id, ResourceCreationOutcome Outcome) { - // ARM create-or-update responses do not distinguish creation from update. - public string Outcome => this.Select("created", "existing", "createdOrUpdated"); + public bool? Created => this.Outcome switch + { + ResourceCreationOutcome.Created => true, + ResourceCreationOutcome.Existing => false, + ResourceCreationOutcome.CreatedOrUpdated => null, + _ => throw new UnreachableException(), + }; - public T Select(T created, T existing, T createdOrUpdated) => - this.Created.HasValue - ? (this.Created.Value ? created : existing) - : createdOrUpdated; + public string OutcomeName => JsonNamingPolicy.CamelCase.ConvertName(this.Outcome.ToString()); }