diff --git a/extensions/import/src/wizard/dataTierApplicationWizard.ts b/extensions/import/src/wizard/dataTierApplicationWizard.ts index 3c1103e9b5..1149dadaa7 100644 --- a/extensions/import/src/wizard/dataTierApplicationWizard.ts +++ b/extensions/import/src/wizard/dataTierApplicationWizard.ts @@ -8,6 +8,7 @@ import * as nls from 'vscode-nls'; import * as sqlops from 'sqlops'; import { SelectOperationPage } from './pages/selectOperationpage'; import { DeployConfigPage } from './pages/deployConfigPage'; +import { DeployPlanPage } from './pages/deployPlanPage'; import { DeployActionPage } from './pages/deployActionPage'; import { DacFxSummaryPage } from './pages/dacFxSummaryPage'; import { ExportConfigPage } from './pages/exportConfigPage'; @@ -38,10 +39,17 @@ export enum Operation { export enum DeployOperationPath { selectOperation, deployOptions, + deployPlan, deployAction, summary } +export enum DeployNewOperationPath { + selectOperation, + deployOptions, + summary +} + export enum ExtractOperationPath { selectOperation, options, @@ -87,6 +95,7 @@ export class DataTierApplicationWizard { this.wizard = sqlops.window.modelviewdialog.createWizard('Data-tier Application Wizard'); let selectOperationWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.selectOperationPageName', 'Select an Operation')); let deployConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.deployConfigPageName', 'Select Deploy Dacpac Settings')); + let deployPlanWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.deployPlanPage', 'Review the deploy plan')); let deployActionWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.deployActionPageName', 'Select Action')); let summaryWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.summaryPageName', 'Summary')); let extractConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.extractConfigPageName', 'Select Extract Dacpac Settings')); @@ -95,6 +104,7 @@ export class DataTierApplicationWizard { this.pages.set('selectOperation', new Page(selectOperationWizardPage)); this.pages.set('deployConfig', new Page(deployConfigWizardPage)); + this.pages.set('deployPlan', new Page(deployPlanWizardPage)); this.pages.set('deployAction', new Page(deployActionWizardPage)); this.pages.set('extractConfig', new Page(extractConfigWizardPage)); this.pages.set('importConfig', new Page(importConfigWizardPage)); @@ -116,6 +126,12 @@ export class DataTierApplicationWizard { await deployConfigDacFxPage.start(); }); + deployPlanWizardPage.registerContent(async (view) => { + let deployPlanDacFxPage = new DeployPlanPage(this, deployPlanWizardPage, this.model, view); + this.pages.get('deployPlan').dacFxPage = deployPlanDacFxPage; + await deployPlanDacFxPage.start(); + }); + deployActionWizardPage.registerContent(async (view) => { let deployActionDacFxPage = new DeployActionPage(this, deployActionWizardPage, this.model, view); this.pages.get('deployAction').dacFxPage = deployActionDacFxPage; @@ -166,7 +182,7 @@ export class DataTierApplicationWizard { } }); - this.wizard.pages = [selectOperationWizardPage, deployConfigWizardPage, deployActionWizardPage, summaryWizardPage]; + this.wizard.pages = [selectOperationWizardPage, deployConfigWizardPage, deployPlanWizardPage, deployActionWizardPage, summaryWizardPage]; this.wizard.generateScriptButton.hidden = true; this.wizard.generateScriptButton.onClick(async () => await this.generateDeployScript()); this.wizard.doneButton.onClick(async () => await this.executeOperation()); @@ -323,10 +339,12 @@ export class DataTierApplicationWizard { break; } } - } else if ((this.selectedOperation === Operation.deploy || this.selectedOperation === Operation.generateDeployScript) && idx === DeployOperationPath.deployAction) { - page = this.pages.get('deployAction'); } else if (this.isSummaryPage(idx)) { page = this.pages.get('summary'); + } else if ((this.selectedOperation === Operation.deploy || this.selectedOperation === Operation.generateDeployScript) && idx === DeployOperationPath.deployPlan) { + page = this.pages.get('deployPlan'); + } else if ((this.selectedOperation === Operation.deploy || this.selectedOperation === Operation.generateDeployScript) && idx === DeployOperationPath.deployAction) { + page = this.pages.get('deployAction'); } return page; @@ -336,9 +354,24 @@ export class DataTierApplicationWizard { return this.selectedOperation === Operation.import && idx === ImportOperationPath.summary || this.selectedOperation === Operation.export && idx === ExportOperationPath.summary || this.selectedOperation === Operation.extract && idx === ExtractOperationPath.summary + || this.selectedOperation === Operation.deploy && !this.model.upgradeExisting && idx === DeployNewOperationPath.summary || (this.selectedOperation === Operation.deploy || this.selectedOperation === Operation.generateDeployScript) && idx === DeployOperationPath.summary; } + public async generateDeployPlan(): Promise { + let service = await DataTierApplicationWizard.getService(this.model.server.providerName); + let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId); + + let result = await service.generateDeployPlan(this.model.filePath, this.model.database, ownerUri, sqlops.TaskExecutionMode.execute); + + if (!result || !result.success) { + vscode.window.showErrorMessage( + localize('alertData.deployPlanErrorMessage', "Generating deploy plan failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown')); + } + + return result.report; + } + private static async getService(providerName: string): Promise { let service = sqlops.dataprotocol.getProvider(providerName, sqlops.DataProviderType.DacFxServicesProvider); return service; diff --git a/extensions/import/src/wizard/pages/dacFxSummaryPage.ts b/extensions/import/src/wizard/pages/dacFxSummaryPage.ts index 4224bc1507..a00f1766f7 100644 --- a/extensions/import/src/wizard/pages/dacFxSummaryPage.ts +++ b/extensions/import/src/wizard/pages/dacFxSummaryPage.ts @@ -131,7 +131,15 @@ export class DacFxSummaryPage extends BasePage { this.table.updateProperties({ data: data, - columns: ['Setting', 'Value'], + columns: [ + { + value: localize('dacfx.settingColumn', 'Setting'), + cssClass: 'align-with-header' + }, + { + value: localize('dacfx.valueColumn', 'Value'), + cssClass: 'align-with-header' + }], width: 700, height: 200 }); diff --git a/extensions/import/src/wizard/pages/deployActionPage.ts b/extensions/import/src/wizard/pages/deployActionPage.ts index ceb769dfac..baffb247da 100644 --- a/extensions/import/src/wizard/pages/deployActionPage.ts +++ b/extensions/import/src/wizard/pages/deployActionPage.ts @@ -54,6 +54,8 @@ export class DeployActionPage extends DacFxConfigPage { } async onPageEnter(): Promise { + // generate script file path in case the database changed since last time the page was entered + this.setDefaultScriptFilePath(); return true; } @@ -120,11 +122,7 @@ export class DeployActionPage extends DacFxConfigPage { this.createFileBrowserParts(); //default filepath - let now = new Date(); - let datetime = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate() + '-' + now.getHours() + '-' + now.getMinutes(); - this.fileTextBox.value= path.join(os.homedir(), this.model.database + '_UpgradeDACScript_' + datetime + '.sql'); - this.model.scriptFilePath = this.fileTextBox.value; - + this.setDefaultScriptFilePath(); this.fileButton.onDidClick(async (click) => { let fileUri = await vscode.window.showSaveDialog( { @@ -151,15 +149,15 @@ export class DeployActionPage extends DacFxConfigPage { return { title: '', components: [ - { - title: localize('dacfx.generatedScriptLocation','Deployment Script Location'), - component: this.fileTextBox, - layout: { - horizontal: true, - componentWidth: 400 - }, - actions: [this.fileButton] - },], + { + title: localize('dacfx.generatedScriptLocation', 'Deployment Script Location'), + component: this.fileTextBox, + layout: { + horizontal: true, + componentWidth: 400 + }, + actions: [this.fileButton] + },], }; } @@ -168,6 +166,13 @@ export class DeployActionPage extends DacFxConfigPage { this.fileButton.enabled = enable; } + private setDefaultScriptFilePath(): void { + let now = new Date(); + let datetime = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate() + '-' + now.getHours() + '-' + now.getMinutes(); + this.fileTextBox.value = path.join(os.homedir(), this.model.database + '_UpgradeDACScript_' + datetime + '.sql'); + this.model.scriptFilePath = this.fileTextBox.value; + } + public setupNavigationValidator() { this.instance.registerNavigationValidator(() => { return true; diff --git a/extensions/import/src/wizard/pages/deployConfigPage.ts b/extensions/import/src/wizard/pages/deployConfigPage.ts index 0cf01695d5..1eac766644 100644 --- a/extensions/import/src/wizard/pages/deployConfigPage.ts +++ b/extensions/import/src/wizard/pages/deployConfigPage.ts @@ -10,7 +10,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as os from 'os'; import { DacFxDataModel } from '../api/models'; -import { DataTierApplicationWizard } from '../dataTierApplicationWizard'; +import { DataTierApplicationWizard, DeployOperationPath, Operation } from '../dataTierApplicationWizard'; import { DacFxConfigPage } from '../api/dacFxConfigPage'; const localize = nls.loadMessageBundle(); @@ -122,6 +122,12 @@ export class DeployConfigPage extends DacFxConfigPage { this.formBuilder.removeFormItem(this.databaseComponent); this.formBuilder.addFormItem(this.databaseDropdownComponent, { horizontal: true, componentWidth: 400 }); this.model.database = (this.databaseDropdown.value).name; + + // add deploy plan and generate script pages + let deployPlanPage = this.instance.pages.get('deployPlan'); + this.instance.wizard.addPage(deployPlanPage.wizardPage, DeployOperationPath.deployPlan); + let deployActionPage = this.instance.pages.get('deployAction'); + this.instance.wizard.addPage(deployActionPage.wizardPage, DeployOperationPath.deployAction); }); newRadioButton.onDidClick(() => { @@ -129,6 +135,11 @@ export class DeployConfigPage extends DacFxConfigPage { this.formBuilder.removeFormItem(this.databaseDropdownComponent); this.formBuilder.addFormItem(this.databaseComponent, { horizontal: true, componentWidth: 400 }); this.model.database = this.databaseTextBox.value; + this.instance.setDoneButton(Operation.deploy); + + // remove deploy plan and generate script pages + this.instance.wizard.removePage(DeployOperationPath.deployAction); + this.instance.wizard.removePage(DeployOperationPath.deployPlan); }); //Initialize with upgrade existing true diff --git a/extensions/import/src/wizard/pages/deployPlanPage.ts b/extensions/import/src/wizard/pages/deployPlanPage.ts new file mode 100644 index 0000000000..2c7282ad42 --- /dev/null +++ b/extensions/import/src/wizard/pages/deployPlanPage.ts @@ -0,0 +1,296 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; +import * as sqlops from 'sqlops'; +import * as nls from 'vscode-nls'; +import * as parser from 'htmlparser2'; +import { DacFxDataModel } from '../api/models'; +import { DataTierApplicationWizard } from '../dataTierApplicationWizard'; +import { DacFxConfigPage } from '../api/dacFxConfigPage'; + +const localize = nls.loadMessageBundle(); + +export enum deployPlanXml { + AlertElement = 'Alert', + OperationElement = 'Operation', + ItemElement = 'Item', + NameAttribute = 'Name', + ValueAttribute = 'Value', + TypeAttribute = 'Type', + IdAttribute = 'Id', + DataIssueAttribute = 'DataIssue' +} + +export class TableObject { + operation: string; + object: string; + type: string; + dataloss: boolean; +} + +export class DeployPlanResult { + columnData: Array>; + dataLossAlerts: Set; +} + +export class DeployPlanPage extends DacFxConfigPage { + protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage; + protected readonly instance: DataTierApplicationWizard; + protected readonly model: DacFxDataModel; + protected readonly view: sqlops.ModelView; + private formBuilder: sqlops.FormBuilder; + private form: sqlops.FormContainer; + private table: sqlops.TableComponent; + private loader: sqlops.LoadingComponent; + private dataLossCheckbox: sqlops.CheckBoxComponent; + private dataLossText: sqlops.TextComponent; + private dataLossComponentGroup: sqlops.FormComponentGroup; + private noDataLossTextComponent: sqlops.FormComponent; + + public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) { + super(instance, wizardPage, model, view); + } + + async start(): Promise { + this.table = this.view.modelBuilder.table().component(); + this.loader = this.view.modelBuilder.loadingComponent().withItem(this.table).component(); + this.dataLossComponentGroup = await this.createDataLossComponents(); + this.noDataLossTextComponent = await this.createNoDataLossText(); + + this.formBuilder = this.view.modelBuilder.formContainer() + .withFormItems( + [ + { + component: this.loader, + title: '' + }, + this.dataLossComponentGroup + ], { + horizontal: true, + }); + this.form = this.formBuilder.component(); + await this.view.initializeModel(this.form); + + return true; + } + + async onPageEnter(): Promise { + // reset checkbox settings + this.formBuilder.addFormItem(this.dataLossComponentGroup, { horizontal: true, componentWidth: 400 }); + this.dataLossCheckbox.checked = false; + this.dataLossCheckbox.enabled = false; + this.formBuilder.removeFormItem(this.noDataLossTextComponent); + + this.loader.loading = true; + this.table.data = []; + await this.populateTable(); + this.loader.loading = false; + return true; + } + + private async populateTable() { + let report = await this.instance.generateDeployPlan(); + let result = this.parseXml(report); + + this.table.updateProperties({ + data: this.getColumnData(result), + columns: this.getTableColumns(result.dataLossAlerts.size > 0), + width: 875, + height: 300 + }); + + if (result.dataLossAlerts.size > 0) { + // update message to list how many operations could result in data loss + this.dataLossText.updateProperties({ + value: localize('dacfx.dataLossTextWithCount', '{0} of the deploy actions listed may result in data loss. Please ensure you have a backup or snapshot available in the event of an issue with the deployment.', result.dataLossAlerts.size) + }); + this.dataLossCheckbox.enabled = true; + } else { + // check checkbox to enable Next button and remove checkbox because there won't be any possible data loss + this.dataLossCheckbox.checked = true; + this.formBuilder.removeFormItem(this.dataLossComponentGroup); + this.formBuilder.addFormItem(this.noDataLossTextComponent, { componentWidth: 300, horizontal: true }); + } + } + + private async createDataLossCheckbox(): Promise { + this.dataLossCheckbox = this.view.modelBuilder.checkBox() + .withValidation(component => component.checked === true) + .withProperties({ + label: localize('dacFx.dataLossCheckbox', 'Proceed despite possible data loss'), + }).component(); + + return { + component: this.dataLossCheckbox, + title: '', + required: true + }; + } + + private async createNoDataLossText(): Promise { + let noDataLossText = this.view.modelBuilder.text() + .withProperties({ + value: localize('dacfx.noDataLossText', 'No data loss will occur from the listed deploy actions.') + }).component(); + + return { + title: '', + component: noDataLossText + }; + } + + private async createDataLossComponents(): Promise { + let dataLossComponent = await this.createDataLossCheckbox(); + this.dataLossText = this.view.modelBuilder.text() + .withProperties({ + value: localize('dacfx.dataLossText', 'The deploy actions may result in data loss. Please ensure you have a backup or snapshot available in the event of an issue with the deployment.') + }).component(); + + return { + title: '', + components: [ + { + component: this.dataLossText, + layout: { + componentWidth: 400, + horizontal: true + }, + title: '' + }, + dataLossComponent + ] + }; + } + + private getColumnData(result: DeployPlanResult): Array> { + // remove data loss column data if there aren't any alerts + let columnData = result.columnData; + if (result.dataLossAlerts.size === 0) { + columnData.forEach(entry => { + entry.shift(); + }); + } + return columnData; + } + + private getTableColumns(dataloss: boolean): sqlops.TableColumn[] { + let columns: sqlops.TableColumn[] = [ + { + value: localize('dacfx.operationColumn', 'Operation'), + width: 75, + cssClass: 'align-with-header', + toolTip: localize('dacfx.operationTooltip', 'Operation(Create, Alter, Delete) that will occur during deployment') + }, + { + value: localize('dacfx.typeColumn', 'Type'), + width: 100, + cssClass: 'align-with-header', + toolTip: localize('dacfx.typeTooltip', 'Type of object that will be affected by deployment') + }, + { + value: localize('dacfx.objectColumn', 'Object'), + width: 300, + cssClass: 'align-with-header', + toolTip: localize('dacfx.objecTooltip', 'Name of object that will be affected by deployment') + }]; + + if (dataloss) { + columns.unshift( + { + value: localize('dacfx.dataLossColumn', 'Data Loss'), + width: 50, + cssClass: 'center-align', + toolTip: localize('dacfx.dataLossTooltip', 'Operations that may result in data loss are marked with a warning sign') + }); + } + return columns; + } + + private parseXml(report: string): DeployPlanResult { + let operations = new Array(); + let dataLossAlerts = new Set(); + + let currentOperation = ''; + let dataIssueAlert = false; + let currentReportSection: deployPlanXml; + let currentTableObj: TableObject; + let p = new parser.Parser({ + onopentagname(name) { + if (name === deployPlanXml.AlertElement) { + currentReportSection = deployPlanXml.AlertElement; + } else if (name === deployPlanXml.OperationElement) { + currentReportSection = deployPlanXml.OperationElement; + } else if (name === deployPlanXml.ItemElement) { + currentTableObj = new TableObject(); + } + }, + onattribute: function (name, value) { + if (currentReportSection === deployPlanXml.AlertElement) { + switch (name) { + case deployPlanXml.NameAttribute: { + // only care about showing data loss alerts + if (value === deployPlanXml.DataIssueAttribute) { + dataIssueAlert = true; + } + break; + } + case deployPlanXml.IdAttribute: { + if (dataIssueAlert) { + dataLossAlerts.add(value); + } + break; + } + } + } else if (currentReportSection === deployPlanXml.OperationElement) { + switch (name) { + case deployPlanXml.NameAttribute: { + currentOperation = value; + break; + } + case deployPlanXml.ValueAttribute: { + currentTableObj.object = value; + break; + } + case deployPlanXml.TypeAttribute: { + currentTableObj.type = value; + break; + } + case deployPlanXml.IdAttribute: { + if (dataLossAlerts.has(value)) { + currentTableObj.dataloss = true; + } + break; + } + } + } + }, + onclosetag: function (name) { + if (name === deployPlanXml.ItemElement) { + currentTableObj.operation = currentOperation; + operations.push(currentTableObj); + } + } + }, { xmlMode: true, decodeEntities: true }); + p.parseChunk(report); + + let data = new Array>(); + operations.forEach(operation => { + let isDataLoss = operation.dataloss ? '⚠️' : ''; + data.push([isDataLoss, operation.operation, operation.type, operation.object]); + }); + + return { + columnData: data, + dataLossAlerts: dataLossAlerts + }; + } + + public setupNavigationValidator() { + this.instance.registerNavigationValidator(() => { + return true; + }); + } +} diff --git a/extensions/import/src/wizard/pages/selectOperationpage.ts b/extensions/import/src/wizard/pages/selectOperationpage.ts index 64bbb7af7f..de13d8dabe 100644 --- a/extensions/import/src/wizard/pages/selectOperationpage.ts +++ b/extensions/import/src/wizard/pages/selectOperationpage.ts @@ -7,7 +7,7 @@ import * as sqlops from 'sqlops'; import * as nls from 'vscode-nls'; import { DacFxDataModel } from '../api/models'; -import { DataTierApplicationWizard, Operation } from '../dataTierApplicationWizard'; +import { DataTierApplicationWizard, Operation, DeployOperationPath, ExtractOperationPath, ImportOperationPath, ExportOperationPath } from '../dataTierApplicationWizard'; import { BasePage } from '../api/basePage'; const localize = nls.loadMessageBundle(); @@ -73,10 +73,12 @@ export class SelectOperationPage extends BasePage { //add deploy pages let configPage = this.instance.pages.get('deployConfig'); - this.instance.wizard.addPage(configPage.wizardPage, 1); + this.instance.wizard.addPage(configPage.wizardPage, DeployOperationPath.deployOptions); + let deployPlanPage = this.instance.pages.get('deployPlan'); + this.instance.wizard.addPage(deployPlanPage.wizardPage, DeployOperationPath.deployPlan); let actionPage = this.instance.pages.get('deployAction'); - this.instance.wizard.addPage(actionPage.wizardPage, 2); - this.addSummaryPage(3); + this.instance.wizard.addPage(actionPage.wizardPage, DeployOperationPath.deployAction); + this.addSummaryPage(DeployOperationPath.summary); // change button text and operation this.instance.setDoneButton(Operation.deploy); @@ -100,8 +102,8 @@ export class SelectOperationPage extends BasePage { // add the extract page let page = this.instance.pages.get('extractConfig'); - this.instance.wizard.addPage(page.wizardPage, 1); - this.addSummaryPage(2); + this.instance.wizard.addPage(page.wizardPage, ExtractOperationPath.options); + this.addSummaryPage(ExtractOperationPath.summary); // change button text and operation this.instance.setDoneButton(Operation.extract); @@ -125,8 +127,8 @@ export class SelectOperationPage extends BasePage { // add the import page let page = this.instance.pages.get('importConfig'); - this.instance.wizard.addPage(page.wizardPage, 1); - this.addSummaryPage(2); + this.instance.wizard.addPage(page.wizardPage, ImportOperationPath.options); + this.addSummaryPage(ImportOperationPath.summary); // change button text and operation this.instance.setDoneButton(Operation.import); @@ -150,8 +152,8 @@ export class SelectOperationPage extends BasePage { // add the export pages let page = this.instance.pages.get('exportConfig'); - this.instance.wizard.addPage(page.wizardPage, 1); - this.addSummaryPage(2); + this.instance.wizard.addPage(page.wizardPage, ExportOperationPath.options); + this.addSummaryPage(ExportOperationPath.summary); // change button text and operation this.instance.setDoneButton(Operation.export); diff --git a/extensions/mssql/src/config.json b/extensions/mssql/src/config.json index fc9f493a92..3fff5b87d1 100644 --- a/extensions/mssql/src/config.json +++ b/extensions/mssql/src/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "1.5.0-alpha.70", + "version": "1.5.0-alpha.71", "downloadFileNames": { "Windows_86": "win-x86-netcoreapp2.2.zip", "Windows_64": "win-x64-netcoreapp2.2.zip", diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 7411aa8878..8665d1a434 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -339,6 +339,13 @@ export interface GenerateDeployScriptParams { taskExecutionMode: TaskExecutionMode; } +export interface GenerateDeployPlanParams { + packageFilePath: string; + databaseName: string; + ownerUri: string; + taskExecutionMode: TaskExecutionMode; +} + export namespace ExportRequest { export const type = new RequestType('dacfx/export'); } @@ -359,4 +366,7 @@ export namespace GenerateDeployScriptRequest { export const type = new RequestType('dacfx/generateDeploymentScript'); } +export namespace GenerateDeployPlanRequest { + export const type = new RequestType('dacfx/generateDeployPlan'); +} // ------------------------------- < DacFx > ------------------------------------ \ No newline at end of file diff --git a/extensions/mssql/src/features.ts b/extensions/mssql/src/features.ts index 35673c671f..553887f6f9 100644 --- a/extensions/mssql/src/features.ts +++ b/extensions/mssql/src/features.ts @@ -113,7 +113,20 @@ export class DacFxServicesFeature extends SqlOpsFeature { return r; }, e => { - client.logFailedRequest(contracts.DeployRequest.type, e); + client.logFailedRequest(contracts.GenerateDeployScriptRequest.type, e); + return Promise.resolve(undefined); + } + ); + }; + + let generateDeployPlan = (packageFilePath: string, targetDatabaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable => { + let params: contracts.GenerateDeployPlanParams = { packageFilePath: packageFilePath, databaseName: targetDatabaseName, ownerUri: ownerUri, taskExecutionMode: taskExecutionMode }; + return client.sendRequest(contracts.GenerateDeployPlanRequest.type, params).then( + r => { + return r; + }, + e => { + client.logFailedRequest(contracts.GenerateDeployPlanRequest.type, e); return Promise.resolve(undefined); } ); @@ -125,7 +138,8 @@ export class DacFxServicesFeature extends SqlOpsFeature { importBacpac, extractDacpac, deployDacpac, - generateDeployScript + generateDeployScript, + generateDeployPlan }); } } diff --git a/src/sql/platform/dacfx/common/dacFxService.ts b/src/sql/platform/dacfx/common/dacFxService.ts index c27f94ca59..688fbe37af 100644 --- a/src/sql/platform/dacfx/common/dacFxService.ts +++ b/src/sql/platform/dacfx/common/dacFxService.ts @@ -23,6 +23,7 @@ export interface IDacFxService { extractDacpac(sourceDatabaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): void; deployDacpac(packageFilePath: string, targetDatabaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): void; generateDeployScript(packageFilePath: string, targetDatabaseName: string, scriptFilePath: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): void; + generateDeployPlan(packageFilePath: string, targetDatabaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): void; } export class DacFxService implements IDacFxService { @@ -68,6 +69,12 @@ export class DacFxService implements IDacFxService { }); } + generateDeployPlan(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { + return this._runAction(ownerUri, (runner) => { + return runner.generateDeployPlan(packageFilePath, databaseName, ownerUri, taskExecutionMode); + }); + } + private _runAction(uri: string, action: (handler: sqlops.DacFxServicesProvider) => Thenable): Thenable { let providerId: string = this._connectionService.getProviderIdFromUri(uri); diff --git a/src/sql/sqlops.d.ts b/src/sql/sqlops.d.ts index a32fb35e20..ad24d1200c 100644 --- a/src/sql/sqlops.d.ts +++ b/src/sql/sqlops.d.ts @@ -1634,6 +1634,10 @@ declare module 'sqlops' { operationId: string; } + export interface GenerateDeployPlanResult extends DacFxResult { + report: string; + } + export interface ExportParams { databaseName: string; packageFilePath: string; @@ -1673,12 +1677,20 @@ declare module 'sqlops' { taskExecutionMode: TaskExecutionMode; } + export interface GenerateDeployPlan { + packageFilePath: string; + databaseName: string; + ownerUri: string; + taskExecutionMode: TaskExecutionMode; + } + export interface DacFxServicesProvider extends DataProvider { exportBacpac(databaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable; importBacpac(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable; extractDacpac(databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable; deployDacpac(packageFilePath: string, databaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable; generateDeployScript(packageFilePath: string, databaseName: string, scriptFilePath: string, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable; + generateDeployPlan(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable; } // Security service interfaces ------------------------------------------------------------------------ diff --git a/src/sql/workbench/api/node/mainThreadDataProtocol.ts b/src/sql/workbench/api/node/mainThreadDataProtocol.ts index 893503f8ff..e7f99f767e 100644 --- a/src/sql/workbench/api/node/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/node/mainThreadDataProtocol.ts @@ -445,6 +445,9 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape { }, generateDeployScript(packageFilePath: string, databaseName: string, scriptFilePath: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { return self._proxy.$generateDeployScript(handle, packageFilePath, databaseName, scriptFilePath, ownerUri, taskExecutionMode); + }, + generateDeployPlan(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { + return self._proxy.$generateDeployPlan(handle, packageFilePath, databaseName, ownerUri, taskExecutionMode); } }); diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 9617c2839e..f7ba79bcc7 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -450,6 +450,11 @@ export abstract class ExtHostDataProtocolShape { */ $generateDeployScript(handle: number, packageFilePath: string, databaseName: string, scriptFilePath: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { throw ni(); } + /** + * DacFx generate deploy plan + */ + $generateDeployPlan(handle: number, packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable { throw ni(); } + } /**