diff --git a/extensions/sql-database-projects/CHANGELOG.md b/extensions/sql-database-projects/CHANGELOG.md index 0127b4b1e0..ff5f08a585 100644 --- a/extensions/sql-database-projects/CHANGELOG.md +++ b/extensions/sql-database-projects/CHANGELOG.md @@ -10,6 +10,7 @@ _The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Added **Rename Symbol** refactoring support for SQL project files. - Added **Move to Schema** refactoring support for SQL project files. +- Updated the default Microsoft.Build.Sql version to `2.*` for new SDK-style projects, enabling projects to automatically fetch the latest available 2.x NuGet release. ## [1.6.1] - 2026-06-03 diff --git a/extensions/sql-database-projects/README.md b/extensions/sql-database-projects/README.md index 8ca23576af..0df7b4681c 100644 --- a/extensions/sql-database-projects/README.md +++ b/extensions/sql-database-projects/README.md @@ -55,7 +55,7 @@ CREATE TABLE [dbo].[Product] ( ### General Settings - `sqlDatabaseProjects.dotnetSDK Location`: The path to the folder containing the `dotnet` folder for the .NET SDK. If not set, the extension will attempt to find the .NET SDK on the system. -- `sqlDatabaseProjects.microsoftBuildSqlVersion`: Version of Microsoft.Build.Sql to use for SQL projects. Controls the SDK version referenced in newly created SDK-style project templates and the binaries used when building non-SDK-style SQL projects. If not set, the extension will use Microsoft.Build.Sql 2.1.0. +- `sqlDatabaseProjects.microsoftBuildSqlVersion`: Version of Microsoft.Build.Sql to use for SQL projects. Controls the SDK version referenced in newly created SDK-style project templates and the binaries used when building non-SDK-style SQL projects. Supports exact versions (e.g. `2.1.0`) and NuGet floating versions (e.g. `2.*`). If not set, the extension will use Microsoft.Build.Sql 2.\*. - `sqlDatabaseProjects.netCoreDoNotAsk`: When true, no longer prompts to install .NET SDK when a supported installation is not found. - `sqlDatabaseProjects.collapseProjectNodes`: Option to set the default state of the project nodes in the database projects view to collapsed. If not set, the extension will default to expanded. diff --git a/extensions/sql-database-projects/l10n/bundle.l10n.json b/extensions/sql-database-projects/l10n/bundle.l10n.json index ddac4cf841..5caa57f088 100644 --- a/extensions/sql-database-projects/l10n/bundle.l10n.json +++ b/extensions/sql-database-projects/l10n/bundle.l10n.json @@ -303,6 +303,9 @@ "Extracting DacFx build DLLs to {0}": "Extracting DacFx build DLLs to {0}", "Error downloading {0}. Error: {1}": "Error downloading {0}. Error: {1}", "Error extracting files from {0}. Error: {1}": "Error extracting files from {0}. Error: {1}", + "No stable versions of {0} found matching {1}.": "No stable versions of {0} found matching {1}.", + "Failed to fetch NuGet index for {0}: HTTP {1}": "Failed to fetch NuGet index for {0}: HTTP {1}", + "Could not resolve Microsoft.Build.Sql version from NuGet ({0}). Using {1}.": "Could not resolve Microsoft.Build.Sql version from NuGet ({0}). Using {1}.", "Only moving files and folders are supported": "Only moving files and folders are supported", "Moving files between projects is not supported": "Moving files between projects is not supported", "Error when moving file from {0} to {1}. Error: {2}": "Error when moving file from {0} to {1}. Error: {2}", diff --git a/extensions/sql-database-projects/package.json b/extensions/sql-database-projects/package.json index bf525199a5..a68aede19c 100644 --- a/extensions/sql-database-projects/package.json +++ b/extensions/sql-database-projects/package.json @@ -81,7 +81,7 @@ }, "sqlDatabaseProjects.microsoftBuildSqlVersion": { "type": "string", - "default": "2.1.0", + "default": "2.*", "description": "%sqlDatabaseProjects.microsoftBuildSqlVersion%" }, "sqlDatabaseProjects.autoCreateFolders": { diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 0fdea658fd..8715e40c1b 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -946,6 +946,19 @@ export function errorDownloading(url: string, error: string) { export function errorExtracting(path: string, error: string) { return l10n.t("Error extracting files from {0}. Error: {1}", path, error); } +export function nugetVersionResolutionFailed(packageName: string, version: string) { + return l10n.t("No stable versions of {0} found matching {1}.", packageName, version); +} +export function nugetIndexFetchFailed(packageName: string, status: number) { + return l10n.t("Failed to fetch NuGet index for {0}: HTTP {1}", packageName, status); +} +export function couldNotResolveNugetVersion(errorMessage: string, fallback: string) { + return l10n.t( + "Could not resolve Microsoft.Build.Sql version from NuGet ({0}). Using {1}.", + errorMessage, + fallback, + ); +} //#endregion diff --git a/extensions/sql-database-projects/src/common/utils.ts b/extensions/sql-database-projects/src/common/utils.ts index 65cfe099e9..90e202bbd5 100644 --- a/extensions/sql-database-projects/src/common/utils.ts +++ b/extensions/sql-database-projects/src/common/utils.ts @@ -17,6 +17,20 @@ import { ISqlProject, SqlTargetPlatform } from "sqldbproj"; import { SystemDatabase } from "./typeHelper"; import { DeploymentScenario } from "./enums"; +/** + * Returns true if version is a valid NuGet floating version ("2.*" or "2.1.*"). + */ +export function isValidMicrosoftBuildSqlVersion(version: string): boolean { + if (!version.endsWith(".*")) { + return false; + } + const parts = version.slice(0, -2).split("."); + if (parts.length < 1 || parts.length > 2) { + return false; + } + return parts.every((p) => /^\d+$/.test(p)); +} + /** * Consolidates on the error message string */ diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index b5db04469e..7b4a1dfcd8 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -18,7 +18,13 @@ import { SqlDatabaseProjectTreeViewProvider } from "./databaseProjectTreeViewPro import { FolderNode, FileNode } from "../models/tree/fileFolderTreeItem"; import { BaseProjectTreeItem } from "../models/tree/baseTreeItem"; import { ImportDataModel } from "../models/api/import"; -import { NetCoreTool, DotNetError, getMicrosoftBuildSqlVersion } from "../tools/netcoreTool"; +import { + NetCoreTool, + DotNetError, + getMicrosoftBuildSqlVersion, + resolveNugetVersion, + OFFLINE_FALLBACK_MICROSOFT_BUILD_SQL_VERSION, +} from "../tools/netcoreTool"; import { BuildHelper } from "../tools/buildHelper"; import { ISystemDatabaseReferenceSettings, @@ -138,7 +144,22 @@ export class ProjectsController { } const sqlProjectsService = await utils.getSqlProjectsService(); - const microsoftBuildSqlSDKStyleDefaultVersion = getMicrosoftBuildSqlVersion(); + + let microsoftBuildSqlSDKStyleDefaultVersion: string; + try { + microsoftBuildSqlSDKStyleDefaultVersion = await resolveNugetVersion( + "Microsoft.Build.Sql", + getMicrosoftBuildSqlVersion(), + ); + } catch (e) { + microsoftBuildSqlSDKStyleDefaultVersion = OFFLINE_FALLBACK_MICROSOFT_BUILD_SQL_VERSION; + void vscode.window.showWarningMessage( + constants.couldNotResolveNugetVersion( + utils.getErrorMessage(e), + microsoftBuildSqlSDKStyleDefaultVersion, + ), + ); + } const projectStyle = creationParams.sdkStyle ? mssqlVscode.ProjectType.SdkStyle : mssqlVscode.ProjectType.LegacyStyle; diff --git a/extensions/sql-database-projects/src/templates/templates.ts b/extensions/sql-database-projects/src/templates/templates.ts index 7d4bb11a65..27989adf89 100644 --- a/extensions/sql-database-projects/src/templates/templates.ts +++ b/extensions/sql-database-projects/src/templates/templates.ts @@ -7,7 +7,7 @@ import * as path from "path"; import { promises as fs } from "fs"; import { ItemType } from "sqldbproj"; import * as constants from "../common/constants"; -import { getMicrosoftBuildSqlVersion } from "../tools/netcoreTool"; +import { getMicrosoftBuildSqlVersion, resolveNugetVersion } from "../tools/netcoreTool"; export let newSqlProjectTemplate: string; export let newSdkSqlProjectTemplate: string; @@ -82,7 +82,15 @@ export async function loadTemplates(templateFolderPath: string) { Promise.resolve( (newSdkSqlProjectTemplate = macroExpansion( await loadTemplate(templateFolderPath, "newSdkSqlProjectTemplate.xml"), - new Map([["MICROSOFT_BUILD_SQL_VERSION", getMicrosoftBuildSqlVersion()]]), + new Map([ + [ + "MICROSOFT_BUILD_SQL_VERSION", + await resolveNugetVersion( + "Microsoft.Build.Sql", + getMicrosoftBuildSqlVersion(), + ), + ], + ]), )), ), loadObjectTypeInfo( diff --git a/extensions/sql-database-projects/src/tools/buildHelper.ts b/extensions/sql-database-projects/src/tools/buildHelper.ts index 06b235338f..4cbefd3fbf 100644 --- a/extensions/sql-database-projects/src/tools/buildHelper.ts +++ b/extensions/sql-database-projects/src/tools/buildHelper.ts @@ -11,7 +11,11 @@ import * as sqldbproj from "sqldbproj"; import * as extractZip from "extract-zip"; import * as constants from "../common/constants"; import { HttpClient } from "../http/httpClient"; -import { getMicrosoftBuildSqlVersion } from "./netcoreTool"; +import { + getMicrosoftBuildSqlVersion, + resolveNugetVersion, + OFFLINE_FALLBACK_MICROSOFT_BUILD_SQL_VERSION, +} from "./netcoreTool"; import { ProjectType } from "../common/typeHelper"; import * as vscodeMssql from "vscode-mssql"; @@ -131,6 +135,20 @@ export class BuildHelper { nugetFolderWithExpectedfiles: string, outputChannel: vscode.OutputChannel, ): Promise { + if (utils.isValidMicrosoftBuildSqlVersion(nugetVersion)) { + try { + nugetVersion = await resolveNugetVersion(nugetName, nugetVersion); + } catch (e) { + nugetVersion = OFFLINE_FALLBACK_MICROSOFT_BUILD_SQL_VERSION; + const fallbackMessage = constants.couldNotResolveNugetVersion( + utils.getErrorMessage(e), + nugetVersion, + ); + outputChannel.appendLine(fallbackMessage); + void vscode.window.showWarningMessage(fallbackMessage); + } + } + const fullNugetName = `${nugetName}.${nugetVersion}`; const fullNugetPath = path.join(this.extensionBuildDir, `${fullNugetName}.nupkg`); diff --git a/extensions/sql-database-projects/src/tools/netcoreTool.ts b/extensions/sql-database-projects/src/tools/netcoreTool.ts index 5d5afc54b9..fd8fb7f6ae 100644 --- a/extensions/sql-database-projects/src/tools/netcoreTool.ts +++ b/extensions/sql-database-projects/src/tools/netcoreTool.ts @@ -9,6 +9,8 @@ import * as os from "os"; import * as path from "path"; import * as semver from "semver"; import * as vscode from "vscode"; +import axios from "axios"; +import { HttpClient } from "../http/httpClient"; import { DoNotAskAgain, Install, @@ -17,6 +19,8 @@ import { UpdateDotnetLocation, loc0ErroredOut1, microsoftBuildSqlVersionKey, + nugetVersionResolutionFailed, + nugetIndexFetchFailed, } from "../common/constants"; import * as utils from "../common/utils"; import { ShellCommandOptions, ShellExecutionHelper } from "./shellExecutionHelper"; @@ -32,42 +36,81 @@ export const macPlatform = "darwin"; export const linuxPlatform = "linux"; export const minSupportedNetCoreVersionForBuild = "8.0.0"; -/** - * Fallback version for Microsoft.Build.Sql when the setting is not configured or invalid. - * NOTE: Keep this in sync with the default value in package.json: - * sqlDatabaseProjects.microsoftBuildSqlVersion.default - */ -export const FALLBACK_MICROSOFT_BUILD_SQL_VERSION = "2.1.0"; +/** Default Microsoft.Build.Sql floating version. Change to target a different major. */ +export const FALLBACK_MICROSOFT_BUILD_SQL_VERSION = "2.*"; -export const enum netCoreInstallState { - netCoreNotPresent, - netCoreVersionNotSupported, - netCoreVersionSupported, -} +/** Exact fallback version used when NuGet resolution fails (offline/proxy). */ +export const OFFLINE_FALLBACK_MICROSOFT_BUILD_SQL_VERSION = "2.2.0"; -const dotnet = os.platform() === "win32" ? "dotnet.exe" : "dotnet"; - -/** - * Returns the configured Microsoft.Build.Sql version. - * - * Resolution order: - * 1. User's configured value (global or workspace settings.json) — if it is a valid semver. - * 2. Package.json default value — returned by config.get() when the user has not overridden the setting. - * 3. FALLBACK_MICROSOFT_BUILD_SQL_VERSION — used only when both of the above are unavailable or - * not a valid semver (e.g. the extension package.json default is missing or the user typed an - * invalid version string). - */ +/** Returns the configured Microsoft.Build.Sql version, falling back to FALLBACK_MICROSOFT_BUILD_SQL_VERSION. */ export function getMicrosoftBuildSqlVersion(): string { const config = vscode.workspace.getConfiguration(DBProjectConfigurationKey); const configured = config.get(microsoftBuildSqlVersionKey)?.trim(); - if (configured && semver.valid(configured)) { + if ( + configured && + (semver.valid(configured) || utils.isValidMicrosoftBuildSqlVersion(configured)) + ) { return configured; } - // Fall back to the hardcoded constant if config value is unavailable or invalid return FALLBACK_MICROSOFT_BUILD_SQL_VERSION; } +/** Thrown when no stable NuGet versions match the requested version prefix. */ +export class NoMatchingNugetVersionError extends Error { + constructor(message: string) { + super(message); + this.name = "NoMatchingNugetVersionError"; + } +} + +/** Resolves a floating NuGet version to the latest matching stable release. Exact versions are returned as-is. Throws on network failure. */ +export async function resolveNugetVersion(packageName: string, version: string): Promise { + if (semver.valid(version)) { + return version; + } + + try { + return await resolveFloatingVersion(packageName, version); + } catch (e) { + if ( + e instanceof NoMatchingNugetVersionError && + version !== FALLBACK_MICROSOFT_BUILD_SQL_VERSION + ) { + return resolveFloatingVersion(packageName, FALLBACK_MICROSOFT_BUILD_SQL_VERSION); + } + throw e; + } +} + +/** Resolves a floating version prefix to the latest stable exact version via the NuGet v3 API. */ +async function resolveFloatingVersion(packageName: string, version: string): Promise { + const indexUrl = `https://api.nuget.org/v3-flatcontainer/${packageName.toLowerCase()}/index.json`; + + const { requestUrl: resolvedUrl, config } = new HttpClient().setupRequest(indexUrl); + config.timeout = 10_000; + + const response = await axios.get<{ versions: string[] }>(resolvedUrl, config); + if (response.status !== 200) { + throw new Error(nugetIndexFetchFailed(packageName, response.status)); + } + + const resolved = semver.maxSatisfying(response.data.versions, version); + if (resolved) { + return resolved; + } + + throw new NoMatchingNugetVersionError(nugetVersionResolutionFailed(packageName, version)); +} + +export const enum netCoreInstallState { + netCoreNotPresent, + netCoreVersionNotSupported, + netCoreVersionSupported, +} + +const dotnet = os.platform() === "win32" ? "dotnet.exe" : "dotnet"; + export class NetCoreTool extends ShellExecutionHelper { private osPlatform: string = os.platform(); private netCoreSdkInstalledVersion: string | undefined; diff --git a/extensions/sql-database-projects/test/netCoreTool.test.ts b/extensions/sql-database-projects/test/netCoreTool.test.ts index 411576f43a..87148a5693 100644 --- a/extensions/sql-database-projects/test/netCoreTool.test.ts +++ b/extensions/sql-database-projects/test/netCoreTool.test.ts @@ -9,12 +9,14 @@ import * as fs from "fs"; import * as path from "path"; import * as vscode from "vscode"; import * as sinon from "sinon"; +import axios from "axios"; import { NetCoreTool, DBProjectConfigurationKey, DotnetInstallLocationKey, FALLBACK_MICROSOFT_BUILD_SQL_VERSION, getMicrosoftBuildSqlVersion, + resolveNugetVersion, } from "../src/tools/netcoreTool"; import { deleteGeneratedTestFolder, generateTestFolderPath } from "./testUtils"; import { createContext, TestContext } from "./testContext"; @@ -159,4 +161,62 @@ suite("NetCoreTool: Net core tests", function (): void { expect(result).to.equal(FALLBACK_MICROSOFT_BUILD_SQL_VERSION); }); }); + + suite("resolveNugetVersion tests", function (): void { + let axiosGetStub: sinon.SinonStub; + + setup(function (): void { + axiosGetStub = sandbox.stub(axios, "get"); + }); + + function stubNugetResponse(versions: string[]): void { + axiosGetStub.resolves({ status: 200, data: { versions } }); + } + + test("Should return exact version unchanged", async function (): Promise { + // No network call expected for exact versions + const result = await resolveNugetVersion("Microsoft.Build.Sql", "2.1.0"); + expect(result).to.equal("2.1.0"); + expect(axiosGetStub.callCount).to.equal(0); + }); + + test("Should resolve floating version to latest matching stable", async function (): Promise { + stubNugetResponse(["2.0.0", "2.1.0", "2.1.1", "2.1.1-preview", "3.0.0"]); + const result = await resolveNugetVersion("Microsoft.Build.Sql", "2.*"); + expect(result).to.equal("2.1.1"); + }); + + test("Should resolve floating minor version to latest matching stable", async function (): Promise { + stubNugetResponse(["2.1.0", "2.1.1", "2.2.0"]); + const result = await resolveNugetVersion("Microsoft.Build.Sql", "2.1.*"); + expect(result).to.equal("2.1.1"); + }); + + test("Should fall back to FALLBACK version when no match found for requested version", async function (): Promise { + // First call returns no matching versions (for "4.*") + // Second call (fallback to "2.*") returns matching versions + let callCount = 0; + axiosGetStub.callsFake(async () => { + callCount++; + const versions = callCount === 1 ? ["3.0.0"] : ["2.0.0", "2.1.0"]; + return { status: 200, data: { versions } }; + }); + const result = await resolveNugetVersion("Microsoft.Build.Sql", "4.*"); + expect(result).to.equal("2.1.0"); + }); + + test("Should throw when network error occurs and no fallback available", async function (): Promise { + axiosGetStub.rejects(new Error("network failure")); + // Use FALLBACK version so no second fallback attempt + try { + await resolveNugetVersion( + "Microsoft.Build.Sql", + FALLBACK_MICROSOFT_BUILD_SQL_VERSION, + ); + expect.fail("Should have thrown"); + } catch (e: unknown) { + expect((e as Error).message).to.include("network failure"); + } + }); + }); }); diff --git a/extensions/sql-database-projects/test/templates.test.ts b/extensions/sql-database-projects/test/templates.test.ts index 688aeee3e6..6099b91d7a 100644 --- a/extensions/sql-database-projects/test/templates.test.ts +++ b/extensions/sql-database-projects/test/templates.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { expect } from "chai"; +import * as semver from "semver"; import * as templates from "../src/templates/templates"; import { shouldThrowSpecificError, getTemplatesRootPath } from "./testUtils"; import { ItemType } from "sqldbproj"; @@ -336,9 +337,24 @@ suite("Templates", function (): void { templates.newSdkSqlProjectTemplate, "newSdkSqlProjectTemplate should not contain unexpanded @@MICROSOFT_BUILD_SQL_VERSION@@ placeholder", ).to.not.include("@@MICROSOFT_BUILD_SQL_VERSION@@"); - expect( - templates.newSdkSqlProjectTemplate, - `newSdkSqlProjectTemplate should contain the configured version "${configuredVersion}"`, - ).to.include(configuredVersion); + if (semver.valid(configuredVersion)) { + // Exact semver configured — the template should contain it as-is. + expect( + templates.newSdkSqlProjectTemplate, + `newSdkSqlProjectTemplate should contain the configured exact version "${configuredVersion}"`, + ).to.include(configuredVersion); + } else { + // Floating version (e.g. "2.*") — the template should contain the resolved + // exact semver, not the floating pattern itself. + const prefix = configuredVersion.slice(0, configuredVersion.lastIndexOf(".*") + 1); // "2." + expect( + templates.newSdkSqlProjectTemplate, + `newSdkSqlProjectTemplate should not contain unresolved floating version "${configuredVersion}"`, + ).to.not.include(configuredVersion); + expect( + templates.newSdkSqlProjectTemplate, + `newSdkSqlProjectTemplate should contain a resolved version starting with "${prefix}"`, + ).to.match(new RegExp(prefix.replace(".", "\\.") + "\\d")); + } }); }); diff --git a/localization/xliff/sql-database-projects.xlf b/localization/xliff/sql-database-projects.xlf index e466cc955e..59969ae8fa 100644 --- a/localization/xliff/sql-database-projects.xlf +++ b/localization/xliff/sql-database-projects.xlf @@ -178,6 +178,9 @@ Could not build project. Error retrieving files needed to build. + + Could not resolve Microsoft.Build.Sql version from NuGet ({0}). Using {1}. + Create @@ -379,6 +382,9 @@ Failed + + Failed to fetch NuGet index for {0}: HTTP {1} + File @@ -508,6 +514,9 @@ No data sources in this project + + No stable versions of {0} found matching {1}. + No {0} found