From 4155ac6d88d6d8bac44fed5c39b11c06e687b132 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:51:47 +0000 Subject: [PATCH 1/3] Initial plan From 8c40fac903ee05b347183a77a60e78ea44bc3ff9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:13:49 +0000 Subject: [PATCH 2/3] Fix: Allow parent config with data-source-files but no data-source Modified RuntimeConfig constructor to allow parent config files to omit data-source and entities properties when data-source-files is provided. The first child's data-source is used as the default when parent has none. Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> --- src/Config/ObjectModel/RuntimeConfig.cs | 54 +++++++++++++---- .../Configuration/RuntimeConfigLoaderTests.cs | 59 +++++++++++++++++++ 2 files changed, 103 insertions(+), 10 deletions(-) diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 1e567da1cd..00ceb649f1 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -272,7 +272,10 @@ public RuntimeConfig( this.Autoentities = Autoentities; this.DefaultDataSourceName = Guid.NewGuid().ToString(); - if (this.DataSource is null) + bool hasDataSourceFiles = DataSourceFiles is not null && DataSourceFiles.SourceFiles is not null && DataSourceFiles.SourceFiles.Any(); + + // Only require data-source if data-source-files is not provided + if (this.DataSource is null && !hasDataSourceFiles) { throw new DataApiBuilderException( message: "data-source is a mandatory property in DAB Config", @@ -280,14 +283,23 @@ public RuntimeConfig( subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); } - // we will set them up with default values - _dataSourceNameToDataSource = new Dictionary + // Initialize data source dictionary - may be empty if parent relies solely on data-source-files + if (this.DataSource is not null) + { + _dataSourceNameToDataSource = new Dictionary + { + { this.DefaultDataSourceName, this.DataSource } + }; + } + else { - { this.DefaultDataSourceName, this.DataSource } - }; + _dataSourceNameToDataSource = new Dictionary(); + } _entityNameToDataSourceName = new Dictionary(); - if (Entities is null) + + // Only require entities if data-source-files is not provided + if (Entities is null && !hasDataSourceFiles) { throw new DataApiBuilderException( message: "entities is a mandatory property in DAB Config", @@ -295,7 +307,13 @@ public RuntimeConfig( subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); } - foreach (KeyValuePair entity in Entities) + // Initialize entities to empty if null (when using data-source-files) + if (Entities is null) + { + this.Entities = new RuntimeEntities(new Dictionary()); + } + + foreach (KeyValuePair entity in this.Entities) { _entityNameToDataSourceName.TryAdd(entity.Key, this.DefaultDataSourceName); } @@ -303,15 +321,15 @@ public RuntimeConfig( // Process data source and entities information for each database in multiple database scenario. this.DataSourceFiles = DataSourceFiles; - if (DataSourceFiles is not null && DataSourceFiles.SourceFiles is not null) + if (hasDataSourceFiles) { - IEnumerable> allEntities = Entities.AsEnumerable(); + IEnumerable> allEntities = this.Entities.AsEnumerable(); // Iterate through all the datasource files and load the config. IFileSystem fileSystem = new FileSystem(); // This loader is not used as a part of hot reload and therefore does not need a handler. FileSystemRuntimeConfigLoader loader = new(fileSystem, handler: null); - foreach (string dataSourceFile in DataSourceFiles.SourceFiles) + foreach (string dataSourceFile in DataSourceFiles!.SourceFiles!) { // Use default replacement settings for environment variable replacement DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true); @@ -320,6 +338,13 @@ public RuntimeConfig( { try { + // If parent has no DataSource, use the first child's DataSource as the default + if (this.DataSource is null && config.DataSource is not null) + { + this.DataSource = config.DataSource; + _dataSourceNameToDataSource[this.DefaultDataSourceName] = this.DataSource; + } + _dataSourceNameToDataSource = _dataSourceNameToDataSource.Concat(config._dataSourceNameToDataSource).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); _entityNameToDataSourceName = _entityNameToDataSourceName.Concat(config._entityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); allEntities = allEntities.Concat(config.Entities.AsEnumerable()); @@ -339,6 +364,15 @@ public RuntimeConfig( this.Entities = new RuntimeEntities(allEntities.ToDictionary(x => x.Key, x => x.Value)); } + // Final validation: ensure we have at least one data source after all loading + if (this.DataSource is null) + { + throw new DataApiBuilderException( + message: "data-source is a mandatory property in DAB Config. When using data-source-files, at least one child config must contain a valid data-source.", + statusCode: HttpStatusCode.UnprocessableEntity, + subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError); + } + SetupDataSourcesUsed(); } diff --git a/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs b/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs index 7dcf837d08..7759a11cdf 100644 --- a/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs +++ b/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs @@ -101,4 +101,63 @@ public async Task FailLoadMultiDataSourceConfigDuplicateEntities(string configPa Assert.IsTrue(error.StartsWith("Deserialization of the configuration file failed during a post-processing step.")); Assert.IsTrue(error.Contains("An item with the same key has already been added.")); } + + /// + /// Test validates that when parent config has no data-source but has data-source-files, + /// the config loads correctly using the first child's data-source as the default. + /// This is the scenario from GitHub issue: Multiple Source Files Config not loading. + /// + [DataTestMethod] + [DataRow(new string[] { "Multidab-config.MsSql.json", "Multidab-config.MySql.json", "Multidab-config.PostgreSql.json" })] + public async Task CanLoadMultiSourceConfigWithoutParentDataSource(IEnumerable dataSourceFiles) + { + // Create a parent config with NO data-source, only data-source-files and runtime + string parentConfig = @"{ + ""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json"", + ""data-source-files"": [" + string.Join(",", dataSourceFiles.Select(f => $"\"{f}\"")) + @"], + ""runtime"": { + ""rest"": { + ""enabled"": true, + ""path"": ""/api"" + }, + ""graphql"": { + ""enabled"": true, + ""path"": ""/graphql"" + }, + ""host"": { + ""mode"": ""development"" + } + } +}"; + + // Load all child config files + Dictionary mockFiles = new() + { + { "dab-config.json", new MockFileData(parentConfig) } + }; + + foreach (string dataSourceFile in dataSourceFiles) + { + string childContent = await File.ReadAllTextAsync(dataSourceFile); + mockFiles.Add(dataSourceFile, new MockFileData(childContent)); + } + + IFileSystem fs = new MockFileSystem(mockFiles); + FileSystemRuntimeConfigLoader loader = new(fs); + + Assert.IsTrue(loader.TryLoadConfig("dab-config.json", out RuntimeConfig runtimeConfig), "Should successfully load config with data-source-files only"); + + // Verify the config loaded correctly + // When parent has no data-source, we get: + // - One entry for parent's DefaultDataSourceName (set from first child's data source) + // - Three entries from child configs (each with their own DefaultDataSourceName) + Assert.IsTrue(runtimeConfig.ListAllDataSources().Count() == 4, "Should have 4 data sources (1 default + 3 from children)"); + Assert.IsTrue(runtimeConfig.SqlDataSourceUsed, "Should have Sql data source"); + + // Verify the first child's data source is used as the default + Assert.AreEqual(DatabaseType.MSSQL, runtimeConfig.DataSource.DatabaseType, "Default datasource should be from first child file (MsSql)"); + + // Verify entities were loaded from children + Assert.IsTrue(runtimeConfig.Entities.Any(), "Should have entities from child configs"); + } } From 60c2f8a8386f643e09089c9f9971015985318d41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 23:18:07 +0000 Subject: [PATCH 3/3] Address code review feedback: improve comments and test assertions Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> --- src/Config/ObjectModel/RuntimeConfig.cs | 3 ++- .../Configuration/RuntimeConfigLoaderTests.cs | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 00ceb649f1..bb9f28be2a 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -338,7 +338,8 @@ public RuntimeConfig( { try { - // If parent has no DataSource, use the first child's DataSource as the default + // If parent has no DataSource, use the first child's DataSource as the default. + // This only happens once - subsequent children skip this block since this.DataSource is no longer null. if (this.DataSource is null && config.DataSource is not null) { this.DataSource = config.DataSource; diff --git a/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs b/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs index 7759a11cdf..4fa1eba175 100644 --- a/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs +++ b/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs @@ -151,9 +151,15 @@ public async Task CanLoadMultiSourceConfigWithoutParentDataSource(IEnumerable allDataSources = runtimeConfig.ListAllDataSources().ToList(); + Assert.AreEqual(4, allDataSources.Count, "Should have 4 data sources (1 default + 3 from children)"); Assert.IsTrue(runtimeConfig.SqlDataSourceUsed, "Should have Sql data source"); + // Verify all three SQL database types from children are present + Assert.IsTrue(allDataSources.Any(ds => ds.DatabaseType == DatabaseType.MSSQL), "Should have MsSql data source"); + Assert.IsTrue(allDataSources.Any(ds => ds.DatabaseType == DatabaseType.MySQL), "Should have MySql data source"); + Assert.IsTrue(allDataSources.Any(ds => ds.DatabaseType == DatabaseType.PostgreSQL), "Should have PostgreSql data source"); + // Verify the first child's data source is used as the default Assert.AreEqual(DatabaseType.MSSQL, runtimeConfig.DataSource.DatabaseType, "Default datasource should be from first child file (MsSql)");