77using System . Threading ;
88using System . Threading . Tasks ;
99using Azure . DataApiBuilder . Auth ;
10+ using Azure . DataApiBuilder . Config . DatabasePrimitives ;
1011using Azure . DataApiBuilder . Config . ObjectModel ;
1112using Azure . DataApiBuilder . Core . Authorization ;
1213using Azure . DataApiBuilder . Core . Configurations ;
14+ using Azure . DataApiBuilder . Core . Services ;
15+ using Azure . DataApiBuilder . Core . Services . MetadataProviders ;
1316using Azure . DataApiBuilder . Mcp . BuiltInTools ;
1417using Azure . DataApiBuilder . Mcp . Model ;
1518using Microsoft . AspNetCore . Http ;
@@ -188,8 +191,74 @@ public async Task DynamicCustomTool_RespectsCustomToolDisabled()
188191 AssertToolDisabledError ( content , "Custom tool is disabled for entity 'GetBook'" ) ;
189192 }
190193
194+ #region View Support Tests
195+
196+ /// <summary>
197+ /// Data-driven test to verify all DML tools allow both table and view entities.
198+ /// This is critical for scenarios like vector data type support, where users must:
199+ /// - Create a view that omits unsupported columns (e.g., vector columns)
200+ /// - Perform DML operations against that view
201+ /// </summary>
202+ /// <param name="toolType">The tool type to test.</param>
203+ /// <param name="sourceType">The entity source type (Table or View).</param>
204+ /// <param name="entityName">The entity name to use.</param>
205+ /// <param name="jsonArguments">The JSON arguments for the tool.</param>
206+ [ DataTestMethod ]
207+ [ DataRow ( "CreateRecord" , "Table" , "{\" entity\" : \" Book\" , \" data\" : {\" id\" : 1, \" title\" : \" Test\" }}" , DisplayName = "CreateRecord allows Table" ) ]
208+ [ DataRow ( "CreateRecord" , "View" , "{\" entity\" : \" BookView\" , \" data\" : {\" id\" : 1, \" title\" : \" Test\" }}" , DisplayName = "CreateRecord allows View" ) ]
209+ [ DataRow ( "ReadRecords" , "Table" , "{\" entity\" : \" Book\" }" , DisplayName = "ReadRecords allows Table" ) ]
210+ [ DataRow ( "ReadRecords" , "View" , "{\" entity\" : \" BookView\" }" , DisplayName = "ReadRecords allows View" ) ]
211+ [ DataRow ( "UpdateRecord" , "Table" , "{\" entity\" : \" Book\" , \" keys\" : {\" id\" : 1}, \" fields\" : {\" title\" : \" Updated\" }}" , DisplayName = "UpdateRecord allows Table" ) ]
212+ [ DataRow ( "UpdateRecord" , "View" , "{\" entity\" : \" BookView\" , \" keys\" : {\" id\" : 1}, \" fields\" : {\" title\" : \" Updated\" }}" , DisplayName = "UpdateRecord allows View" ) ]
213+ [ DataRow ( "DeleteRecord" , "Table" , "{\" entity\" : \" Book\" , \" keys\" : {\" id\" : 1}}" , DisplayName = "DeleteRecord allows Table" ) ]
214+ [ DataRow ( "DeleteRecord" , "View" , "{\" entity\" : \" BookView\" , \" keys\" : {\" id\" : 1}}" , DisplayName = "DeleteRecord allows View" ) ]
215+ public async Task DmlTool_AllowsTablesAndViews ( string toolType , string sourceType , string jsonArguments )
216+ {
217+ // Arrange
218+ RuntimeConfig config = sourceType == "View"
219+ ? CreateConfigWithViewEntity ( )
220+ : CreateConfigWithDmlToolEnabledEntity ( ) ;
221+ IServiceProvider serviceProvider = CreateServiceProvider ( config ) ;
222+ IMcpTool tool = CreateTool ( toolType ) ;
223+
224+ JsonDocument arguments = JsonDocument . Parse ( jsonArguments ) ;
225+
226+ // Act
227+ CallToolResult result = await tool . ExecuteAsync ( arguments , serviceProvider , CancellationToken . None ) ;
228+
229+ // Assert - Should NOT be a source type blocking error (InvalidEntity)
230+ // Other errors like missing metadata are acceptable since we're testing source type validation
231+ if ( result . IsError == true )
232+ {
233+ JsonElement content = ParseResultContent ( result ) ;
234+
235+ if ( content . TryGetProperty ( "error" , out JsonElement error ) &&
236+ error . TryGetProperty ( "type" , out JsonElement errorType ) )
237+ {
238+ string errorTypeValue = errorType . GetString ( ) ?? string . Empty ;
239+
240+ // This error type indicates the tool is blocking based on source type
241+ Assert . AreNotEqual ( "InvalidEntity" , errorTypeValue ,
242+ $ "{ sourceType } entities should not be blocked with InvalidEntity") ;
243+ }
244+ }
245+ }
246+
247+ #endregion
248+
191249 #region Helper Methods
192250
251+ /// <summary>
252+ /// Helper method to parse the JSON content from a CallToolResult without re-executing the tool.
253+ /// </summary>
254+ /// <param name="result">The result from executing an MCP tool.</param>
255+ /// <returns>The parsed JsonElement from the result's content.</returns>
256+ private static JsonElement ParseResultContent ( CallToolResult result )
257+ {
258+ TextContentBlock firstContent = ( TextContentBlock ) result . Content [ 0 ] ;
259+ return JsonDocument . Parse ( firstContent . Text ) . RootElement ;
260+ }
261+
193262 /// <summary>
194263 /// Helper method to execute an MCP tool and return the parsed JsonElement from the result.
195264 /// </summary>
@@ -200,8 +269,7 @@ public async Task DynamicCustomTool_RespectsCustomToolDisabled()
200269 private static async Task < JsonElement > RunToolAsync ( IMcpTool tool , JsonDocument arguments , IServiceProvider serviceProvider )
201270 {
202271 CallToolResult result = await tool . ExecuteAsync ( arguments , serviceProvider , CancellationToken . None ) ;
203- TextContentBlock firstContent = ( TextContentBlock ) result . Content [ 0 ] ;
204- return JsonDocument . Parse ( firstContent . Text ) . RootElement ;
272+ return ParseResultContent ( result ) ;
205273 }
206274
207275 /// <summary>
@@ -517,8 +585,63 @@ private static RuntimeConfig CreateConfigWithRuntimeDisabledButEntityEnabled()
517585 ) ;
518586 }
519587
588+ /// <summary>
589+ /// Creates a runtime config with a view entity.
590+ /// This is the key scenario for vector data type support.
591+ /// </summary>
592+ private static RuntimeConfig CreateConfigWithViewEntity ( )
593+ {
594+ Dictionary < string , Entity > entities = new ( )
595+ {
596+ [ "BookView" ] = new Entity (
597+ Source : new EntitySource (
598+ Object : "dbo.vBooks" ,
599+ Type : EntitySourceType . View ,
600+ Parameters : null ,
601+ KeyFields : new [ ] { "id" }
602+ ) ,
603+ GraphQL : new ( "BookView" , "BookViews" ) ,
604+ Fields : null ,
605+ Rest : new ( Enabled : true ) ,
606+ Permissions : new [ ] { new EntityPermission ( Role : "anonymous" , Actions : new [ ] {
607+ new EntityAction ( Action : EntityActionOperation . Read , Fields : null , Policy : null ) ,
608+ new EntityAction ( Action : EntityActionOperation . Create , Fields : null , Policy : null ) ,
609+ new EntityAction ( Action : EntityActionOperation . Update , Fields : null , Policy : null ) ,
610+ new EntityAction ( Action : EntityActionOperation . Delete , Fields : null , Policy : null )
611+ } ) } ,
612+ Mappings : null ,
613+ Relationships : null ,
614+ Mcp : new EntityMcpOptions ( customToolEnabled : false , dmlToolsEnabled : true )
615+ )
616+ } ;
617+
618+ return new RuntimeConfig (
619+ Schema : "test-schema" ,
620+ DataSource : new DataSource ( DatabaseType : DatabaseType . MSSQL , ConnectionString : "" , Options : null ) ,
621+ Runtime : new (
622+ Rest : new ( ) ,
623+ GraphQL : new ( ) ,
624+ Mcp : new (
625+ Enabled : true ,
626+ Path : "/mcp" ,
627+ DmlTools : new (
628+ describeEntities : true ,
629+ readRecords : true ,
630+ createRecord : true ,
631+ updateRecord : true ,
632+ deleteRecord : true ,
633+ executeEntity : true
634+ )
635+ ) ,
636+ Host : new ( Cors : null , Authentication : null , Mode : HostMode . Development )
637+ ) ,
638+ Entities : new ( entities )
639+ ) ;
640+ }
641+
520642 /// <summary>
521643 /// Creates a service provider with mocked dependencies for testing MCP tools.
644+ /// Includes metadata provider mocks so tests can reach source type validation.
522645 /// </summary>
523646 private static IServiceProvider CreateServiceProvider ( RuntimeConfig config )
524647 {
@@ -540,6 +663,54 @@ private static IServiceProvider CreateServiceProvider(RuntimeConfig config)
540663 mockHttpContextAccessor . Setup ( x => x . HttpContext ) . Returns ( mockHttpContext . Object ) ;
541664 services . AddSingleton ( mockHttpContextAccessor . Object ) ;
542665
666+ // Add metadata provider mocks so tests can reach source type validation.
667+ // This is required for DmlTool_AllowsTablesAndViews to actually test the source type behavior.
668+ Mock < ISqlMetadataProvider > mockSqlMetadataProvider = new ( ) ;
669+ Dictionary < string , DatabaseObject > entityToDatabaseObject = new ( ) ;
670+
671+ // Add database objects for each entity in the config
672+ if ( config . Entities != null )
673+ {
674+ foreach ( KeyValuePair < string , Entity > kvp in config . Entities )
675+ {
676+ string entityName = kvp . Key ;
677+ Entity entity = kvp . Value ;
678+ EntitySourceType sourceType = entity . Source . Type ?? EntitySourceType . Table ;
679+
680+ DatabaseObject dbObject ;
681+ if ( sourceType == EntitySourceType . View )
682+ {
683+ dbObject = new DatabaseView ( "dbo" , entity . Source . Object )
684+ {
685+ SourceType = EntitySourceType . View
686+ } ;
687+ }
688+ else if ( sourceType == EntitySourceType . StoredProcedure )
689+ {
690+ dbObject = new DatabaseStoredProcedure ( "dbo" , entity . Source . Object )
691+ {
692+ SourceType = EntitySourceType . StoredProcedure
693+ } ;
694+ }
695+ else
696+ {
697+ dbObject = new DatabaseTable ( "dbo" , entity . Source . Object )
698+ {
699+ SourceType = EntitySourceType . Table
700+ } ;
701+ }
702+
703+ entityToDatabaseObject [ entityName ] = dbObject ;
704+ }
705+ }
706+
707+ mockSqlMetadataProvider . Setup ( x => x . EntityToDatabaseObject ) . Returns ( entityToDatabaseObject ) ;
708+ mockSqlMetadataProvider . Setup ( x => x . GetDatabaseType ( ) ) . Returns ( DatabaseType . MSSQL ) ;
709+
710+ Mock < IMetadataProviderFactory > mockMetadataProviderFactory = new ( ) ;
711+ mockMetadataProviderFactory . Setup ( x => x . GetMetadataProvider ( It . IsAny < string > ( ) ) ) . Returns ( mockSqlMetadataProvider . Object ) ;
712+ services . AddSingleton ( mockMetadataProviderFactory . Object ) ;
713+
543714 services . AddLogging ( ) ;
544715
545716 return services . BuildServiceProvider ( ) ;
0 commit comments