diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index b8f1ea58e0..428ff81f9d 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -14,8 +14,6 @@ export const dataSourcesFileName = 'datasources.json'; export const sqlprojExtension = '.sqlproj'; export const sqlFileExtension = '.sql'; export const schemaCompareExtensionId = 'microsoft.schema-compare'; -export const sqlDatabaseProjectExtensionId = 'microsoft.sql-database-projects'; -export const mssqlExtensionId = 'microsoft.mssql'; export const master = 'master'; export const masterDacpac = 'master.dacpac'; export const msdb = 'msdb'; @@ -215,7 +213,7 @@ export function invalidInput(input: string) { return localize('invalidInput', "I export function invalidProjectPropertyValue(propertyName: string) { return localize('invalidPropertyValue', "Invalid value specified for the property '{0}' in .sqlproj file", propertyName); } export function unableToCreatePublishConnection(input: string) { return localize('unableToCreatePublishConnection', "Unable to construct connection: {0}", input); } export function circularProjectReference(project1: string, project2: string) { return localize('cicularProjectReference', "Circular reference from project {0} to project {1}", project1, project2); } -export function mssqlNotFound(mssqlConfigDir: string) { return localize('mssqlNotFound', "Could not get mssql extension's install location at {0}", mssqlConfigDir); } +export function sqlToolsServiceNotFound(sqlToolsServiceDir: string) { return localize('mssqlNotFound', "Could not get SQL Tools Service install location at {0}", sqlToolsServiceDir); } export function projBuildFailed(errorMessage: string) { return localize('projBuildFailed', "Build failed. Check output pane for more details. {0}", errorMessage); } export function unexpectedProjectContext(uri: string) { return localize('unexpectedProjectContext', "Unable to establish project context. Command invoked from unexpected location: {0}", uri); } export function unableToPerformAction(action: string, uri: string) { return localize('unableToPerformAction', "Unable to locate '{0}' target: '{1}'", action, uri); } diff --git a/extensions/sql-database-projects/src/common/utils.ts b/extensions/sql-database-projects/src/common/utils.ts index 92df925983..c2ae4f2560 100644 --- a/extensions/sql-database-projects/src/common/utils.ts +++ b/extensions/sql-database-projects/src/common/utils.ts @@ -13,6 +13,7 @@ import * as dataworkspace from 'dataworkspace'; import * as mssql from '../../../mssql'; import * as vscodeMssql from 'vscode-mssql'; import { promises as fs } from 'fs'; +import { Project } from '../models/project'; /** * Consolidates on the error message string @@ -253,6 +254,7 @@ export function getDataWorkspaceExtensionApi(): dataworkspace.IExtension { } export type IDacFxService = mssql.IDacFxService | vscodeMssql.IDacFxService; +export type ISchemaCompareService = mssql.ISchemaCompareService | vscodeMssql.ISchemaCompareService; export async function getDacFxService(): Promise { if (getAzdataApi()) { @@ -265,18 +267,40 @@ export async function getDacFxService(): Promise { } } +export async function getSchemaCompareService(): Promise { + if (getAzdataApi()) { + const ext = vscode.extensions.getExtension(mssql.extension.name) as vscode.Extension; + const api = await ext.activate(); + return api.schemaCompare; + } else { + const api = await getVscodeMssqlApi(); + return api.schemaCompare; + } +} + export async function getVscodeMssqlApi(): Promise { const ext = vscode.extensions.getExtension(vscodeMssql.extension.name) as vscode.Extension; return ext.activate(); } /* - * Returns the default deployment options from DacFx + * Returns the default deployment options from DacFx, filtered to appropriate options for the given project. */ -export async function GetDefaultDeploymentOptions(): Promise { - const service = (vscode.extensions.getExtension(mssql.extension.name)!.exports as mssql.IExtension).schemaCompare; - const result = await service.schemaCompareGetDefaultOptions(); +export async function getDefaultPublishDeploymentOptions(project: Project): Promise { + const schemaCompareService = await getSchemaCompareService(); + const result = await schemaCompareService.schemaCompareGetDefaultOptions(); + const deploymentOptions = result.defaultDeploymentOptions; + // re-include database-scoped credentials + if (getAzdataApi()) { + deploymentOptions.excludeObjectTypes = (deploymentOptions as mssql.DeploymentOptions).excludeObjectTypes.filter(x => x !== mssql.SchemaObjectType.DatabaseScopedCredentials); + } else { + deploymentOptions.excludeObjectTypes = (deploymentOptions as vscodeMssql.DeploymentOptions).excludeObjectTypes.filter(x => x !== vscodeMssql.SchemaObjectType.DatabaseScopedCredentials); + } + // this option needs to be true for same database references validation to work + if (project.databaseReferences.length > 0) { + deploymentOptions.includeCompositeObjects = true; + } return result.defaultDeploymentOptions; } diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index dccca5aa07..f971aa3f49 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -39,6 +39,19 @@ import { SqlTargetPlatform } from 'sqldbproj'; const maxTableLength = 10; +/** + * This is a duplicate of the TaskExecutionMode from azdata.d.ts/vscode-mssql.d.ts, which is needed + * for using when running in VS Code since we don't have an actual implementation of the enum at runtime + * (unlike azdata which is injected by the extension host). Even specifying it as a const enum in the + * typings file currently doesn't work as the TypeScript compiler doesn't currently inline const enum + * values imported as "import type" https://github.com/microsoft/TypeScript/issues/40344 + */ +export enum TaskExecutionMode { + execute = 0, + script = 1, + executeAndScript = 2 +} + /** * Controller for managing lifecycle of projects */ @@ -252,7 +265,7 @@ export class ProjectsController { return publishDatabaseDialog; } else { - launchPublishDatabaseQuickpick(project); + launchPublishDatabaseQuickpick(project, this); return undefined; } } @@ -268,9 +281,7 @@ export class ProjectsController { const telemetryProps: Record = {}; const telemetryMeasures: Record = {}; const buildStartTime = new Date().getTime(); - const dacpacPath = await this.buildProject(project); - const buildEndTime = new Date().getTime(); telemetryMeasures.buildDuration = buildEndTime - buildStartTime; telemetryProps.buildSucceeded = (dacpacPath !== '').toString(); @@ -287,7 +298,6 @@ export class ProjectsController { // copy dacpac to temp location before publishing const tempPath = path.join(os.tmpdir(), `${path.parse(dacpacPath).name}_${new Date().getTime()}${constants.sqlprojExtension}`); await fs.copyFile(dacpacPath, tempPath); - const dacFxService = await utils.getDacFxService(); let result: mssql.DacFxResult; @@ -308,19 +318,19 @@ export class ProjectsController { if (publish) { telemetryProps.publishAction = 'deploy'; if (azdataApi) { - result = await (dacFxService as mssql.IDacFxService).deployDacpac(tempPath, settings.databaseName, true, settings.connectionUri, azdataApi.TaskExecutionMode.execute, settings.sqlCmdVariables, settings.deploymentOptions); + result = await (dacFxService as mssql.IDacFxService).deployDacpac(tempPath, settings.databaseName, true, settings.connectionUri, azdataApi.TaskExecutionMode.execute, settings.sqlCmdVariables, settings.deploymentOptions as mssql.DeploymentOptions); } else { - // TODO@chgagnon Fix typing - result = await (dacFxService as mssqlVscode.IDacFxService).deployDacpac(tempPath, settings.databaseName, true, settings.connectionUri, azdataApi!.TaskExecutionMode.execute, settings.sqlCmdVariables, settings.deploymentOptions); + // Have to cast to unknown first to get around compiler error since the mssqlVscode doesn't exist as an actual module at runtime + result = await (dacFxService as mssqlVscode.IDacFxService).deployDacpac(tempPath, settings.databaseName, true, settings.connectionUri, TaskExecutionMode.execute as unknown as mssqlVscode.TaskExecutionMode, settings.sqlCmdVariables, settings.deploymentOptions as mssqlVscode.DeploymentOptions); } } else { telemetryProps.publishAction = 'generateScript'; if (azdataApi) { - result = await (dacFxService as mssql.IDacFxService).generateDeployScript(tempPath, settings.databaseName, settings.connectionUri, azdataApi.TaskExecutionMode.script, settings.sqlCmdVariables, settings.deploymentOptions); + result = await (dacFxService as mssql.IDacFxService).generateDeployScript(tempPath, settings.databaseName, settings.connectionUri, azdataApi.TaskExecutionMode.script, settings.sqlCmdVariables, settings.deploymentOptions as mssql.DeploymentOptions); } else { - // TODO@chgagnon Fix typing - result = await (dacFxService as mssqlVscode.IDacFxService).generateDeployScript(tempPath, settings.databaseName, settings.connectionUri, undefined/*mssqlVscode.TaskExecutionMode.script*/, settings.sqlCmdVariables, settings.deploymentOptions); + // Have to cast to unknown first to get around compiler error since the mssqlVscode doesn't exist as an actual module at runtime + result = await (dacFxService as mssqlVscode.IDacFxService).generateDeployScript(tempPath, settings.databaseName, settings.connectionUri, TaskExecutionMode.script as unknown as mssqlVscode.TaskExecutionMode, settings.sqlCmdVariables, settings.deploymentOptions as mssqlVscode.DeploymentOptions); } } @@ -337,10 +347,8 @@ export class ProjectsController { const currentPublishIndex = this.publishInfo.findIndex(d => d.startDate === currentPublishTimeInfo); this.publishInfo[currentPublishIndex].status = Status.failed; this.publishInfo[currentPublishIndex].timeToCompleteAction = utils.timeConversion(timeToFailurePublish); - throw err; } - const actionEndTime = new Date().getTime(); const timeToPublish = actionEndTime - actionStartTime; telemetryProps.actionDuration = timeToPublish.toString(); diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts index 6e848cf767..9eb8a0af35 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts @@ -11,7 +11,7 @@ import * as utils from '../common/utils'; import { Project } from '../models/project'; import { SqlConnectionDataSource } from '../models/dataSources/sqlConnectionStringSource'; import { IDeploySettings } from '../models/IDeploySettings'; -import { DeploymentOptions, SchemaObjectType } from '../../../mssql/src/mssql'; +import { DeploymentOptions } from '../../../mssql/src/mssql'; import { IconPathHelper } from '../common/iconHelper'; import { cssStyles } from '../common/uiConstants'; import { getConnectionName } from './utils'; @@ -221,15 +221,8 @@ export class PublishDatabaseDialog { // eventually, database options will be configurable in this dialog // but for now, just send the default DacFx deployment options if no options were loaded from a publish profile if (!this.deploymentOptions) { - this.deploymentOptions = await utils.GetDefaultDeploymentOptions(); - - // re-include database-scoped credentials - this.deploymentOptions.excludeObjectTypes = this.deploymentOptions.excludeObjectTypes.filter(x => x !== SchemaObjectType.DatabaseScopedCredentials); - - // this option needs to be true for same database references validation to work - if (this.project.databaseReferences.length > 0) { - this.deploymentOptions.includeCompositeObjects = true; - } + // We only use the dialog in ADS context currently so safe to cast to the mssql DeploymentOptions here + this.deploymentOptions = await utils.getDefaultPublishDeploymentOptions(this.project) as DeploymentOptions; } return this.deploymentOptions; diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseQuickpick.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseQuickpick.ts index d4334e7c66..3ead5e86ed 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseQuickpick.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseQuickpick.ts @@ -8,13 +8,15 @@ import * as constants from '../common/constants'; import { Project } from '../models/project'; import { PublishProfile, readPublishProfile } from '../models/publishProfile/publishProfile'; import { promptForPublishProfile } from './publishDatabaseDialog'; -import { getVscodeMssqlApi } from '../common/utils'; +import { getDefaultPublishDeploymentOptions, getVscodeMssqlApi } from '../common/utils'; import { IConnectionInfo } from 'vscode-mssql'; +import { ProjectsController } from '../controllers/projectController'; +import { IDeploySettings } from '../models/IDeploySettings'; /** * Create flow for Publishing a database using only VS Code-native APIs such as QuickPick */ -export async function launchPublishDatabaseQuickpick(project: Project): Promise { +export async function launchPublishDatabaseQuickpick(project: Project, projectController: ProjectsController): Promise { // 1. Select publish settings file (optional) // Create custom quickpick so we can control stuff like displaying the loading indicator @@ -192,12 +194,13 @@ export async function launchPublishDatabaseQuickpick(project: Project): Promise< // TODO@chgagnon: Get deployment options // 6. Generate script/publish - // let settings: IDeploySettings | IGenerateScriptSettings = { - // databaseName: databaseName, - // serverName: connectionProfile!.server, - // connectionUri: '', // TODO@chgagnon: Get from connection profile - // sqlCmdVariables: sqlCmdVariables, - // deploymentOptions: undefined, // await this.getDeploymentOptions(), - // profileUsed: !!publishProfile - // }; + let settings: IDeploySettings = { + databaseName: databaseName, + serverName: connectionProfile!.server, + connectionUri: connectionUri, + sqlCmdVariables: sqlCmdVariables, + deploymentOptions: await getDefaultPublishDeploymentOptions(project), + profileUsed: !!publishProfile + }; + await projectController.publishOrScriptProject(project, settings, action === constants.publish); } diff --git a/extensions/sql-database-projects/src/models/IDeploySettings.ts b/extensions/sql-database-projects/src/models/IDeploySettings.ts index c124fede5b..7575861818 100644 --- a/extensions/sql-database-projects/src/models/IDeploySettings.ts +++ b/extensions/sql-database-projects/src/models/IDeploySettings.ts @@ -3,7 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DeploymentOptions } from '../../../mssql/src/mssql'; +import { DeploymentOptions as mssqlDeploymentOptions } from '../../../mssql/src/mssql'; +import { DeploymentOptions as vscodeMssqlDeploymentOptions } from 'vscode-mssql'; + +export type DeploymentOptions = mssqlDeploymentOptions | vscodeMssqlDeploymentOptions; export interface IDeploySettings { databaseName: string; diff --git a/extensions/sql-database-projects/src/tools/buildHelper.ts b/extensions/sql-database-projects/src/tools/buildHelper.ts index e74ab978ce..7c6e822df3 100644 --- a/extensions/sql-database-projects/src/tools/buildHelper.ts +++ b/extensions/sql-database-projects/src/tools/buildHelper.ts @@ -6,10 +6,12 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as os from 'os'; -import * as constants from './../common/constants'; import { promises as fs } from 'fs'; import * as utils from '../common/utils'; -import { mssqlNotFound } from '../common/constants'; +import { sqlToolsServiceNotFound } from '../common/constants'; +import * as mssql from '../../../mssql/src/mssql'; +import * as vscodeMssql from 'vscode-mssql'; +import * as sqldbproj from 'sqldbproj'; const buildDirectory = 'BuildDirectory'; const buildFiles: string[] = [ @@ -33,7 +35,7 @@ export class BuildHelper { private initialized: boolean = false; constructor() { - this.extensionDir = vscode.extensions.getExtension(constants.sqlDatabaseProjectExtensionId)?.extensionPath ?? ''; + this.extensionDir = vscode.extensions.getExtension(sqldbproj.extension.name)?.extensionPath ?? ''; this.extensionBuildDir = path.join(this.extensionDir, buildDirectory); } @@ -59,18 +61,35 @@ export class BuildHelper { this.initialized = true; } - // get mssql sqltoolsservice path + /** + * Gets the path to the SQL Tools Service installation + * @returns + */ private async getBuildDirPathFromMssqlTools(): Promise { - const mssqlConfigDir = vscode.extensions.getExtension(constants.mssqlExtensionId)?.extensionPath ?? ''; + let mssqlConfigDir = ''; + if (utils.getAzdataApi()) { + mssqlConfigDir = vscode.extensions.getExtension(mssql.extension.name)?.extensionPath ?? ''; + } else { + // VS Code MSSQL extension has its tools service config in a slightly different spot + mssqlConfigDir = path.join(vscode.extensions.getExtension(vscodeMssql.extension.name)?.extensionPath ?? '', 'out', 'src'); + } + if (await utils.exists(path.join(mssqlConfigDir, 'config.json'))) { const rawConfig = await fs.readFile(path.join(mssqlConfigDir, 'config.json')); const config = JSON.parse(rawConfig.toString()); - const installDir = config.installDirectory?.replace('{#version#}', config.version).replace('{#platform#}', this.getPlatform()); + let installDir = ''; + if (utils.getAzdataApi()) { + installDir = config.installDirectory?.replace('{#version#}', config.version).replace('{#platform#}', this.getPlatform()); + } else { + // VS Code MSSQL extension has its config.json + installDir = config.service?.installDir?.replace('{#version#}', config.version).replace('{#platform#}', this.getPlatform()); + } + if (installDir) { return path.join(mssqlConfigDir, installDir); } } - throw new Error(mssqlNotFound(mssqlConfigDir)); + throw new Error(sqlToolsServiceNotFound(mssqlConfigDir)); } private getPlatform(): string {