Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
15ee5d0
initial changes
ssreerama Jan 20, 2026
6767671
Merge branch 'main' of https://github.com/microsoft/vscode-mssql
ssreerama Jan 20, 2026
b8b34b8
Merge branch 'main' of https://github.com/microsoft/vscode-mssql
ssreerama Jan 20, 2026
8b2063d
Merge branch 'main' of https://github.com/microsoft/vscode-mssql
ssreerama Jan 20, 2026
45f37e3
Merge branch 'main' of https://github.com/microsoft/vscode-mssql
ssreerama Jan 20, 2026
32ecd9a
Merge branch 'main' of https://github.com/microsoft/vscode-mssql
ssreerama Jan 20, 2026
04d7e2f
Merge branch 'main' of https://github.com/microsoft/vscode-mssql
ssreerama Jan 21, 2026
9901eb3
all changes complete, need to update tests if we have any
ssreerama Jan 22, 2026
fb24d28
loc
ssreerama Jan 22, 2026
0891c2c
improvements
ssreerama Jan 22, 2026
ae51225
updating changelog
ssreerama Jan 30, 2026
0b2c425
Merge branch 'main' into sai/bug_TaskDefWrongPlace
ssreerama Feb 3, 2026
144d08d
fixing test with task.json removal
ssreerama Feb 3, 2026
f398f72
addressing copilot review comments
ssreerama Feb 3, 2026
7896816
comments addressed
ssreerama Feb 4, 2026
1154f23
LOC
ssreerama Feb 4, 2026
7595651
Merge branch 'main' into sai/bug_TaskDefWrongPlace
ssreerama Feb 5, 2026
6e4731f
Merge branch 'main' into sai/bug_TaskDefWrongPlace
ssreerama Feb 6, 2026
e17bb5f
adding const to constants file and throwing error
ssreerama Feb 6, 2026
9e453ba
remove comment for absense of the else block
ssreerama Feb 6, 2026
c51dc21
updating the test to follow new pattern
ssreerama Feb 6, 2026
5328027
Merge branch 'main' into sai/bug_TaskDefWrongPlace
ssreerama Feb 6, 2026
dba88a2
updating the recently merged test that follows old pattern
ssreerama Feb 6, 2026
4e5b2a5
test fixes
ssreerama Feb 6, 2026
bfd8d76
remove dead constant
ssreerama Feb 7, 2026
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
2 changes: 2 additions & 0 deletions extensions/sql-database-projects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ All notable changes to the SQL Database Projects extension will be documented in

## [1.5.7] - 2026-02-27

- Added five new SQL object templates: Schema, Table-Valued Function, Trigger, Database Trigger, and Sequence.
- Fixed an issue where the SQL project build task was being created at the project level instead of the workspace level.
- Fixed an issue where system dacpac files were missing from the BuildDirectory, causing build failures for projects with system database references.

## [1.5.6] - 2026-01-28
Expand Down
5 changes: 4 additions & 1 deletion extensions/sql-database-projects/l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -379,5 +379,8 @@
"Move": "Move",
"Error when renaming file from {0} to {1}. Error: {2}": "Error when renaming file from {0} to {1}. Error: {2}",
"Unhandled node type for move": "Unhandled node type for move",
"A tasks.json file already exists in your workspace. Adding SQL project build task to the existing file.": "A tasks.json file already exists in your workspace. Adding SQL project build task to the existing file."
"A SQL Projects build task has been added to the existing tasks.json file.": "A SQL Projects build task has been added to the existing tasks.json file.",
"Builds the {0} SQL project": "Builds the {0} SQL project",
"Error updating existing tasks.json: {0}": "Error updating existing tasks.json: {0}",
"Invalid format in tasks.json: expected 'tasks' to be an array. Please fix the tasks.json file and try again.": "Invalid format in tasks.json: expected 'tasks' to be an array. Please fix the tasks.json file and try again."
}

This file was deleted.

18 changes: 16 additions & 2 deletions extensions/sql-database-projects/src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export const msdb = 'msdb';
export const MicrosoftDatatoolsSchemaSqlSql = 'Microsoft.Data.Tools.Schema.Sql.Sql';
export const databaseSchemaProvider = 'DatabaseSchemaProvider';
export const sqlProjectSdk = 'Microsoft.Build.Sql';
export const sdkStyleProjectStyleName = 'SdkStyle';
export const legacyStyleProjectStyleName = 'LegacyStyle';
export const problemMatcher = '$sqlproj-problem-matcher';
export const sqlProjTaskType = 'sqlproj-build';
export const dotnet = 'dotnet';
Expand Down Expand Up @@ -726,6 +728,18 @@ export const unhandledMoveNode = l10n.t("Unhandled node type for move");
//#endregion

//#region tasks.json
export const updatingExistingTasksJson = l10n.t("A tasks.json file already exists in your workspace. Adding SQL project build task to the existing file.");
export const sqlProjectBuildTaskLabel = 'sqlproj: Build';
export const updatingExistingTasksJson = l10n.t("A SQL Projects build task has been added to the existing tasks.json file.");
export const netCoreBuildArg = '/p:NetCoreBuild=true';
export const systemDacpacsLocationArgPrefix = '/p:SystemDacpacsLocation=';
export const netCoreTargetsPathArgPrefix = '/p:NETCoreTargetsPath=';
export const tasksJsonVersion = '2.0.0';
export const vscodeFolderName = '.vscode';
export const tasksJsonFileName = 'tasks.json';
export const processTaskType = 'process';
export const sqlprojBuildTaskLabelPrefix = 'sqlproj: Build';
export function getSqlProjectBuildTaskLabel(projectName: string): string { return `${sqlprojBuildTaskLabelPrefix} ${projectName}`; }
export function getSqlProjectBuildTaskDetail(projectName: string): string { return l10n.t("Builds the {0} SQL project", projectName); }
export function tasksJsonUpdateError(error: string): string { return l10n.t("Error updating existing tasks.json: {0}", error); }
export const tasksJsonInvalidTasksArrayError = l10n.t("Invalid format in tasks.json: expected 'tasks' to be an array. Please fix the tasks.json file and try again.");

//#endregion
3 changes: 2 additions & 1 deletion extensions/sql-database-projects/src/common/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@ export enum TelemetryActions {
SchemaComparisonFinished = 'SchemaComparisonFinished',
SchemaComparisonStarted = 'SchemaComparisonStarted',
rename = "rename",
move = "move"
move = "move",
tasksJsonError = "tasksJsonError"
}
39 changes: 39 additions & 0 deletions extensions/sql-database-projects/src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -873,3 +873,42 @@ export function throwIfFailed(result: azdataType.ResultStatus | vscodeMssql.Resu
throw new Error(constants.errorPrefix(result.errorMessage));
}
}

//#region Build Task helpers

export interface SqlProjectBuildTaskOptions {
projectName: string;
buildArgs: string[];
isDefault: boolean;
}

/**
* Creates a SQL project build task definition for tasks.json
*/
export function createSqlProjectBuildTask(options: SqlProjectBuildTaskOptions): object {
return {
label: constants.getSqlProjectBuildTaskLabel(options.projectName),
type: constants.processTaskType,
command: constants.dotnet,
args: options.buildArgs,
problemMatcher: constants.problemMatcher,
isBackground: true,
group: {
kind: constants.build,
isDefault: options.isDefault
},
detail: constants.getSqlProjectBuildTaskDetail(options.projectName)
};
}

/**
* Creates a new tasks.json structure with the given tasks
*/
export function createTasksJson(tasks: object[]): object {
return {
version: constants.tasksJsonVersion,
tasks: tasks
};
}

//#endregion
Original file line number Diff line number Diff line change
Expand Up @@ -261,13 +261,10 @@ export class ProjectsController {

// Determine the target folder: workspace root if available, otherwise project folder
const targetFolder = workspaceFolder ? workspaceFolder.uri.fsPath : project.projectFolderPath;
const vscodeFolder = path.join(targetFolder, '.vscode');
const tasksJsonPath = path.join(vscodeFolder, 'tasks.json');
const vscodeFolder = path.join(targetFolder, constants.vscodeFolderName);
const tasksJsonPath = path.join(vscodeFolder, constants.tasksJsonFileName);

// Generate the new SQL project build task from template
const tasksTemplate = templates.get(ItemType.tasks);
const newTasksContent = templates.macroExpansion(tasksTemplate.templateScript, new Map([['ConfigureDefaultBuild', configureDefaultBuild.toString()]]));
const newTasksJson = JSON.parse(newTasksContent);
const projectName = path.basename(project.projectFilePath, constants.sqlprojExtension);

// Check if tasks.json already exists at workspace level
if (await utils.exists(tasksJsonPath)) {
Expand All @@ -276,40 +273,90 @@ export class ProjectsController {
const existingContent = await fs.readFile(tasksJsonPath, 'utf8');
const existingTasksJson = JSON.parse(existingContent);

// Ensure tasks array exists
if (existingTasksJson.tasks !== undefined && !Array.isArray(existingTasksJson.tasks)) {
// This error is caught below — project creation still succeeds, only the tasks.json update is skipped
throw new Error(constants.tasksJsonInvalidTasksArrayError);
}

// Initialize tasks array if it doesn't exist yet
if (!existingTasksJson.tasks) {
existingTasksJson.tasks = [];
}

// Check if the SQL project build task already exists
// Check if a build task for this specific project already exists
const taskLabel = constants.getSqlProjectBuildTaskLabel(projectName);
const sqlBuildTaskExists = existingTasksJson.tasks.some(
(task: { label?: string; type?: string }) =>
task.label === constants.sqlProjectBuildTaskLabel ||
(task.label === 'Build' && task.type === 'shell')
(task: { label?: string }) =>
task.label === taskLabel
);

if (!sqlBuildTaskExists) {
// Merge the new task(s) into existing tasks
existingTasksJson.tasks.push(...newTasksJson.tasks);
// Only create the task when we need to add it
const newTask = this.createBuildTaskForProject(project, projectName, configureDefaultBuild);

// Merge the new task into existing tasks
existingTasksJson.tasks.push(newTask);

// Write back the merged tasks.json
await fs.writeFile(tasksJsonPath, JSON.stringify(existingTasksJson, null, '\t'), 'utf8');

// Show notification to user
void vscode.window.showInformationMessage(constants.updatingExistingTasksJson);
}
// If task already exists, do nothing
} catch (error) {
// If parsing fails, log error and skip
this._outputChannel.appendLine(`Error parsing existing tasks.json: ${error}`);
// If parsing fails, log error and notify user with option to view output
const errorMessage = utils.getErrorMessage(error);
this._outputChannel.appendLine(constants.tasksJsonUpdateError(errorMessage));

TelemetryReporter.createErrorEvent2(TelemetryViews.ProjectController, TelemetryActions.tasksJsonError, error)
.withAdditionalProperties({
hasWorkspaceFolder: (workspaceFolder !== undefined).toString(),
errorMessage: errorMessage
})
.send();

void utils.showErrorMessageWithOutputChannel(constants.tasksJsonUpdateError, errorMessage, this._outputChannel);
}
} else {
// Create new tasks.json at workspace level
// Create new tasks.json - only create the task when needed
const newTask = this.createBuildTaskForProject(project, projectName, configureDefaultBuild);
const newTasksJson = utils.createTasksJson([newTask]);

await fs.mkdir(vscodeFolder, { recursive: true });
await fs.writeFile(tasksJsonPath, JSON.stringify(newTasksJson, null, '\t'), 'utf8');

// If created inside the project folder (no workspace), add to project's None items
if (!workspaceFolder) {
await project.addNoneItem(utils.convertSlashesForSqlProj(`${constants.vscodeFolderName}/${constants.tasksJsonFileName}`));
}
}
}

// Note: We don't add tasks.json to the project's None items since it's at workspace level
/**
* Creates a build task object for the given project
*/
private createBuildTaskForProject(project: ISqlProject, projectName: string, configureDefaultBuild: boolean): object {
// Use forward slashes for cross-platform compatibility (dotnet accepts both)
const projectFilePathNormalized = utils.getPlatformSafeFileEntryPath(project.projectFilePath);

// Build args array for process execution (avoids shell escaping issues)
const buildArgs: string[] = [constants.build, projectFilePathNormalized];
if (project.sqlProjStyleName !== constants.sdkStyleProjectStyleName) {
// Legacy projects need additional build arguments
// No quotes needed - process execution handles paths with spaces correctly
const buildDirPath = utils.getPlatformSafeFileEntryPath(this.buildHelper.extensionBuildDirPath);
buildArgs.push(
constants.netCoreBuildArg,
`${constants.netCoreTargetsPathArgPrefix}${buildDirPath}`,
`${constants.systemDacpacsLocationArgPrefix}${buildDirPath}`
);
}

return utils.createSqlProjectBuildTask({
projectName,
buildArgs,
isDefault: configureDefaultBuild
});
}

private async addFileToProjectFromTemplate(project: ISqlProject, itemType: templates.ProjectScriptType, relativePath: string, expansionMacros: Map<string, string>): Promise<string> {
Expand Down
4 changes: 2 additions & 2 deletions extensions/sql-database-projects/src/models/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ export class Project implements ISqlProject {

public get sqlProjStyleName(): string {
if (utils.getAzdataApi()) {
return this.sqlProjStyle === mssql.ProjectType.SdkStyle ? 'SdkStyle' : 'LegacyStyle';
return this.sqlProjStyle === mssql.ProjectType.SdkStyle ? constants.sdkStyleProjectStyleName : constants.legacyStyleProjectStyleName;
} else {
return this.sqlProjStyle === vscodeMssql.ProjectType.SdkStyle ? 'SdkStyle' : 'LegacyStyle';
return this.sqlProjStyle === vscodeMssql.ProjectType.SdkStyle ? constants.sdkStyleProjectStyleName : constants.legacyStyleProjectStyleName;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ export async function loadTemplates(templateFolderPath: string) {
loadObjectTypeInfo(ItemType.externalStream, constants.externalStreamFriendlyName, templateFolderPath, 'newTsqlExternalStreamTemplate.sql'),
loadObjectTypeInfo(ItemType.externalStreamingJob, constants.externalStreamingJobFriendlyName, templateFolderPath, 'newTsqlExternalStreamingJobTemplate.sql'),
loadObjectTypeInfo(ItemType.publishProfile, constants.publishProfileFriendlyName, templateFolderPath, 'newPublishProfileTemplate.publish.xml'),
loadObjectTypeInfo(ItemType.tasks, constants.tasksJsonFriendlyName, templateFolderPath, 'tasksTemplate.json'),
loadObjectTypeInfo(ItemType.trigger, constants.triggerFriendlyName, templateFolderPath, 'newTsqlTriggerTemplate.sql'),
loadObjectTypeInfo(ItemType.databaseTrigger, constants.databaseTriggerFriendlyName, templateFolderPath, 'newTsqlDatabaseTriggerTemplate.sql'),
loadObjectTypeInfo(ItemType.sequence, constants.sequenceFriendlyName, templateFolderPath, 'newTsqlSequenceTemplate.sql')
Expand Down
6 changes: 3 additions & 3 deletions extensions/sql-database-projects/src/tools/buildHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ export class BuildHelper {
public constructBuildArguments(buildDirPath: string, sqlProjStyle: ProjectType): string[] {
buildDirPath = utils.getQuotedPath(buildDirPath);
const args: string[] = [
'/p:NetCoreBuild=true',
`/p:SystemDacpacsLocation=${buildDirPath}`
constants.netCoreBuildArg,
`${constants.systemDacpacsLocationArgPrefix}${buildDirPath}`
];

// Adding NETCoreTargetsPath only for non-SDK style projects
Expand All @@ -198,7 +198,7 @@ export class BuildHelper {
: sqlProjStyle === vscodeMssql.ProjectType.SdkStyle;

if (!isSdkStyle) {
args.push(`/p:NETCoreTargetsPath=${buildDirPath}`);
args.push(`${constants.netCoreTargetsPathArgPrefix}${buildDirPath}`);
}

return args;
Expand Down
5 changes: 3 additions & 2 deletions extensions/sql-database-projects/test/buildHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { TestContext, createContext } from './testContext';
import { ProjectType } from 'mssql';
import * as sqldbproj from 'sqldbproj';
import * as constants from '../src/common/constants';
import * as utils from '../src/common/utils';

suite('BuildHelper: Build Helper tests', function (): void {
test('Should get correct build arguments for legacy-style projects', function (): void {
Expand Down Expand Up @@ -96,7 +97,7 @@ suite('BuildHelper: Build Helper tests', function (): void {
const success = await buildHelper.createBuildDirFolder(testContext.outputChannel);

// Verify that the build directory was created successfully
should(success).equal(true, 'Build directory creation should succeed');
expect(success, 'Build directory creation should succeed').to.equal(true);

const buildDirPath = buildHelper.extensionBuildDirPath;

Expand Down Expand Up @@ -128,7 +129,7 @@ suite('BuildHelper: Build Helper tests', function (): void {
for (const fileName of allRequiredFiles) {
const filePath = path.join(buildDirPath, fileName);
const exists = await utils.exists(filePath);
should(exists).equal(true, `Required file '${fileName}' should exist in build directory at ${filePath}`);
expect(exists, `Required file '${fileName}' should exist in build directory at ${filePath}`).to.equal(true);
}
});
});
Expand Down
43 changes: 35 additions & 8 deletions extensions/sql-database-projects/test/projectController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,35 @@ suite('ProjectsController', function (): void {

setup(function (): void {
testContext = createContext();

const emptyResultSet = { ...mockDacFxResult };
sinon.stub(utils, 'getSqlProjectsService').resolves({
openProject: async () => undefined,
createProject: async () => mockDacFxResult,
getProjectProperties: async () => ({
...mockDacFxResult,
projectGuid: 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575',
configuration: 'Debug',
outputPath: 'bin/Debug',
defaultCollation: '',
databaseSource: '',
databaseSchemaProvider: 'Microsoft.Data.Tools.Schema.Sql.SqlAzureV12DatabaseSchemaProvider',
projectStyle: mssql.ProjectType.SdkStyle,
}),
getCrossPlatformCompatibility: async () => ({ ...emptyResultSet, isCrossPlatformCompatible: true }),
getSqlCmdVariables: async () => ({ ...emptyResultSet, sqlCmdVariables: [] }),
getDatabaseReferences: async () => ({
...emptyResultSet,
dacpacReferences: [],
sqlProjectReferences: [],
systemDatabaseReferences: [],
nugetPackageReferences: [],
}),
getPreDeploymentScripts: async () => ({ ...emptyResultSet, scripts: [] }),
getPostDeploymentScripts: async () => ({ ...emptyResultSet, scripts: [] }),
getNoneItems: async () => ({ ...emptyResultSet, scripts: [] }),
getSqlObjectScripts: async () => ({ ...emptyResultSet, scripts: [] }),
getFolders: async () => ({ ...emptyResultSet, folders: [] }),
} as unknown as mssql.ISqlProjectsService);
});

Expand Down Expand Up @@ -385,17 +412,17 @@ suite('ProjectsController', function (): void {

// Assert: tasks.json exists at workspace level
const exists = await utils.exists(tasksJsonPath);
should(exists).be.true('.vscode/tasks.json should be created at workspace level when configureDefaultBuild is true');
expect(exists, '.vscode/tasks.json should be created at workspace level when configureDefaultBuild is true').to.be.true;

// If exists, check if isDefault is true in any build task
if (exists) {
const tasksJsonContent = await fs.readFile(tasksJsonPath, 'utf-8');
const tasksJson = JSON.parse(tasksJsonContent);

should(tasksJson.tasks).be.Array().and.have.length(1);
expect(tasksJson.tasks, 'tasks should be an array').to.be.an('array').with.lengthOf(1);
const task = tasksJson.tasks[0];
should(task.group).not.be.undefined();
should(task.group.isDefault).equal('true', 'The build task should have isDefault: true');
expect(task.group, 'task group should be defined').to.not.be.undefined;
expect(task.group.isDefault, 'The build task should have isDefault: true (boolean)').to.equal(true);
}
});

Expand Down Expand Up @@ -445,12 +472,12 @@ suite('ProjectsController', function (): void {
const tasksJsonContent = await fs.readFile(tasksJsonPath, 'utf-8');
const tasksJson = JSON.parse(tasksJsonContent);

should(tasksJson.tasks).be.Array().and.have.length(2);
should(tasksJson.tasks[0].label).equal('Existing Task', 'Existing task should be preserved');
should(tasksJson.tasks[1].label).equal('Build', 'SQL build task should be added');
expect(tasksJson.tasks, 'tasks should be an array').to.be.an('array').with.lengthOf(2);
expect(tasksJson.tasks[0].label, 'Existing task should be preserved').to.equal('Existing Task');
expect(tasksJson.tasks[1].label, 'SQL build task should be added').to.equal(constants.getSqlProjectBuildTaskLabel('TestProjectWithTasks'));

// Assert: notification was shown
should(showInfoSpy.calledWith(constants.updatingExistingTasksJson)).be.true('Should show notification when updating existing tasks.json');
expect(showInfoSpy.calledWith(constants.updatingExistingTasksJson), 'Should show notification when updating existing tasks.json').to.be.true;
});

async function verifyFolderAdded(folderName: string, projController: ProjectsController, project: Project, node: BaseProjectTreeItem): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion extensions/sql-database-projects/test/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ suite('Templates', function (): void {
test('Should load all templates from files', async function (): Promise<void> {
// check expected counts

const numScriptObjectTypes = 17;
const numScriptObjectTypes = 16;

expect(templates.projectScriptTypes().length).to.equal(numScriptObjectTypes, `Expected ${numScriptObjectTypes} script object types to be loaded`);
expect(Object.keys(templates.projectScriptTypes()).length).to.equal(numScriptObjectTypes, `Expected ${numScriptObjectTypes} keys in script object types`);
Expand Down
Loading
Loading