Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions extensions/sql-database-projects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion extensions/sql-database-projects/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion extensions/sql-database-projects/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
},
"sqlDatabaseProjects.microsoftBuildSqlVersion": {
"type": "string",
"default": "2.1.0",
"default": "2.*",
"description": "%sqlDatabaseProjects.microsoftBuildSqlVersion%"
},
"sqlDatabaseProjects.autoCreateFolders": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ 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,
} from "../tools/netcoreTool";
import { BuildHelper } from "../tools/buildHelper";
import {
ISystemDatabaseReferenceSettings,
Expand Down Expand Up @@ -138,7 +143,10 @@ export class ProjectsController {
}

const sqlProjectsService = await utils.getSqlProjectsService();
const microsoftBuildSqlSDKStyleDefaultVersion = getMicrosoftBuildSqlVersion();
const microsoftBuildSqlSDKStyleDefaultVersion = await resolveNugetVersion(
"Microsoft.Build.Sql",
getMicrosoftBuildSqlVersion(),
);
const projectStyle = creationParams.sdkStyle
? mssqlVscode.ProjectType.SdkStyle
: mssqlVscode.ProjectType.LegacyStyle;
Expand Down
12 changes: 10 additions & 2 deletions extensions/sql-database-projects/src/templates/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
),
Comment on lines +87 to +91
],
]),
Comment on lines 83 to +93
)),
),
loadObjectTypeInfo(
Expand Down
16 changes: 15 additions & 1 deletion extensions/sql-database-projects/src/tools/buildHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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 } from "./netcoreTool";
import { ProjectType } from "../common/typeHelper";
import * as vscodeMssql from "vscode-mssql";

Expand Down Expand Up @@ -131,6 +131,20 @@ export class BuildHelper {
nugetFolderWithExpectedfiles: string,
outputChannel: vscode.OutputChannel,
): Promise<boolean> {
// Resolve NuGet floating versions (e.g. "2.*") to an exact version via the NuGet v3 API
// before constructing the download URL, which requires an exact version.
if (/^\d+(\.\d+)*\.\*$/.test(nugetVersion)) {
try {
nugetVersion = await resolveNugetVersion(nugetName, nugetVersion);
outputChannel.appendLine(`Resolved ${nugetName} \u2192 ${nugetVersion}`);
} catch (e) {
Comment on lines +136 to +140
const errorMessage = utils.getErrorMessage(e);
outputChannel.appendLine(errorMessage);
void vscode.window.showErrorMessage(errorMessage);
return false;
}
}

Comment on lines +134 to +147
const fullNugetName = `${nugetName}.${nugetVersion}`;
const fullNugetPath = path.join(this.extensionBuildDir, `${fullNugetName}.nupkg`);

Expand Down
111 changes: 95 additions & 16 deletions extensions/sql-database-projects/src/tools/netcoreTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import * as child_process from "child_process";
import * as fs from "fs";
import * as https from "https";
import * as os from "os";
import * as path from "path";
import * as semver from "semver";
Expand Down Expand Up @@ -32,13 +33,6 @@ 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";

export const enum netCoreInstallState {
netCoreNotPresent,
netCoreVersionNotSupported,
Expand All @@ -47,27 +41,112 @@ export const enum netCoreInstallState {

const dotnet = os.platform() === "win32" ? "dotnet.exe" : "dotnet";

/**
* Default Microsoft.Build.Sql version. Uses a NuGet floating version so that projects and
* legacy DLL downloads always target the latest 2.x release. To upgrade to 3.x in the future,
* change only this constant (and the matching default in package.json).
*/
export const FALLBACK_MICROSOFT_BUILD_SQL_VERSION = "2.*";

/**
* Returns true if the version string is a valid semver or a NuGet floating version (e.g. "2.*", "2.1.*").
*/
function isValidMicrosoftBuildSqlVersion(version: string): boolean {
return !!semver.valid(version) || /^\d+(\.\d+)*\.\*$/.test(version);
}

/**
* 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).
* Accepts both exact semver versions and NuGet floating versions (e.g. "2.*").
* When a floating version is returned, callers that construct NuGet download URLs must resolve
* it to an exact version first via the NuGet v3 index API.
* Falls back to FALLBACK_MICROSOFT_BUILD_SQL_VERSION if the user-configured value is absent or invalid.
*/
export function getMicrosoftBuildSqlVersion(): string {
const config = vscode.workspace.getConfiguration(DBProjectConfigurationKey);
const configured = config.get<string>(microsoftBuildSqlVersionKey)?.trim();
if (configured && semver.valid(configured)) {
if (configured && isValidMicrosoftBuildSqlVersion(configured)) {
return configured;
}

// Fall back to the hardcoded constant if config value is unavailable or invalid
return FALLBACK_MICROSOFT_BUILD_SQL_VERSION;
}

/**
* Resolves a NuGet floating version (e.g. "2.*", "2.1.*") to the latest matching stable
* exact version by querying the NuGet v3 flat-container index.
* If the version is already exact (valid semver), it is returned as-is.
* If the requested floating version has no matching stable releases on NuGet, falls back to
* FALLBACK_MICROSOFT_BUILD_SQL_VERSION and shows a VS Code warning.
* Throws only if the fallback also cannot be resolved.
*/
export async function resolveNugetVersion(packageName: string, version: string): Promise<string> {
if (semver.valid(version)) {
return version; // Already exact — nothing to resolve.
}

try {
return await resolveFloatingVersion(packageName, version);
} catch {
// The requested version (e.g. "4.*") has no stable matches — fall back to the default.
if (version !== FALLBACK_MICROSOFT_BUILD_SQL_VERSION) {
void vscode.window.showWarningMessage(
`No stable versions of ${packageName} found matching ${version}. ` +
`Falling back to ${FALLBACK_MICROSOFT_BUILD_SQL_VERSION}.`,
);
return resolveFloatingVersion(packageName, FALLBACK_MICROSOFT_BUILD_SQL_VERSION);
}
throw new Error(
`No stable versions of ${packageName} found matching ${FALLBACK_MICROSOFT_BUILD_SQL_VERSION}.`,
);
Comment on lines +93 to +101
}
Comment on lines +88 to +102
}
Comment on lines +83 to +103

/**
* Core NuGet v3 index lookup — resolves a floating version prefix to the latest stable exact
* version. Throws if no match is found or the network call fails.
*/
async function resolveFloatingVersion(packageName: string, version: string): Promise<string> {
// "2.*" → prefix "2." | "2.1.*" → prefix "2.1."
const prefix = version.slice(0, version.lastIndexOf("*"));
const indexUrl = `https://api.nuget.org/v3-flatcontainer/${packageName.toLowerCase()}/index.json`;

return new Promise<string>((resolve, reject) => {
https
.get(indexUrl, (res) => {
Comment on lines +112 to +116
let data = "";
res.on("data", (chunk: string) => (data += chunk));
res.on("end", () => {
try {
const { versions } = JSON.parse(data) as { versions: string[] };
// Stable versions only (no pre-release), matching the prefix, pick the last (highest).
const matching = versions.filter(
(v) => v.startsWith(prefix) && !v.includes("-"),
);
if (matching.length > 0) {
resolve(matching[matching.length - 1]);
} else {
Comment on lines +122 to +128
reject(
new Error(
`No stable versions of ${packageName} found matching ${version}`,
),
);
}
Comment on lines +129 to +134
} catch (e) {
reject(
new Error(
`Failed to parse NuGet index for ${packageName}: ${(e as Error).message}`,
),
);
}
Comment on lines +135 to +141
});
})
.on("error", (e) =>
reject(new Error(`Failed to fetch NuGet index for ${packageName}: ${e.message}`)),
);
Comment on lines +144 to +146
});
}

export class NetCoreTool extends ShellExecutionHelper {
private osPlatform: string = os.platform();
private netCoreSdkInstalledVersion: string | undefined;
Expand Down
78 changes: 78 additions & 0 deletions extensions/sql-database-projects/test/netCoreTool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import * as fs from "fs";
import * as path from "path";
import * as vscode from "vscode";
import * as sinon from "sinon";
import * as https from "https";
import { EventEmitter } from "events";
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";
Expand Down Expand Up @@ -159,4 +162,79 @@ suite("NetCoreTool: Net core tests", function (): void {
expect(result).to.equal(FALLBACK_MICROSOFT_BUILD_SQL_VERSION);
});
});

test("resolveNugetVersion: exact version is returned as-is without calling NuGet API", async function (): Promise<void> {
const getSpy = sandbox.spy(https, "get");
// Older user-configured version must be respected — no upgrade to latest
expect(await resolveNugetVersion("Microsoft.Build.Sql", "2.0.0")).to.equal("2.0.0");
expect(await resolveNugetVersion("Microsoft.Build.Sql", "2.1.0")).to.equal("2.1.0");
expect(getSpy.called, "NuGet API should not be called for exact versions").to.be.false;
});

test("resolveNugetVersion: floating version resolves to latest stable match", async function (): Promise<void> {
const versions = ["2.0.0", "2.1.0", "2.2.0", "2.3.0-preview", "3.0.0"];
(sandbox.stub(https, "get") as sinon.SinonStub).callsFake(
(_url: string, callback: (res: EventEmitter) => void) => {
const res = new EventEmitter();
callback(res);
res.emit("data", JSON.stringify({ versions }));
res.emit("end");
return new EventEmitter();
},
);
expect(await resolveNugetVersion("Microsoft.Build.Sql", "2.*")).to.equal("2.2.0");
expect(await resolveNugetVersion("Microsoft.Build.Sql", "2.0.*")).to.equal("2.0.0");
});

test("resolveNugetVersion: throws when no stable versions match", async function (): Promise<void> {
(sandbox.stub(https, "get") as sinon.SinonStub).callsFake(
(_url: string, callback: (res: EventEmitter) => void) => {
const res = new EventEmitter();
callback(res);
res.emit("data", JSON.stringify({ versions: ["3.0.0"] }));
res.emit("end");
return new EventEmitter();
},
);
let threw = false;
try {
await resolveNugetVersion("Microsoft.Build.Sql", "2.*");
} catch {
threw = true;
}
expect(threw, "should throw when no versions match").to.be.true;
});

test("resolveNugetVersion: falls back to FALLBACK_MICROSOFT_BUILD_SQL_VERSION when configured version has no stable match", async function (): Promise<void> {
// Both calls go to the same NuGet index URL; versions contains only 2.x entries.
// First call (for "4.*") finds no 4.x match → triggers fallback.
// Second call (for "2.*") finds 2.2.0 → returned.
(sandbox.stub(https, "get") as sinon.SinonStub).callsFake(
(_url: string, callback: (res: EventEmitter) => void) => {
const res = new EventEmitter();
callback(res);
res.emit("data", JSON.stringify({ versions: ["2.0.0", "2.2.0"] }));
res.emit("end");
return new EventEmitter();
},
);
sandbox.stub(vscode.window, "showWarningMessage").resolves(undefined);
const result = await resolveNugetVersion("Microsoft.Build.Sql", "4.*");
expect(result).to.equal("2.2.0");
});

test("resolveNugetVersion: throws when NuGet API call fails", async function (): Promise<void> {
(sandbox.stub(https, "get") as sinon.SinonStub).callsFake(() => {
const req = new EventEmitter();
setImmediate(() => req.emit("error", new Error("network failure")));
return req;
});
let threw = false;
try {
await resolveNugetVersion("Microsoft.Build.Sql", "2.*");
} catch {
threw = true;
}
expect(threw, "should throw on network error").to.be.true;
});
});
Loading