From a0bd291af1979351d87343cefd5d28a1f2f0fe2b Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Wed, 10 Jun 2026 10:51:38 +0530 Subject: [PATCH 01/35] chore(deps): upgrade to .NET 10 and Microsoft.Data.SqlClient 6.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Joint runtime + driver bump that prerequisites issue #2768 (MSSQL JSON data type support). Behavior-preserving — no production logic changes. WHY BOTH BUMPS ARE REQUIRED TOGETHER ==================================== SqlDbType.Json (numeric value 35) is a BCL enum value in System.Data, added in .NET 9 and present in .NET 10. It is NOT a Microsoft.Data.SqlClient symbol — SqlClient does not get to add values to a BCL enum it does not own. On .NET 8, the SqlDbType enum stops at DateTimeOffset = 34, so Enum.TryParse("json", ignoreCase: true, out _) in TypeHelper.GetSystemTypeFromSqlDbType returns false regardless of which SqlClient version is installed. The companion feature PR for #2768 plans to add a single dictionary entry [SqlDbType.Json] = typeof(string) which simply will not compile on net8.0. Both upgrades must therefore land together as a single prerequisite. WHY .NET 10 AND NOT .NET 9 ========================== - .NET 10 is the current Long-Term Support release (Nov 2025+). - .NET 9 is Standard-Term Support, EOL May 2026. - DAB's current .NET 8 line reaches EOL November 2026. Going straight to LTS avoids a second forced upgrade inside 12 months. SCOPE OF CHANGES (NO BEHAVIOR CHANGE INTENDED) ============================================== Runtime / SDK pin: - global.json: 8.0.420 -> 10.0.301 Target framework (all 11 src/**/*.csproj): - net8.0 -> net10.0 NuGet packages (Directory.Packages.props): - Microsoft.Data.SqlClient 5.2.3 -> 6.0.2 - Microsoft.Extensions.Caching.Memory 8.0.1 -> 10.0.0 - Microsoft.Extensions.Caching.Abstractions 9.0.0 -> 10.0.0 - Microsoft.Extensions.Primitives 9.0.0 -> 10.0.0 - Microsoft.Extensions.Configuration.Binder 9.0.0 -> 10.0.0 - Microsoft.Extensions.Configuration.Json 9.0.0 -> 10.0.0 - Microsoft.Extensions.Caching.StackExchangeRedis 9.0.3 -> 10.0.0 The Microsoft.Extensions.* bumps are required because OpenTelemetry's transitive chain (Microsoft.Extensions.Logging.Configuration 10.0.0) demands Configuration.Binder >= 10.0.0 — staying on 9.0.0 produces NU1605 package-downgrade errors (warning-as-error). Service.csproj (Microsoft.NET.Sdk.Web) — drop now-redundant PackageReferences (NU1510, warning-as-error): - Microsoft.Extensions.Configuration.Binder (in shared framework) - Microsoft.Extensions.Configuration.Json (in shared framework) Azure DevOps pipelines (9 occurrences across 8 files): - UseDotNet@2 version: 8.0.x -> 10.0.x Container base images (Dockerfile): - mcr.microsoft.com/dotnet/sdk:8.0-cbl-mariner2.0 -> 10.0-azurelinux3.0 - mcr.microsoft.com/dotnet/aspnet:8.0-cbl-mariner2.0 -> 10.0-azurelinux3.0 (cbl-mariner2.0 tag does not exist for .NET 9+; Azure Linux 3.0 is Microsoft's documented successor.) Aspire AppHost (src/Aspire.AppHost/AppHost.cs): - WithArgs("-f", "net8.0") -> WithArgs("-f", "net10.0") (mssql + pg) Build/publish scripts: - scripts/publish.ps1: $dotnetTargetFrameworks net8.0 -> net10.0 - scripts/create-manifest-file.ps1: $dotnetTargetFrameworks + hashtable keys net8.0_{rid} -> net10.0_{rid} (TODO marker: release-engineering to confirm download URLs/hashes resolve) License file rename: - external_licenses/Microsoft.Data.SqlClient.SNI.5.2.0.License.txt -> external_licenses/Microsoft.Data.SqlClient.SNI.6.0.0.License.txt - scripts/notice-generation.ps1: path updated to match - TODO marker at top of license file: contents still 5.2.0 text; release-engineering to refresh from upstream before merge. SUPPRESSED ASPDEPR008 (warning-as-error) — DELIBERATE ===================================================== ASP.NET Core 10 deprecates IWebHostBuilder / IWebHost in favor of WebApplicationBuilder / IHost. Two locations in this repo still consume the obsolete types and would block compilation under the repo's TreatWarningsAsErrors=true setting: - src/Service/Program.cs (2 sites): test-only helpers CreateWebHostBuilder + CreateWebHostFromInMemoryUpdatableConfBuilder are consumed by the existing TestServer fixture which takes IWebHostBuilder. - src/Service.Tests/Configuration/ConfigurationTests.cs (3 sites): the consumer of those helpers. Migrating from WebHost to HostBuilder/WebApplicationBuilder is a behavior-affecting refactor and explicitly OUT OF SCOPE for this no-behavior-change prerequisite PR. Suppression is added at the project level (NoWarn=ASPDEPR008 on Service.csproj + Service.Tests.csproj) with an inline TODO comment pointing at this branch and a follow-up issue link placeholder. VALIDATION ========== dotnet --version : 10.0.301 dotnet restore --nologo : 11/11 projects restored (8.05s avg) dotnet build -c Debug : 0 warnings, 0 errors, 10.32s Multi-engine test categories (MsSql, PostgreSql, MySql, CosmosDb_NoSql, DwSql) were NOT run locally; they MUST run green on the Azure DevOps multi-engine matrix before this PR merges. That matrix is the gate. NEXT STEPS BEFORE MERGE ======================= 1. Release-engineering: refresh external_licenses/Microsoft.Data.SqlClient.SNI.6.0.0.License.txt from upstream for the SNI 6.0.0 version (currently still 5.2.0 text with a TODO marker at top). 2. Release-engineering: confirm scripts/create-manifest-file.ps1 net10.0_{linux,win,osx}-x64 download URLs and SHA hashes resolve once the .NET 10 publish cycle runs. 3. Multi-engine CI matrix runs green (MsSql, PostgreSql, MySql, CosmosDb_NoSql, DwSql) — that is the merge gate. 4. File follow-up issue for ASPDEPR008 migration (WebHost -> WebApplicationBuilder / IHost) so the NoWarn suppressions can be removed in a future behavior-changing PR. --- .pipelines/cosmos-pipelines.yml | 2 +- .pipelines/dwsql-pipelines.yml | 4 ++-- .pipelines/mssql-pipelines.yml | 2 +- .pipelines/mysql-pipelines.yml | 2 +- .pipelines/pg-pipelines.yml | 2 +- .pipelines/templates/build-pipelines.yml | 2 +- .pipelines/templates/mssql-test-steps.yml | 2 +- .pipelines/templates/static-tools.yml | 2 +- Dockerfile | 4 ++-- ...osoft.Data.SqlClient.SNI.6.0.0.License.txt} | 5 +++++ global.json | 2 +- scripts/create-manifest-file.ps1 | 18 +++++++++++------- scripts/notice-generation.ps1 | 2 +- scripts/publish.ps1 | 6 +++--- src/Aspire.AppHost/AppHost.cs | 4 ++-- src/Aspire.AppHost/Aspire.AppHost.csproj | 2 +- src/Auth/Azure.DataApiBuilder.Auth.csproj | 2 +- .../Azure.DataApiBuilder.Mcp.csproj | 2 +- src/Cli.Tests/Cli.Tests.csproj | 2 +- src/Cli/Cli.csproj | 2 +- src/Config/Azure.DataApiBuilder.Config.csproj | 2 +- src/Core/Azure.DataApiBuilder.Core.csproj | 2 +- src/Directory.Packages.props | 14 +++++++------- .../Azure.DataApiBuilder.Product.csproj | 2 +- ...ataApiBuilder.Service.GraphQLBuilder.csproj | 2 +- .../Azure.DataApiBuilder.Service.Tests.csproj | 10 ++++++++-- .../Azure.DataApiBuilder.Service.csproj | 16 +++++++++++----- 27 files changed, 69 insertions(+), 48 deletions(-) rename external_licenses/{Microsoft.Data.SqlClient.SNI.5.2.0.License.txt => Microsoft.Data.SqlClient.SNI.6.0.0.License.txt} (96%) diff --git a/.pipelines/cosmos-pipelines.yml b/.pipelines/cosmos-pipelines.yml index c664879288..bb596b7702 100644 --- a/.pipelines/cosmos-pipelines.yml +++ b/.pipelines/cosmos-pipelines.yml @@ -57,7 +57,7 @@ steps: displayName: Setup .NET SDK v8.0.x inputs: packageType: sdk - version: 8.0.x + version: 10.0.x installationPath: $(Agent.ToolsDirectory)/dotnet - task: NuGetToolInstaller@1 diff --git a/.pipelines/dwsql-pipelines.yml b/.pipelines/dwsql-pipelines.yml index bc1e36dd6d..61f1f3ff17 100644 --- a/.pipelines/dwsql-pipelines.yml +++ b/.pipelines/dwsql-pipelines.yml @@ -50,7 +50,7 @@ jobs: displayName: Setup .NET SDK v8.0.x inputs: packageType: sdk - version: 8.0.x + version: 10.0.x - task: NuGetToolInstaller@1 @@ -179,7 +179,7 @@ jobs: displayName: Setup .NET SDK v8.0.x inputs: packageType: sdk - version: 8.0.x + version: 10.0.x - task: NuGetToolInstaller@1 diff --git a/.pipelines/mssql-pipelines.yml b/.pipelines/mssql-pipelines.yml index c11bfa133c..66321a9448 100644 --- a/.pipelines/mssql-pipelines.yml +++ b/.pipelines/mssql-pipelines.yml @@ -52,7 +52,7 @@ jobs: displayName: Setup .NET SDK v8.0.x inputs: packageType: sdk - version: 8.0.x + version: 10.0.x - task: NuGetToolInstaller@1 diff --git a/.pipelines/mysql-pipelines.yml b/.pipelines/mysql-pipelines.yml index e6fe59d6c0..2207509792 100644 --- a/.pipelines/mysql-pipelines.yml +++ b/.pipelines/mysql-pipelines.yml @@ -49,7 +49,7 @@ jobs: displayName: Setup .NET SDK v8.0.x inputs: packageType: sdk - version: 8.0.x + version: 10.0.x - task: NuGetToolInstaller@1 diff --git a/.pipelines/pg-pipelines.yml b/.pipelines/pg-pipelines.yml index 6cd223aca6..93dfafd6fc 100644 --- a/.pipelines/pg-pipelines.yml +++ b/.pipelines/pg-pipelines.yml @@ -44,7 +44,7 @@ jobs: displayName: Setup .NET SDK v8.0.x inputs: packageType: sdk - version: 8.0.x + version: 10.0.x - task: NuGetToolInstaller@1 diff --git a/.pipelines/templates/build-pipelines.yml b/.pipelines/templates/build-pipelines.yml index 5f166084a3..3963ff521a 100644 --- a/.pipelines/templates/build-pipelines.yml +++ b/.pipelines/templates/build-pipelines.yml @@ -57,7 +57,7 @@ steps: displayName: Setup .NET SDK v8.0.x inputs: packageType: sdk - version: 8.0.x + version: 10.0.x installationPath: $(Agent.ToolsDirectory)/dotnet - task: NuGetToolInstaller@1 diff --git a/.pipelines/templates/mssql-test-steps.yml b/.pipelines/templates/mssql-test-steps.yml index e834132639..96bb8e45f5 100644 --- a/.pipelines/templates/mssql-test-steps.yml +++ b/.pipelines/templates/mssql-test-steps.yml @@ -34,7 +34,7 @@ steps: displayName: Setup .NET SDK v8.0.x inputs: packageType: sdk - version: 8.0.x + version: 10.0.x - task: NuGetToolInstaller@1 diff --git a/.pipelines/templates/static-tools.yml b/.pipelines/templates/static-tools.yml index 14bc25764f..e24d088726 100644 --- a/.pipelines/templates/static-tools.yml +++ b/.pipelines/templates/static-tools.yml @@ -24,7 +24,7 @@ jobs: displayName: Setup .NET SDK v8.0.x inputs: packageType: sdk - version: 8.0.x + version: 10.0.x # Analyze source and build output text files for credentials - task: CredScan@3 diff --git a/Dockerfile b/Dockerfile index 21dfece640..a1532beb1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,13 @@ # Version values referenced from https://hub.docker.com/_/microsoft-dotnet-aspnet -FROM mcr.microsoft.com/dotnet/sdk:8.0-cbl-mariner2.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0-azurelinux3.0 AS build WORKDIR /src COPY [".", "./"] RUN dotnet build "./src/Service/Azure.DataApiBuilder.Service.csproj" -c Docker -o /out -r linux-x64 -FROM mcr.microsoft.com/dotnet/aspnet:8.0-cbl-mariner2.0 AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:10.0-azurelinux3.0 AS runtime COPY --from=build /out /App # Add default dab-config.json to /App in the image diff --git a/external_licenses/Microsoft.Data.SqlClient.SNI.5.2.0.License.txt b/external_licenses/Microsoft.Data.SqlClient.SNI.6.0.0.License.txt similarity index 96% rename from external_licenses/Microsoft.Data.SqlClient.SNI.5.2.0.License.txt rename to external_licenses/Microsoft.Data.SqlClient.SNI.6.0.0.License.txt index ec940f9243..42dc7d59a9 100644 --- a/external_licenses/Microsoft.Data.SqlClient.SNI.5.2.0.License.txt +++ b/external_licenses/Microsoft.Data.SqlClient.SNI.6.0.0.License.txt @@ -1,3 +1,8 @@ +[TODO prereq-PR Usr/sogh/upgrade-net10-sqlclient6: This file was renamed from +Microsoft.Data.SqlClient.SNI.5.2.0.License.txt for the SqlClient 5.2.3 -> 6.0.2 +bump. Please refresh contents from the upstream license URL for the new SNI +version 6.0.0 before merge. The body below is still the 5.2.0 text.] + MICROSOFT SOFTWARE LICENSE TERMS MICROSOFT.DATA.SQLCLIENT.SNI LIBRARY diff --git a/global.json b/global.json index df2d0ef27d..f365c8c0e0 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.420", + "version": "10.0.301", "rollForward": "latestFeature" } } diff --git a/scripts/create-manifest-file.ps1 b/scripts/create-manifest-file.ps1 index 85e1579ba4..f10a78719b 100644 --- a/scripts/create-manifest-file.ps1 +++ b/scripts/create-manifest-file.ps1 @@ -21,7 +21,11 @@ if ($isReleaseBuild -eq 'true') } # Generating hash for DAB packages -$dotnetTargetFrameworks = "net8.0" +# TODO(prereq-PR Usr/sogh/upgrade-net10-sqlclient6): String-only swap of +# net8.0 -> net10.0. Release-engineering: please confirm the +# net10.0_{linux,win,osx}-x64 download URLs and SHA hashes referenced +# downstream still resolve after the .NET 10 publish cycle runs. +$dotnetTargetFrameworks = "net10.0" $RIDs = "win-x64", "linux-x64", "osx-x64" [hashtable]$frameworkPlatformDownloadMetadata = @{} [hashtable]$frameworkPlatformFileHashMetadata = @{} @@ -62,16 +66,16 @@ $latestBlock = @' "releaseDate": "${releaseDate}", "files": { "linux-x64":{ - "url": "$($frameworkPlatformDownloadMetadata["net8.0_linux-x64"])", - "sha": "$($frameworkPlatformFileHashMetadata["net8.0_linux-x64"])" + "url": "$($frameworkPlatformDownloadMetadata["net10.0_linux-x64"])", + "sha": "$($frameworkPlatformFileHashMetadata["net10.0_linux-x64"])" }, "win-x64":{ - "url": "$($frameworkPlatformDownloadMetadata["net8.0_win-x64"])", - "sha": "$($frameworkPlatformFileHashMetadata["net8.0_win-x64"])" + "url": "$($frameworkPlatformDownloadMetadata["net10.0_win-x64"])", + "sha": "$($frameworkPlatformFileHashMetadata["net10.0_win-x64"])" }, "osx-x64":{ - "url": "$($frameworkPlatformDownloadMetadata["net8.0_osx-x64"])", - "sha": "$($frameworkPlatformFileHashMetadata["net8.0_osx-x64"])" + "url": "$($frameworkPlatformDownloadMetadata["net10.0_osx-x64"])", + "sha": "$($frameworkPlatformFileHashMetadata["net10.0_osx-x64"])" }, "nuget": { "url": "${download_url_nuget_cli}", diff --git a/scripts/notice-generation.ps1 b/scripts/notice-generation.ps1 index 3b263a4b8f..0d01e2167d 100644 --- a/scripts/notice-generation.ps1 +++ b/scripts/notice-generation.ps1 @@ -15,7 +15,7 @@ Invoke-WebRequest $chiliCreamLicenseMetadataURL -UseBasicParsing | Out-File $chiliCreamLicenseSavePath # Define the path to the license file in your repository and Read the content of the license file -$sqlClientSNILicenseFilePath = "$BuildSourcesDir/external_licenses/Microsoft.Data.SqlClient.SNI.5.2.0.License.txt" +$sqlClientSNILicenseFilePath = "$BuildSourcesDir/external_licenses/Microsoft.Data.SqlClient.SNI.6.0.0.License.txt" $sqlClientSNILicense = Get-Content -Path $sqlClientSNILicenseFilePath -Raw # Replace erroneous copyright, using [System.IO.File] for better performance than Get-Content and Set-Content diff --git a/scripts/publish.ps1 b/scripts/publish.ps1 index 8716f36928..5a3d0b5b59 100644 --- a/scripts/publish.ps1 +++ b/scripts/publish.ps1 @@ -10,12 +10,12 @@ param ( ) $BuildRoot = Split-Path $PSScriptRoot -Parent -$dotnetTargetFrameworks = "net8.0" +$dotnetTargetFrameworks = "net10.0" $RIDs = "win-x64", "linux-x64", "osx-x64" # Runs dotnet publish for each target framework and RID. # Example results: -# \dotnetpublishout\publish\Release\net8.0\win-x64\dab +# \dotnetpublishout\publish\Release\net10.0\win-x64\dab if ($Package) { foreach ($targetFramework in $dotnetTargetFrameworks) @@ -30,7 +30,7 @@ if ($Package) # Zips the published output for each target framework and RID. # For example: -# \dotnetpublishout\publish\Release\net8.0\win-x64\dab_net8.0_win-x64-0.14.123-rc.zip +# \dotnetpublishout\publish\Release\net10.0\win-x64\dab_net10.0_win-x64-0.14.123-rc.zip if ($CreateZip) { foreach ($targetFramework in $dotnetTargetFrameworks) diff --git a/src/Aspire.AppHost/AppHost.cs b/src/Aspire.AppHost/AppHost.cs index 3e4e3f597c..428b75a499 100644 --- a/src/Aspire.AppHost/AppHost.cs +++ b/src/Aspire.AppHost/AppHost.cs @@ -25,7 +25,7 @@ } var mssqlService = builder.AddProject("mssql-service", "Development") - .WithArgs("-f", "net8.0") + .WithArgs("-f", "net10.0") .WithEndpoint(endpointName: "https", (e) => e.Port = 1234) .WithEndpoint(endpointName: "http", (e) => e.Port = 2345) .WithEnvironment("db-type", "mssql") @@ -65,7 +65,7 @@ } var pgService = builder.AddProject("pg-service", "Development") - .WithArgs("-f", "net8.0") + .WithArgs("-f", "net10.0") .WithEndpoint(endpointName: "https", (e) => e.Port = 1234) .WithEndpoint(endpointName: "http", (e) => e.Port = 2345) .WithEnvironment("db-type", "postgresql") diff --git a/src/Aspire.AppHost/Aspire.AppHost.csproj b/src/Aspire.AppHost/Aspire.AppHost.csproj index 79d44e51a1..d47030e64f 100644 --- a/src/Aspire.AppHost/Aspire.AppHost.csproj +++ b/src/Aspire.AppHost/Aspire.AppHost.csproj @@ -4,7 +4,7 @@ Exe - net8.0 + net10.0 enable NU1603;NU1605 enable diff --git a/src/Auth/Azure.DataApiBuilder.Auth.csproj b/src/Auth/Azure.DataApiBuilder.Auth.csproj index 9537d48834..3a34ee30c0 100644 --- a/src/Auth/Azure.DataApiBuilder.Auth.csproj +++ b/src/Auth/Azure.DataApiBuilder.Auth.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable $(BaseOutputPath)\engine diff --git a/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj b/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj index c1e4f9cfe4..f7c1e831ec 100644 --- a/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj +++ b/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable diff --git a/src/Cli.Tests/Cli.Tests.csproj b/src/Cli.Tests/Cli.Tests.csproj index a8081f5250..1d9c1e83f0 100644 --- a/src/Cli.Tests/Cli.Tests.csproj +++ b/src/Cli.Tests/Cli.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable false diff --git a/src/Cli/Cli.csproj b/src/Cli/Cli.csproj index 3cacab6b4b..7ae0276a39 100644 --- a/src/Cli/Cli.csproj +++ b/src/Cli/Cli.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 Cli enable enable diff --git a/src/Config/Azure.DataApiBuilder.Config.csproj b/src/Config/Azure.DataApiBuilder.Config.csproj index 6b5bdf0955..1a6e44704d 100644 --- a/src/Config/Azure.DataApiBuilder.Config.csproj +++ b/src/Config/Azure.DataApiBuilder.Config.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable $(BaseOutputPath)\engine diff --git a/src/Core/Azure.DataApiBuilder.Core.csproj b/src/Core/Azure.DataApiBuilder.Core.csproj index 412dd316ab..3497fa516a 100644 --- a/src/Core/Azure.DataApiBuilder.Core.csproj +++ b/src/Core/Azure.DataApiBuilder.Core.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable ./nupkg diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 8bb02bc13c..a9bc031835 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -32,9 +32,9 @@ - - - + + + @@ -43,9 +43,9 @@ - - - + + + @@ -81,7 +81,7 @@ - + diff --git a/src/Product/Azure.DataApiBuilder.Product.csproj b/src/Product/Azure.DataApiBuilder.Product.csproj index a385f21a53..f2237e2fba 100644 --- a/src/Product/Azure.DataApiBuilder.Product.csproj +++ b/src/Product/Azure.DataApiBuilder.Product.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable $(BaseOutputPath)\engine diff --git a/src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj b/src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj index c2df983509..87c2a5637e 100644 --- a/src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj +++ b/src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable $(BaseOutputPath)\engine diff --git a/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj b/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj index ae274a4dc2..b7f7cbb054 100644 --- a/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj +++ b/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj @@ -1,11 +1,17 @@ - net8.0 + net10.0 false disable $(BaseOutputPath)\tests - NU1603 + + NU1603;ASPDEPR008 diff --git a/src/Service/Azure.DataApiBuilder.Service.csproj b/src/Service/Azure.DataApiBuilder.Service.csproj index 2ba352c02f..eba3437571 100644 --- a/src/Service/Azure.DataApiBuilder.Service.csproj +++ b/src/Service/Azure.DataApiBuilder.Service.csproj @@ -1,12 +1,20 @@ - + - net8.0 + net10.0 Debug;Release;Docker $(BaseOutputPath)\engine win-x64;linux-x64;osx-x64 true - NU1603 + + NU1603;ASPDEPR008 @@ -64,8 +72,6 @@ - - From 1064d7ff2c780b0c05d9558e4d01555943744668 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Wed, 17 Jun 2026 17:29:18 -0700 Subject: [PATCH 02/35] Update SqlClient 6.1.5 --- scripts/notice-generation.ps1 | 2 +- src/Config/Azure.DataApiBuilder.Config.csproj | 1 + src/Directory.Packages.props | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/notice-generation.ps1 b/scripts/notice-generation.ps1 index 0d01e2167d..dc9303332a 100644 --- a/scripts/notice-generation.ps1 +++ b/scripts/notice-generation.ps1 @@ -15,7 +15,7 @@ Invoke-WebRequest $chiliCreamLicenseMetadataURL -UseBasicParsing | Out-File $chiliCreamLicenseSavePath # Define the path to the license file in your repository and Read the content of the license file -$sqlClientSNILicenseFilePath = "$BuildSourcesDir/external_licenses/Microsoft.Data.SqlClient.SNI.6.0.0.License.txt" +$sqlClientSNILicenseFilePath = "$BuildSourcesDir/external_licenses/Microsoft.Data.SqlClient.SNI.6.1.5.License.txt" $sqlClientSNILicense = Get-Content -Path $sqlClientSNILicenseFilePath -Raw # Replace erroneous copyright, using [System.IO.File] for better performance than Get-Content and Set-Content diff --git a/src/Config/Azure.DataApiBuilder.Config.csproj b/src/Config/Azure.DataApiBuilder.Config.csproj index 1a6e44704d..0d1655df76 100644 --- a/src/Config/Azure.DataApiBuilder.Config.csproj +++ b/src/Config/Azure.DataApiBuilder.Config.csproj @@ -28,6 +28,7 @@ + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index a9bc031835..bbbcf31b08 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -43,7 +43,7 @@ - + From 96f5324b2d723f6b09c0a51ab1ba7f26af1f7781 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 18 Jun 2026 11:14:39 +0530 Subject: [PATCH 03/35] Address PR review feedback and align ASP.NET Core packages to 10.x - Fix UseDotNet displayName v8.0.x -> v10.0.x across pipeline files - Replace internal branch-path TODOs with durable follow-up notes - Rename SNI license to 6.0.2 (actual runtime version) and fix notice-generation reference - Bump Microsoft.AspNetCore.{TestHost,Authorization,Authentication.JwtBearer,Mvc.Testing} to 10.0.0 --- .pipelines/cosmos-pipelines.yml | 2 +- .pipelines/dwsql-pipelines.yml | 4 ++-- .pipelines/mssql-pipelines.yml | 2 +- .pipelines/mysql-pipelines.yml | 2 +- .pipelines/pg-pipelines.yml | 2 +- .pipelines/templates/build-pipelines.yml | 2 +- .pipelines/templates/mssql-test-steps.yml | 2 +- .pipelines/templates/static-tools.yml | 2 +- ...txt => Microsoft.Data.SqlClient.SNI.6.0.2.License.txt} | 5 ----- scripts/create-manifest-file.ps1 | 7 +++---- scripts/notice-generation.ps1 | 2 +- src/Directory.Packages.props | 8 ++++---- .../Azure.DataApiBuilder.Service.Tests.csproj | 3 ++- src/Service/Azure.DataApiBuilder.Service.csproj | 6 ++++-- 14 files changed, 23 insertions(+), 26 deletions(-) rename external_licenses/{Microsoft.Data.SqlClient.SNI.6.0.0.License.txt => Microsoft.Data.SqlClient.SNI.6.0.2.License.txt} (96%) diff --git a/.pipelines/cosmos-pipelines.yml b/.pipelines/cosmos-pipelines.yml index bb596b7702..0f5a7eb427 100644 --- a/.pipelines/cosmos-pipelines.yml +++ b/.pipelines/cosmos-pipelines.yml @@ -54,7 +54,7 @@ steps: # Per Microsoft Learn Docs, "Selecting the .NET SDK version is independent from # specifying the runtime version a project targets." - task: UseDotNet@2 - displayName: Setup .NET SDK v8.0.x + displayName: Setup .NET SDK v10.0.x inputs: packageType: sdk version: 10.0.x diff --git a/.pipelines/dwsql-pipelines.yml b/.pipelines/dwsql-pipelines.yml index 61f1f3ff17..d8fe8d0455 100644 --- a/.pipelines/dwsql-pipelines.yml +++ b/.pipelines/dwsql-pipelines.yml @@ -47,7 +47,7 @@ jobs: # Per Microsoft Learn Docs, "Selecting the .NET SDK version is independent from # specifying the runtime version a project targets." - task: UseDotNet@2 - displayName: Setup .NET SDK v8.0.x + displayName: Setup .NET SDK v10.0.x inputs: packageType: sdk version: 10.0.x @@ -176,7 +176,7 @@ jobs: # Per Microsoft Learn Docs, "Selecting the .NET SDK version is independent from # specifying the runtime version a project targets." - task: UseDotNet@2 - displayName: Setup .NET SDK v8.0.x + displayName: Setup .NET SDK v10.0.x inputs: packageType: sdk version: 10.0.x diff --git a/.pipelines/mssql-pipelines.yml b/.pipelines/mssql-pipelines.yml index 66321a9448..bdfb5e37e6 100644 --- a/.pipelines/mssql-pipelines.yml +++ b/.pipelines/mssql-pipelines.yml @@ -49,7 +49,7 @@ jobs: # Per Microsoft Learn Docs, "Selecting the .NET SDK version is independent from # specifying the runtime version a project targets." - task: UseDotNet@2 - displayName: Setup .NET SDK v8.0.x + displayName: Setup .NET SDK v10.0.x inputs: packageType: sdk version: 10.0.x diff --git a/.pipelines/mysql-pipelines.yml b/.pipelines/mysql-pipelines.yml index 2207509792..d25fbabe7c 100644 --- a/.pipelines/mysql-pipelines.yml +++ b/.pipelines/mysql-pipelines.yml @@ -46,7 +46,7 @@ jobs: # Per Microsoft Learn Docs, "Selecting the .NET SDK version is independent from # specifying the runtime version a project targets." - task: UseDotNet@2 - displayName: Setup .NET SDK v8.0.x + displayName: Setup .NET SDK v10.0.x inputs: packageType: sdk version: 10.0.x diff --git a/.pipelines/pg-pipelines.yml b/.pipelines/pg-pipelines.yml index 93dfafd6fc..14f290ecbc 100644 --- a/.pipelines/pg-pipelines.yml +++ b/.pipelines/pg-pipelines.yml @@ -41,7 +41,7 @@ jobs: # Per Microsoft Learn Docs, "Selecting the .NET SDK version is independent from # specifying the runtime version a project targets." - task: UseDotNet@2 - displayName: Setup .NET SDK v8.0.x + displayName: Setup .NET SDK v10.0.x inputs: packageType: sdk version: 10.0.x diff --git a/.pipelines/templates/build-pipelines.yml b/.pipelines/templates/build-pipelines.yml index 3963ff521a..e6e7ca3f13 100644 --- a/.pipelines/templates/build-pipelines.yml +++ b/.pipelines/templates/build-pipelines.yml @@ -54,7 +54,7 @@ steps: # Per Microsoft Learn Docs, "Selecting the .NET SDK version is independent from # specifying the runtime version a project targets." - task: UseDotNet@2 - displayName: Setup .NET SDK v8.0.x + displayName: Setup .NET SDK v10.0.x inputs: packageType: sdk version: 10.0.x diff --git a/.pipelines/templates/mssql-test-steps.yml b/.pipelines/templates/mssql-test-steps.yml index 96bb8e45f5..565273f27c 100644 --- a/.pipelines/templates/mssql-test-steps.yml +++ b/.pipelines/templates/mssql-test-steps.yml @@ -31,7 +31,7 @@ steps: # Per Microsoft Learn Docs, "Selecting the .NET SDK version is independent from # specifying the runtime version a project targets." - task: UseDotNet@2 - displayName: Setup .NET SDK v8.0.x + displayName: Setup .NET SDK v10.0.x inputs: packageType: sdk version: 10.0.x diff --git a/.pipelines/templates/static-tools.yml b/.pipelines/templates/static-tools.yml index e24d088726..20465297ea 100644 --- a/.pipelines/templates/static-tools.yml +++ b/.pipelines/templates/static-tools.yml @@ -21,7 +21,7 @@ jobs: clean: true # if true, execute `execute git clean -ffdx && git reset --hard HEAD` before fetching - task: UseDotNet@2 - displayName: Setup .NET SDK v8.0.x + displayName: Setup .NET SDK v10.0.x inputs: packageType: sdk version: 10.0.x diff --git a/external_licenses/Microsoft.Data.SqlClient.SNI.6.0.0.License.txt b/external_licenses/Microsoft.Data.SqlClient.SNI.6.0.2.License.txt similarity index 96% rename from external_licenses/Microsoft.Data.SqlClient.SNI.6.0.0.License.txt rename to external_licenses/Microsoft.Data.SqlClient.SNI.6.0.2.License.txt index 42dc7d59a9..ec940f9243 100644 --- a/external_licenses/Microsoft.Data.SqlClient.SNI.6.0.0.License.txt +++ b/external_licenses/Microsoft.Data.SqlClient.SNI.6.0.2.License.txt @@ -1,8 +1,3 @@ -[TODO prereq-PR Usr/sogh/upgrade-net10-sqlclient6: This file was renamed from -Microsoft.Data.SqlClient.SNI.5.2.0.License.txt for the SqlClient 5.2.3 -> 6.0.2 -bump. Please refresh contents from the upstream license URL for the new SNI -version 6.0.0 before merge. The body below is still the 5.2.0 text.] - MICROSOFT SOFTWARE LICENSE TERMS MICROSOFT.DATA.SQLCLIENT.SNI LIBRARY diff --git a/scripts/create-manifest-file.ps1 b/scripts/create-manifest-file.ps1 index f10a78719b..d2a521bdf2 100644 --- a/scripts/create-manifest-file.ps1 +++ b/scripts/create-manifest-file.ps1 @@ -21,10 +21,9 @@ if ($isReleaseBuild -eq 'true') } # Generating hash for DAB packages -# TODO(prereq-PR Usr/sogh/upgrade-net10-sqlclient6): String-only swap of -# net8.0 -> net10.0. Release-engineering: please confirm the -# net10.0_{linux,win,osx}-x64 download URLs and SHA hashes referenced -# downstream still resolve after the .NET 10 publish cycle runs. +# TODO: Release-engineering - confirm the net10.0_{linux,win,osx}-x64 download +# URLs and SHA hashes referenced downstream still resolve after the .NET 10 +# publish cycle runs. (Add a tracking issue number/link here once created.) $dotnetTargetFrameworks = "net10.0" $RIDs = "win-x64", "linux-x64", "osx-x64" [hashtable]$frameworkPlatformDownloadMetadata = @{} diff --git a/scripts/notice-generation.ps1 b/scripts/notice-generation.ps1 index dc9303332a..a7aa620bfe 100644 --- a/scripts/notice-generation.ps1 +++ b/scripts/notice-generation.ps1 @@ -15,7 +15,7 @@ Invoke-WebRequest $chiliCreamLicenseMetadataURL -UseBasicParsing | Out-File $chiliCreamLicenseSavePath # Define the path to the license file in your repository and Read the content of the license file -$sqlClientSNILicenseFilePath = "$BuildSourcesDir/external_licenses/Microsoft.Data.SqlClient.SNI.6.1.5.License.txt" +$sqlClientSNILicenseFilePath = "$BuildSourcesDir/external_licenses/Microsoft.Data.SqlClient.SNI.6.0.2.License.txt" $sqlClientSNILicense = Get-Content -Path $sqlClientSNILicenseFilePath -Raw # Replace erroneous copyright, using [System.IO.File] for better performance than Get-Content and Set-Content diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index bbbcf31b08..414e489b45 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -24,12 +24,12 @@ - - + + - - + + diff --git a/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj b/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj index b7f7cbb054..f75b61d01f 100644 --- a/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj +++ b/src/Service.Tests/Azure.DataApiBuilder.Service.Tests.csproj @@ -10,7 +10,8 @@ test-only helpers. Migrating to IHost is behavior-affecting refactor work tracked separately; suppressed here so the .NET 10 + SqlClient 6.x prerequisite PR stays behavior-preserving. - TODO(prereq-PR Usr/sogh/upgrade-net10-sqlclient6): file a follow-up issue. --> + TODO: file a follow-up issue to migrate off IWebHostBuilder/IWebHost + to IHost and remove this ASPDEPR008 suppression. --> NU1603;ASPDEPR008 diff --git a/src/Service/Azure.DataApiBuilder.Service.csproj b/src/Service/Azure.DataApiBuilder.Service.csproj index eba3437571..a03f9690c0 100644 --- a/src/Service/Azure.DataApiBuilder.Service.csproj +++ b/src/Service/Azure.DataApiBuilder.Service.csproj @@ -12,8 +12,10 @@ TestServer fixture takes IWebHostBuilder. Migrating to WebApplicationBuilder/HostBuilder is behavior-affecting refactor work tracked separately; suppressed here so the .NET 10 + SqlClient - 6.x prerequisite PR stays behavior-preserving. TODO(prereq-PR - Usr/sogh/upgrade-net10-sqlclient6): file a follow-up issue. --> + 6.x prerequisite PR stays behavior-preserving. TODO: file a + follow-up issue to migrate off WebHost (IWebHostBuilder/IWebHost) + to WebApplicationBuilder/IHost and remove this ASPDEPR008 + suppression. --> NU1603;ASPDEPR008 From 280151bd2082f62f1fb20362615b6ba400eba57d Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 18 Jun 2026 11:27:15 +0530 Subject: [PATCH 04/35] Pin MessagePack to 2.5.301 to fix NU1903 transitive vulnerability Aspire.Hosting pulls MessagePack 2.5.192 transitively (via KubernetesClient), which has high-severity advisory GHSA-hv8m-jj95-wg3x (CVE-2026-48109). The repo treats NU1903 as error, failing restore. Pin to patched 2.5.301 via CPM and add a direct reference in Aspire.AppHost to force the transitive upgrade. --- src/Aspire.AppHost/Aspire.AppHost.csproj | 3 +++ src/Directory.Packages.props | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/Aspire.AppHost/Aspire.AppHost.csproj b/src/Aspire.AppHost/Aspire.AppHost.csproj index d47030e64f..4fbe70cdcf 100644 --- a/src/Aspire.AppHost/Aspire.AppHost.csproj +++ b/src/Aspire.AppHost/Aspire.AppHost.csproj @@ -15,6 +15,9 @@ + + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 414e489b45..56523d0a94 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -32,6 +32,9 @@ + + From 1a7d3091583e483551baad5ffaafa71fb2cb6111 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 18 Jun 2026 11:38:29 +0530 Subject: [PATCH 05/35] Formatting fixes --- .../Factories/MutationEngineFactory.cs | 3 +- .../SqlUpdateQueryStructure.cs | 30 +++++++++---------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Core/Resolvers/Factories/MutationEngineFactory.cs b/src/Core/Resolvers/Factories/MutationEngineFactory.cs index 9b4df9f8bf..086559ac9d 100644 --- a/src/Core/Resolvers/Factories/MutationEngineFactory.cs +++ b/src/Core/Resolvers/Factories/MutationEngineFactory.cs @@ -107,7 +107,8 @@ public IMutationEngine GetMutationEngine(DatabaseType databaseType) $"{nameof(databaseType)}:{databaseType} could not be found within the config", HttpStatusCode.BadRequest, DataApiBuilderException.SubStatusCodes.DataSourceNotFound); - }; + } + ; return mutationEngine; } diff --git a/src/Core/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs index ecbbf3fc5c..2fffa19ef5 100644 --- a/src/Core/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs @@ -122,26 +122,26 @@ public SqlUpdateStructure( Predicates.Add(CreatePredicateForParam(new KeyValuePair(pkBackingColumn, param.Value))); } else // Unpack the input argument type as columns to update - if (param.Key == UpdateAndPatchMutationBuilder.INPUT_ARGUMENT_NAME) - { - IDictionary updateFields = - GQLMutArgumentToDictParams(context, UpdateAndPatchMutationBuilder.INPUT_ARGUMENT_NAME, mutationParams); - - foreach (KeyValuePair field in updateFields) + if (param.Key == UpdateAndPatchMutationBuilder.INPUT_ARGUMENT_NAME) { - string fieldBackingColumn = field.Key; - if (sqlMetadataProvider.TryGetBackingColumn(entityName, field.Key, out string? resolvedBackingColumn) - && !string.IsNullOrWhiteSpace(resolvedBackingColumn)) - { - fieldBackingColumn = resolvedBackingColumn; - } + IDictionary updateFields = + GQLMutArgumentToDictParams(context, UpdateAndPatchMutationBuilder.INPUT_ARGUMENT_NAME, mutationParams); - if (columns.Contains(fieldBackingColumn)) + foreach (KeyValuePair field in updateFields) { - UpdateOperations.Add(CreatePredicateForParam(new KeyValuePair(key: fieldBackingColumn, field.Value))); + string fieldBackingColumn = field.Key; + if (sqlMetadataProvider.TryGetBackingColumn(entityName, field.Key, out string? resolvedBackingColumn) + && !string.IsNullOrWhiteSpace(resolvedBackingColumn)) + { + fieldBackingColumn = resolvedBackingColumn; + } + + if (columns.Contains(fieldBackingColumn)) + { + UpdateOperations.Add(CreatePredicateForParam(new KeyValuePair(key: fieldBackingColumn, field.Value))); + } } } - } } if (UpdateOperations.Count == 0) From a6ecbb930c202bb92cf695229270ac4e12d45edc Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 18 Jun 2026 11:45:09 +0530 Subject: [PATCH 06/35] Formatting fixes --- src/Core/Resolvers/Factories/MutationEngineFactory.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Resolvers/Factories/MutationEngineFactory.cs b/src/Core/Resolvers/Factories/MutationEngineFactory.cs index 086559ac9d..08a5fea2e3 100644 --- a/src/Core/Resolvers/Factories/MutationEngineFactory.cs +++ b/src/Core/Resolvers/Factories/MutationEngineFactory.cs @@ -108,7 +108,6 @@ public IMutationEngine GetMutationEngine(DatabaseType databaseType) HttpStatusCode.BadRequest, DataApiBuilderException.SubStatusCodes.DataSourceNotFound); } - ; return mutationEngine; } From 5544ef414749cfa126175938f3d1fa47b54ebede Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 18 Jun 2026 11:54:03 +0530 Subject: [PATCH 07/35] Remove unused variable --- .../OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs index b5964a04d2..41771ad363 100644 --- a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs @@ -22,7 +22,6 @@ public class CLRtoJsonValueTypeUnitTests private const string SQLDBTYPE_RESOLUTION_ERROR = "failed to resolve to SqlDbType."; private const string SQLDBTYPE_UNEXPECTED_RESOLUTION_ERROR = "should have resolved to a SqlDbType."; private const string JSONDATATYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated JsonDataType."; - private const string DBTYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated DbType."; /// /// Validates that: From 5e3175bbf00f6937dcc00e5ea10ca05543b85cb3 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 18 Jun 2026 13:20:32 +0530 Subject: [PATCH 08/35] Fix SqlTestHelper for Microsoft.Data.SqlClient 6.x SqlError constructor change SqlClient 6.x changed the internal SqlError 9-parameter constructor: the win32ErrorCode parameter is now Int32 (was UInt32 in 5.x), and there are now multiple 9-parameter overloads. Select the constructor by exact parameter types and pass (int)0 for win32ErrorCode so CreateSqlException works across SqlClient versions. --- src/Service.Tests/SqlTests/SqlTestHelper.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Service.Tests/SqlTests/SqlTestHelper.cs b/src/Service.Tests/SqlTests/SqlTestHelper.cs index cf65c9a9f5..32041c8fc8 100644 --- a/src/Service.Tests/SqlTests/SqlTestHelper.cs +++ b/src/Service.Tests/SqlTests/SqlTestHelper.cs @@ -552,15 +552,24 @@ public static SqlException CreateSqlException(int number, string message = "") // Get all the available non-public,non-static constructors for SqlError class. constructorsArray = typeof(SqlError).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance); - // At this point the ConstructorInfo[] for SqlError has 2 entries: One constructor with 8 parameters, - // and one with 9 parameters. We can choose either of them to create an object of SqlError type. - ConstructorInfo nineParamsConstructor = constructorsArray.FirstOrDefault(c => c.GetParameters().Length == 9); + // Microsoft.Data.SqlClient exposes multiple internal SqlError constructors (including more than + // one with nine parameters). Select the specific overload by matching its exact parameter types + // so we are resilient to constructor ordering and signature changes across SqlClient versions: + // (int infoNumber, byte errorState, byte errorClass, string server, string errorMessage, + // string procedure, int lineNumber, int win32ErrorCode, Exception exception) + Type[] expectedParameterTypes = new[] + { + typeof(int), typeof(byte), typeof(byte), typeof(string), typeof(string), + typeof(string), typeof(int), typeof(int), typeof(Exception) + }; + ConstructorInfo sqlErrorConstructor = constructorsArray.FirstOrDefault(c => + c.GetParameters().Select(p => p.ParameterType).SequenceEqual(expectedParameterTypes)); // Create SqlError object. // For details on what the parameters stand for please refer: // https://learn.microsoft.com/en-us/dotnet/api/system.data.sqlclient.sqlerror.number#examples - SqlError sqlError = (nineParamsConstructor - .Invoke(new object[] { number, (byte)0, (byte)0, "", "", "", (int)0, (uint)0, null }) as SqlError)!; + SqlError sqlError = (sqlErrorConstructor + .Invoke(new object[] { number, (byte)0, (byte)0, "", "", "", (int)0, (int)0, null }) as SqlError)!; errorList.Add(sqlError); // Create SqlException object From f840076e2ae930da125fb2ae5a2c8901de5783ac Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Thu, 18 Jun 2026 14:07:43 +0530 Subject: [PATCH 09/35] Treat empty ASPNETCORE_URLS as valid in ValidateAspNetCoreUrls Starting with .NET 10, setting an environment variable to an empty string preserves it as an empty value instead of deleting it (which previously surfaced here as null). This caused ValidateAspNetCoreUrls to treat an empty ASPNETCORE_URLS as invalid and exit with error, a regression from .NET 8 where an empty value was equivalent to unset. Explicitly treat an empty value as valid (Kestrel falls back to default URLs), preserving the prior behavior. Whitespace-only values remain invalid. --- src/Service/Program.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Service/Program.cs b/src/Service/Program.cs index e83dde30c7..7f315f0655 100644 --- a/src/Service/Program.cs +++ b/src/Service/Program.cs @@ -531,6 +531,15 @@ internal static bool ValidateAspNetCoreUrls() return true; // If the environment variable is missing, then it cannot be invalid. } + if (string.IsNullOrEmpty(urls)) + { + // An empty value is equivalent to the variable being unset: Kestrel falls back to its + // default URLs, so this is valid. Note that starting with .NET 10, setting an environment + // variable to an empty string preserves it as "" instead of deleting it (which previously + // surfaced here as null), so this case must be handled explicitly. + return true; + } + if (string.IsNullOrWhiteSpace(urls)) { return false; From ed820fb4f6ae39b5ec203829bce6febd36ef0393 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Fri, 19 Jun 2026 13:32:52 +0530 Subject: [PATCH 10/35] Revert unrelated changes to match main (PR review feedback) These three files were modified by an earlier merge in a way unrelated to the .NET 10 / SqlClient 6.x upgrade, reverting recent main formatting/cleanup: - CLRtoJsonValueTypeUnitTests.cs: restore the DBTYPE_RESOLUTION_ERROR constant that was inadvertently removed. - SqlUpdateQueryStructure.cs: restore main's formatting of the update-fields block. - MutationEngineFactory.cs: restore main's version. Restoring them to match main keeps this PR scoped to the upgrade and addresses reviewer feedback (constant removal and formatting). --- .../Factories/MutationEngineFactory.cs | 2 +- .../SqlUpdateQueryStructure.cs | 30 +++++++++---------- .../CLRtoJsonValueTypeUnitTests.cs | 1 + 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Core/Resolvers/Factories/MutationEngineFactory.cs b/src/Core/Resolvers/Factories/MutationEngineFactory.cs index 08a5fea2e3..9b4df9f8bf 100644 --- a/src/Core/Resolvers/Factories/MutationEngineFactory.cs +++ b/src/Core/Resolvers/Factories/MutationEngineFactory.cs @@ -107,7 +107,7 @@ public IMutationEngine GetMutationEngine(DatabaseType databaseType) $"{nameof(databaseType)}:{databaseType} could not be found within the config", HttpStatusCode.BadRequest, DataApiBuilderException.SubStatusCodes.DataSourceNotFound); - } + }; return mutationEngine; } diff --git a/src/Core/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs index 2fffa19ef5..ecbbf3fc5c 100644 --- a/src/Core/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs @@ -122,26 +122,26 @@ public SqlUpdateStructure( Predicates.Add(CreatePredicateForParam(new KeyValuePair(pkBackingColumn, param.Value))); } else // Unpack the input argument type as columns to update - if (param.Key == UpdateAndPatchMutationBuilder.INPUT_ARGUMENT_NAME) + if (param.Key == UpdateAndPatchMutationBuilder.INPUT_ARGUMENT_NAME) + { + IDictionary updateFields = + GQLMutArgumentToDictParams(context, UpdateAndPatchMutationBuilder.INPUT_ARGUMENT_NAME, mutationParams); + + foreach (KeyValuePair field in updateFields) { - IDictionary updateFields = - GQLMutArgumentToDictParams(context, UpdateAndPatchMutationBuilder.INPUT_ARGUMENT_NAME, mutationParams); + string fieldBackingColumn = field.Key; + if (sqlMetadataProvider.TryGetBackingColumn(entityName, field.Key, out string? resolvedBackingColumn) + && !string.IsNullOrWhiteSpace(resolvedBackingColumn)) + { + fieldBackingColumn = resolvedBackingColumn; + } - foreach (KeyValuePair field in updateFields) + if (columns.Contains(fieldBackingColumn)) { - string fieldBackingColumn = field.Key; - if (sqlMetadataProvider.TryGetBackingColumn(entityName, field.Key, out string? resolvedBackingColumn) - && !string.IsNullOrWhiteSpace(resolvedBackingColumn)) - { - fieldBackingColumn = resolvedBackingColumn; - } - - if (columns.Contains(fieldBackingColumn)) - { - UpdateOperations.Add(CreatePredicateForParam(new KeyValuePair(key: fieldBackingColumn, field.Value))); - } + UpdateOperations.Add(CreatePredicateForParam(new KeyValuePair(key: fieldBackingColumn, field.Value))); } } + } } if (UpdateOperations.Count == 0) diff --git a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs index 41771ad363..b5964a04d2 100644 --- a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs @@ -22,6 +22,7 @@ public class CLRtoJsonValueTypeUnitTests private const string SQLDBTYPE_RESOLUTION_ERROR = "failed to resolve to SqlDbType."; private const string SQLDBTYPE_UNEXPECTED_RESOLUTION_ERROR = "should have resolved to a SqlDbType."; private const string JSONDATATYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated JsonDataType."; + private const string DBTYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated DbType."; /// /// Validates that: From 3735ff46167db8562c3908f427427deab7adee50 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Fri, 19 Jun 2026 14:05:52 +0530 Subject: [PATCH 11/35] Fix .NET 10 formatter whitespace/style on Core mutation files --- .../Factories/MutationEngineFactory.cs | 2 +- .../SqlUpdateQueryStructure.cs | 30 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Core/Resolvers/Factories/MutationEngineFactory.cs b/src/Core/Resolvers/Factories/MutationEngineFactory.cs index 9b4df9f8bf..08a5fea2e3 100644 --- a/src/Core/Resolvers/Factories/MutationEngineFactory.cs +++ b/src/Core/Resolvers/Factories/MutationEngineFactory.cs @@ -107,7 +107,7 @@ public IMutationEngine GetMutationEngine(DatabaseType databaseType) $"{nameof(databaseType)}:{databaseType} could not be found within the config", HttpStatusCode.BadRequest, DataApiBuilderException.SubStatusCodes.DataSourceNotFound); - }; + } return mutationEngine; } diff --git a/src/Core/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs index ecbbf3fc5c..2fffa19ef5 100644 --- a/src/Core/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs @@ -122,26 +122,26 @@ public SqlUpdateStructure( Predicates.Add(CreatePredicateForParam(new KeyValuePair(pkBackingColumn, param.Value))); } else // Unpack the input argument type as columns to update - if (param.Key == UpdateAndPatchMutationBuilder.INPUT_ARGUMENT_NAME) - { - IDictionary updateFields = - GQLMutArgumentToDictParams(context, UpdateAndPatchMutationBuilder.INPUT_ARGUMENT_NAME, mutationParams); - - foreach (KeyValuePair field in updateFields) + if (param.Key == UpdateAndPatchMutationBuilder.INPUT_ARGUMENT_NAME) { - string fieldBackingColumn = field.Key; - if (sqlMetadataProvider.TryGetBackingColumn(entityName, field.Key, out string? resolvedBackingColumn) - && !string.IsNullOrWhiteSpace(resolvedBackingColumn)) - { - fieldBackingColumn = resolvedBackingColumn; - } + IDictionary updateFields = + GQLMutArgumentToDictParams(context, UpdateAndPatchMutationBuilder.INPUT_ARGUMENT_NAME, mutationParams); - if (columns.Contains(fieldBackingColumn)) + foreach (KeyValuePair field in updateFields) { - UpdateOperations.Add(CreatePredicateForParam(new KeyValuePair(key: fieldBackingColumn, field.Value))); + string fieldBackingColumn = field.Key; + if (sqlMetadataProvider.TryGetBackingColumn(entityName, field.Key, out string? resolvedBackingColumn) + && !string.IsNullOrWhiteSpace(resolvedBackingColumn)) + { + fieldBackingColumn = resolvedBackingColumn; + } + + if (columns.Contains(fieldBackingColumn)) + { + UpdateOperations.Add(CreatePredicateForParam(new KeyValuePair(key: fieldBackingColumn, field.Value))); + } } } - } } if (UpdateOperations.Count == 0) From a808e13252e0201566e8e6557a1b0564ecc56397 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Fri, 19 Jun 2026 14:11:39 +0530 Subject: [PATCH 12/35] remove unused variable due to strict check by .Net 10 --- .../OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs index b5964a04d2..41771ad363 100644 --- a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs @@ -22,7 +22,6 @@ public class CLRtoJsonValueTypeUnitTests private const string SQLDBTYPE_RESOLUTION_ERROR = "failed to resolve to SqlDbType."; private const string SQLDBTYPE_UNEXPECTED_RESOLUTION_ERROR = "should have resolved to a SqlDbType."; private const string JSONDATATYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated JsonDataType."; - private const string DBTYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated DbType."; /// /// Validates that: From fbb8159b7e7f0d98aa2f9a71cbf80131704e7343 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Mon, 22 Jun 2026 15:08:59 +0530 Subject: [PATCH 13/35] try adding DBTYPE_RESOLUTION_ERROR back --- .../OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs index 41771ad363..b5964a04d2 100644 --- a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs @@ -22,6 +22,7 @@ public class CLRtoJsonValueTypeUnitTests private const string SQLDBTYPE_RESOLUTION_ERROR = "failed to resolve to SqlDbType."; private const string SQLDBTYPE_UNEXPECTED_RESOLUTION_ERROR = "should have resolved to a SqlDbType."; private const string JSONDATATYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated JsonDataType."; + private const string DBTYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated DbType."; /// /// Validates that: From f3f5583c264632a7506dcacf1360c433c57f1fab Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Mon, 22 Jun 2026 15:40:58 +0530 Subject: [PATCH 14/35] Removing unused variable which fails in pipeline --- .../OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs index b5964a04d2..41771ad363 100644 --- a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs @@ -22,7 +22,6 @@ public class CLRtoJsonValueTypeUnitTests private const string SQLDBTYPE_RESOLUTION_ERROR = "failed to resolve to SqlDbType."; private const string SQLDBTYPE_UNEXPECTED_RESOLUTION_ERROR = "should have resolved to a SqlDbType."; private const string JSONDATATYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated JsonDataType."; - private const string DBTYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated DbType."; /// /// Validates that: From bd87fa05e442bd8e4894b7cfbd8d1894a5edd93b Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Mon, 22 Jun 2026 15:49:21 +0530 Subject: [PATCH 15/35] Suppress unused variable warning --- .../OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs index 41771ad363..c54fe6db3d 100644 --- a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs @@ -23,6 +23,11 @@ public class CLRtoJsonValueTypeUnitTests private const string SQLDBTYPE_UNEXPECTED_RESOLUTION_ERROR = "should have resolved to a SqlDbType."; private const string JSONDATATYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated JsonDataType."; + // Placeholder for future DbType resolution assertions (mirrors JSONDATATYPE_RESOLUTION_ERROR). + // Reserved for when DbType mapping is supported; intentionally retained though not yet referenced. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Reserved for future DbType support; currently unused, suppressing IDE0051.")] + private const string DBTYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated DbType."; + /// /// Validates that: /// 1. String representations of SqlDbType provided by SQL Server/Azure SQL DB resolve to a SqlDbType enum From d19a55f1d8fba92d54f5407d63a8026419488baa Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Mon, 8 Jun 2026 13:52:50 -0700 Subject: [PATCH 16/35] Update SqlClient to 6.1.5 --- src/Config/Azure.DataApiBuilder.Config.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/Azure.DataApiBuilder.Config.csproj b/src/Config/Azure.DataApiBuilder.Config.csproj index 0d1655df76..0c81d6eba4 100644 --- a/src/Config/Azure.DataApiBuilder.Config.csproj +++ b/src/Config/Azure.DataApiBuilder.Config.csproj @@ -1,4 +1,4 @@ - + net10.0 From 3cc1c0b4cbf4af82c3b210350422457e9d4ed4d6 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Tue, 9 Jun 2026 16:16:35 -0700 Subject: [PATCH 17/35] First additions to vector data type --- src/Core/Models/SqlTypeConstants.cs | 3 ++- src/Core/Resolvers/MsSqlQueryBuilder.cs | 6 ++---- src/Core/Services/TypeHelper.cs | 7 +++++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Core/Models/SqlTypeConstants.cs b/src/Core/Models/SqlTypeConstants.cs index 6315e88936..d29c83f0c7 100644 --- a/src/Core/Models/SqlTypeConstants.cs +++ b/src/Core/Models/SqlTypeConstants.cs @@ -48,6 +48,7 @@ public static class SqlTypeConstants { "datetime2", true }, // SqlDbType.DateTime2 { "datetimeoffset", true }, // SqlDbType.DateTimeOffset { "", false }, // SqlDbType.Udt and SqlDbType.Structured provided by SQL as empty strings (unsupported) - { "numeric", true} // Not present in SqlDbType, however can be returned by sql functions like LAG and should map to decimal. + { "numeric", true}, // Not present in SqlDbType, however can be returned by sql functions like LAG and should map to decimal. + { "vector", true } // SqlDbTypeExtensions.Vector }; } diff --git a/src/Core/Resolvers/MsSqlQueryBuilder.cs b/src/Core/Resolvers/MsSqlQueryBuilder.cs index 7adedc64d4..7769dcf94b 100644 --- a/src/Core/Resolvers/MsSqlQueryBuilder.cs +++ b/src/Core/Resolvers/MsSqlQueryBuilder.cs @@ -666,8 +666,7 @@ AND ty.name IN N'hierarchyid', N'sql_variant', N'xml', - N'rowversion', - N'vector' + N'rowversion' ) ) THEN 1 ELSE 0 @@ -712,8 +711,7 @@ AND ty.name IN N'hierarchyid', N'sql_variant', N'xml', - N'rowversion', - N'vector' + N'rowversion' ) ) ) diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index 0a95744abb..576bcd7c29 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -8,6 +8,7 @@ using Azure.DataApiBuilder.Core.Services.OpenAPI; using Azure.DataApiBuilder.Service.Exceptions; using HotChocolate.Language; +using Microsoft.Data; using Microsoft.OData.Edm; namespace Azure.DataApiBuilder.Core.Services @@ -77,7 +78,8 @@ public static class TypeHelper [typeof(TimeOnly)] = JsonDataType.String, [typeof(object)] = JsonDataType.Object, [typeof(DateTime)] = JsonDataType.String, - [typeof(DateTimeOffset)] = JsonDataType.String + [typeof(DateTimeOffset)] = JsonDataType.String, + [typeof(float[])] = JsonDataType.Array }; /// @@ -111,7 +113,8 @@ public static class TypeHelper [SqlDbType.TinyInt] = typeof(byte), [SqlDbType.UniqueIdentifier] = typeof(Guid), [SqlDbType.VarBinary] = typeof(byte[]), - [SqlDbType.VarChar] = typeof(string) + [SqlDbType.VarChar] = typeof(string), + [SqlDbTypeExtensions.Vector] = typeof(float[]) }; private static Dictionary _sqlDbDateTimeTypeToDbType = new() From 2fea5cedf980cdae8a74930a03f86186de8d8d26 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Wed, 10 Jun 2026 09:45:49 -0700 Subject: [PATCH 18/35] New changes --- src/Core/Services/TypeHelper.cs | 1 + src/Service.GraphQLBuilder/Sql/SchemaConverter.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index 576bcd7c29..dc8c59c4a2 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -162,6 +162,7 @@ public static EdmPrimitiveTypeKind GetEdmPrimitiveTypeFromSystemType(Type column "Date" => EdmPrimitiveTypeKind.Date, "TimeOnly" => EdmPrimitiveTypeKind.TimeOfDay, "TimeSpan" => EdmPrimitiveTypeKind.TimeOfDay, + "SqlVector`1" => EdmPrimitiveTypeKind.String, _ => throw new ArgumentException($"Column type" + $" {columnSystemType.Name} not yet supported.") }; diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 622376dc13..94c7af9bfc 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -577,6 +577,7 @@ public static string GetGraphQLTypeFromSystemType(Type type) "Byte[]" => BYTEARRAY_TYPE, "TimeOnly" => LOCALTIME_TYPE, "TimeSpan" => LOCALTIME_TYPE, + "SqlVector`1" => STRING_TYPE, _ => throw new DataApiBuilderException( message: $"Column type {type} not handled by case. Please add a case resolving {type} to the appropriate GraphQL type", statusCode: HttpStatusCode.InternalServerError, From 1e4937617d6b57ec80d8ad6762782f0147978358 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Wed, 17 Jun 2026 14:19:41 -0700 Subject: [PATCH 19/35] Make vector array type --- src/Core/Services/MetadataProviders/SqlMetadataProvider.cs | 3 ++- src/Core/Services/TypeHelper.cs | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 7d9708bdb0..d326eceb99 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -19,6 +19,7 @@ using Azure.DataApiBuilder.Core.Resolvers.Factories; using Azure.DataApiBuilder.Service.Exceptions; using HotChocolate.Language; +using Microsoft.Data.SqlTypes; using Microsoft.Extensions.Logging; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; using KeyNotFoundException = System.Collections.Generic.KeyNotFoundException; @@ -1488,7 +1489,7 @@ private async Task PopulateSourceDefinitionAsync( // Detect array types: concrete array types (e.g., int[]) have IsArray=true, // while Npgsql reports abstract System.Array for PostgreSQL array columns. // byte[] is excluded since it maps to the bytea/ByteArray scalar type. - bool isArrayType = (systemType.IsArray && systemType != typeof(byte[])) || systemType == typeof(Array); + bool isArrayType = (systemType.IsArray && systemType != typeof(byte[])) || systemType == typeof(Array) || systemType == typeof(SqlVector); ColumnDefinition column = new() { diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index dc8c59c4a2..36b5552040 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -9,6 +9,7 @@ using Azure.DataApiBuilder.Service.Exceptions; using HotChocolate.Language; using Microsoft.Data; +using Microsoft.Data.SqlTypes; using Microsoft.OData.Edm; namespace Azure.DataApiBuilder.Core.Services @@ -47,7 +48,8 @@ public static class TypeHelper [typeof(byte[])] = DbType.Binary, [typeof(TimeOnly)] = DbType.Time, [typeof(TimeSpan)] = DbType.Time, - [typeof(object)] = DbType.Object + [typeof(object)] = DbType.Object, + [typeof(SqlVector)] = DbType.Object }; /// @@ -79,7 +81,7 @@ public static class TypeHelper [typeof(object)] = JsonDataType.Object, [typeof(DateTime)] = JsonDataType.String, [typeof(DateTimeOffset)] = JsonDataType.String, - [typeof(float[])] = JsonDataType.Array + [typeof(SqlVector)] = JsonDataType.String }; /// From 98e6434cb0c89daa5a875ce6ae66e544f8851d2f Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Fri, 19 Jun 2026 13:17:48 -0700 Subject: [PATCH 20/35] Read --- src/Core/Resolvers/BaseSqlQueryBuilder.cs | 12 ++++++++++++ src/Core/Resolvers/MsSqlQueryBuilder.cs | 2 +- src/Core/Services/TypeHelper.cs | 4 ++-- src/Service.GraphQLBuilder/Sql/SchemaConverter.cs | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Core/Resolvers/BaseSqlQueryBuilder.cs b/src/Core/Resolvers/BaseSqlQueryBuilder.cs index a509e9d842..7ed2813749 100644 --- a/src/Core/Resolvers/BaseSqlQueryBuilder.cs +++ b/src/Core/Resolvers/BaseSqlQueryBuilder.cs @@ -8,6 +8,7 @@ using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes; +using Microsoft.Data.SqlTypes; using static Azure.DataApiBuilder.Service.Exceptions.DataApiBuilderException; namespace Azure.DataApiBuilder.Core.Resolvers @@ -212,6 +213,17 @@ protected virtual string Build(OrderByColumn column, bool printDirection = true) /// protected string Build(LabelledColumn column) { + //if (GetColumnSystemType()) + return Build(column as Column) + " AS " + QuoteIdentifier(column.Label); + } + + protected string Build(LabelledColumn column, Type columnType) + { + if (columnType == typeof(SqlVector)) + { + return "JSON_QUERY(CAST(CAST(" + Build(column as Column) + " AS JSON) AS NVARCHAR(max))) AS " + QuoteIdentifier(column.Label); + } + return Build(column as Column) + " AS " + QuoteIdentifier(column.Label); } diff --git a/src/Core/Resolvers/MsSqlQueryBuilder.cs b/src/Core/Resolvers/MsSqlQueryBuilder.cs index 7769dcf94b..4cb08d4070 100644 --- a/src/Core/Resolvers/MsSqlQueryBuilder.cs +++ b/src/Core/Resolvers/MsSqlQueryBuilder.cs @@ -456,7 +456,7 @@ private string WrappedColumns(SqlQueryStructure structure) structure.Columns.Select( c => structure.IsSubqueryColumn(c) ? WrapSubqueryColumn(c, structure.JoinQueries[c.TableAlias!]) + $" AS {QuoteIdentifier(c.Label)}" : - Build(c) + Build(c, structure.GetColumnSystemType(c.ColumnName)) )); } diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index 36b5552040..2a07f55a4f 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -81,7 +81,7 @@ public static class TypeHelper [typeof(object)] = JsonDataType.Object, [typeof(DateTime)] = JsonDataType.String, [typeof(DateTimeOffset)] = JsonDataType.String, - [typeof(SqlVector)] = JsonDataType.String + [typeof(SqlVector)] = JsonDataType.Number }; /// @@ -164,7 +164,7 @@ public static EdmPrimitiveTypeKind GetEdmPrimitiveTypeFromSystemType(Type column "Date" => EdmPrimitiveTypeKind.Date, "TimeOnly" => EdmPrimitiveTypeKind.TimeOfDay, "TimeSpan" => EdmPrimitiveTypeKind.TimeOfDay, - "SqlVector`1" => EdmPrimitiveTypeKind.String, + "SqlVector`1" => EdmPrimitiveTypeKind.Single, _ => throw new ArgumentException($"Column type" + $" {columnSystemType.Name} not yet supported.") }; diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 94c7af9bfc..6f7f9bc69f 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -577,7 +577,7 @@ public static string GetGraphQLTypeFromSystemType(Type type) "Byte[]" => BYTEARRAY_TYPE, "TimeOnly" => LOCALTIME_TYPE, "TimeSpan" => LOCALTIME_TYPE, - "SqlVector`1" => STRING_TYPE, + "SqlVector`1" => SINGLE_TYPE, _ => throw new DataApiBuilderException( message: $"Column type {type} not handled by case. Please add a case resolving {type} to the appropriate GraphQL type", statusCode: HttpStatusCode.InternalServerError, From 0137653600f265ad373b9a513e2b8758ecfb3f3c Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Mon, 22 Jun 2026 09:37:08 -0700 Subject: [PATCH 21/35] More changes --- src/Core/Resolvers/BaseSqlQueryBuilder.cs | 2 +- .../Services/MetadataProviders/MsSqlMetadataProvider.cs | 8 ++++++++ src/Core/Services/TypeHelper.cs | 5 ++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Core/Resolvers/BaseSqlQueryBuilder.cs b/src/Core/Resolvers/BaseSqlQueryBuilder.cs index 7ed2813749..5b6b878ea8 100644 --- a/src/Core/Resolvers/BaseSqlQueryBuilder.cs +++ b/src/Core/Resolvers/BaseSqlQueryBuilder.cs @@ -219,7 +219,7 @@ protected string Build(LabelledColumn column) protected string Build(LabelledColumn column, Type columnType) { - if (columnType == typeof(SqlVector)) + if (columnType == typeof(Single[])) { return "JSON_QUERY(CAST(CAST(" + Build(column as Column) + " AS JSON) AS NVARCHAR(max))) AS " + QuoteIdentifier(column.Label); } diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index cdb54a2ac2..4de7e40e05 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -15,6 +15,7 @@ using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder; using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlTypes; using Microsoft.Extensions.Logging; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; @@ -132,6 +133,13 @@ protected override void PopulateColumnDefinitionWithHasDefaultAndDbType( columnDefinition.DbType = dbType; } } + + if (columnDefinition.SystemType == typeof(SqlVector)) + { + columnDefinition.IsArrayType = true; + columnDefinition.ElementSystemType = typeof(Single); + columnDefinition.SystemType = columnDefinition.ElementSystemType.MakeArrayType(); + } } } } diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index 2a07f55a4f..22fe1cf035 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -49,7 +49,7 @@ public static class TypeHelper [typeof(TimeOnly)] = DbType.Time, [typeof(TimeSpan)] = DbType.Time, [typeof(object)] = DbType.Object, - [typeof(SqlVector)] = DbType.Object + [typeof(SqlVector)] = DbType.Single }; /// @@ -116,7 +116,7 @@ public static class TypeHelper [SqlDbType.UniqueIdentifier] = typeof(Guid), [SqlDbType.VarBinary] = typeof(byte[]), [SqlDbType.VarChar] = typeof(string), - [SqlDbTypeExtensions.Vector] = typeof(float[]) + [SqlDbTypeExtensions.Vector] = typeof(float) }; private static Dictionary _sqlDbDateTimeTypeToDbType = new() @@ -164,7 +164,6 @@ public static EdmPrimitiveTypeKind GetEdmPrimitiveTypeFromSystemType(Type column "Date" => EdmPrimitiveTypeKind.Date, "TimeOnly" => EdmPrimitiveTypeKind.TimeOfDay, "TimeSpan" => EdmPrimitiveTypeKind.TimeOfDay, - "SqlVector`1" => EdmPrimitiveTypeKind.Single, _ => throw new ArgumentException($"Column type" + $" {columnSystemType.Name} not yet supported.") }; From e7d72c83e1e57898889a688aae6b0addc7593097 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Mon, 22 Jun 2026 13:49:07 -0700 Subject: [PATCH 22/35] Add write abilities --- .../Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs | 2 ++ src/Core/Services/MetadataProviders/SqlMetadataProvider.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index 99a5b1e72c..a95d91f630 100644 --- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -4,6 +4,7 @@ using System.Data; using System.Globalization; using System.Net; +using System.Text.Json; using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Config.ObjectModel; @@ -452,6 +453,7 @@ protected static object ParseParamAsSystemType(string param, Type systemType) "Guid" => Guid.Parse(param), "TimeOnly" => TimeOnly.Parse(param), "TimeSpan" => TimeOnly.Parse(param), + "Single[]" => JsonSerializer.Deserialize(param) ?? Array.Empty(), _ => throw new NotSupportedException($"{systemType.Name} is not supported") }; } diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index d326eceb99..3ecafb0c05 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -1501,7 +1501,7 @@ private async Task PopulateSourceDefinitionAsync( // An auto-increment column is also considered as a read-only column. For other types of read-only columns, // the flag is populated later via PopulateColumnDefinitionsWithReadOnlyFlag() method. // Array columns are also treated as read-only until write support for array types is implemented. - IsReadOnly = (bool)columnInfoFromAdapter["IsAutoIncrement"] || isArrayType + IsReadOnly = (bool)columnInfoFromAdapter["IsAutoIncrement"] || (isArrayType && systemType != typeof(SqlVector)) }; // Tests may try to add the same column simultaneously From 36ff6ebb0cef0cd93f84756b5f37d1b0c2fdb03a Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Mon, 22 Jun 2026 15:14:12 -0700 Subject: [PATCH 23/35] Add writing capabilities --- src/Core/Models/SqlTypeConstants.cs | 2 +- src/Core/Resolvers/MsSqlQueryExecutor.cs | 14 ++++++++++++++ .../MetadataProviders/MsSqlMetadataProvider.cs | 16 +++++++++------- src/Core/Services/TypeHelper.cs | 2 +- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/Core/Models/SqlTypeConstants.cs b/src/Core/Models/SqlTypeConstants.cs index d29c83f0c7..c2d970e439 100644 --- a/src/Core/Models/SqlTypeConstants.cs +++ b/src/Core/Models/SqlTypeConstants.cs @@ -49,6 +49,6 @@ public static class SqlTypeConstants { "datetimeoffset", true }, // SqlDbType.DateTimeOffset { "", false }, // SqlDbType.Udt and SqlDbType.Structured provided by SQL as empty strings (unsupported) { "numeric", true}, // Not present in SqlDbType, however can be returned by sql functions like LAG and should map to decimal. - { "vector", true } // SqlDbTypeExtensions.Vector + { "vector", true } // SqlDbType.Vector }; } diff --git a/src/Core/Resolvers/MsSqlQueryExecutor.cs b/src/Core/Resolvers/MsSqlQueryExecutor.cs index 2b5807c8fd..b4f84757b3 100644 --- a/src/Core/Resolvers/MsSqlQueryExecutor.cs +++ b/src/Core/Resolvers/MsSqlQueryExecutor.cs @@ -18,6 +18,7 @@ using Azure.Identity; using Microsoft.AspNetCore.Http; using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlTypes; using Microsoft.Extensions.Logging; namespace Azure.DataApiBuilder.Core.Resolvers @@ -695,6 +696,19 @@ public override SqlCommand PrepareDbCommand( parameter.Size = parameterEntry.Value.Length.Value; } + // if sqldbtype is vector then set the value as an SqlVector object + if (parameter.SqlDbType is SqlDbType.Vector) + { + List values = new(); + foreach (float val in (Array)parameter.Value) + { + values.Add(val); + } + + SqlVector value = new(values.ToArray()); + parameter.Value = value; + } + cmd.Parameters.Add(parameter); } } diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index 4de7e40e05..0e9b1407ed 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -115,6 +115,15 @@ protected override void PopulateColumnDefinitionWithHasDefaultAndDbType( columnDefinition.DbType = TypeHelper.GetDbTypeFromSystemType(columnDefinition.SystemType); string sqlDbTypeName = (string)columnInfo["DATA_TYPE"]; + + if (columnDefinition.SystemType == typeof(SqlVector)) + { + sqlDbTypeName = "vector"; // Currently the "DATA_TYPE" column returns "varbinary" for vector type columns. This is a known issue https://learn.microsoft.com/en-us/sql/t-sql/data-types/vector-data-type?view=sql-server-ver17&tabs=csharp#known-issues + columnDefinition.IsArrayType = true; + columnDefinition.ElementSystemType = typeof(Single); + columnDefinition.SystemType = columnDefinition.ElementSystemType.MakeArrayType(); + } + if (Enum.TryParse(sqlDbTypeName, ignoreCase: true, out SqlDbType sqlDbType)) { // The DbType enum in .NET does not distinguish between VarChar and NVarChar. Both are mapped to DbType.String. @@ -133,13 +142,6 @@ protected override void PopulateColumnDefinitionWithHasDefaultAndDbType( columnDefinition.DbType = dbType; } } - - if (columnDefinition.SystemType == typeof(SqlVector)) - { - columnDefinition.IsArrayType = true; - columnDefinition.ElementSystemType = typeof(Single); - columnDefinition.SystemType = columnDefinition.ElementSystemType.MakeArrayType(); - } } } } diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index 22fe1cf035..5862b9030e 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -116,7 +116,7 @@ public static class TypeHelper [SqlDbType.UniqueIdentifier] = typeof(Guid), [SqlDbType.VarBinary] = typeof(byte[]), [SqlDbType.VarChar] = typeof(string), - [SqlDbTypeExtensions.Vector] = typeof(float) + [SqlDbType.Vector] = typeof(float) }; private static Dictionary _sqlDbDateTimeTypeToDbType = new() From 8a0edd29f206ac405923410795f2d64987656ded Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Wed, 24 Jun 2026 14:07:39 -0700 Subject: [PATCH 24/35] Add tests --- config-generators/mssql-commands.txt | 2 + src/Core/Resolvers/BaseSqlQueryBuilder.cs | 11 - src/Core/Resolvers/MsSqlQueryBuilder.cs | 2 +- .../BaseSqlQueryStructure.cs | 21 +- src/Core/Services/TypeHelper.cs | 1 - src/Service.Tests/DatabaseSchema-MsSql.sql | 24 ++ .../RestApiTests/MsSqlRestVectorTypesTests.cs | 286 ++++++++++++++++++ src/Service.Tests/dab-config.MsSql.json | 56 +++- 8 files changed, 387 insertions(+), 16 deletions(-) create mode 100644 src/Service.Tests/SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs diff --git a/config-generators/mssql-commands.txt b/config-generators/mssql-commands.txt index e3f2541983..99e138b846 100644 --- a/config-generators/mssql-commands.txt +++ b/config-generators/mssql-commands.txt @@ -16,6 +16,8 @@ add Broker --config "dab-config.MsSql.json" --source brokers --permissions "anon add WebsiteUser --config "dab-config.MsSql.json" --source website_users --permissions "anonymous:create,read,delete,update" add WebsiteUser_MM --config "dab-config.MsSql.json" --source website_users_mm --graphql "websiteuser_mm:websiteusers_mm" --permissions "anonymous:*" add SupportedType --config "dab-config.MsSql.json" --source type_table --permissions "anonymous:create,read,delete,update" +add VectorType --config "dab-config.MsSql.json" --source vector_type_table --rest true --graphql false --permissions "anonymous:create,read,delete,update" +update VectorType --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" update stocks_price --config "dab-config.MsSql.json" --permissions "anonymous:read" update stocks_price --config "dab-config.MsSql.json" --permissions "TestNestedFilterFieldIsNull_ColumnForbidden:read" --fields.exclude "price" diff --git a/src/Core/Resolvers/BaseSqlQueryBuilder.cs b/src/Core/Resolvers/BaseSqlQueryBuilder.cs index 5b6b878ea8..2aaff44ff0 100644 --- a/src/Core/Resolvers/BaseSqlQueryBuilder.cs +++ b/src/Core/Resolvers/BaseSqlQueryBuilder.cs @@ -8,7 +8,6 @@ using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes; -using Microsoft.Data.SqlTypes; using static Azure.DataApiBuilder.Service.Exceptions.DataApiBuilderException; namespace Azure.DataApiBuilder.Core.Resolvers @@ -217,16 +216,6 @@ protected string Build(LabelledColumn column) return Build(column as Column) + " AS " + QuoteIdentifier(column.Label); } - protected string Build(LabelledColumn column, Type columnType) - { - if (columnType == typeof(Single[])) - { - return "JSON_QUERY(CAST(CAST(" + Build(column as Column) + " AS JSON) AS NVARCHAR(max))) AS " + QuoteIdentifier(column.Label); - } - - return Build(column as Column) + " AS " + QuoteIdentifier(column.Label); - } - /// /// Build each column and join by ", " separator /// diff --git a/src/Core/Resolvers/MsSqlQueryBuilder.cs b/src/Core/Resolvers/MsSqlQueryBuilder.cs index 4cb08d4070..7769dcf94b 100644 --- a/src/Core/Resolvers/MsSqlQueryBuilder.cs +++ b/src/Core/Resolvers/MsSqlQueryBuilder.cs @@ -456,7 +456,7 @@ private string WrappedColumns(SqlQueryStructure structure) structure.Columns.Select( c => structure.IsSubqueryColumn(c) ? WrapSubqueryColumn(c, structure.JoinQueries[c.TableAlias!]) + $" AS {QuoteIdentifier(c.Label)}" : - Build(c, structure.GetColumnSystemType(c.ColumnName)) + Build(c) )); } diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index a95d91f630..c790404261 100644 --- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -453,11 +453,30 @@ protected static object ParseParamAsSystemType(string param, Type systemType) "Guid" => Guid.Parse(param), "TimeOnly" => TimeOnly.Parse(param), "TimeSpan" => TimeOnly.Parse(param), - "Single[]" => JsonSerializer.Deserialize(param) ?? Array.Empty(), + "Single[]" => ParseArrayIntoSystemType(param, systemType), _ => throw new NotSupportedException($"{systemType.Name} is not supported") }; } + private static object ParseArrayIntoSystemType(string param, Type systemType) + { + switch (systemType.Name) + { + case "Single[]": + List list = new(); + object[] values = JsonSerializer.Deserialize(param) ?? Array.Empty(); + foreach (object value in values) + { + string stringValue = value.ToString() ?? string.Empty; + list.Add(ParseParamAsSystemType(stringValue, typeof(Single))); + } + + return list.ToArray(); + default: + throw new NotSupportedException($"{systemType.Name} is not supported"); + } + } + /// /// Very similar to GQLArgumentToDictParams but only extracts the argument names from /// the specified field which means that the method does not need a middleware context diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index 5862b9030e..aae651c068 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -8,7 +8,6 @@ using Azure.DataApiBuilder.Core.Services.OpenAPI; using Azure.DataApiBuilder.Service.Exceptions; using HotChocolate.Language; -using Microsoft.Data; using Microsoft.Data.SqlTypes; using Microsoft.OData.Edm; diff --git a/src/Service.Tests/DatabaseSchema-MsSql.sql b/src/Service.Tests/DatabaseSchema-MsSql.sql index 4e87394aee..91bdd69f59 100644 --- a/src/Service.Tests/DatabaseSchema-MsSql.sql +++ b/src/Service.Tests/DatabaseSchema-MsSql.sql @@ -40,6 +40,7 @@ DROP TABLE IF EXISTS stocks; DROP TABLE IF EXISTS comics; DROP TABLE IF EXISTS brokers; DROP TABLE IF EXISTS type_table; +DROP TABLE IF EXISTS vector_type_table; DROP TABLE IF EXISTS trees; DROP TABLE IF EXISTS fungi; DROP TABLE IF EXISTS empty_table; @@ -232,6 +233,12 @@ CREATE TABLE type_table( uuid_types uniqueidentifier DEFAULT newid() ); +CREATE TABLE vector_type_table( + id int IDENTITY(5001, 1) PRIMARY KEY, + vector_data vector(3), + vector_data_max vector(1998) +); + CREATE TABLE trees ( treeId int PRIMARY KEY, species varchar(max), @@ -608,6 +615,23 @@ VALUES INSERT INTO type_table(id, uuid_types) values(10, 'D1D021A8-47B4-4AE4-B718-98E89C41A161'); SET IDENTITY_INSERT type_table OFF +SET IDENTITY_INSERT vector_type_table ON +INSERT INTO vector_type_table(id, vector_data) +VALUES + (1, '[0.5, 0.25, 0.75]'), + (2, '[1.5, -2.5, 3.5]'), + (3, NULL), + (4, '[1.0, 2.0, 3.0]'), + (5, '[4.0, 5.0, 6.0]'), + (6, '[7.0, 8.0, 9.0]'); + +INSERT INTO vector_type_table(id, vector_data_max) +VALUES (7, CAST('[' + ( + SELECT STRING_AGG(CAST(value AS NVARCHAR(MAX)), ',') WITHIN GROUP (ORDER BY value) + FROM GENERATE_SERIES(1, 1998) +) + ']' AS vector(1998))); +SET IDENTITY_INSERT vector_type_table OFF + SET IDENTITY_INSERT sales ON INSERT INTO sales(id, item_name, subtotal, tax) VALUES (1, 'Watch', 249.00, 20.59), (2, 'Montior', 120.50, 11.12); SET IDENTITY_INSERT sales OFF diff --git a/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs b/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs new file mode 100644 index 0000000000..b0ae21f094 --- /dev/null +++ b/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs @@ -0,0 +1,286 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.SqlTests.RestApiTests +{ + /// + /// Tests for SQL Server vector column support via REST endpoints (read and write). + /// Verifies that vector columns are returned as JSON arrays of numbers via REST GET requests + /// and can be inserted/updated/deleted via REST POST/PUT/PATCH/DELETE requests. + /// This mirrors the pattern used for PostgreSQL array types in + /// . + /// NOTE: The vector data type requires SQL Server 2025 / Azure SQL. + /// + [TestClass, TestCategory(TestCategory.MSSQL)] + public class MsSqlRestVectorTypesTests : SqlTestBase + { + private const string VECTOR_TYPE_REST_PATH = "api/VectorType"; + + /// + /// Tolerance used when comparing the single-precision components of a vector, + /// since vector(N) stores 32-bit floats which may not round-trip exactly through JSON. + /// + private const double VECTOR_COMPONENT_DELTA = 0.0001; + + [ClassInitialize] + public static async Task SetupAsync(TestContext context) + { + DatabaseEngine = TestCategory.MSSQL; + await InitializeTestFixture(); + } + + #region Read Tests + + [DataTestMethod] + [DataRow(VECTOR_TYPE_REST_PATH, 7, new[] { 0.5f, 0.25f, 0.75f }, DisplayName = "GET for Vector data type")] + [DataRow($"{VECTOR_TYPE_REST_PATH}/id/2", 1, new[] { 1.5f, -2.5f, 3.5f }, DisplayName = "GET for Vector data type by primary key")] + [DataRow($"{VECTOR_TYPE_REST_PATH}/id/3", 1, null, DisplayName = "GET for Vector data type with null vector")] + public async Task GetVectorTypeList(string vectorRestPath, int expectedItems, float[] expectedValues) + { + HttpResponseMessage response = await HttpClient.GetAsync(vectorRestPath); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + + string body = await response.Content.ReadAsStringAsync(); + JsonElement root = JsonDocument.Parse(body).RootElement; + JsonElement items = root.GetProperty("value"); + + Assert.AreEqual(expectedItems, items.GetArrayLength(), $"Expected {expectedItems} items, got {items.GetArrayLength()}"); + + // Records are ordered by the primary key ascending, so the first record is id = 1. + JsonElement first = items[0]; + AssertVectorEquals(first.GetProperty("vector_data"), expectedValues); + } + + /// + /// GET /api/VectorType/id/7 - Verify that a vector using the maximum supported dimension count (1998) + /// round-trips through REST and is returned with the correct number of dimensions. + /// + [TestMethod] + public async Task GetVectorTypeWithMaxDimensions() + { + HttpResponseMessage response = await HttpClient.GetAsync($"{VECTOR_TYPE_REST_PATH}/id/7"); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + + string body = await response.Content.ReadAsStringAsync(); + JsonElement value = JsonDocument.Parse(body).RootElement.GetProperty("value")[0]; + + Assert.AreEqual(7, value.GetProperty("id").GetInt32()); + + JsonElement maxVector = value.GetProperty("vector_data_max"); + Assert.AreEqual(JsonValueKind.Array, maxVector.ValueKind, "Expected the maximum-dimension vector to be serialized as a JSON array."); + Assert.AreEqual(1998, maxVector.GetArrayLength(), "Expected the maximum-dimension vector to have 1998 components."); + + int i = 1; + foreach (JsonElement vectorVal in maxVector.EnumerateArray()) + { + Assert.AreEqual(i, vectorVal.GetDouble(), VECTOR_COMPONENT_DELTA); + i++; + } + } + + #endregion + + #region Write Tests + + /// + /// POST /api/VectorType - Verify that a new record with a vector value can be inserted and is + /// returned (and persisted) as a JSON array. + /// + [DataTestMethod] + [DataRow("{ \"vector_data\": [0.125, 0.25, 0.5] }", new[] { 0.125f, 0.25f, 0.5f }, true, DisplayName = "Insert valid vector")] + [DataRow("{ \"vector_data\": null }", null, true, DisplayName = "Insert valid null vector")] + [DataRow("{ \"vector_data\": [5e-1, 2.5e-1, 7.5e-1] }", new[] { 0.5f, 0.25f, 0.75f }, true, DisplayName = "Insert valid vector with scientific notation")] + [DataRow("{ \"vector_data\": [\"0.5\", \"0.25\", \"0.75\"] }", new[] { 0.5f, 0.25f, 0.75f }, true, DisplayName = "Insert valid vector with numbers as string values")] + [DataRow("{ \"vector_data\": [1.25, 2.25, 3.25, 4.25] }", null, false, DisplayName = "Insert invalid vector with too many dimensions")] + [DataRow("{ \"vector_data\": [\"not\", \"a\", \"number\"] }", null, false, DisplayName = "Insert invalid vector with invalid values")] + public async Task InsertVectorType(string requestBody, float[] expectedValue, bool expectedSuccess) + { + HttpResponseMessage postResponse = await HttpClient.PostAsync( + VECTOR_TYPE_REST_PATH, + new StringContent(requestBody, Encoding.UTF8, "application/json")); + + if (expectedSuccess) + { + Assert.AreEqual(HttpStatusCode.Created, postResponse.StatusCode); + + JsonElement postElement = JsonDocument.Parse(await postResponse.Content.ReadAsStringAsync()) + .RootElement.GetProperty("value")[0]; + int newId = postElement.GetProperty("id").GetInt32(); + + // Confirm the value was persisted by reading it back. + JsonElement readBack = await GetRecordByIdAsync(newId); + AssertVectorEquals(readBack.GetProperty("vector_data"), expectedValue); + await DeleteVectorType(newId); + } + else + { + Assert.IsFalse(postResponse.IsSuccessStatusCode, "Expected that inserting vector should fail."); + } + } + + /// + /// PUT /api/VectorType/id/4 - Verify that an existing record's vector value is replaced (full update). + /// + [TestMethod] + public async Task PutVectorType_Update() + { + float[] expected = new[] { 9.5f, 8.5f, 7.5f }; + string requestBody = "{ \"vector_data\": [9.5, 8.5, 7.5] }"; + + HttpResponseMessage response = await HttpClient.PutAsync( + $"{VECTOR_TYPE_REST_PATH}/id/4", + new StringContent(requestBody, Encoding.UTF8, "application/json")); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + + JsonElement updated = JsonDocument.Parse(await response.Content.ReadAsStringAsync()) + .RootElement.GetProperty("value")[0]; + Assert.AreEqual(4, updated.GetProperty("id").GetInt32()); + + JsonElement readBack = await GetRecordByIdAsync(4); + AssertVectorEquals(readBack.GetProperty("vector_data"), expected); + } + + /// + /// PATCH /api/VectorType/id/5 - Verify that an existing record's vector value is updated. + /// + [TestMethod] + public async Task PatchVectorType_Update() + { + float[] expected = new[] { 1.25f, 2.25f, 3.25f }; + string requestBody = "{ \"vector_data\": [1.25, 2.25, 3.25] }"; + + HttpResponseMessage response = await HttpClient.PatchAsync( + $"{VECTOR_TYPE_REST_PATH}/id/5", + new StringContent(requestBody, Encoding.UTF8, "application/json")); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + + JsonElement updated = JsonDocument.Parse(await response.Content.ReadAsStringAsync()) + .RootElement.GetProperty("value")[0]; + Assert.AreEqual(5, updated.GetProperty("id").GetInt32()); + + JsonElement readBack = await GetRecordByIdAsync(5); + AssertVectorEquals(readBack.GetProperty("vector_data"), expected); + } + + #endregion + + #region Query Option Tests + + [DataTestMethod] + [DataRow("?$filter=vector_data%20eq%201", DisplayName = "Fail GET with $filter on vector column")] + [DataRow("?$orderby=vector_data ASC", DisplayName = "Fail GET with $orderby on vector column")] + public async Task ArgumentsOnVectorColumnFail(string queryOptions) + { + HttpResponseMessage response = await HttpClient.GetAsync($"{VECTOR_TYPE_REST_PATH}{queryOptions}"); + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode, "Query options on vector columns should be rejected."); + } + + /// + /// GET /api/VectorType?$first=2&$orderby=id - Verify that pagination works for an entity that has a + /// vector column. The first request returns a page plus a nextLink, and issuing a second request using + /// the $after token extracted from that nextLink also succeeds. + /// + [TestMethod] + public async Task FindWithFirstThenAfterPaginationSucceedsVectorType() + { + // First page: limit to two records, ordered by primary key for a deterministic cursor. + HttpResponseMessage firstPageResponse = await HttpClient.GetAsync($"{VECTOR_TYPE_REST_PATH}?$first=2&$orderby=id"); + Assert.AreEqual(HttpStatusCode.OK, firstPageResponse.StatusCode); + + JsonElement firstPageRoot = JsonDocument.Parse(await firstPageResponse.Content.ReadAsStringAsync()).RootElement; + Assert.AreEqual(2, firstPageRoot.GetProperty("value").GetArrayLength(), "Expected the first page to contain exactly two records."); + Assert.IsTrue(firstPageRoot.TryGetProperty("nextLink", out JsonElement nextLinkElement), "Expected a nextLink on the first page."); + + // Extract the $after token from the nextLink and use it to request the next page. + string afterToken = ExtractAfterToken(nextLinkElement.GetString()); + Assert.IsFalse(string.IsNullOrEmpty(afterToken), "Expected a non-empty $after token in the nextLink."); + + HttpResponseMessage secondPageResponse = await HttpClient.GetAsync($"{VECTOR_TYPE_REST_PATH}?$first=2&$orderby=id&$after={afterToken}"); + Assert.AreEqual(HttpStatusCode.OK, secondPageResponse.StatusCode, "Expected the request using the $after token to succeed."); + + JsonElement secondPageRoot = JsonDocument.Parse(await secondPageResponse.Content.ReadAsStringAsync()).RootElement; + Assert.IsTrue(secondPageRoot.GetProperty("value").GetArrayLength() >= 1, "Expected the second page to contain at least one record."); + } + + #endregion + + #region Helpers + + /// + /// DELETE /api/VectorType/id/6 - Verify that a record with a vector column can be deleted and is + /// no longer retrievable. + /// + private static async Task DeleteVectorType(int id) + { + HttpResponseMessage deleteResponse = await HttpClient.DeleteAsync($"{VECTOR_TYPE_REST_PATH}/id/{id}"); + Assert.AreEqual(HttpStatusCode.NoContent, deleteResponse.StatusCode); + } + + /// + /// Fetches a single VectorType record by its primary key and returns the record element. + /// + private static async Task GetRecordByIdAsync(int id) + { + HttpResponseMessage response = await HttpClient.GetAsync($"{VECTOR_TYPE_REST_PATH}/id/{id}"); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + string body = await response.Content.ReadAsStringAsync(); + return JsonDocument.Parse(body).RootElement.GetProperty("value")[0].Clone(); + } + + /// + /// Extracts the raw (URL-encoded) value of the $after query parameter from a pagination nextLink. + /// The token is returned exactly as emitted by the engine so it can be replayed verbatim in a + /// follow-up request without any re-encoding. + /// + private static string ExtractAfterToken(string nextLink) + { + if (string.IsNullOrEmpty(nextLink)) + { + return string.Empty; + } + + const string afterParam = "$after="; + int afterIndex = nextLink.IndexOf(afterParam, StringComparison.Ordinal); + if (afterIndex < 0) + { + return string.Empty; + } + + string afterToken = nextLink.Substring(afterIndex + afterParam.Length); + int ampersandIndex = afterToken.IndexOf('&'); + return ampersandIndex >= 0 ? afterToken.Substring(0, ampersandIndex) : afterToken; + } + + /// + /// Asserts that the given JSON element is an array whose components match the expected vector + /// within . + /// + private static void AssertVectorEquals(JsonElement actual, float[] expected) + { + if (expected == null) + { + Assert.AreEqual(JsonValueKind.Null, actual.ValueKind, "Expected a null vector, but got a non-null value."); + return; + } + + Assert.AreEqual(expected.Length, actual.GetArrayLength(), "Vector dimension mismatch."); + + int i = 0; + foreach (JsonElement element in actual.EnumerateArray()) + { + Assert.AreEqual(expected[i], element.GetDouble(), VECTOR_COMPONENT_DELTA, $"Vector component at expected {expected[i]} and got {element.GetDouble()}."); + i++; + } + } + + #endregion + } +} diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index 6a41d8ee13..9c2f1d4474 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -2,7 +2,7 @@ "$schema": "https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json", "data-source": { "database-type": "mssql", - "connection-string": "Server=tcp:127.0.0.1,1433;Persist Security Info=False;User ID=sa;Password=REPLACEME;MultipleActiveResultSets=False;Connection Timeout=30;", + "connection-string": "Server=localhost\\MSSQLSERVER01;Database=ElPapuDatabase;Trusted_Connection=True;Integrated Security=true;MultipleActiveResultSets=False;Connection Timeout=5;Encrypt=False;", "options": { "set-session-context": true } @@ -1802,6 +1802,58 @@ } ] }, + "VectorType": { + "source": { + "object": "vector_type_table", + "type": "table" + }, + "graphql": { + "enabled": false, + "type": { + "singular": "VectorType", + "plural": "VectorTypes" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "create" + }, + { + "action": "read" + }, + { + "action": "delete" + }, + { + "action": "update" + } + ] + }, + { + "role": "authenticated", + "actions": [ + { + "action": "create" + }, + { + "action": "read" + }, + { + "action": "delete" + }, + { + "action": "update" + } + ] + } + ] + }, "stocks_price": { "source": { "object": "stocks_price", @@ -4022,4 +4074,4 @@ ] } } -} \ No newline at end of file +} From ebe6b28d492057a46bea8c8ccb0a80f26c7f8a13 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Wed, 24 Jun 2026 15:01:49 -0700 Subject: [PATCH 25/35] Fix tests --- src/Core/Resolvers/BaseSqlQueryBuilder.cs | 1 - .../BaseSqlQueryStructure.cs | 30 +++++++++++++------ .../MsSqlMetadataProvider.cs | 2 +- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/Core/Resolvers/BaseSqlQueryBuilder.cs b/src/Core/Resolvers/BaseSqlQueryBuilder.cs index 2aaff44ff0..a509e9d842 100644 --- a/src/Core/Resolvers/BaseSqlQueryBuilder.cs +++ b/src/Core/Resolvers/BaseSqlQueryBuilder.cs @@ -212,7 +212,6 @@ protected virtual string Build(OrderByColumn column, bool printDirection = true) /// protected string Build(LabelledColumn column) { - //if (GetColumnSystemType()) return Build(column as Column) + " AS " + QuoteIdentifier(column.Label); } diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index c790404261..f846d82e15 100644 --- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -458,23 +458,35 @@ protected static object ParseParamAsSystemType(string param, Type systemType) }; } + /// + /// Takes the array of the parameter we are going to parse and converts each element to the specified system type. + /// + /// + /// + /// + /// private static object ParseArrayIntoSystemType(string param, Type systemType) { + Type typeOfArray; switch (systemType.Name) { case "Single[]": - List list = new(); - object[] values = JsonSerializer.Deserialize(param) ?? Array.Empty(); - foreach (object value in values) - { - string stringValue = value.ToString() ?? string.Empty; - list.Add(ParseParamAsSystemType(stringValue, typeof(Single))); - } - - return list.ToArray(); + typeOfArray = typeof(Single); + break; + default: throw new NotSupportedException($"{systemType.Name} is not supported"); } + + List list = new(); + object[] values = JsonSerializer.Deserialize(param) ?? Array.Empty(); + foreach (object value in values) + { + string stringValue = value.ToString() ?? string.Empty; + list.Add(ParseParamAsSystemType(stringValue, typeOfArray)); + } + + return list.ToArray(); } /// diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index 0e9b1407ed..506c65528f 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -123,7 +123,7 @@ protected override void PopulateColumnDefinitionWithHasDefaultAndDbType( columnDefinition.ElementSystemType = typeof(Single); columnDefinition.SystemType = columnDefinition.ElementSystemType.MakeArrayType(); } - + if (Enum.TryParse(sqlDbTypeName, ignoreCase: true, out SqlDbType sqlDbType)) { // The DbType enum in .NET does not distinguish between VarChar and NVarChar. Both are mapped to DbType.String. From 7e45fe2a7a84cfb98fb510536840bfd6b4d0c0f3 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Wed, 24 Jun 2026 15:26:59 -0700 Subject: [PATCH 26/35] Fix syntax --- src/Core/Services/MetadataProviders/SqlMetadataProvider.cs | 5 ++--- .../SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 3ecafb0c05..7d9708bdb0 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -19,7 +19,6 @@ using Azure.DataApiBuilder.Core.Resolvers.Factories; using Azure.DataApiBuilder.Service.Exceptions; using HotChocolate.Language; -using Microsoft.Data.SqlTypes; using Microsoft.Extensions.Logging; using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming; using KeyNotFoundException = System.Collections.Generic.KeyNotFoundException; @@ -1489,7 +1488,7 @@ private async Task PopulateSourceDefinitionAsync( // Detect array types: concrete array types (e.g., int[]) have IsArray=true, // while Npgsql reports abstract System.Array for PostgreSQL array columns. // byte[] is excluded since it maps to the bytea/ByteArray scalar type. - bool isArrayType = (systemType.IsArray && systemType != typeof(byte[])) || systemType == typeof(Array) || systemType == typeof(SqlVector); + bool isArrayType = (systemType.IsArray && systemType != typeof(byte[])) || systemType == typeof(Array); ColumnDefinition column = new() { @@ -1501,7 +1500,7 @@ private async Task PopulateSourceDefinitionAsync( // An auto-increment column is also considered as a read-only column. For other types of read-only columns, // the flag is populated later via PopulateColumnDefinitionsWithReadOnlyFlag() method. // Array columns are also treated as read-only until write support for array types is implemented. - IsReadOnly = (bool)columnInfoFromAdapter["IsAutoIncrement"] || (isArrayType && systemType != typeof(SqlVector)) + IsReadOnly = (bool)columnInfoFromAdapter["IsAutoIncrement"] || isArrayType }; // Tests may try to add the same column simultaneously diff --git a/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs b/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs index b0ae21f094..aa18ee2ba4 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs @@ -99,7 +99,7 @@ public async Task GetVectorTypeWithMaxDimensions() [DataRow("{ \"vector_data\": null }", null, true, DisplayName = "Insert valid null vector")] [DataRow("{ \"vector_data\": [5e-1, 2.5e-1, 7.5e-1] }", new[] { 0.5f, 0.25f, 0.75f }, true, DisplayName = "Insert valid vector with scientific notation")] [DataRow("{ \"vector_data\": [\"0.5\", \"0.25\", \"0.75\"] }", new[] { 0.5f, 0.25f, 0.75f }, true, DisplayName = "Insert valid vector with numbers as string values")] - [DataRow("{ \"vector_data\": [1.25, 2.25, 3.25, 4.25] }", null, false, DisplayName = "Insert invalid vector with too many dimensions")] + [DataRow("{ \"vector_data\": [1.25, 2.25, 3.25, 4.25] }", null, false, DisplayName = "Insert invalid vector with more dimensions than allowed")] [DataRow("{ \"vector_data\": [\"not\", \"a\", \"number\"] }", null, false, DisplayName = "Insert invalid vector with invalid values")] public async Task InsertVectorType(string requestBody, float[] expectedValue, bool expectedSuccess) { @@ -143,7 +143,7 @@ public async Task PutVectorType_Update() JsonElement updated = JsonDocument.Parse(await response.Content.ReadAsStringAsync()) .RootElement.GetProperty("value")[0]; Assert.AreEqual(4, updated.GetProperty("id").GetInt32()); - + JsonElement readBack = await GetRecordByIdAsync(4); AssertVectorEquals(readBack.GetProperty("vector_data"), expected); } @@ -165,7 +165,7 @@ public async Task PatchVectorType_Update() JsonElement updated = JsonDocument.Parse(await response.Content.ReadAsStringAsync()) .RootElement.GetProperty("value")[0]; Assert.AreEqual(5, updated.GetProperty("id").GetInt32()); - + JsonElement readBack = await GetRecordByIdAsync(5); AssertVectorEquals(readBack.GetProperty("vector_data"), expected); } From aef7af95fdbcad89f95c556e57cd4eb2212e3f23 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Wed, 24 Jun 2026 15:44:24 -0700 Subject: [PATCH 27/35] Fix syntax error --- .../Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index f846d82e15..42dd7db702 100644 --- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -473,7 +473,7 @@ private static object ParseArrayIntoSystemType(string param, Type systemType) case "Single[]": typeOfArray = typeof(Single); break; - + default: throw new NotSupportedException($"{systemType.Name} is not supported"); } From e4799270701ac5f7495634f9db38cb23c8d0ecad Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Thu, 25 Jun 2026 10:30:16 -0700 Subject: [PATCH 28/35] Changes based on copilot --- .../Sql Query Structures/BaseSqlQueryStructure.cs | 7 +++++++ src/Core/Services/OpenAPI/OpenApiDocumentor.cs | 9 ++++++++- src/Core/Services/TypeHelper.cs | 2 +- src/Service.Tests/dab-config.MsSql.json | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index 42dd7db702..cf39cfa43b 100644 --- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -465,6 +465,7 @@ protected static object ParseParamAsSystemType(string param, Type systemType) /// /// /// + /// private static object ParseArrayIntoSystemType(string param, Type systemType) { Type typeOfArray; @@ -478,6 +479,12 @@ private static object ParseArrayIntoSystemType(string param, Type systemType) throw new NotSupportedException($"{systemType.Name} is not supported"); } + using JsonDocument arg = JsonDocument.Parse(param); + if (arg.RootElement.ValueKind != JsonValueKind.Array) + { + throw new FormatException($"Expected an array for {systemType.Name} but got {arg.RootElement.ValueKind}"); + } + List list = new(); object[] values = JsonSerializer.Deserialize(param) ?? Array.Empty(); foreach (object value in values) diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index 979da52eb6..e66968de13 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -1484,12 +1484,18 @@ private static OpenApiSchema CreateComponentSchema( if (metadataProvider.TryGetBackingColumn(entityName, field, out string? backingColumnValue) && !string.IsNullOrEmpty(backingColumnValue)) { string typeMetadata = string.Empty; + string subTypeMetadata = string.Empty; string formatMetadata = string.Empty; string? fieldDescription = null; if (dbObject.SourceDefinition.Columns.TryGetValue(backingColumnValue, out ColumnDefinition? columnDef)) { typeMetadata = TypeHelper.GetJsonDataTypeFromSystemType(columnDef.SystemType).ToString().ToLower(); + + if (string.Equals(typeMetadata, JsonDataType.Array.ToString().ToLower(), StringComparison.OrdinalIgnoreCase)) + { + subTypeMetadata = TypeHelper.GetJsonDataTypeFromSystemType(columnDef.ElementSystemType!).ToString().ToLower(); + } } if (entityConfig?.Fields != null) @@ -1502,7 +1508,8 @@ private static OpenApiSchema CreateComponentSchema( { Type = typeMetadata, Format = formatMetadata, - Description = fieldDescription + Description = fieldDescription, + Items = !string.IsNullOrWhiteSpace(subTypeMetadata) ? new OpenApiSchema() { Type = subTypeMetadata } : null }); } } diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index aae651c068..db47fe6ce9 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -80,7 +80,7 @@ public static class TypeHelper [typeof(object)] = JsonDataType.Object, [typeof(DateTime)] = JsonDataType.String, [typeof(DateTimeOffset)] = JsonDataType.String, - [typeof(SqlVector)] = JsonDataType.Number + [typeof(Single[])] = JsonDataType.Array }; /// diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index 9c2f1d4474..4de4f52b5f 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -2,7 +2,7 @@ "$schema": "https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json", "data-source": { "database-type": "mssql", - "connection-string": "Server=localhost\\MSSQLSERVER01;Database=ElPapuDatabase;Trusted_Connection=True;Integrated Security=true;MultipleActiveResultSets=False;Connection Timeout=5;Encrypt=False;", + "connection-string": "Server=tcp:127.0.0.1,1433;Persist Security Info=False;User ID=sa;Password=REPLACEME;MultipleActiveResultSets=False;Connection Timeout=30;", "options": { "set-session-context": true } From 0e96e32cbf2202e70f603cad153f8133c613bbdf Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Thu, 25 Jun 2026 15:30:15 -0700 Subject: [PATCH 29/35] Upgrade docker sql version --- scripts/start-mssql-server.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/start-mssql-server.bash b/scripts/start-mssql-server.bash index 5268a5b807..c68d80f9ff 100644 --- a/scripts/start-mssql-server.bash +++ b/scripts/start-mssql-server.bash @@ -27,7 +27,7 @@ echo "forceencryption = 1" >> $CERT_DIR/mssql.conf cat $CERT_DIR/mssql.conf # Start mssql-server by volume mounting the cert, key and conf files. -docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$DOCKER_SQL_PASS" -p 1433:1433 --name customerdb -h customerdb -v $CERT_DIR/mssql.conf:/var/opt/mssql/mssql.conf -v $CERT_DIR/mssql.pem:/var/opt/mssql/mssql.pem -v $CERT_DIR/mssql.key:/var/opt/mssql/mssql.key -d mcr.microsoft.com/mssql/server:2019-latest +docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$DOCKER_SQL_PASS" -p 1433:1433 --name customerdb -h customerdb -v $CERT_DIR/mssql.conf:/var/opt/mssql/mssql.conf -v $CERT_DIR/mssql.pem:/var/opt/mssql/mssql.pem -v $CERT_DIR/mssql.key:/var/opt/mssql/mssql.key -d mcr.microsoft.com/mssql/server:2022-latest sleep 30 docker logs customerdb From 84b73b1db207983bcbeeeb1d3e2bf59b23b9649a Mon Sep 17 00:00:00 2001 From: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:31:06 -0700 Subject: [PATCH 30/35] Fix utf8 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Config/Azure.DataApiBuilder.Config.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/Azure.DataApiBuilder.Config.csproj b/src/Config/Azure.DataApiBuilder.Config.csproj index 0c81d6eba4..0d1655df76 100644 --- a/src/Config/Azure.DataApiBuilder.Config.csproj +++ b/src/Config/Azure.DataApiBuilder.Config.csproj @@ -1,4 +1,4 @@ - + net10.0 From 99954f62ee6061efe1e86d3d7b94a6ea893b6730 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Thu, 25 Jun 2026 15:34:44 -0700 Subject: [PATCH 31/35] Upgrade docker sql version --- scripts/start-mssql-server.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/start-mssql-server.bash b/scripts/start-mssql-server.bash index c68d80f9ff..4b1f0a0a42 100644 --- a/scripts/start-mssql-server.bash +++ b/scripts/start-mssql-server.bash @@ -27,7 +27,7 @@ echo "forceencryption = 1" >> $CERT_DIR/mssql.conf cat $CERT_DIR/mssql.conf # Start mssql-server by volume mounting the cert, key and conf files. -docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$DOCKER_SQL_PASS" -p 1433:1433 --name customerdb -h customerdb -v $CERT_DIR/mssql.conf:/var/opt/mssql/mssql.conf -v $CERT_DIR/mssql.pem:/var/opt/mssql/mssql.pem -v $CERT_DIR/mssql.key:/var/opt/mssql/mssql.key -d mcr.microsoft.com/mssql/server:2022-latest +docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$DOCKER_SQL_PASS" -p 1433:1433 --name customerdb -h customerdb -v $CERT_DIR/mssql.conf:/var/opt/mssql/mssql.conf -v $CERT_DIR/mssql.pem:/var/opt/mssql/mssql.pem -v $CERT_DIR/mssql.key:/var/opt/mssql/mssql.key -d mcr.microsoft.com/mssql/server:2025-latest sleep 30 docker logs customerdb From 6404bdcaf3108c5c13cce9cdc183fc169e1f1358 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Thu, 25 Jun 2026 16:02:28 -0700 Subject: [PATCH 32/35] Change to sql 2025 in pipeline --- .pipelines/mssql-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pipelines/mssql-pipelines.yml b/.pipelines/mssql-pipelines.yml index bdfb5e37e6..d9a2201695 100644 --- a/.pipelines/mssql-pipelines.yml +++ b/.pipelines/mssql-pipelines.yml @@ -177,7 +177,7 @@ jobs: # for the linux job above. data-source.connection-string: Server=(localdb)\MSSQLLocalDB;Persist Security Info=False;Integrated Security=True;MultipleActiveResultSets=False;Connection Timeout=30;TrustServerCertificate=True; InstallerUrl: https://download.microsoft.com/download/7/c/1/7c14e92e-bdcb-4f89-b7cf-93543e7112d1/SqlLocalDB.msi - SqlVersionCode: '15.0' + SqlVersionCode: '17.0' steps: - template: templates/mssql-test-steps.yml @@ -200,7 +200,7 @@ jobs: # for the linux job above. data-source.connection-string: Server=(localdb)\MSSQLLocalDB;Persist Security Info=False;Integrated Security=True;MultipleActiveResultSets=False;Connection Timeout=30;TrustServerCertificate=True; InstallerUrl: https://download.microsoft.com/download/7/c/1/7c14e92e-bdcb-4f89-b7cf-93543e7112d1/SqlLocalDB.msi - SqlVersionCode: '15.0' + SqlVersionCode: '17.0' steps: - template: templates/mssql-test-steps.yml From f96fefb493d01b5f7155248ce1acf512ef54762a Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Fri, 26 Jun 2026 15:45:15 -0700 Subject: [PATCH 33/35] Fixed how result is returned in insert --- src/Core/Resolvers/QueryExecutor.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Core/Resolvers/QueryExecutor.cs b/src/Core/Resolvers/QueryExecutor.cs index d96b19af38..5bf50fdd40 100644 --- a/src/Core/Resolvers/QueryExecutor.cs +++ b/src/Core/Resolvers/QueryExecutor.cs @@ -13,6 +13,7 @@ using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Service.Exceptions; using Microsoft.AspNetCore.Http; +using Microsoft.Data.SqlTypes; using Microsoft.Extensions.Logging; using Polly; using Polly.Retry; @@ -502,7 +503,7 @@ public async Task { if (!ConfigProvider.GetConfig().MaxResponseSizeLogicEnabled()) { - dbResultSetRow.Columns.Add(columnName, dbDataReader[columnName]); + dbResultSetRow.Columns.Add(columnName, GetColumnInformation(dbDataReader, columnName)); } else { @@ -885,6 +886,22 @@ private void ValidateSize(long availableSizeBytes, long sizeToBeReadBytes) } } + /// + /// Helper function to get column information from the DbDataReader and handle special cases like SqlVector. + /// + /// + /// + /// + private static object GetColumnInformation(DbDataReader dbDataReader, string columnName) + { + if (dbDataReader[columnName] is SqlVector columnValue) + { + return columnValue.Memory; + } + + return dbDataReader[columnName]; + } + internal virtual void AddDbExecutionTimeToMiddlewareContext(long time) { HttpContext? httpContext = HttpContextAccessor?.HttpContext; From 8f2eb9213f0fcf9e18cdb1335878ba081e2db5fd Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Wed, 1 Jul 2026 13:45:14 -0700 Subject: [PATCH 34/35] Changes based on comments --- src/Core/Resolvers/QueryExecutor.cs | 4 +- .../BaseSqlQueryStructure.cs | 23 +++++----- .../Sql/SchemaConverter.cs | 1 - .../RestApiTests/MsSqlRestVectorTypesTests.cs | 42 +++++++++++++++++-- 4 files changed, 52 insertions(+), 18 deletions(-) diff --git a/src/Core/Resolvers/QueryExecutor.cs b/src/Core/Resolvers/QueryExecutor.cs index 5bf50fdd40..981252bf8f 100644 --- a/src/Core/Resolvers/QueryExecutor.cs +++ b/src/Core/Resolvers/QueryExecutor.cs @@ -555,7 +555,7 @@ public DbResultSet { if (!ConfigProvider.GetConfig().MaxResponseSizeLogicEnabled()) { - dbResultSetRow.Columns.Add(columnName, dbDataReader[columnName]); + dbResultSetRow.Columns.Add(columnName, GetColumnInformation(dbDataReader, columnName)); } else { @@ -823,7 +823,7 @@ internal int StreamDataIntoDbResultSetRow(DbDataReader dbDataReader, DbResultSet { dataRead = columnSize; ValidateSize(availableBytes, dataRead); - dbResultSetRow.Columns.Add(columnName, dbDataReader[columnName]); + dbResultSetRow.Columns.Add(columnName, GetColumnInformation(dbDataReader, columnName)); } return dataRead; diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs index cf39cfa43b..c6593047a3 100644 --- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs @@ -479,21 +479,22 @@ private static object ParseArrayIntoSystemType(string param, Type systemType) throw new NotSupportedException($"{systemType.Name} is not supported"); } - using JsonDocument arg = JsonDocument.Parse(param); - if (arg.RootElement.ValueKind != JsonValueKind.Array) + try { - throw new FormatException($"Expected an array for {systemType.Name} but got {arg.RootElement.ValueKind}"); - } + List list = new(); + object[] values = JsonSerializer.Deserialize(param) ?? Array.Empty(); + for (int i = 0; i < values.Length; i++) + { + string stringValue = values[i]?.ToString() ?? string.Empty; + values[i] = ParseParamAsSystemType(stringValue, typeOfArray); + } - List list = new(); - object[] values = JsonSerializer.Deserialize(param) ?? Array.Empty(); - foreach (object value in values) + return values; + } + catch { - string stringValue = value.ToString() ?? string.Empty; - list.Add(ParseParamAsSystemType(stringValue, typeOfArray)); + throw new FormatException($"Expected an array for {systemType.Name} but got an unexpected value"); } - - return list.ToArray(); } /// diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs index 6f7f9bc69f..622376dc13 100644 --- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs +++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs @@ -577,7 +577,6 @@ public static string GetGraphQLTypeFromSystemType(Type type) "Byte[]" => BYTEARRAY_TYPE, "TimeOnly" => LOCALTIME_TYPE, "TimeSpan" => LOCALTIME_TYPE, - "SqlVector`1" => SINGLE_TYPE, _ => throw new DataApiBuilderException( message: $"Column type {type} not handled by case. Please add a case resolving {type} to the appropriate GraphQL type", statusCode: HttpStatusCode.InternalServerError, diff --git a/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs b/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs index aa18ee2ba4..4b6a9ccf6d 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs @@ -127,11 +127,12 @@ public async Task InsertVectorType(string requestBody, float[] expectedValue, bo } /// - /// PUT /api/VectorType/id/4 - Verify that an existing record's vector value is replaced (full update). + /// PUT Verify that an existing record's vector value is replaced (full update). /// [TestMethod] public async Task PutVectorType_Update() { + // Change vector value float[] expected = new[] { 9.5f, 8.5f, 7.5f }; string requestBody = "{ \"vector_data\": [9.5, 8.5, 7.5] }"; @@ -146,28 +147,61 @@ public async Task PutVectorType_Update() JsonElement readBack = await GetRecordByIdAsync(4); AssertVectorEquals(readBack.GetProperty("vector_data"), expected); + + // Restore vector value to original + expected = new[] { 1.0f, 2.0f, 3.0f }; + requestBody = "{ \"vector_data\": [1.0, 2.0, 3.0] }"; + + HttpResponseMessage response = await HttpClient.PutAsync( + $"{VECTOR_TYPE_REST_PATH}/id/4", + new StringContent(requestBody, Encoding.UTF8, "application/json")); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + + JsonElement updated = JsonDocument.Parse(await response.Content.ReadAsStringAsync()) + .RootElement.GetProperty("value")[0]; + Assert.AreEqual(4, updated.GetProperty("id").GetInt32()); + + JsonElement readBack = await GetRecordByIdAsync(4); + AssertVectorEquals(readBack.GetProperty("vector_data"), expected); } /// - /// PATCH /api/VectorType/id/5 - Verify that an existing record's vector value is updated. + /// PATCH Verify that an existing record's vector value is updated. /// [TestMethod] public async Task PatchVectorType_Update() { + // Change vector value float[] expected = new[] { 1.25f, 2.25f, 3.25f }; string requestBody = "{ \"vector_data\": [1.25, 2.25, 3.25] }"; HttpResponseMessage response = await HttpClient.PatchAsync( - $"{VECTOR_TYPE_REST_PATH}/id/5", + $"{VECTOR_TYPE_REST_PATH}/id/4", new StringContent(requestBody, Encoding.UTF8, "application/json")); Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); JsonElement updated = JsonDocument.Parse(await response.Content.ReadAsStringAsync()) .RootElement.GetProperty("value")[0]; - Assert.AreEqual(5, updated.GetProperty("id").GetInt32()); + Assert.AreEqual(4, updated.GetProperty("id").GetInt32()); JsonElement readBack = await GetRecordByIdAsync(5); AssertVectorEquals(readBack.GetProperty("vector_data"), expected); + + // Restore vector value to original + expected = new[] { 1.0f, 2.0f, 3.0f }; + requestBody = "{ \"vector_data\": [1.0, 2.0, 3.0] }"; + + HttpResponseMessage response = await HttpClient.PutAsync( + $"{VECTOR_TYPE_REST_PATH}/id/4", + new StringContent(requestBody, Encoding.UTF8, "application/json")); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + + JsonElement updated = JsonDocument.Parse(await response.Content.ReadAsStringAsync()) + .RootElement.GetProperty("value")[0]; + Assert.AreEqual(4, updated.GetProperty("id").GetInt32()); + + JsonElement readBack = await GetRecordByIdAsync(4); + AssertVectorEquals(readBack.GetProperty("vector_data"), expected); } #endregion From c357a598195cc49f1c5a4cd3b9ff3304f0c20734 Mon Sep 17 00:00:00 2001 From: Ruben Cerna Date: Wed, 1 Jul 2026 13:52:36 -0700 Subject: [PATCH 35/35] Fix syntax --- .../RestApiTests/MsSqlRestVectorTypesTests.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs b/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs index 4b6a9ccf6d..15f496e902 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestVectorTypesTests.cs @@ -152,17 +152,17 @@ public async Task PutVectorType_Update() expected = new[] { 1.0f, 2.0f, 3.0f }; requestBody = "{ \"vector_data\": [1.0, 2.0, 3.0] }"; - HttpResponseMessage response = await HttpClient.PutAsync( + HttpResponseMessage restoreResponse = await HttpClient.PutAsync( $"{VECTOR_TYPE_REST_PATH}/id/4", new StringContent(requestBody, Encoding.UTF8, "application/json")); - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.AreEqual(HttpStatusCode.OK, restoreResponse.StatusCode); - JsonElement updated = JsonDocument.Parse(await response.Content.ReadAsStringAsync()) + JsonElement restoreUpdated = JsonDocument.Parse(await restoreResponse.Content.ReadAsStringAsync()) .RootElement.GetProperty("value")[0]; - Assert.AreEqual(4, updated.GetProperty("id").GetInt32()); + Assert.AreEqual(4, restoreUpdated.GetProperty("id").GetInt32()); - JsonElement readBack = await GetRecordByIdAsync(4); - AssertVectorEquals(readBack.GetProperty("vector_data"), expected); + JsonElement restoreReadBack = await GetRecordByIdAsync(4); + AssertVectorEquals(restoreReadBack.GetProperty("vector_data"), expected); } /// @@ -184,24 +184,24 @@ public async Task PatchVectorType_Update() .RootElement.GetProperty("value")[0]; Assert.AreEqual(4, updated.GetProperty("id").GetInt32()); - JsonElement readBack = await GetRecordByIdAsync(5); + JsonElement readBack = await GetRecordByIdAsync(4); AssertVectorEquals(readBack.GetProperty("vector_data"), expected); // Restore vector value to original expected = new[] { 1.0f, 2.0f, 3.0f }; requestBody = "{ \"vector_data\": [1.0, 2.0, 3.0] }"; - HttpResponseMessage response = await HttpClient.PutAsync( + HttpResponseMessage restoreResponse = await HttpClient.PutAsync( $"{VECTOR_TYPE_REST_PATH}/id/4", new StringContent(requestBody, Encoding.UTF8, "application/json")); - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.AreEqual(HttpStatusCode.OK, restoreResponse.StatusCode); - JsonElement updated = JsonDocument.Parse(await response.Content.ReadAsStringAsync()) + JsonElement restoreUpdated = JsonDocument.Parse(await restoreResponse.Content.ReadAsStringAsync()) .RootElement.GetProperty("value")[0]; - Assert.AreEqual(4, updated.GetProperty("id").GetInt32()); + Assert.AreEqual(4, restoreUpdated.GetProperty("id").GetInt32()); - JsonElement readBack = await GetRecordByIdAsync(4); - AssertVectorEquals(readBack.GetProperty("vector_data"), expected); + JsonElement restoreReadBack = await GetRecordByIdAsync(4); + AssertVectorEquals(restoreReadBack.GetProperty("vector_data"), expected); } #endregion