From 54fb1b115fa85e9c3fa4e985f65c9263b3baf159 Mon Sep 17 00:00:00 2001 From: Joymax Andrade do Nascimento Date: Fri, 4 Sep 2026 17:29:33 -0300 Subject: [PATCH 1/5] Honor field permissions when reading table schema --- .../MetadataProviders/SqlMetadataProvider.cs | 349 +++++++++++++++++- 1 file changed, 346 insertions(+), 3 deletions(-) diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 9517c41781..6b31d7c700 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -69,6 +69,11 @@ public abstract class SqlMetadataProvider : protected const int NUMBER_OF_RESTRICTIONS = 4; + /// + /// Wildcard used in the permissions "fields" section to denote every field. + /// + private const string FIELD_WILDCARD = "*"; + protected string ConnectionString { get; init; } protected IQueryBuilder SqlQueryBuilder { get; init; } @@ -1528,10 +1533,26 @@ private async Task PopulateSourceDefinitionAsync( using DataTableReader reader = new(dataTable); DataTable schemaTable = reader.GetSchemaTable(); RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig(); + + // Columns the entity's permissions allow to be read. The schema DataTable is cached + // per schema.table and may therefore carry columns permitted only for a sibling + // entity, so the restriction is re-applied per entity here. + PermittedColumns? permittedColumns = entity is null + ? null + : ResolvePermittedColumnsForEntity(entity); + foreach (DataRow columnInfoFromAdapter in schemaTable.Rows) { string columnName = columnInfoFromAdapter["ColumnName"].ToString()!; + if (permittedColumns is not null + && !permittedColumns.IsUnrestricted + && !permittedColumns.IsColumnPermitted(columnName) + && !sourceDefinition.PrimaryKey.Contains(columnName)) + { + continue; + } + if (runtimeConfig.IsGraphQLEnabled && entity is not null && IsGraphQLReservedName(entity, columnName, graphQLEnabledGlobally: runtimeConfig.IsGraphQLEnabled)) @@ -1685,7 +1706,7 @@ private async Task GetTableWithSchemaFromDataSetAsync( { try { - dataTable = await FillSchemaForTableAsync(schemaName, tableName); + dataTable = await FillSchemaForTableAsync(schemaName, tableName, entityName); } catch (Exception ex) when (ex is not DataApiBuilderException) { @@ -1753,10 +1774,16 @@ private async Task ValidateDatabaseConnection() /// /// Using a data adapter, obtains the schema of the given table name /// and adds the corresponding DataTable to the entities data set. + /// When the entities backed by this database object restrict the readable + /// fields through permissions ("fields.include"/"fields.exclude"), the projection + /// is narrowed to those columns instead of "SELECT *". This avoids the provider + /// having to materialize CLR types it cannot handle (e.g. geometry/geography/hierarchyid), + /// which otherwise fails during schema discovery even though the column is not exposed. /// private async Task FillSchemaForTableAsync( string schemaName, - string tableName) + string tableName, + string? entityName = null) { using ConnectionT conn = new(); // If connection string is set to empty string @@ -1802,14 +1829,330 @@ private async Task FillSchemaForTableAsync( }; string tableNameWithSchemaPrefix = GetTableNameWithSchemaPrefix(schemaName, tableName); + + // Resolve the columns the configuration actually allows to be read for this + // database object. When nothing is restricted, the original SELECT * is preserved. + PermittedColumns permittedColumns = ResolvePermittedColumnsForDatabaseObject( + schemaName: schemaName, + tableName: tableName, + entityName: entityName); + + string projection = await BuildSchemaProjectionAsync(schemaName, tableName, permittedColumns); + selectCommand.CommandText - = $"SELECT * FROM {tableNameWithSchemaPrefix}"; + = $"SELECT {projection} FROM {tableNameWithSchemaPrefix}"; adapterForTable.SelectCommand = selectCommand; DataTable[] dataTable = adapterForTable.FillSchema(EntitiesDataSet, SchemaType.Source, tableNameWithSchemaPrefix); return dataTable[0]; } + /// + /// Describes which backing (database) columns of a database object the runtime + /// configuration allows to be read. + /// + /// + /// True when at least one permission reads every field ("fields" absent, "include" absent, + /// or "include": ["*"]). + /// + /// Backing columns explicitly listed in an "include" section. + /// + /// Backing columns excluded from every wildcard permission and never explicitly included. + /// + private sealed record PermittedColumns(bool AllColumns, HashSet Included, HashSet Excluded) + { + /// + /// No column restriction could be derived from the configuration. + /// + public bool IsUnrestricted => AllColumns && Excluded.Count == 0; + + /// + /// Whether the given backing column is readable per the configuration. + /// + public bool IsColumnPermitted(string columnName) + { + if (Included.Contains(columnName)) + { + return true; + } + + if (Excluded.Contains(columnName)) + { + return false; + } + + return AllColumns; + } + } + + /// + /// Resolves the readable columns for a database object. + /// Because the schema DataTable is cached per schema.table, the result combines the + /// permissions of every entity in this data source backed by that same object: a column + /// has to be read if any of those entities can read it. + /// + /// Schema of the database object. + /// Name of the database object. + /// Entity that triggered the schema discovery, when known. + private PermittedColumns ResolvePermittedColumnsForDatabaseObject( + string schemaName, + string tableName, + string? entityName) + { + HashSet included = new(StringComparer.Ordinal); + HashSet? excluded = null; + bool allColumns = false; + bool matchedAnyEntity = false; + + foreach ((string candidateEntityName, Entity candidateEntity) in Entities) + { + // Only consider entities backed by the same database object. + if (EntityToDatabaseObject.TryGetValue(candidateEntityName, out DatabaseObject? databaseObject)) + { + if (!string.Equals(databaseObject.SchemaName, schemaName, StringComparison.OrdinalIgnoreCase) + || !string.Equals(databaseObject.Name, tableName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + } + else if (!string.Equals(candidateEntityName, entityName, StringComparison.Ordinal)) + { + // Database object not inferred yet: only the entity that triggered the read applies. + continue; + } + + matchedAnyEntity = true; + PermittedColumns entityPermittedColumns = ResolvePermittedColumnsForEntity(candidateEntity); + + included.UnionWith(entityPermittedColumns.Included); + + if (entityPermittedColumns.AllColumns) + { + allColumns = true; + + // A column is only droppable when every wildcard permission excludes it. + if (excluded is null) + { + excluded = new(entityPermittedColumns.Excluded, StringComparer.Ordinal); + } + else + { + excluded.IntersectWith(entityPermittedColumns.Excluded); + } + } + } + + if (!matchedAnyEntity) + { + return new(AllColumns: true, Included: new(StringComparer.Ordinal), Excluded: new(StringComparer.Ordinal)); + } + + excluded ??= new(StringComparer.Ordinal); + excluded.ExceptWith(included); + + return new(allColumns, included, excluded); + } + + /// + /// Resolves the readable columns of a single entity from the "fields.include" / + /// "fields.exclude" sections of its permissions. Exposed names (mappings and field + /// aliases) are translated back to their database column names, and configured primary + /// key fields are always kept, since the runtime cannot operate on the entity without them. + /// + private static PermittedColumns ResolvePermittedColumnsForEntity(Entity entity) + { + HashSet included = new(StringComparer.Ordinal); + HashSet? excluded = null; + bool allColumns = false; + + // Exposed name -> backing column name. + Dictionary exposedToBackingName = new(StringComparer.Ordinal); + + if (entity.Mappings is not null) + { + foreach ((string backingName, string exposedName) in entity.Mappings) + { + if (!string.IsNullOrWhiteSpace(exposedName)) + { + exposedToBackingName[exposedName] = backingName; + } + } + } + + if (entity.Fields is not null) + { + foreach (FieldMetadata field in entity.Fields) + { + if (!string.IsNullOrWhiteSpace(field.Alias)) + { + exposedToBackingName[field.Alias!] = field.Name; + } + } + } + + if (entity.Permissions is null || entity.Permissions.Length == 0) + { + // Nothing configured: the whole object is read, as before. + return new(AllColumns: true, included, new HashSet(StringComparer.Ordinal)); + } + + foreach (EntityPermission permission in entity.Permissions) + { + if (permission.Actions is null) + { + allColumns = true; + excluded = new(StringComparer.Ordinal); + continue; + } + + foreach (EntityAction action in permission.Actions) + { + EntityActionFields? fields = action.Fields; + + HashSet actionExcluded = new(StringComparer.Ordinal); + if (fields?.Exclude is not null) + { + if (fields.Exclude.Contains(FIELD_WILDCARD)) + { + // This permission reads no field at all, so it contributes no column. + continue; + } + + foreach (string field in fields.Exclude) + { + actionExcluded.Add(ResolveBackingName(field, exposedToBackingName)); + } + } + + // No "fields" section, no "include" section, or an explicit wildcard, + // means every column not listed in "exclude" is readable. + bool includesEveryField = fields is null + || fields.Include is null + || fields.Include.Contains(FIELD_WILDCARD); + + if (includesEveryField) + { + allColumns = true; + + if (excluded is null) + { + excluded = actionExcluded; + } + else + { + excluded.IntersectWith(actionExcluded); + } + + continue; + } + + foreach (string field in fields!.Include!) + { + string backingName = ResolveBackingName(field, exposedToBackingName); + if (!actionExcluded.Contains(backingName)) + { + included.Add(backingName); + } + } + } + } + + // Primary keys are structural: never drop them from the projection. + if (entity.Source is not null && entity.Source.KeyFields is not null) + { + foreach (string keyField in entity.Source.KeyFields) + { + included.Add(ResolveBackingName(keyField, exposedToBackingName)); + } + } + + if (entity.Fields is not null) + { + foreach (FieldMetadata field in entity.Fields.Where(f => f.PrimaryKey)) + { + included.Add(ResolveBackingName(field.Name, exposedToBackingName)); + } + } + + excluded ??= new(StringComparer.Ordinal); + excluded.ExceptWith(included); + + return new(allColumns, included, excluded); + } + + /// + /// Translates a configured (exposed) field name into its backing column name. + /// + private static string ResolveBackingName(string fieldName, Dictionary exposedToBackingName) + { + return exposedToBackingName.TryGetValue(fieldName, out string? backingName) ? backingName : fieldName; + } + + /// + /// Builds the projection used to read the schema of a database object, narrowed to the + /// columns the configuration allows to be read. Returns "*" when no restriction applies + /// or when the column list cannot be determined. + /// + private async Task BuildSchemaProjectionAsync( + string schemaName, + string tableName, + PermittedColumns permittedColumns) + { + if (permittedColumns.IsUnrestricted) + { + return "*"; + } + + List columnsToRead; + + if (permittedColumns.AllColumns) + { + // "include": ["*"] with an "exclude" list: enumerate the columns from the catalog + // (metadata only, so unsupported CLR types are never materialized) and drop the + // excluded ones. + List allColumnNames = new(); + try + { + DataTable columnsInTable = await GetColumnsAsync(schemaName, tableName); + + foreach (DataRow columnInfo in columnsInTable.Rows) + { + if (columnInfo["COLUMN_NAME"] is string columnName) + { + allColumnNames.Add(columnName); + } + } + } + catch (Exception ex) + { + _logger.LogDebug( + "Unable to enumerate the columns of {schemaName}.{tableName} to honor the configured field exclusions: {message}", + schemaName, + tableName, + ex.Message); + return "*"; + } + + if (allColumnNames.Count == 0) + { + return "*"; + } + + columnsToRead = allColumnNames.Where(permittedColumns.IsColumnPermitted).ToList(); + } + else + { + columnsToRead = permittedColumns.Included.ToList(); + } + + if (columnsToRead.Count == 0) + { + return "*"; + } + + return string.Join(", ", columnsToRead.Select(column => SqlQueryBuilder.QuoteIdentifier(column))); + } + /// /// Gets the correctly formatted table name with schema as prefix, if one exists. /// A schema prefix is simply the correctly formatted and prefixed schema name that From c330065e9b0d3de3d5599f24bccdc96425d05007 Mon Sep 17 00:00:00 2001 From: Joymax Andrade do Nascimento Date: Fri, 4 Sep 2026 21:13:00 -0300 Subject: [PATCH 2/5] Add MSSQL fixture and test for column excluded by field permissions --- config-generators/mssql-commands.txt | 1 + src/Service.Tests/DatabaseSchema-MsSql.sql | 14 ++++++++ ...tReadingRuntimeConfigForMsSql.verified.txt | 32 ++++++++++++++++++ .../UnitTests/SqlMetadataProviderUnitTests.cs | 32 ++++++++++++++++++ src/Service.Tests/dab-config.MsSql.json | 33 +++++++++++++++++++ 5 files changed, 112 insertions(+) diff --git a/config-generators/mssql-commands.txt b/config-generators/mssql-commands.txt index 277b9878f0..0b1fe8ca1d 100644 --- a/config-generators/mssql-commands.txt +++ b/config-generators/mssql-commands.txt @@ -22,6 +22,7 @@ add VectorType --config "dab-config.MsSql.json" --source vector_type_table --res update VectorType --config "dab-config.MsSql.json" --permissions "authenticated:create,read,delete,update" update VectorOwner --config "dab-config.MsSql.json" --relationship vectors --target.entity VectorType --cardinality many --relationship.fields "id:owner_id" update VectorType --config "dab-config.MsSql.json" --relationship owner --target.entity VectorOwner --cardinality one --relationship.fields "owner_id:id" +add GeometryType --config "dab-config.MsSql.json" --source geometry_type_table --rest true --graphql true --permissions "anonymous:read" --fields.include "id,name" add Profile --config "dab-config.MsSql.json" --source profiles --rest true --graphql true --permissions "anonymous:create,read,delete,update" update Profile --config "dab-config.MsSql.json" --permissions "authenticated:create,read,delete,update" add stocks_price --config "dab-config.MsSql.json" --source stocks_price --permissions "authenticated:create,read,update,delete" diff --git a/src/Service.Tests/DatabaseSchema-MsSql.sql b/src/Service.Tests/DatabaseSchema-MsSql.sql index 587edb29c3..ce63144076 100644 --- a/src/Service.Tests/DatabaseSchema-MsSql.sql +++ b/src/Service.Tests/DatabaseSchema-MsSql.sql @@ -44,6 +44,7 @@ DROP TABLE IF EXISTS brokers; DROP TABLE IF EXISTS type_table; DROP TABLE IF EXISTS vector_type_table; DROP TABLE IF EXISTS vector_owners; +DROP TABLE IF EXISTS geometry_type_table; DROP TABLE IF EXISTS profiles; DROP TABLE IF EXISTS trees; DROP TABLE IF EXISTS fungi; @@ -252,6 +253,12 @@ CREATE TABLE vector_type_table( CONSTRAINT FK_vector_type_table_owner FOREIGN KEY (owner_id) REFERENCES vector_owners(id) ON DELETE CASCADE ); +CREATE TABLE geometry_type_table( + id int IDENTITY(5001, 1) PRIMARY KEY, + name varchar(100) NOT NULL, + geom geometry NULL +); + CREATE TABLE profiles( id int IDENTITY(5001, 1) PRIMARY KEY, metadata json NULL @@ -656,6 +663,13 @@ VALUES (7, CAST('[' + ( ) + ']' AS vector(1998))); SET IDENTITY_INSERT vector_type_table OFF +SET IDENTITY_INSERT geometry_type_table ON +INSERT INTO geometry_type_table(id, name, geom) +VALUES + (1, 'point', geometry::STGeomFromText('POINT(1 2)', 0)), + (2, 'null geometry', NULL); +SET IDENTITY_INSERT geometry_type_table OFF + SET IDENTITY_INSERT profiles ON INSERT INTO profiles(id, metadata) VALUES diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt index 3c3b8224e4..57e7548405 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt @@ -1910,6 +1910,38 @@ } } }, + { + GeometryType: { + Source: { + Object: geometry_type_table, + Type: Table + }, + GraphQL: { + Singular: GeometryType, + Plural: GeometryTypes, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: Read, + Fields: { + Include: [ + id, + name + ] + } + } + ] + } + ] + } + }, { Profile: { Source: { diff --git a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs index dd6ad7d27e..50ed2026f0 100644 --- a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs +++ b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs @@ -396,6 +396,38 @@ public async Task ValidateInferredRelationshipInfoForMsSql() ValidateInferredRelationshipInfoForTables(); } + /// + /// Test to validate that a table holding a column whose CLR type the data provider cannot + /// resolve - here a geometry column - is still usable when the entity permissions enumerate + /// the readable fields and that column is not among them. + /// Metadata inference must succeed and the unreadable column must be absent from the + /// inferred source definition, so it never reaches the OData or GraphQL type maps. + /// + [TestMethod, TestCategory(TestCategory.MSSQL)] + public async Task ValidateColumnExcludedByFieldPermissionsIsNotInferred() + { + DatabaseEngine = TestCategory.MSSQL; + await SetupTestFixtureAndInferMetadata(); + + Assert.IsTrue( + _sqlMetadataProvider.GetEntityNamesAndDbObjects().TryGetValue("GeometryType", out DatabaseObject databaseObject), + message: "Metadata inference failed for the entity backed by a table with a geometry column."); + + SourceDefinition sourceDefinition = databaseObject.SourceDefinition; + + Assert.IsTrue( + sourceDefinition.Columns.ContainsKey("id"), + message: "The primary key column is expected in the source definition."); + Assert.IsTrue( + sourceDefinition.Columns.ContainsKey("name"), + message: "A column listed in fields.include is expected in the source definition."); + Assert.IsFalse( + sourceDefinition.Columns.ContainsKey("geom"), + message: "A column absent from fields.include is not expected in the source definition."); + + TestHelper.UnsetAllDABEnvironmentVariables(); + } + /// /// Test to validate successful inference of relationship data based on data provided in the config and the metadata /// collected from the MySql database. diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index 670f390d4c..fa80aa2e29 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -1990,6 +1990,39 @@ } } }, + "GeometryType": { + "source": { + "object": "geometry_type_table", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "GeometryType", + "plural": "GeometryTypes" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "read", + "fields": { + "exclude": [], + "include": [ + "id", + "name" + ] + } + } + ] + } + ] + }, "Profile": { "source": { "object": "profiles", From 0d1330376e652e1b8589879e7137628ae717e3e1 Mon Sep 17 00:00:00 2001 From: Joymax Andrade do Nascimento Date: Fri, 4 Sep 2026 21:37:01 -0300 Subject: [PATCH 3/5] Match column names case-insensitively when resolving field permissions --- .../MetadataProviders/SqlMetadataProvider.cs | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 6b31d7c700..1b9c10ebac 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -1548,7 +1548,7 @@ private async Task PopulateSourceDefinitionAsync( if (permittedColumns is not null && !permittedColumns.IsUnrestricted && !permittedColumns.IsColumnPermitted(columnName) - && !sourceDefinition.PrimaryKey.Contains(columnName)) + && !sourceDefinition.PrimaryKey.Contains(columnName, StringComparer.OrdinalIgnoreCase)) { continue; } @@ -1850,6 +1850,9 @@ private async Task FillSchemaForTableAsync( /// /// Describes which backing (database) columns of a database object the runtime /// configuration allows to be read. + /// Column names are matched case-insensitively, consistent with the comparer used by + /// SourceDefinition.Columns and with the field name lookups in this class, so a + /// configuration whose casing differs from the database schema still resolves. /// /// /// True when at least one permission reads every field ("fields" absent, "include" absent, @@ -1899,7 +1902,7 @@ private PermittedColumns ResolvePermittedColumnsForDatabaseObject( string tableName, string? entityName) { - HashSet included = new(StringComparer.Ordinal); + HashSet included = new(StringComparer.OrdinalIgnoreCase); HashSet? excluded = null; bool allColumns = false; bool matchedAnyEntity = false; @@ -1933,7 +1936,7 @@ private PermittedColumns ResolvePermittedColumnsForDatabaseObject( // A column is only droppable when every wildcard permission excludes it. if (excluded is null) { - excluded = new(entityPermittedColumns.Excluded, StringComparer.Ordinal); + excluded = new(entityPermittedColumns.Excluded, StringComparer.OrdinalIgnoreCase); } else { @@ -1944,10 +1947,10 @@ private PermittedColumns ResolvePermittedColumnsForDatabaseObject( if (!matchedAnyEntity) { - return new(AllColumns: true, Included: new(StringComparer.Ordinal), Excluded: new(StringComparer.Ordinal)); + return new(AllColumns: true, Included: new(StringComparer.OrdinalIgnoreCase), Excluded: new(StringComparer.OrdinalIgnoreCase)); } - excluded ??= new(StringComparer.Ordinal); + excluded ??= new(StringComparer.OrdinalIgnoreCase); excluded.ExceptWith(included); return new(allColumns, included, excluded); @@ -1961,12 +1964,12 @@ private PermittedColumns ResolvePermittedColumnsForDatabaseObject( /// private static PermittedColumns ResolvePermittedColumnsForEntity(Entity entity) { - HashSet included = new(StringComparer.Ordinal); + HashSet included = new(StringComparer.OrdinalIgnoreCase); HashSet? excluded = null; bool allColumns = false; // Exposed name -> backing column name. - Dictionary exposedToBackingName = new(StringComparer.Ordinal); + Dictionary exposedToBackingName = new(StringComparer.OrdinalIgnoreCase); if (entity.Mappings is not null) { @@ -1993,7 +1996,7 @@ private static PermittedColumns ResolvePermittedColumnsForEntity(Entity entity) if (entity.Permissions is null || entity.Permissions.Length == 0) { // Nothing configured: the whole object is read, as before. - return new(AllColumns: true, included, new HashSet(StringComparer.Ordinal)); + return new(AllColumns: true, included, new HashSet(StringComparer.OrdinalIgnoreCase)); } foreach (EntityPermission permission in entity.Permissions) @@ -2001,7 +2004,7 @@ private static PermittedColumns ResolvePermittedColumnsForEntity(Entity entity) if (permission.Actions is null) { allColumns = true; - excluded = new(StringComparer.Ordinal); + excluded = new(StringComparer.OrdinalIgnoreCase); continue; } @@ -2009,7 +2012,7 @@ private static PermittedColumns ResolvePermittedColumnsForEntity(Entity entity) { EntityActionFields? fields = action.Fields; - HashSet actionExcluded = new(StringComparer.Ordinal); + HashSet actionExcluded = new(StringComparer.OrdinalIgnoreCase); if (fields?.Exclude is not null) { if (fields.Exclude.Contains(FIELD_WILDCARD)) @@ -2074,7 +2077,7 @@ private static PermittedColumns ResolvePermittedColumnsForEntity(Entity entity) } } - excluded ??= new(StringComparer.Ordinal); + excluded ??= new(StringComparer.OrdinalIgnoreCase); excluded.ExceptWith(included); return new(allColumns, included, excluded); From 9b7d458154acbef6a6869eea7a2d2cba5eb3e058 Mon Sep 17 00:00:00 2001 From: Joymax Andrade do Nascimento Date: Fri, 4 Sep 2026 21:59:27 -0300 Subject: [PATCH 4/5] Only narrow the schema projection when the primary key is configured --- .../MetadataProviders/SqlMetadataProvider.cs | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 1b9c10ebac..d9b26da03d 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -1959,9 +1959,17 @@ private PermittedColumns ResolvePermittedColumnsForDatabaseObject( /// /// Resolves the readable columns of a single entity from the "fields.include" / /// "fields.exclude" sections of its permissions. Exposed names (mappings and field - /// aliases) are translated back to their database column names, and configured primary - /// key fields are always kept, since the runtime cannot operate on the entity without them. + /// aliases) are translated back to their database column names, and the configured + /// primary key is always kept, since the runtime cannot operate on the entity without it. /// + /// + /// The projection is only narrowed for entities whose primary key is known from the + /// configuration ("fields[].primary-key" or "source.key-fields"). When the primary key is + /// instead inferred from the schema read itself - see the fallback to + /// DataTable.PrimaryKey in PopulateObjectDefinitionForEntity - narrowing + /// could drop the key column and leave the entity with no primary key, so every column is + /// read as before. + /// private static PermittedColumns ResolvePermittedColumnsForEntity(Entity entity) { HashSet included = new(StringComparer.OrdinalIgnoreCase); @@ -1993,9 +2001,28 @@ private static PermittedColumns ResolvePermittedColumnsForEntity(Entity entity) } } - if (entity.Permissions is null || entity.Permissions.Length == 0) + HashSet configuredPrimaryKey = new(StringComparer.OrdinalIgnoreCase); + + if (entity.Fields is not null) + { + foreach (FieldMetadata field in entity.Fields.Where(f => f.PrimaryKey)) + { + configuredPrimaryKey.Add(ResolveBackingName(field.Name, exposedToBackingName)); + } + } + + if (configuredPrimaryKey.Count == 0 && entity.Source is not null && entity.Source.KeyFields is not null) + { + foreach (string keyField in entity.Source.KeyFields) + { + configuredPrimaryKey.Add(ResolveBackingName(keyField, exposedToBackingName)); + } + } + + // Without a configured primary key, the key itself is inferred from this schema read, + // so a narrowed projection could drop it and leave the entity unusable. Read it all. + if (entity.Permissions is null || entity.Permissions.Length == 0 || configuredPrimaryKey.Count == 0) { - // Nothing configured: the whole object is read, as before. return new(AllColumns: true, included, new HashSet(StringComparer.OrdinalIgnoreCase)); } @@ -2060,22 +2087,8 @@ private static PermittedColumns ResolvePermittedColumnsForEntity(Entity entity) } } - // Primary keys are structural: never drop them from the projection. - if (entity.Source is not null && entity.Source.KeyFields is not null) - { - foreach (string keyField in entity.Source.KeyFields) - { - included.Add(ResolveBackingName(keyField, exposedToBackingName)); - } - } - - if (entity.Fields is not null) - { - foreach (FieldMetadata field in entity.Fields.Where(f => f.PrimaryKey)) - { - included.Add(ResolveBackingName(field.Name, exposedToBackingName)); - } - } + // The primary key is structural: never drop it from the projection. + included.UnionWith(configuredPrimaryKey); excluded ??= new(StringComparer.OrdinalIgnoreCase); excluded.ExceptWith(included); From 7a82fa7865b8b1e120fd7f858f916fce4efc9714 Mon Sep 17 00:00:00 2001 From: Joymax Andrade do Nascimento Date: Fri, 4 Sep 2026 22:22:11 -0300 Subject: [PATCH 5/5] Skip columns whose data type the provider cannot map, instead of honoring field permissions --- config-generators/mssql-commands.txt | 2 +- .../MsSqlMetadataProvider.cs | 18 + .../MetadataProviders/SqlMetadataProvider.cs | 377 +++--------------- ...tReadingRuntimeConfigForMsSql.verified.txt | 8 +- .../UnitTests/SqlMetadataProviderUnitTests.cs | 17 +- src/Service.Tests/dab-config.MsSql.json | 9 +- 6 files changed, 82 insertions(+), 349 deletions(-) diff --git a/config-generators/mssql-commands.txt b/config-generators/mssql-commands.txt index 0b1fe8ca1d..53674ef59c 100644 --- a/config-generators/mssql-commands.txt +++ b/config-generators/mssql-commands.txt @@ -22,7 +22,7 @@ add VectorType --config "dab-config.MsSql.json" --source vector_type_table --res update VectorType --config "dab-config.MsSql.json" --permissions "authenticated:create,read,delete,update" update VectorOwner --config "dab-config.MsSql.json" --relationship vectors --target.entity VectorType --cardinality many --relationship.fields "id:owner_id" update VectorType --config "dab-config.MsSql.json" --relationship owner --target.entity VectorOwner --cardinality one --relationship.fields "owner_id:id" -add GeometryType --config "dab-config.MsSql.json" --source geometry_type_table --rest true --graphql true --permissions "anonymous:read" --fields.include "id,name" +add GeometryType --config "dab-config.MsSql.json" --source geometry_type_table --rest true --graphql true --permissions "anonymous:read" add Profile --config "dab-config.MsSql.json" --source profiles --rest true --graphql true --permissions "anonymous:create,read,delete,update" update Profile --config "dab-config.MsSql.json" --permissions "authenticated:create,read,delete,update" add stocks_price --config "dab-config.MsSql.json" --source stocks_price --permissions "authenticated:create,read,update,delete" diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index 20de74a96d..9176b60566 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Immutable; using System.Data; using System.Data.Common; using System.Net; @@ -44,6 +45,23 @@ public MsSqlMetadataProvider( _runtimeConfigProvider = runtimeConfigProvider; } + /// + /// SQL Server CLR user-defined types. Microsoft.Data.SqlClient resolves their CLR type + /// through the Microsoft.SqlServer.Types assembly, which Data API builder does not + /// reference, so the reader reports no type for the column and the data adapter fails. + /// Deliberately limited to the types that cannot be read at all: timestamp, xml and vector + /// columns do resolve to a CLR type and are left untouched. + /// + private static readonly ImmutableHashSet _unsupportedColumnDataTypes = + ImmutableHashSet.Create( + StringComparer.OrdinalIgnoreCase, + "geometry", + "geography", + "hierarchyid"); + + /// + protected override ImmutableHashSet UnsupportedColumnDataTypes => _unsupportedColumnDataTypes; + public override string GetDefaultSchemaName() { return "dbo"; diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index d9b26da03d..d801290044 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Immutable; using System.Collections.ObjectModel; using System.Data; using System.Data.Common; @@ -70,9 +71,15 @@ public abstract class SqlMetadataProvider : protected const int NUMBER_OF_RESTRICTIONS = 4; /// - /// Wildcard used in the permissions "fields" section to denote every field. + /// Column data types, as reported by the "Columns" schema collection, that the data + /// provider cannot map to a CLR type. Reading such a column makes + /// fail with + /// "DataReader.GetFieldType(N) returned null", which takes down the whole database object + /// even when the column itself is never exposed. They are therefore left out of the + /// projection used for schema discovery. + /// Empty by default: a provider only lists a type here when it genuinely cannot resolve it. /// - private const string FIELD_WILDCARD = "*"; + protected virtual ImmutableHashSet UnsupportedColumnDataTypes => ImmutableHashSet.Empty; protected string ConnectionString { get; init; } @@ -1534,25 +1541,10 @@ private async Task PopulateSourceDefinitionAsync( DataTable schemaTable = reader.GetSchemaTable(); RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig(); - // Columns the entity's permissions allow to be read. The schema DataTable is cached - // per schema.table and may therefore carry columns permitted only for a sibling - // entity, so the restriction is re-applied per entity here. - PermittedColumns? permittedColumns = entity is null - ? null - : ResolvePermittedColumnsForEntity(entity); - foreach (DataRow columnInfoFromAdapter in schemaTable.Rows) { string columnName = columnInfoFromAdapter["ColumnName"].ToString()!; - if (permittedColumns is not null - && !permittedColumns.IsUnrestricted - && !permittedColumns.IsColumnPermitted(columnName) - && !sourceDefinition.PrimaryKey.Contains(columnName, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - if (runtimeConfig.IsGraphQLEnabled && entity is not null && IsGraphQLReservedName(entity, columnName, graphQLEnabledGlobally: runtimeConfig.IsGraphQLEnabled)) @@ -1706,7 +1698,7 @@ private async Task GetTableWithSchemaFromDataSetAsync( { try { - dataTable = await FillSchemaForTableAsync(schemaName, tableName, entityName); + dataTable = await FillSchemaForTableAsync(schemaName, tableName); } catch (Exception ex) when (ex is not DataApiBuilderException) { @@ -1774,16 +1766,13 @@ private async Task ValidateDatabaseConnection() /// /// Using a data adapter, obtains the schema of the given table name /// and adds the corresponding DataTable to the entities data set. - /// When the entities backed by this database object restrict the readable - /// fields through permissions ("fields.include"/"fields.exclude"), the projection - /// is narrowed to those columns instead of "SELECT *". This avoids the provider - /// having to materialize CLR types it cannot handle (e.g. geometry/geography/hierarchyid), - /// which otherwise fails during schema discovery even though the column is not exposed. + /// Columns whose data type the data provider cannot map to a CLR type are left out of the + /// projection, because the data adapter refuses to build a schema mapping for them and the + /// whole object would otherwise be unreachable. See . /// private async Task FillSchemaForTableAsync( string schemaName, - string tableName, - string? entityName = null) + string tableName) { using ConnectionT conn = new(); // If connection string is set to empty string @@ -1830,14 +1819,7 @@ private async Task FillSchemaForTableAsync( string tableNameWithSchemaPrefix = GetTableNameWithSchemaPrefix(schemaName, tableName); - // Resolve the columns the configuration actually allows to be read for this - // database object. When nothing is restricted, the original SELECT * is preserved. - PermittedColumns permittedColumns = ResolvePermittedColumnsForDatabaseObject( - schemaName: schemaName, - tableName: tableName, - entityName: entityName); - - string projection = await BuildSchemaProjectionAsync(schemaName, tableName, permittedColumns); + string projection = await BuildSchemaProjectionAsync(schemaName, tableName); selectCommand.CommandText = $"SELECT {projection} FROM {tableNameWithSchemaPrefix}"; @@ -1848,325 +1830,70 @@ private async Task FillSchemaForTableAsync( } /// - /// Describes which backing (database) columns of a database object the runtime - /// configuration allows to be read. - /// Column names are matched case-insensitively, consistent with the comparer used by - /// SourceDefinition.Columns and with the field name lookups in this class, so a - /// configuration whose casing differs from the database schema still resolves. + /// Builds the projection used to read the schema of a database object. Returns "*" unless + /// the object holds columns whose data type this provider cannot map to a CLR type, in + /// which case those columns are named out of the projection so the rest stays reachable. + /// The column list comes from the "Columns" schema collection, which reads catalog metadata + /// only and therefore never has to materialize the offending type. /// - /// - /// True when at least one permission reads every field ("fields" absent, "include" absent, - /// or "include": ["*"]). - /// - /// Backing columns explicitly listed in an "include" section. - /// - /// Backing columns excluded from every wildcard permission and never explicitly included. - /// - private sealed record PermittedColumns(bool AllColumns, HashSet Included, HashSet Excluded) - { - /// - /// No column restriction could be derived from the configuration. - /// - public bool IsUnrestricted => AllColumns && Excluded.Count == 0; - - /// - /// Whether the given backing column is readable per the configuration. - /// - public bool IsColumnPermitted(string columnName) - { - if (Included.Contains(columnName)) - { - return true; - } - - if (Excluded.Contains(columnName)) - { - return false; - } - - return AllColumns; - } - } - - /// - /// Resolves the readable columns for a database object. - /// Because the schema DataTable is cached per schema.table, the result combines the - /// permissions of every entity in this data source backed by that same object: a column - /// has to be read if any of those entities can read it. - /// - /// Schema of the database object. - /// Name of the database object. - /// Entity that triggered the schema discovery, when known. - private PermittedColumns ResolvePermittedColumnsForDatabaseObject( - string schemaName, - string tableName, - string? entityName) + private async Task BuildSchemaProjectionAsync(string schemaName, string tableName) { - HashSet included = new(StringComparer.OrdinalIgnoreCase); - HashSet? excluded = null; - bool allColumns = false; - bool matchedAnyEntity = false; - - foreach ((string candidateEntityName, Entity candidateEntity) in Entities) - { - // Only consider entities backed by the same database object. - if (EntityToDatabaseObject.TryGetValue(candidateEntityName, out DatabaseObject? databaseObject)) - { - if (!string.Equals(databaseObject.SchemaName, schemaName, StringComparison.OrdinalIgnoreCase) - || !string.Equals(databaseObject.Name, tableName, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - } - else if (!string.Equals(candidateEntityName, entityName, StringComparison.Ordinal)) - { - // Database object not inferred yet: only the entity that triggered the read applies. - continue; - } - - matchedAnyEntity = true; - PermittedColumns entityPermittedColumns = ResolvePermittedColumnsForEntity(candidateEntity); - - included.UnionWith(entityPermittedColumns.Included); - - if (entityPermittedColumns.AllColumns) - { - allColumns = true; - - // A column is only droppable when every wildcard permission excludes it. - if (excluded is null) - { - excluded = new(entityPermittedColumns.Excluded, StringComparer.OrdinalIgnoreCase); - } - else - { - excluded.IntersectWith(entityPermittedColumns.Excluded); - } - } - } - - if (!matchedAnyEntity) + if (UnsupportedColumnDataTypes.Count == 0) { - return new(AllColumns: true, Included: new(StringComparer.OrdinalIgnoreCase), Excluded: new(StringComparer.OrdinalIgnoreCase)); - } - - excluded ??= new(StringComparer.OrdinalIgnoreCase); - excluded.ExceptWith(included); - - return new(allColumns, included, excluded); - } - - /// - /// Resolves the readable columns of a single entity from the "fields.include" / - /// "fields.exclude" sections of its permissions. Exposed names (mappings and field - /// aliases) are translated back to their database column names, and the configured - /// primary key is always kept, since the runtime cannot operate on the entity without it. - /// - /// - /// The projection is only narrowed for entities whose primary key is known from the - /// configuration ("fields[].primary-key" or "source.key-fields"). When the primary key is - /// instead inferred from the schema read itself - see the fallback to - /// DataTable.PrimaryKey in PopulateObjectDefinitionForEntity - narrowing - /// could drop the key column and leave the entity with no primary key, so every column is - /// read as before. - /// - private static PermittedColumns ResolvePermittedColumnsForEntity(Entity entity) - { - HashSet included = new(StringComparer.OrdinalIgnoreCase); - HashSet? excluded = null; - bool allColumns = false; - - // Exposed name -> backing column name. - Dictionary exposedToBackingName = new(StringComparer.OrdinalIgnoreCase); - - if (entity.Mappings is not null) - { - foreach ((string backingName, string exposedName) in entity.Mappings) - { - if (!string.IsNullOrWhiteSpace(exposedName)) - { - exposedToBackingName[exposedName] = backingName; - } - } - } - - if (entity.Fields is not null) - { - foreach (FieldMetadata field in entity.Fields) - { - if (!string.IsNullOrWhiteSpace(field.Alias)) - { - exposedToBackingName[field.Alias!] = field.Name; - } - } - } - - HashSet configuredPrimaryKey = new(StringComparer.OrdinalIgnoreCase); - - if (entity.Fields is not null) - { - foreach (FieldMetadata field in entity.Fields.Where(f => f.PrimaryKey)) - { - configuredPrimaryKey.Add(ResolveBackingName(field.Name, exposedToBackingName)); - } - } - - if (configuredPrimaryKey.Count == 0 && entity.Source is not null && entity.Source.KeyFields is not null) - { - foreach (string keyField in entity.Source.KeyFields) - { - configuredPrimaryKey.Add(ResolveBackingName(keyField, exposedToBackingName)); - } + return "*"; } - // Without a configured primary key, the key itself is inferred from this schema read, - // so a narrowed projection could drop it and leave the entity unusable. Read it all. - if (entity.Permissions is null || entity.Permissions.Length == 0 || configuredPrimaryKey.Count == 0) - { - return new(AllColumns: true, included, new HashSet(StringComparer.OrdinalIgnoreCase)); - } + List readableColumns = new(); + List skippedColumns = new(); - foreach (EntityPermission permission in entity.Permissions) + try { - if (permission.Actions is null) - { - allColumns = true; - excluded = new(StringComparer.OrdinalIgnoreCase); - continue; - } + DataTable columnsInTable = await GetColumnsAsync(schemaName, tableName); - foreach (EntityAction action in permission.Actions) + foreach (DataRow columnInfo in columnsInTable.Rows) { - EntityActionFields? fields = action.Fields; - - HashSet actionExcluded = new(StringComparer.OrdinalIgnoreCase); - if (fields?.Exclude is not null) + if (columnInfo["COLUMN_NAME"] is not string columnName) { - if (fields.Exclude.Contains(FIELD_WILDCARD)) - { - // This permission reads no field at all, so it contributes no column. - continue; - } - - foreach (string field in fields.Exclude) - { - actionExcluded.Add(ResolveBackingName(field, exposedToBackingName)); - } + continue; } - // No "fields" section, no "include" section, or an explicit wildcard, - // means every column not listed in "exclude" is readable. - bool includesEveryField = fields is null - || fields.Include is null - || fields.Include.Contains(FIELD_WILDCARD); + string? dataType = columnInfo["DATA_TYPE"] as string; - if (includesEveryField) + if (dataType is not null && UnsupportedColumnDataTypes.Contains(dataType)) { - allColumns = true; - - if (excluded is null) - { - excluded = actionExcluded; - } - else - { - excluded.IntersectWith(actionExcluded); - } - - continue; + skippedColumns.Add($"{columnName} ({dataType})"); } - - foreach (string field in fields!.Include!) + else { - string backingName = ResolveBackingName(field, exposedToBackingName); - if (!actionExcluded.Contains(backingName)) - { - included.Add(backingName); - } + readableColumns.Add(columnName); } } } - - // The primary key is structural: never drop it from the projection. - included.UnionWith(configuredPrimaryKey); - - excluded ??= new(StringComparer.OrdinalIgnoreCase); - excluded.ExceptWith(included); - - return new(allColumns, included, excluded); - } - - /// - /// Translates a configured (exposed) field name into its backing column name. - /// - private static string ResolveBackingName(string fieldName, Dictionary exposedToBackingName) - { - return exposedToBackingName.TryGetValue(fieldName, out string? backingName) ? backingName : fieldName; - } - - /// - /// Builds the projection used to read the schema of a database object, narrowed to the - /// columns the configuration allows to be read. Returns "*" when no restriction applies - /// or when the column list cannot be determined. - /// - private async Task BuildSchemaProjectionAsync( - string schemaName, - string tableName, - PermittedColumns permittedColumns) - { - if (permittedColumns.IsUnrestricted) + catch (Exception ex) when (ex is not DataApiBuilderException) { + // The column list is a best-effort optimization: without it the read below behaves + // exactly as it did before, failing loudly if an unsupported type is present. + _logger.LogDebug( + "Unable to enumerate the columns of {schemaName}.{tableName}: {message}", + schemaName, + tableName, + ex.Message); return "*"; } - List columnsToRead; - - if (permittedColumns.AllColumns) - { - // "include": ["*"] with an "exclude" list: enumerate the columns from the catalog - // (metadata only, so unsupported CLR types are never materialized) and drop the - // excluded ones. - List allColumnNames = new(); - try - { - DataTable columnsInTable = await GetColumnsAsync(schemaName, tableName); - - foreach (DataRow columnInfo in columnsInTable.Rows) - { - if (columnInfo["COLUMN_NAME"] is string columnName) - { - allColumnNames.Add(columnName); - } - } - } - catch (Exception ex) - { - _logger.LogDebug( - "Unable to enumerate the columns of {schemaName}.{tableName} to honor the configured field exclusions: {message}", - schemaName, - tableName, - ex.Message); - return "*"; - } - - if (allColumnNames.Count == 0) - { - return "*"; - } - - columnsToRead = allColumnNames.Where(permittedColumns.IsColumnPermitted).ToList(); - } - else - { - columnsToRead = permittedColumns.Included.ToList(); - } - - if (columnsToRead.Count == 0) + if (skippedColumns.Count == 0 || readableColumns.Count == 0) { return "*"; } - return string.Join(", ", columnsToRead.Select(column => SqlQueryBuilder.QuoteIdentifier(column))); + _logger.LogWarning( + "Skipping column(s) of {schemaName}.{tableName} whose data type is not supported: {skippedColumns}. " + + "They are not exposed through REST, GraphQL or MCP.", + schemaName, + tableName, + string.Join(", ", skippedColumns)); + + return string.Join(", ", readableColumns.Select(column => SqlQueryBuilder.QuoteIdentifier(column))); } /// diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt index 57e7548405..b0ce94e022 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt @@ -1929,13 +1929,7 @@ Role: anonymous, Actions: [ { - Action: Read, - Fields: { - Include: [ - id, - name - ] - } + Action: Read } ] } diff --git a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs index 50ed2026f0..c7dbe7d518 100644 --- a/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs +++ b/src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs @@ -397,14 +397,15 @@ public async Task ValidateInferredRelationshipInfoForMsSql() } /// - /// Test to validate that a table holding a column whose CLR type the data provider cannot - /// resolve - here a geometry column - is still usable when the entity permissions enumerate - /// the readable fields and that column is not among them. - /// Metadata inference must succeed and the unreadable column must be absent from the - /// inferred source definition, so it never reaches the OData or GraphQL type maps. + /// Test to validate that a table holding a column whose data type the data provider cannot + /// map to a CLR type - here a geometry column - is still usable: metadata inference must + /// succeed and the unsupported column must be absent from the inferred source definition, + /// so it never reaches the OData or GraphQL type maps. + /// The entity places no field restriction, so this covers the column being skipped on the + /// strength of its type alone. /// [TestMethod, TestCategory(TestCategory.MSSQL)] - public async Task ValidateColumnExcludedByFieldPermissionsIsNotInferred() + public async Task ValidateUnsupportedColumnTypeIsNotInferred() { DatabaseEngine = TestCategory.MSSQL; await SetupTestFixtureAndInferMetadata(); @@ -420,10 +421,10 @@ public async Task ValidateColumnExcludedByFieldPermissionsIsNotInferred() message: "The primary key column is expected in the source definition."); Assert.IsTrue( sourceDefinition.Columns.ContainsKey("name"), - message: "A column listed in fields.include is expected in the source definition."); + message: "A column with a supported data type is expected in the source definition."); Assert.IsFalse( sourceDefinition.Columns.ContainsKey("geom"), - message: "A column absent from fields.include is not expected in the source definition."); + message: "A column whose data type cannot be mapped is not expected in the source definition."); TestHelper.UnsetAllDABEnvironmentVariables(); } diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index fa80aa2e29..fc614d0e86 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -2010,14 +2010,7 @@ "role": "anonymous", "actions": [ { - "action": "read", - "fields": { - "exclude": [], - "include": [ - "id", - "name" - ] - } + "action": "read" } ] }