/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; import * as constants from '../common/constants'; import * as utils from '../common/utils'; import type * as azdataType from 'azdata'; import * as vscode from 'vscode'; import * as mssql from 'mssql'; import * as vscodeMssql from 'vscode-mssql'; import { promises as fs } from 'fs'; import { Uri, window } from 'vscode'; import { EntryType, IDatabaseReferenceProjectEntry, ISqlProject, ItemType } from 'sqldbproj'; import { DataSource } from './dataSources/dataSources'; import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings, INugetPackageReferenceSettings, IUserDatabaseReferenceSettings } from './IDatabaseReferenceSettings'; import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; import { DacpacReferenceProjectEntry, FileProjectEntry, NugetPackageReferenceProjectEntry, SqlProjectReferenceProjectEntry, SystemDatabaseReferenceProjectEntry } from './projectEntry'; import { ResultStatus } from 'azdata'; import { BaseProjectTreeItem } from './tree/baseTreeItem'; import { FolderNode, NoneNode, PostDeployNode, PreDeployNode, PublishProfileNode, SqlObjectFileNode } from './tree/fileFolderTreeItem'; import { ProjectType, GetScriptsResult, GetFoldersResult } from '../common/typeHelper'; /** * Represents the configuration based on the Configuration property in the sqlproj */ enum Configuration { Debug = 'Debug', // default used if the Configuration property is not specified Release = 'Release', Output = 'Output' // if a string besides debug or release is used, then Output is used as the configuration } /** * Class representing a Project, and providing functions for operating on it */ export class Project implements ISqlProject { private sqlProjService!: utils.ISqlProjectsService; private _projectFilePath: string; private _projectFileName: string; private _projectGuid: string | undefined; private _sqlObjectScripts: FileProjectEntry[] = []; private _folders: FileProjectEntry[] = []; private _dataSources: DataSource[] = []; private _databaseReferences: IDatabaseReferenceProjectEntry[] = []; private _sqlCmdVariables: Map = new Map(); private _preDeployScripts: FileProjectEntry[] = []; private _postDeployScripts: FileProjectEntry[] = []; private _noneDeployScripts: FileProjectEntry[] = []; private _sqlProjStyle: ProjectType; private _isCrossPlatformCompatible: boolean = false; private _outputPath: string = ''; private _configuration: Configuration = Configuration.Debug; private _databaseSource: string = ''; private _publishProfiles: FileProjectEntry[] = []; private _defaultCollation: string = ''; private _databaseSchemaProvider: string = ''; //#endregion //#region Public Properties public get dacpacOutputPath(): string { return path.join(this.outputPath, `${this._projectFileName}.dacpac`); } public get projectFolderPath() { return Uri.file(path.dirname(this._projectFilePath)).fsPath; } public get projectFilePath(): string { return this._projectFilePath; } public get projectFileName(): string { return this._projectFileName; } public get projectGuid(): string | undefined { return this._projectGuid; } public get sqlObjectScripts(): FileProjectEntry[] { return this._sqlObjectScripts; } public get folders(): FileProjectEntry[] { return this._folders; } public get dataSources(): DataSource[] { return this._dataSources; } public get databaseReferences(): IDatabaseReferenceProjectEntry[] { return this._databaseReferences; } public get sqlCmdVariables(): Map { return this._sqlCmdVariables; } public get preDeployScripts(): FileProjectEntry[] { return this._preDeployScripts; } public get postDeployScripts(): FileProjectEntry[] { return this._postDeployScripts; } public get noneDeployScripts(): FileProjectEntry[] { return this._noneDeployScripts; } public get sqlProjStyle(): ProjectType { return this._sqlProjStyle; } public get sqlProjStyleName(): string { if (utils.getAzdataApi()) { return this.sqlProjStyle === mssql.ProjectType.SdkStyle ? 'SdkStyle' : 'LegacyStyle'; } else { return this.sqlProjStyle === vscodeMssql.ProjectType.SdkStyle ? 'SdkStyle' : 'LegacyStyle'; } } public get isCrossPlatformCompatible(): boolean { return this._isCrossPlatformCompatible; } public get outputPath(): string { return this._outputPath; } public get configuration(): Configuration { return this._configuration; } public get publishProfiles(): FileProjectEntry[] { return this._publishProfiles; } //#endregion constructor(projectFilePath: string) { this._projectFilePath = projectFilePath; this._projectFileName = path.basename(projectFilePath, '.sqlproj'); if (utils.getAzdataApi()) { this._sqlProjStyle = mssql.ProjectType.SdkStyle; } else { this._sqlProjStyle = vscodeMssql.ProjectType.SdkStyle } } /** * Open and load a .sqlproj file * @param projectFilePath * @param promptIfNeedsUpdating whether or not to prompt the user if the project needs to be updated * @param reload whether to reload the project from the project file * @returns */ public static async openProject(projectFilePath: string, promptIfNeedsUpdating: boolean = false, reload: boolean = false): Promise { const proj = new Project(projectFilePath); proj.sqlProjService = await utils.getSqlProjectsService(); if (reload) { // close the project in STS so that it will reload the project from the .sqlproj, rather than using the cached Project in STS await proj.sqlProjService.closeProject(projectFilePath); } await proj.readProjFile(); if (promptIfNeedsUpdating) { await this.checkPromptCrossPlatStatus(proj, false /* don't block the thread until the prompt*/); } return proj; } /** * If project does not support cross-plat building, prompts the user for whether to update and updates if accepted * @param project * @param blockingPrompt whether to block the thread until the user updates, or to fire and forget * @returns true if the project is updated after return, false if the user rejected the prompt */ public static async checkPromptCrossPlatStatus(project: Project, blockingPrompt: boolean): Promise { if (project.isCrossPlatformCompatible) { return true; } if (blockingPrompt) { const result = await window.showWarningMessage(constants.updateProjectForCrossPlatform(project.projectFileName), { modal: true }, constants.yesString, constants.noString); if (result === constants.yesString) { await project.updateProjectForCrossPlatform(); } } else { // use "void" with a .then() to not block the UI thread while prompting the user void window.showErrorMessage(constants.updateProjectForCrossPlatform(project.projectFileName), constants.yesString, constants.noString).then( async (result) => { if (result === constants.yesString) { try { await project.updateProjectForCrossPlatform(); } catch (error) { void window.showErrorMessage(utils.getErrorMessage(utils.getErrorMessage(error))); } } } ); } return project.isCrossPlatformCompatible; } /** * Reads the project setting and contents from the file */ public async readProjFile(): Promise { this.resetProject(); await this.readProjectProperties(); await this.readSqlCmdVariables(); await this.readDatabaseReferences(); // get pre and post deploy scripts specified in the sqlproj await this.readPreDeployScripts(true); await this.readPostDeployScripts(true); await this.readNoneItems(); // also populates list of publish profiles, determined by file extension await this.readSqlObjectScripts(); // get SQL object scripts await this.readFolders(); // get folders } //#region Reader helpers private async readProjectProperties(): Promise { let sqlProjService; if (utils.getAzdataApi()) { sqlProjService = this.sqlProjService as mssql.ISqlProjectsService; } else { sqlProjService = this.sqlProjService as vscodeMssql.ISqlProjectsService; } const result = await sqlProjService.getProjectProperties(this.projectFilePath); utils.throwIfFailed(result); this._projectGuid = result.projectGuid; switch (result.configuration.toLowerCase()) { case Configuration.Debug.toString().toLowerCase(): this._configuration = Configuration.Debug; break; case Configuration.Release.toString().toLowerCase(): this._configuration = Configuration.Release; break; default: this._configuration = Configuration.Output; // if the configuration doesn't match release or debug, the dacpac will get created in ./bin/Output } this._outputPath = path.isAbsolute(result.outputPath) ? result.outputPath : path.join(this.projectFolderPath, utils.getPlatformSafeFileEntryPath(result.outputPath)); this._databaseSource = result.databaseSource ?? ''; this._defaultCollation = result.defaultCollation; this._databaseSchemaProvider = result.databaseSchemaProvider; this._sqlProjStyle = result.projectStyle; await this.readCrossPlatformCompatibility(); } private async readCrossPlatformCompatibility(): Promise { const result = await this.sqlProjService.getCrossPlatformCompatibility(this.projectFilePath) utils.throwIfFailed(result); this._isCrossPlatformCompatible = result.isCrossPlatformCompatible; } private async readSqlCmdVariables(): Promise { const sqlcmdVariablesResult = await this.sqlProjService.getSqlCmdVariables(this.projectFilePath); if (!sqlcmdVariablesResult.success && sqlcmdVariablesResult.errorMessage) { throw new Error(constants.errorReadingProject(constants.sqlCmdVariables, this.projectFilePath, sqlcmdVariablesResult.errorMessage)); } this._sqlCmdVariables = new Map(); for (const variable of sqlcmdVariablesResult.sqlCmdVariables) { this._sqlCmdVariables.set(variable.varName, variable.defaultValue); // store the default value that's specified in the .sqlproj } } /** * Gets all the files specified by and removes all the files specified by * and all files included by the default glob of the folder of the sqlproj if it's an sdk style project */ private async readSqlObjectScripts(): Promise { const filesSet: Set = new Set(); var result: GetScriptsResult = await this.sqlProjService.getSqlObjectScripts(this.projectFilePath); utils.throwIfFailed(result); if (result.scripts?.length > 0) { // empty array from SqlToolsService is deserialized as null for (var script of result.scripts) { filesSet.add(script); } } // create a FileProjectEntry for each file const sqlObjectScriptEntries: FileProjectEntry[] = []; for (let f of Array.from(filesSet.values())) { // read file to check if it has a "Create Table" statement const fullPath = path.join(utils.getPlatformSafeFileEntryPath(this.projectFolderPath), utils.getPlatformSafeFileEntryPath(f)); const containsCreateTableStatement: boolean = await utils.fileContainsCreateTableStatement(fullPath, this.getProjectTargetVersion()); sqlObjectScriptEntries.push(this.createFileProjectEntry(f, EntryType.File, undefined, containsCreateTableStatement)); } this._sqlObjectScripts = sqlObjectScriptEntries; } private async readFolders(): Promise { var result: GetFoldersResult = await this.sqlProjService.getFolders(this.projectFilePath); utils.throwIfFailed(result); const folderEntries: FileProjectEntry[] = []; if (result.folders?.length > 0) { // empty array from SqlToolsService is deserialized as null for (var folderPath of result.folders) { // Don't include folders that aren't supported: // 1. Don't add Properties folder since it isn't supported in ADS.In SSDT, it isn't a physical folder, but it's specified in legacy sql projects // to display the Properties node in the project tree. // 2. Don't add external folders (relative path starts with "..") if (folderPath === constants.Properties || folderPath.startsWith(constants.RelativeOuterPath)) { continue; } folderEntries.push(this.createFileProjectEntry(folderPath, EntryType.Folder)); } } this._folders = folderEntries; } private async readPreDeployScripts(warnIfMultiple: boolean = false): Promise { var result: GetScriptsResult = await this.sqlProjService.getPreDeploymentScripts(this.projectFilePath); utils.throwIfFailed(result); const preDeploymentScriptEntries: FileProjectEntry[] = []; if (result.scripts?.length > 0) { // empty array from SqlToolsService is deserialized as null for (var scriptPath of result.scripts) { preDeploymentScriptEntries.push(this.createFileProjectEntry(scriptPath, EntryType.File)); } } if (preDeploymentScriptEntries.length > 1 && warnIfMultiple) { void window.showWarningMessage(constants.prePostDeployCount, constants.okString); } this._preDeployScripts = preDeploymentScriptEntries; } private async readPostDeployScripts(warnIfMultiple: boolean = false): Promise { var result: GetScriptsResult = await this.sqlProjService.getPostDeploymentScripts(this.projectFilePath); utils.throwIfFailed(result); const postDeploymentScriptEntries: FileProjectEntry[] = []; if (result.scripts?.length > 0) { // empty array from SqlToolsService is deserialized as null for (var scriptPath of result.scripts) { postDeploymentScriptEntries.push(this.createFileProjectEntry(scriptPath, EntryType.File)); } } if (postDeploymentScriptEntries.length > 1 && warnIfMultiple) { void window.showWarningMessage(constants.prePostDeployCount, constants.okString); } this._postDeployScripts = postDeploymentScriptEntries; } private async readNoneItems(): Promise { let sqlProjService; if (utils.getAzdataApi()) { sqlProjService = (await utils.getSqlProjectsService()) as mssql.ISqlProjectsService; } else { sqlProjService = (await utils.getSqlProjectsService()) as vscodeMssql.ISqlProjectsService; } var result: GetScriptsResult = await sqlProjService.getNoneItems(this.projectFilePath); utils.throwIfFailed(result); const noneItemEntries: FileProjectEntry[] = []; if (result.scripts?.length > 0) { // empty array from SqlToolsService is deserialized as null for (var path of result.scripts) { noneItemEntries.push(this.createFileProjectEntry(path, EntryType.File)); } } this._noneDeployScripts = []; this._publishProfiles = []; for (const entry of noneItemEntries) { if (utils.isPublishProfile(entry.relativePath)) { this._publishProfiles.push(entry); } else { this._noneDeployScripts.push(entry); } } } private async readDatabaseReferences(): Promise { this._databaseReferences = []; const databaseReferencesResult = await this.sqlProjService.getDatabaseReferences(this.projectFilePath); for (const dacpacReference of databaseReferencesResult.dacpacReferences) { this._databaseReferences.push(new DacpacReferenceProjectEntry({ dacpacFileLocation: Uri.file(dacpacReference.dacpacPath), suppressMissingDependenciesErrors: dacpacReference.suppressMissingDependencies, databaseVariableLiteralValue: dacpacReference.databaseVariableLiteralName, databaseName: dacpacReference.databaseVariable?.varName, databaseVariable: dacpacReference.databaseVariable?.value, serverName: dacpacReference.serverVariable?.varName, serverVariable: dacpacReference.serverVariable?.value })); } for (const projectReference of databaseReferencesResult.sqlProjectReferences) { this._databaseReferences.push(new SqlProjectReferenceProjectEntry({ projectName: path.basename(utils.getPlatformSafeFileEntryPath(projectReference.projectPath), constants.sqlprojExtension), projectGuid: projectReference.projectGuid ?? '', suppressMissingDependenciesErrors: projectReference.suppressMissingDependencies, projectRelativePath: Uri.file(utils.getPlatformSafeFileEntryPath(projectReference.projectPath)), databaseVariableLiteralValue: projectReference.databaseVariableLiteralName, databaseName: projectReference.databaseVariable?.varName, databaseVariable: projectReference.databaseVariable?.value, serverName: projectReference.serverVariable?.varName, serverVariable: projectReference.serverVariable?.value })); } for (const systemDbReference of databaseReferencesResult.systemDatabaseReferences) { let systemDb; if (utils.getAzdataApi()) { systemDb = systemDbReference.systemDb === mssql.SystemDatabase.Master ? constants.master : constants.msdb; } else { systemDb = systemDbReference.systemDb === vscodeMssql.SystemDatabase.Master ? constants.master : constants.msdb; } this._databaseReferences.push(new SystemDatabaseReferenceProjectEntry( systemDb, systemDbReference.databaseVariableLiteralName, systemDbReference.suppressMissingDependencies)); } for (const nupkgReference of databaseReferencesResult.nugetPackageReferences) { this._databaseReferences.push(new NugetPackageReferenceProjectEntry({ packageName: nupkgReference.packageName, packageVersion: nupkgReference.packageVersion, suppressMissingDependenciesErrors: nupkgReference.suppressMissingDependencies, databaseVariableLiteralValue: nupkgReference.databaseVariableLiteralName, databaseName: nupkgReference.databaseVariable?.varName, databaseVariable: nupkgReference.databaseVariable?.value, serverName: nupkgReference.serverVariable?.varName, serverVariable: nupkgReference.serverVariable?.value })); } } //#endregion private resetProject(): void { this._sqlObjectScripts = []; this._databaseReferences = []; this._sqlCmdVariables = new Map(); this._preDeployScripts = []; this._postDeployScripts = []; this._noneDeployScripts = []; this._outputPath = ''; this._configuration = Configuration.Debug; this._publishProfiles = []; } public async updateProjectForCrossPlatform(): Promise { if (this.isCrossPlatformCompatible) { return; } TelemetryReporter.sendActionEvent(TelemetryViews.ProjectController, TelemetryActions.updateProjectForRoundtrip); // due to bug in DacFx.Projects, if a backup file already exists this will fail // workaround is to rename the existing backup if (await utils.exists(this.projectFilePath + '_backup')) { let counter = 2; while (await utils.exists(this.projectFilePath + '_backup' + counter)) { counter++; } await fs.rename(this.projectFilePath + '_backup', this.projectFilePath + '_backup' + counter); } const result = await this.sqlProjService.updateProjectForCrossPlatform(this.projectFilePath); utils.throwIfFailed(result); await this.readCrossPlatformCompatibility(); } //#region Add/Delete/Exclude functions //#region Folders /** * Adds a folder to the project, and saves the project file * @param relativeFolderPath Relative path of the folder */ public async addFolder(relativeFolderPath: string): Promise { if (relativeFolderPath.endsWith('\\')) { relativeFolderPath = relativeFolderPath.slice(0, -1); } const result = await this.sqlProjService.addFolder(this.projectFilePath, relativeFolderPath); utils.throwIfFailed(result); // Note: adding a folder does not mean adding the contents of the folder. // SDK projects may still need to adjust their include/exclude globs, and Legacy projects must still include each file // in order for the contents of the folders to be added. await this.readFolders(); } public async deleteFolder(relativeFolderPath: string): Promise { const result = await this.sqlProjService.deleteFolder(this.projectFilePath, relativeFolderPath); utils.throwIfFailed(result); await this.readSqlObjectScripts(); await this.readPreDeployScripts(); await this.readPostDeployScripts(); await this.readNoneItems(); await this.readFolders(); } public async excludeFolder(relativeFolderPath: string): Promise { const result = await this.sqlProjService.excludeFolder(this.projectFilePath, relativeFolderPath); utils.throwIfFailed(result); await this.readSqlObjectScripts(); await this.readPreDeployScripts(); await this.readPostDeployScripts(); await this.readNoneItems(); await this.readFolders(); } public async moveFolder(relativeSourcePath: string, relativeDestinationPath: string): Promise { const result = await this.sqlProjService.moveFolder(this.projectFilePath, relativeSourcePath, relativeDestinationPath); utils.throwIfFailed(result); await this.readSqlObjectScripts(); await this.readPreDeployScripts(); await this.readPostDeployScripts(); await this.readNoneItems(); await this.readFolders(); } //#endregion //#region SQL object scripts public async addSqlObjectScript(relativePath: string, reloadAfter: boolean = true): Promise { const result = await this.sqlProjService.addSqlObjectScript(this.projectFilePath, relativePath); utils.throwIfFailed(result); if (reloadAfter) { await this.readSqlObjectScripts(); await this.readFolders(); } } public async addSqlObjectScripts(relativePaths: string[]): Promise { for (const path of relativePaths) { await this.addSqlObjectScript(path, false /* reloadAfter */); } await this.readSqlObjectScripts(); await this.readFolders(); } public async deleteSqlObjectScript(relativePath: string): Promise { const result = await this.sqlProjService.deleteSqlObjectScript(this.projectFilePath, relativePath); utils.throwIfFailed(result); await this.readSqlObjectScripts(); await this.readFolders(); } public async excludeSqlObjectScript(relativePath: string): Promise { const result = await this.sqlProjService.excludeSqlObjectScript(this.projectFilePath, relativePath); utils.throwIfFailed(result); await this.readSqlObjectScripts(); await this.readFolders(); } //#endregion //#region Pre-deployment scripts public async addPreDeploymentScript(relativePath: string): Promise { if (this.preDeployScripts.length > 0) { void vscode.window.showInformationMessage(constants.deployScriptExists(constants.PreDeploy)); } const result = await this.sqlProjService.addPreDeploymentScript(this.projectFilePath, relativePath); utils.throwIfFailed(result); await this.readPreDeployScripts(); await this.readNoneItems(); await this.readFolders(); } public async deletePreDeploymentScript(relativePath: string): Promise { const result = await this.sqlProjService.deletePreDeploymentScript(this.projectFilePath, relativePath); utils.throwIfFailed(result); await this.readPreDeployScripts(); await this.readFolders(); } public async excludePreDeploymentScript(relativePath: string): Promise { const result = await this.sqlProjService.excludePreDeploymentScript(this.projectFilePath, relativePath); utils.throwIfFailed(result); await this.readPreDeployScripts(); await this.readFolders(); } //#endregion //#region Post-deployment scripts public async addPostDeploymentScript(relativePath: string): Promise { if (this.postDeployScripts.length > 0) { void vscode.window.showInformationMessage(constants.deployScriptExists(constants.PostDeploy)); } const result = await this.sqlProjService.addPostDeploymentScript(this.projectFilePath, relativePath); utils.throwIfFailed(result); await this.readPostDeployScripts(); await this.readNoneItems(); await this.readFolders(); } public async deletePostDeploymentScript(relativePath: string): Promise { const result = await this.sqlProjService.deletePostDeploymentScript(this.projectFilePath, relativePath); utils.throwIfFailed(result); await this.readPostDeployScripts(); await this.readFolders(); } public async excludePostDeploymentScript(relativePath: string): Promise { const result = await this.sqlProjService.excludePostDeploymentScript(this.projectFilePath, relativePath); utils.throwIfFailed(result); await this.readPostDeployScripts(); await this.readFolders(); } //#endregion //#region None items public async addNoneItem(relativePath: string): Promise { const result = await this.sqlProjService.addNoneItem(this.projectFilePath, relativePath); utils.throwIfFailed(result); await this.readNoneItems(); await this.readFolders(); } public async deleteNoneItem(relativePath: string): Promise { const result = await this.sqlProjService.deleteNoneItem(this.projectFilePath, relativePath); utils.throwIfFailed(result); await this.readNoneItems(); await this.readFolders(); } public async excludeNoneItem(relativePath: string): Promise { const result = await this.sqlProjService.excludeNoneItem(this.projectFilePath, relativePath); utils.throwIfFailed(result); await this.readNoneItems(); await this.readFolders(); } //#endregion //#endregion /** * Writes a file to disk if contents are provided, adds that file to the project, and writes it to disk * * @param relativeFilePath Relative path of the file * @param contents Contents to be written to the new file * @param itemType Type of the project entry to add. This maps to the build action for the item. */ public async addScriptItem(relativeFilePath: string, contents?: string, itemType?: string): Promise { // Check if file already has been added to sqlproj const normalizedRelativeFilePath = utils.convertSlashesForSqlProj(relativeFilePath); const existingEntry = this.sqlObjectScripts.find(f => f.relativePath.toUpperCase() === normalizedRelativeFilePath.toUpperCase()); if (existingEntry) { return existingEntry; } // Ensure the file exists // TODO: can be pushed down to DacFx const absoluteFilePath = path.join(this.projectFolderPath, relativeFilePath); await utils.ensureFileExists(absoluteFilePath, contents); switch (itemType) { case ItemType.preDeployScript: await this.addPreDeploymentScript(relativeFilePath); break; case ItemType.postDeployScript: await this.addPostDeploymentScript(relativeFilePath); break; default: await this.addSqlObjectScript(relativeFilePath); break; } return this.createFileProjectEntry(normalizedRelativeFilePath, EntryType.File); } /** * Adds a file to the project, and saves the project file * * @param filePath Absolute path of the file */ public async addExistingItem(filePath: string): Promise { const exists = await utils.exists(filePath); if (!exists) { throw new Error(constants.noFileExist(filePath)); } const normalizedRelativeFilePath = utils.convertSlashesForSqlProj(path.relative(this.projectFolderPath, filePath)); let result: ResultStatus; if (path.extname(filePath) === constants.sqlFileExtension) { result = await this.sqlProjService.addSqlObjectScript(this.projectFilePath, normalizedRelativeFilePath) await this.readSqlObjectScripts(); } else { result = await this.sqlProjService.addNoneItem(this.projectFilePath, normalizedRelativeFilePath); await this.readNoneItems(); } utils.throwIfFailed(result); await this.readFolders(); return this.createFileProjectEntry(normalizedRelativeFilePath, EntryType.File); } /** * Set the target platform of the project * @param compatLevel compat level of project */ public async changeTargetPlatform(compatLevel: string): Promise { if (this.getProjectTargetVersion() === compatLevel) { return; } TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.changePlatformType) .withAdditionalProperties({ from: this.getProjectTargetVersion(), to: compatLevel }) .send(); this._databaseSchemaProvider = `${constants.MicrosoftDatatoolsSchemaSqlSql}${compatLevel}${constants.databaseSchemaProvider}`; const result = await this.sqlProjService.setDatabaseSchemaProvider(this.projectFilePath, this._databaseSchemaProvider); utils.throwIfFailed(result); } /** * Gets the project target version specified in the DSP property in the sqlproj */ public getProjectTargetVersion(): string { // Get version from dsp, which is a string like "Microsoft.Data.Tools.Schema.Sql.Sql130DatabaseSchemaProvider" // Remove prefix and suffix to only get the actual version number/name. For the example above, the result should be just '130'. const version = this._databaseSchemaProvider.substring( constants.MicrosoftDatatoolsSchemaSqlSql.length, this._databaseSchemaProvider.length - constants.databaseSchemaProvider.length); // make sure version is valid if (!Array.from(constants.targetPlatformToVersion.values()).includes(version)) { throw new Error(constants.invalidDataSchemaProvider); } return version; } /** * Gets the default database collation set in the project. * * @returns Default collation for the database set in the project. */ public getDatabaseDefaultCollation(): string { return this._defaultCollation; } //#region Database References /** * Adds reference to the appropriate system database dacpac to the project */ public async addSystemDatabaseReference(settings: ISystemDatabaseReferenceSettings): Promise { // check if reference to this database already exists if (this.databaseReferences.find(r => r.referenceName === settings.databaseVariableLiteralValue)) { throw new Error(constants.databaseReferenceAlreadyExists); } let systemDb, referenceType, result, sqlProjService; if (utils.getAzdataApi()) { systemDb = settings.systemDb as mssql.SystemDatabase; referenceType = settings.systemDbReferenceType as mssql.SystemDbReferenceType; sqlProjService = this.sqlProjService as mssql.ISqlProjectsService; result = await sqlProjService.addSystemDatabaseReference(this.projectFilePath, systemDb, settings.suppressMissingDependenciesErrors, referenceType, settings.databaseVariableLiteralValue); } else { systemDb = settings.systemDb as vscodeMssql.SystemDatabase; referenceType = settings.systemDbReferenceType as vscodeMssql.SystemDbReferenceType; sqlProjService = this.sqlProjService as vscodeMssql.ISqlProjectsService; result = await sqlProjService.addSystemDatabaseReference(this.projectFilePath, systemDb, settings.suppressMissingDependenciesErrors, referenceType, settings.databaseVariableLiteralValue); } if (!result.success && result.errorMessage) { throw new Error(constants.errorAddingDatabaseReference(utils.systemDatabaseToString(settings.systemDb), result.errorMessage)); } await this.readDatabaseReferences(); } /** * Adds reference to a dacpac to the project */ public async addDatabaseReference(settings: IDacpacReferenceSettings): Promise { const databaseReferenceEntry = new DacpacReferenceProjectEntry(settings); await this.addUserDatabaseReference(settings, databaseReferenceEntry); } /** * Adds reference to a another project in the workspace */ public async addProjectReference(settings: IProjectReferenceSettings): Promise { const projectReferenceEntry = new SqlProjectReferenceProjectEntry(settings); await this.addUserDatabaseReference(settings, projectReferenceEntry); } public async addNugetPackageReference(settings: INugetPackageReferenceSettings): Promise { const nupkgReferenceEntry = new NugetPackageReferenceProjectEntry(settings); await this.addUserDatabaseReference(settings, nupkgReferenceEntry); } private async addUserDatabaseReference(settings: IUserDatabaseReferenceSettings, reference: SqlProjectReferenceProjectEntry | DacpacReferenceProjectEntry | NugetPackageReferenceProjectEntry): Promise { // check if reference to this database already exists if (this.databaseReferenceExists(reference)) { throw new Error(constants.databaseReferenceAlreadyExists); } // create database variable if (settings.databaseVariable && settings.databaseName) { await this.sqlProjService.addSqlCmdVariable(this.projectFilePath, settings.databaseVariable, settings.databaseName); // create server variable - only can be set when there's also a database variable (reference to different database on different server) if (settings.serverVariable && settings.serverName) { await this.sqlProjService.addSqlCmdVariable(this.projectFilePath, settings.serverVariable, settings.serverName); } await this.readSqlCmdVariables(); } const databaseLiteral = settings.databaseVariable ? undefined : settings.databaseName; let result, referenceName; if (reference instanceof SqlProjectReferenceProjectEntry) { referenceName = (settings).projectName; result = await this.sqlProjService.addSqlProjectReference(this.projectFilePath, reference.pathForSqlProj(), reference.projectGuid, settings.suppressMissingDependenciesErrors, settings.databaseVariable, settings.serverVariable, databaseLiteral) } else if (reference instanceof DacpacReferenceProjectEntry) { referenceName = (settings).dacpacFileLocation.fsPath; result = await this.sqlProjService.addDacpacReference(this.projectFilePath, reference.pathForSqlProj(), settings.suppressMissingDependenciesErrors, settings.databaseVariable, settings.serverVariable, databaseLiteral) } else {// nupkg reference referenceName = (settings).packageName; result = await this.sqlProjService.addNugetPackageReference(this.projectFilePath, reference.packageName, (settings).packageVersion, settings.suppressMissingDependenciesErrors, settings.databaseVariable, settings.serverVariable, databaseLiteral) } if (!result.success && result.errorMessage) { throw new Error(constants.errorAddingDatabaseReference(referenceName, result.errorMessage)); } await this.readDatabaseReferences(); } private databaseReferenceExists(entry: IDatabaseReferenceProjectEntry): boolean { const found = this._databaseReferences.find(reference => reference.pathForSqlProj() === entry.pathForSqlProj()) !== undefined; return found; } public async deleteDatabaseReferenceByEntry(entry: IDatabaseReferenceProjectEntry): Promise { await this.deleteDatabaseReference(entry.pathForSqlProj()); } public async deleteDatabaseReference(name: string): Promise { const result = await this.sqlProjService.deleteDatabaseReference(this.projectFilePath, name); utils.throwIfFailed(result); await this.readDatabaseReferences(); } //#endregion //#region SQLCMD Variables /** * Adds a SQLCMD variable to the project * @param name name of the variable * @param defaultValue */ public async addSqlCmdVariable(name: string, defaultValue: string): Promise { const result = await this.sqlProjService.addSqlCmdVariable(this.projectFilePath, name, defaultValue); utils.throwIfFailed(result); await this.readSqlCmdVariables(); } /** * Updates a SQLCMD variable in the project * @param name name of the variable * @param defaultValue */ public async updateSqlCmdVariable(name: string, defaultValue: string): Promise { const result = await this.sqlProjService.updateSqlCmdVariable(this.projectFilePath, name, defaultValue); utils.throwIfFailed(result); await this.readSqlCmdVariables(); } public async deleteSqlCmdVariable(variableName: string): Promise { const result = await this.sqlProjService.deleteSqlCmdVariable(this.projectFilePath, variableName); utils.throwIfFailed(result); await this.readSqlCmdVariables(); } //#endregion /** * Appends given database source to the DatabaseSource property element. * If property element does not exist, then new one will be created. * * @param databaseSource Source of the database to add */ public async addDatabaseSource(databaseSource: string): Promise { if (databaseSource.includes(';')) { throw Error(constants.invalidProjectPropertyValueProvided(';')); } const sources: string[] = this.getDatabaseSourceValues(); const index = sources.findIndex(x => x === databaseSource); if (index !== -1) { return; } sources.push(databaseSource); const result = await this.sqlProjService.setDatabaseSource(this.projectFilePath, sources.join(';')); utils.throwIfFailed(result); await this.readProjectProperties(); } /** * Removes database source from the DatabaseSource property element. * If no sources remain, then property element will be removed from the project file. * * @param databaseSource Source of the database to remove */ public async removeDatabaseSource(databaseSource: string): Promise { if (databaseSource.includes(';')) { throw Error(constants.invalidProjectPropertyValueProvided(';')); } const sources: string[] = this.getDatabaseSourceValues(); const index = sources.findIndex(x => x === databaseSource); if (index === -1) { return; } sources.splice(index, 1); const result = await this.sqlProjService.setDatabaseSource(this.projectFilePath, sources.join(';')); utils.throwIfFailed(result); await this.readProjectProperties(); } /** * Gets an array of all database sources specified in the project. * * @returns Array of all database sources */ public getDatabaseSourceValues(): string[] { return this._databaseSource.trim() === '' ? [] : this._databaseSource.split(';'); } public createFileProjectEntry(relativePath: string, entryType: EntryType, sqlObjectType?: string, containsCreateTableStatement?: boolean): FileProjectEntry { let platformSafeRelativePath = utils.getPlatformSafeFileEntryPath(relativePath); return new FileProjectEntry( Uri.file(path.join(this.projectFolderPath, platformSafeRelativePath)), utils.convertSlashesForSqlProj(relativePath), entryType, sqlObjectType, containsCreateTableStatement); } /** * Moves a file to a different location * @param node Node being moved * @param destinationRelativePath path of the destination, relative to .sqlproj */ public async move(node: BaseProjectTreeItem, destinationRelativePath: string): Promise { // trim off the project folder at the beginning of the relative path stored in the tree const projectRelativeUri = vscode.Uri.file(path.basename(this.projectFilePath, constants.sqlprojExtension)); const originalRelativePath = utils.trimUri(projectRelativeUri, node.relativeProjectUri); destinationRelativePath = utils.trimUri(projectRelativeUri, vscode.Uri.file(destinationRelativePath)); if (originalRelativePath === destinationRelativePath) { return { success: true, errorMessage: '' }; } let result; if (node instanceof SqlObjectFileNode) { result = await this.sqlProjService.moveSqlObjectScript(this.projectFilePath, originalRelativePath, destinationRelativePath) } else if (node instanceof PreDeployNode) { result = await this.sqlProjService.movePreDeploymentScript(this.projectFilePath, originalRelativePath, destinationRelativePath) } else if (node instanceof PostDeployNode) { result = await this.sqlProjService.movePostDeploymentScript(this.projectFilePath, originalRelativePath, destinationRelativePath) } else if (node instanceof NoneNode || node instanceof PublishProfileNode) { result = await this.sqlProjService.moveNoneItem(this.projectFilePath, originalRelativePath, destinationRelativePath); } else if (node instanceof FolderNode) { result = await this.sqlProjService.moveFolder(this.projectFilePath, originalRelativePath, destinationRelativePath); } else { result = { success: false, errorMessage: constants.unhandledMoveNode } } return result; } }