diff --git a/src/Cli.Tests/AutoSimulateTests.cs b/src/Cli.Tests/AutoSimulateTests.cs
new file mode 100644
index 0000000000..14f4412f7c
--- /dev/null
+++ b/src/Cli.Tests/AutoSimulateTests.cs
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Cli.Tests;
+
+///
+/// Tests for the auto-config-simulate CLI command.
+///
+[TestClass]
+public class AutoSimulateTests
+{
+ private IFileSystem? _fileSystem;
+ private FileSystemRuntimeConfigLoader? _runtimeConfigLoader;
+
+ [TestInitialize]
+ public void TestInitialize()
+ {
+ _fileSystem = FileSystemUtils.ProvisionMockFileSystem();
+ _runtimeConfigLoader = new FileSystemRuntimeConfigLoader(_fileSystem);
+
+ ILoggerFactory loggerFactory = TestLoggerSupport.ProvisionLoggerFactory();
+ ConfigGenerator.SetLoggerForCliConfigGenerator(loggerFactory.CreateLogger());
+ SetCliUtilsLogger(loggerFactory.CreateLogger());
+ }
+
+ [TestCleanup]
+ public void TestCleanup()
+ {
+ _fileSystem = null;
+ _runtimeConfigLoader = null;
+ }
+
+ ///
+ /// Tests that the simulate command fails when no autoentities are defined in the config.
+ ///
+ [TestMethod]
+ public void TestSimulateAutoentities_NoAutoentitiesDefined()
+ {
+ // Arrange: create an MSSQL config without autoentities
+ InitOptions initOptions = CreateBasicInitOptionsForMsSqlWithConfig(config: TEST_RUNTIME_CONFIG_FILE);
+ Assert.IsTrue(TryGenerateConfig(initOptions, _runtimeConfigLoader!, _fileSystem!));
+
+ AutoConfigSimulateOptions options = new(config: TEST_RUNTIME_CONFIG_FILE);
+
+ // Act
+ bool success = TrySimulateAutoentities(options, _runtimeConfigLoader!, _fileSystem!);
+
+ // Assert
+ Assert.IsFalse(success);
+ }
+
+ ///
+ /// Tests that the simulate command options parse the output path correctly.
+ ///
+ [TestMethod]
+ public void TestSimulateAutoentitiesOptions_OutputPathParsed()
+ {
+ // Arrange
+ string outputPath = "simulation-output.csv";
+
+ // Act
+ AutoConfigSimulateOptions options = new(output: outputPath, config: TEST_RUNTIME_CONFIG_FILE);
+
+ // Assert
+ Assert.AreEqual(outputPath, options.Output);
+ Assert.AreEqual(TEST_RUNTIME_CONFIG_FILE, options.Config);
+ }
+
+ ///
+ /// Tests that the simulate command options default output to null (console output).
+ ///
+ [TestMethod]
+ public void TestSimulateAutoentitiesOptions_DefaultOutputIsNull()
+ {
+ // Arrange & Act
+ AutoConfigSimulateOptions options = new();
+
+ // Assert
+ Assert.IsNull(options.Output);
+ }
+}
diff --git a/src/Cli/Commands/AutoConfigSimulateOptions.cs b/src/Cli/Commands/AutoConfigSimulateOptions.cs
new file mode 100644
index 0000000000..205c8d7d9e
--- /dev/null
+++ b/src/Cli/Commands/AutoConfigSimulateOptions.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.IO.Abstractions;
+using Azure.DataApiBuilder.Config;
+using Azure.DataApiBuilder.Product;
+using Cli.Constants;
+using CommandLine;
+using Microsoft.Extensions.Logging;
+using static Cli.Utils;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+
+namespace Cli.Commands
+{
+ ///
+ /// Command options for the auto-config-simulate verb.
+ /// Simulates autoentities generation by querying the database and displaying
+ /// which entities would be created for each filter definition.
+ ///
+ [Verb("auto-config-simulate", isDefault: false, HelpText = "Simulate autoentities generation by querying the database and displaying the results.", Hidden = false)]
+ public class AutoConfigSimulateOptions : Options
+ {
+ public AutoConfigSimulateOptions(
+ string? output = null,
+ string? config = null)
+ : base(config)
+ {
+ Output = output;
+ }
+
+ [Option('o', "output", Required = false, HelpText = "Path to output CSV file. If not specified, results are printed to the console.")]
+ public string? Output { get; }
+
+ public int Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
+ {
+ logger.LogInformation("{productName} {version}", PRODUCT_NAME, ProductInfo.GetProductVersion());
+ bool isSuccess = ConfigGenerator.TrySimulateAutoentities(this, loader, fileSystem);
+ if (isSuccess)
+ {
+ return CliReturnCode.SUCCESS;
+ }
+ else
+ {
+ logger.LogError("Failed to simulate autoentities.");
+ return CliReturnCode.GENERAL_ERROR;
+ }
+ }
+ }
+}
diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs
index 6d1fcf946d..31c7e91e9f 100644
--- a/src/Cli/ConfigGenerator.cs
+++ b/src/Cli/ConfigGenerator.cs
@@ -4,14 +4,17 @@
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO.Abstractions;
+using System.Text;
using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Config.Converters;
using Azure.DataApiBuilder.Config.NamingPolicies;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core;
using Azure.DataApiBuilder.Core.Configurations;
+using Azure.DataApiBuilder.Core.Resolvers;
using Azure.DataApiBuilder.Service;
using Cli.Commands;
+using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Serilog;
using static Cli.Utils;
@@ -3130,6 +3133,198 @@ private static AutoentityPatterns BuildAutoentityPatterns(AutoConfigOptions opti
return parsedPermissions;
}
+ // Column names returned by the autoentities SQL query.
+ private const string AUTOENTITIES_COLUMN_ENTITY_NAME = "entity_name";
+ private const string AUTOENTITIES_COLUMN_OBJECT = "object";
+ private const string AUTOENTITIES_COLUMN_SCHEMA = "schema";
+
+ ///
+ /// Simulates the autoentities generation by querying the database and displaying
+ /// which entities would be created for each autoentities filter definition.
+ /// When an output file path is provided, results are written as CSV; otherwise they are printed to the console.
+ ///
+ /// The simulate options provided by the user.
+ /// The config loader to read the existing config.
+ /// The filesystem used for reading the config file and writing output.
+ /// True if the simulation completed successfully; otherwise, false.
+ public static bool TrySimulateAutoentities(AutoConfigSimulateOptions options, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
+ {
+ if (!TryGetConfigFileBasedOnCliPrecedence(loader, options.Config, out string runtimeConfigFile))
+ {
+ return false;
+ }
+
+ // Load config with env var replacement so the connection string is fully resolved.
+ DeserializationVariableReplacementSettings replacementSettings = new(doReplaceEnvVar: true);
+ if (!loader.TryLoadConfig(runtimeConfigFile, out RuntimeConfig? runtimeConfig, replacementSettings: replacementSettings))
+ {
+ _logger.LogError("Failed to read the config file: {runtimeConfigFile}.", runtimeConfigFile);
+ return false;
+ }
+
+ if (runtimeConfig.DataSource.DatabaseType != DatabaseType.MSSQL)
+ {
+ _logger.LogError("Autoentities simulation is only supported for MSSQL databases. Current database type: {DatabaseType}.", runtimeConfig.DataSource.DatabaseType);
+ return false;
+ }
+
+ if (runtimeConfig.Autoentities?.Autoentities is null || runtimeConfig.Autoentities.Autoentities.Count == 0)
+ {
+ _logger.LogError("No autoentities definitions found in the config file.");
+ return false;
+ }
+
+ string connectionString = runtimeConfig.DataSource.ConnectionString;
+ if (string.IsNullOrWhiteSpace(connectionString))
+ {
+ _logger.LogError("Connection string is missing or empty in config file.");
+ return false;
+ }
+
+ MsSqlQueryBuilder queryBuilder = new();
+ string query = queryBuilder.BuildGetAutoentitiesQuery();
+
+ Dictionary> results = new();
+
+ try
+ {
+ using SqlConnection connection = new(connectionString);
+ connection.Open();
+
+ foreach ((string filterName, Autoentity autoentity) in runtimeConfig.Autoentities.Autoentities)
+ {
+ string include = string.Join(",", autoentity.Patterns.Include);
+ string exclude = string.Join(",", autoentity.Patterns.Exclude);
+ string namePattern = autoentity.Patterns.Name;
+
+ List<(string, string, string)> filterResults = new();
+
+ using SqlCommand command = new(query, connection);
+ command.Parameters.AddWithValue("@include_pattern", include);
+ command.Parameters.AddWithValue("@exclude_pattern", exclude);
+ command.Parameters.AddWithValue("@name_pattern", namePattern);
+
+ using SqlDataReader reader = command.ExecuteReader();
+ while (reader.Read())
+ {
+ string entityName = reader[AUTOENTITIES_COLUMN_ENTITY_NAME]?.ToString() ?? string.Empty;
+ string objectName = reader[AUTOENTITIES_COLUMN_OBJECT]?.ToString() ?? string.Empty;
+ string schemaName = reader[AUTOENTITIES_COLUMN_SCHEMA]?.ToString() ?? string.Empty;
+
+ if (!string.IsNullOrWhiteSpace(entityName) && !string.IsNullOrWhiteSpace(objectName))
+ {
+ filterResults.Add((entityName, schemaName, objectName));
+ }
+ }
+
+ results[filterName] = filterResults;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError("Failed to query the database: {Message}", ex.Message);
+ return false;
+ }
+
+ if (!string.IsNullOrWhiteSpace(options.Output))
+ {
+ return WriteSimulationResultsToCsvFile(options.Output, results, fileSystem);
+ }
+ else
+ {
+ WriteSimulationResultsToConsole(results);
+ return true;
+ }
+ }
+
+ ///
+ /// Writes the autoentities simulation results to the console in a human-readable format.
+ /// Results are grouped by filter name with entity-to-database-object mappings.
+ ///
+ /// The simulation results keyed by filter (definition) name.
+ private static void WriteSimulationResultsToConsole(Dictionary> results)
+ {
+ Console.WriteLine("AutoEntities Simulation Results");
+ Console.WriteLine();
+
+ foreach ((string filterName, List<(string EntityName, string SchemaName, string ObjectName)> matches) in results)
+ {
+ Console.WriteLine($"Filter: {filterName}");
+ Console.WriteLine($"Matches: {matches.Count}");
+ Console.WriteLine();
+
+ if (matches.Count == 0)
+ {
+ Console.WriteLine("(no matches)");
+ }
+ else
+ {
+ int maxEntityNameLength = matches.Max(m => m.EntityName.Length);
+ foreach ((string entityName, string schemaName, string objectName) in matches)
+ {
+ Console.WriteLine($"{entityName.PadRight(maxEntityNameLength)} -> {schemaName}.{objectName}");
+ }
+ }
+
+ Console.WriteLine();
+ }
+ }
+
+ ///
+ /// Writes the autoentities simulation results to a CSV file.
+ /// The file includes a header row followed by one row per matched entity.
+ /// If the file already exists it is overwritten.
+ ///
+ /// The file path to write the CSV output to.
+ /// The simulation results keyed by filter (definition) name.
+ /// The filesystem abstraction used for writing the file.
+ /// True if the file was written successfully; otherwise, false.
+ private static bool WriteSimulationResultsToCsvFile(
+ string outputPath,
+ Dictionary> results,
+ IFileSystem fileSystem)
+ {
+ try
+ {
+ StringBuilder sb = new();
+ sb.AppendLine("filter_name,entity_name,database_object");
+
+ foreach ((string filterName, List<(string EntityName, string SchemaName, string ObjectName)> matches) in results)
+ {
+ foreach ((string entityName, string schemaName, string objectName) in matches)
+ {
+ sb.AppendLine($"{QuoteCsvValue(filterName)},{QuoteCsvValue(entityName)},{QuoteCsvValue($"{schemaName}.{objectName}")}");
+ }
+ }
+
+ fileSystem.File.WriteAllText(outputPath, sb.ToString());
+ _logger.LogInformation("Simulation results written to {outputPath}.", outputPath);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError("Failed to write output file: {Message}", ex.Message);
+ return false;
+ }
+ }
+
+ ///
+ /// Quotes a value for inclusion in a CSV field.
+ /// If the value contains a comma, double-quote, or newline, it is wrapped in double-quotes
+ /// and any embedded double-quotes are escaped by doubling them.
+ ///
+ /// The value to quote.
+ /// A properly escaped CSV field value.
+ private static string QuoteCsvValue(string value)
+ {
+ if (value.Contains(',') || value.Contains('"') || value.Contains('\n') || value.Contains('\r'))
+ {
+ return $"\"{value.Replace("\"", "\"\"")}\"";
+ }
+
+ return value;
+ }
+
///
/// Attempts to update the Azure Key Vault configuration options based on the provided values.
/// Validates that any user-provided parameter value is valid and updates the runtime configuration accordingly.
diff --git a/src/Cli/Program.cs b/src/Cli/Program.cs
index e4732da095..de16ed27f5 100644
--- a/src/Cli/Program.cs
+++ b/src/Cli/Program.cs
@@ -58,7 +58,7 @@ public static int Execute(string[] args, ILogger cliLogger, IFileSystem fileSyst
});
// Parsing user arguments and executing required methods.
- int result = parser.ParseArguments(args)
+ int result = parser.ParseArguments(args)
.MapResult(
(InitOptions options) => options.Handler(cliLogger, loader, fileSystem),
(AddOptions options) => options.Handler(cliLogger, loader, fileSystem),
@@ -68,6 +68,7 @@ public static int Execute(string[] args, ILogger cliLogger, IFileSystem fileSyst
(AddTelemetryOptions options) => options.Handler(cliLogger, loader, fileSystem),
(ConfigureOptions options) => options.Handler(cliLogger, loader, fileSystem),
(AutoConfigOptions options) => options.Handler(cliLogger, loader, fileSystem),
+ (AutoConfigSimulateOptions options) => options.Handler(cliLogger, loader, fileSystem),
(ExportOptions options) => options.Handler(cliLogger, loader, fileSystem),
errors => DabCliParserErrorHandler.ProcessErrorsAndReturnExitCode(errors));