mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-15 09:35:37 -05:00
* Read/Send ExtractTarget information from/to STS * Remove comment * Cleanup comment and update azdata dependency
1295 lines
49 KiB
TypeScript
1295 lines
49 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as azdata from 'azdata';
|
|
import * as vscode from 'vscode';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import * as sqldbproj from 'sqldbproj';
|
|
import * as mssql from 'mssql';
|
|
import * as loc from './localizedConstants';
|
|
import { SchemaCompareOptionsDialog } from './dialogs/schemaCompareOptionsDialog';
|
|
import { TelemetryReporter, TelemetryViews } from './telemetry';
|
|
import { getTelemetryErrorType, getEndpointName, verifyConnectionAndGetOwnerUri, getRootPath, getSchemaCompareEndpointString, getDataWorkspaceExtensionApi } from './utils';
|
|
import { SchemaCompareDialog } from './dialogs/schemaCompareDialog';
|
|
import { isNullOrUndefined } from 'util';
|
|
|
|
// Do not localize this, this is used to decide the icon for the editor.
|
|
// TODO : In future icon should be decided based on language id (scmp) and not resource name
|
|
const schemaCompareResourceName = 'Schema Compare';
|
|
|
|
enum ResetButtonState {
|
|
noSourceTarget,
|
|
beforeCompareStart,
|
|
comparing,
|
|
afterCompareComplete
|
|
}
|
|
|
|
export class SchemaCompareMainWindow {
|
|
private differencesTable: azdata.TableComponent;
|
|
private loader: azdata.LoadingComponent;
|
|
private startText: azdata.TextComponent;
|
|
private waitText: azdata.TextComponent;
|
|
private editor: azdata.workspace.ModelViewEditor;
|
|
private diffEditor: azdata.DiffEditorComponent;
|
|
private splitView: azdata.SplitViewContainer;
|
|
private flexModel: azdata.FlexContainer;
|
|
private noDifferencesLabel: azdata.TextComponent;
|
|
private sourceTargetFlexLayout: azdata.FlexContainer;
|
|
protected switchButton: azdata.ButtonComponent;
|
|
protected compareButton: azdata.ButtonComponent;
|
|
protected cancelCompareButton: azdata.ButtonComponent;
|
|
protected optionsButton: azdata.ButtonComponent;
|
|
protected generateScriptButton: azdata.ButtonComponent;
|
|
protected applyButton: azdata.ButtonComponent;
|
|
protected openScmpButton: azdata.ButtonComponent;
|
|
protected selectSourceButton: azdata.ButtonComponent;
|
|
protected selectTargetButton: azdata.ButtonComponent;
|
|
protected saveScmpButton: azdata.ButtonComponent;
|
|
private SchemaCompareActionMap: Map<Number, string>;
|
|
private operationId: string;
|
|
protected comparisonResult: mssql.SchemaCompareResult;
|
|
private sourceNameComponent: azdata.InputBoxComponent;
|
|
private targetNameComponent: azdata.InputBoxComponent;
|
|
private deploymentOptions: mssql.DeploymentOptions;
|
|
private schemaCompareOptionDialog: SchemaCompareOptionsDialog;
|
|
private tablelistenersToDispose: vscode.Disposable[] = [];
|
|
private originalSourceExcludes = new Map<string, mssql.DiffEntry>();
|
|
private originalTargetExcludes = new Map<string, mssql.DiffEntry>();
|
|
private sourceTargetSwitched = false;
|
|
private sourceName: string;
|
|
private targetName: string;
|
|
private scmpSourceExcludes: mssql.SchemaCompareObjectId[];
|
|
private scmpTargetExcludes: mssql.SchemaCompareObjectId[];
|
|
private diffEntryRowMap = new Map<string, number>();
|
|
private showIncludeExcludeWaitingMessage: boolean = true;
|
|
|
|
public sourceEndpointInfo: mssql.SchemaCompareEndpointInfo;
|
|
public targetEndpointInfo: mssql.SchemaCompareEndpointInfo;
|
|
|
|
public promise;
|
|
|
|
public schemaCompareDialog: SchemaCompareDialog;
|
|
|
|
constructor(private schemaCompareService?: mssql.ISchemaCompareService, private extensionContext?: vscode.ExtensionContext, private view?: azdata.ModelView) {
|
|
this.SchemaCompareActionMap = new Map<Number, string>();
|
|
this.SchemaCompareActionMap[mssql.SchemaUpdateAction.Delete] = loc.deleteAction;
|
|
this.SchemaCompareActionMap[mssql.SchemaUpdateAction.Change] = loc.changeAction;
|
|
this.SchemaCompareActionMap[mssql.SchemaUpdateAction.Add] = loc.addAction;
|
|
|
|
this.editor = azdata.workspace.createModelViewEditor(loc.SchemaCompareLabel, { retainContextWhenHidden: true, supportsSave: true, resourceName: schemaCompareResourceName }, 'SchemaCompareEditor');
|
|
}
|
|
|
|
// schema compare can get started with four contexts for the source:
|
|
// 1. undefined
|
|
// 2. connection profile
|
|
// 3. dacpac
|
|
// 4. project
|
|
public async start(sourceContext: any, targetContext: mssql.SchemaCompareEndpointInfo = undefined, comparisonResult: mssql.SchemaCompareResult = undefined): Promise<void> {
|
|
let source: mssql.SchemaCompareEndpointInfo;
|
|
let target: mssql.SchemaCompareEndpointInfo;
|
|
|
|
const targetIsSetAsProject: boolean = targetContext && targetContext.endpointType === mssql.SchemaCompareEndpointType.Project;
|
|
|
|
// if schema compare was launched from a db or a connection profile, set that as the source
|
|
let profile: azdata.IConnectionProfile;
|
|
|
|
if (targetIsSetAsProject) {
|
|
profile = sourceContext;
|
|
target = targetContext;
|
|
} else {
|
|
profile = sourceContext ? <azdata.IConnectionProfile>sourceContext.connectionProfile : undefined;
|
|
}
|
|
|
|
let sourceDacpac = undefined;
|
|
let sourceProject = undefined;
|
|
|
|
if (!profile && sourceContext as string && (sourceContext as string).endsWith('.dacpac')) {
|
|
sourceDacpac = sourceContext as string;
|
|
} else if (!profile) {
|
|
sourceProject = sourceContext as string;
|
|
}
|
|
|
|
if (profile) {
|
|
let ownerUri = await azdata.connection.getUriForConnection((profile.id));
|
|
let usr = profile.userName;
|
|
if (!usr) {
|
|
usr = loc.defaultText;
|
|
}
|
|
|
|
source = {
|
|
endpointType: mssql.SchemaCompareEndpointType.Database,
|
|
serverDisplayName: `${profile.serverName} (${usr})`,
|
|
serverName: profile.serverName,
|
|
databaseName: profile.databaseName,
|
|
ownerUri: ownerUri,
|
|
packageFilePath: '',
|
|
connectionDetails: undefined,
|
|
connectionName: profile.connectionName,
|
|
projectFilePath: '',
|
|
targetScripts: [],
|
|
dataSchemaProvider: '',
|
|
extractTarget: mssql.ExtractTarget.schemaObjectType
|
|
};
|
|
} else if (sourceDacpac) {
|
|
source = {
|
|
endpointType: mssql.SchemaCompareEndpointType.Dacpac,
|
|
serverDisplayName: '',
|
|
serverName: '',
|
|
databaseName: '',
|
|
ownerUri: '',
|
|
packageFilePath: sourceDacpac,
|
|
connectionDetails: undefined,
|
|
projectFilePath: '',
|
|
targetScripts: [],
|
|
dataSchemaProvider: '',
|
|
extractTarget: mssql.ExtractTarget.schemaObjectType
|
|
};
|
|
} else if (sourceProject) {
|
|
source = {
|
|
endpointType: mssql.SchemaCompareEndpointType.Project,
|
|
packageFilePath: '',
|
|
serverDisplayName: '',
|
|
serverName: '',
|
|
databaseName: '',
|
|
ownerUri: '',
|
|
connectionDetails: undefined,
|
|
projectFilePath: sourceProject,
|
|
targetScripts: [],
|
|
dataSchemaProvider: undefined,
|
|
extractTarget: mssql.ExtractTarget.schemaObjectType
|
|
};
|
|
}
|
|
|
|
await this.launch(source, target, false, comparisonResult);
|
|
}
|
|
|
|
/**
|
|
* Primary functional entrypoint for opening the schema comparison window, and optionally running it.
|
|
* @param source
|
|
* @param target
|
|
* @param runComparison whether to immediately run the schema comparison. Requires both source and target to be specified. Cannot be true when comparisonResult is set.
|
|
* @param comparisonResult a pre-computed schema comparison result to display. Cannot be set when runComparison is true.
|
|
*/
|
|
public async launch(source: mssql.SchemaCompareEndpointInfo | undefined, target: mssql.SchemaCompareEndpointInfo | undefined, runComparison: boolean = false, comparisonResult: mssql.SchemaCompareResult | undefined): Promise<void> {
|
|
if (runComparison && comparisonResult) {
|
|
throw new Error('Cannot both pass a comparison result and request a new comparison be run.');
|
|
}
|
|
|
|
this.sourceEndpointInfo = source;
|
|
this.targetEndpointInfo = target;
|
|
|
|
await this.GetDefaultDeploymentOptions();
|
|
await Promise.all([
|
|
this.registerContent(),
|
|
this.editor.openEditor()
|
|
]);
|
|
|
|
if (comparisonResult) {
|
|
await this.execute(comparisonResult);
|
|
} else if (runComparison) {
|
|
if (!source || !target) {
|
|
throw new Error('source and target must both be set when runComparison is true.');
|
|
}
|
|
|
|
await this.startCompare();
|
|
}
|
|
}
|
|
|
|
private async registerContent(): Promise<void> {
|
|
return new Promise<void>((resolve) => {
|
|
this.editor.registerContent(async (view) => {
|
|
if (isNullOrUndefined(this.view)) {
|
|
this.view = view;
|
|
}
|
|
|
|
this.differencesTable = this.view.modelBuilder.table().withProps({
|
|
data: [],
|
|
title: loc.differencesTableTitle,
|
|
columns: []
|
|
}).component();
|
|
|
|
this.diffEditor = this.view.modelBuilder.diffeditor().withProperties({
|
|
contentLeft: os.EOL,
|
|
contentRight: os.EOL,
|
|
height: 500,
|
|
title: loc.diffEditorTitle
|
|
}).component();
|
|
|
|
this.splitView = this.view.modelBuilder.splitViewContainer().component();
|
|
|
|
let sourceTargetLabels = this.view.modelBuilder.flexContainer().component();
|
|
|
|
this.sourceTargetFlexLayout = this.view.modelBuilder.flexContainer().component();
|
|
|
|
this.createSwitchButton();
|
|
this.createCompareButton();
|
|
this.createCancelButton();
|
|
this.createGenerateScriptButton();
|
|
this.createApplyButton();
|
|
this.createOptionsButton();
|
|
this.createOpenScmpButton();
|
|
this.createSaveScmpButton();
|
|
this.createSourceAndTargetButtons();
|
|
|
|
this.sourceName = getEndpointName(this.sourceEndpointInfo);
|
|
this.targetName = getEndpointName(this.targetEndpointInfo);
|
|
|
|
this.sourceNameComponent = this.view.modelBuilder.inputBox().withProps({
|
|
value: this.sourceName,
|
|
title: this.sourceName,
|
|
enabled: false
|
|
}).component();
|
|
|
|
this.targetNameComponent = this.view.modelBuilder.inputBox().withProps({
|
|
value: this.targetName,
|
|
title: this.targetName,
|
|
enabled: false
|
|
}).component();
|
|
|
|
this.resetButtons(ResetButtonState.noSourceTarget);
|
|
|
|
let toolBar = this.view.modelBuilder.toolbarContainer();
|
|
toolBar.addToolbarItems([{
|
|
component: this.compareButton
|
|
}, {
|
|
component: this.cancelCompareButton
|
|
}, {
|
|
component: this.generateScriptButton
|
|
}, {
|
|
component: this.applyButton
|
|
}, {
|
|
component: this.optionsButton,
|
|
toolbarSeparatorAfter: true
|
|
}, {
|
|
component: this.switchButton,
|
|
toolbarSeparatorAfter: true
|
|
}, {
|
|
component: this.openScmpButton
|
|
}, {
|
|
component: this.saveScmpButton
|
|
}]);
|
|
|
|
let sourceLabel = this.view.modelBuilder.text().withProps({
|
|
value: loc.sourceTitle,
|
|
CSSStyles: { 'margin-bottom': '0px' }
|
|
}).component();
|
|
|
|
let targetLabel = this.view.modelBuilder.text().withProps({
|
|
value: loc.targetTitle,
|
|
CSSStyles: { 'margin-bottom': '0px' }
|
|
}).component();
|
|
|
|
let arrowLabel = this.view.modelBuilder.text().withProps({
|
|
// allow-any-unicode-next-line
|
|
value: '➔'
|
|
}).component();
|
|
|
|
sourceTargetLabels.addItem(sourceLabel, { CSSStyles: { 'width': '55%', 'margin-left': '15px', 'font-size': 'larger', 'font-weight': 'bold' } });
|
|
sourceTargetLabels.addItem(targetLabel, { CSSStyles: { 'width': '45%', 'font-size': 'larger', 'font-weight': 'bold' } });
|
|
this.sourceTargetFlexLayout.addItem(this.sourceNameComponent, { CSSStyles: { 'width': '40%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px', 'margin-right': '10px' } });
|
|
this.sourceTargetFlexLayout.addItem(this.selectSourceButton, { CSSStyles: { 'margin-top': '10px' } });
|
|
this.sourceTargetFlexLayout.addItem(arrowLabel, { CSSStyles: { 'width': '10%', 'font-size': 'larger', 'text-align-last': 'center' } });
|
|
this.sourceTargetFlexLayout.addItem(this.targetNameComponent, { CSSStyles: { 'width': '40%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px', 'margin-right': '10px' } });
|
|
this.sourceTargetFlexLayout.addItem(this.selectTargetButton, { CSSStyles: { 'margin-top': '10px' } });
|
|
|
|
this.loader = this.view.modelBuilder.loadingComponent().component();
|
|
this.waitText = this.view.modelBuilder.text().withProps({
|
|
value: loc.waitText
|
|
}).component();
|
|
|
|
this.startText = this.view.modelBuilder.text().withProps({
|
|
value: loc.startText
|
|
}).component();
|
|
|
|
this.noDifferencesLabel = this.view.modelBuilder.text().withProps({
|
|
value: loc.noDifferencesText
|
|
}).component();
|
|
|
|
this.flexModel = this.view.modelBuilder.flexContainer().component();
|
|
this.flexModel.addItem(toolBar.component(), { flex: 'none' });
|
|
this.flexModel.addItem(sourceTargetLabels, { flex: 'none' });
|
|
this.flexModel.addItem(this.sourceTargetFlexLayout, { flex: 'none' });
|
|
this.flexModel.addItem(this.startText, { CSSStyles: { 'margin': 'auto' } });
|
|
|
|
this.flexModel.setLayout({
|
|
flexFlow: 'column',
|
|
height: '100%'
|
|
});
|
|
|
|
await this.view.initializeModel(this.flexModel);
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
// update source and target name to display
|
|
public updateSourceAndTarget() {
|
|
this.sourceName = getEndpointName(this.sourceEndpointInfo);
|
|
this.targetName = getEndpointName(this.targetEndpointInfo);
|
|
|
|
this.sourceNameComponent.updateProperties({
|
|
value: this.sourceName,
|
|
title: this.sourceName
|
|
});
|
|
this.targetNameComponent.updateProperties({
|
|
value: this.targetName,
|
|
title: this.targetName
|
|
});
|
|
if (!this.sourceName || !this.targetName || this.sourceName === ' ' || this.targetName === ' ') {
|
|
this.resetButtons(ResetButtonState.noSourceTarget);
|
|
} else {
|
|
// reset buttons to before comparison state
|
|
this.resetButtons(ResetButtonState.beforeCompareStart);
|
|
}
|
|
}
|
|
|
|
public setDeploymentOptions(deploymentOptions: mssql.DeploymentOptions): void {
|
|
this.deploymentOptions = deploymentOptions;
|
|
}
|
|
|
|
private async populateProjectScripts(endpointInfo: mssql.SchemaCompareEndpointInfo): Promise<void> {
|
|
if (endpointInfo.endpointType !== mssql.SchemaCompareEndpointType.Project) {
|
|
return;
|
|
}
|
|
|
|
const databaseProjectsExtension = vscode.extensions.getExtension(loc.sqlDatabaseProjectExtensionId);
|
|
|
|
if (databaseProjectsExtension) {
|
|
endpointInfo.targetScripts = await (await databaseProjectsExtension.activate() as sqldbproj.IExtension).getProjectScriptFiles(endpointInfo.projectFilePath);
|
|
}
|
|
}
|
|
|
|
public async execute(comparisonResult: mssql.SchemaCompareCompletionResult = undefined) {
|
|
const service = await this.getService();
|
|
|
|
if (comparisonResult) {
|
|
this.operationId = comparisonResult.operationId;
|
|
this.comparisonResult = comparisonResult;
|
|
this.flexModel.removeItem(this.startText);
|
|
} else {
|
|
TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaComparisonStarted');
|
|
|
|
if (!this.operationId) {
|
|
// create once per page
|
|
this.operationId = generateGuid();
|
|
}
|
|
|
|
await Promise.all([this.populateProjectScripts(this.sourceEndpointInfo), this.populateProjectScripts(this.targetEndpointInfo)]);
|
|
|
|
this.comparisonResult = await service.schemaCompare(this.operationId, this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions);
|
|
|
|
if (!this.comparisonResult || !this.comparisonResult.success) {
|
|
TelemetryReporter.createErrorEvent2(TelemetryViews.SchemaCompareMainWindow, 'SchemaComparisonFailed', undefined, undefined, getTelemetryErrorType(this.comparisonResult?.errorMessage))
|
|
.withAdditionalProperties({
|
|
operationId: this.comparisonResult.operationId
|
|
}).send();
|
|
|
|
vscode.window.showErrorMessage(loc.compareErrorMessage(this.comparisonResult?.errorMessage));
|
|
|
|
// reset state so a new comparison can be made
|
|
this.resetWindow();
|
|
|
|
return;
|
|
}
|
|
|
|
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaComparisonFinished')
|
|
.withAdditionalProperties({
|
|
'endTime': Date.now().toString(),
|
|
'operationId': this.comparisonResult.operationId
|
|
}).send();
|
|
}
|
|
|
|
let data = this.getAllDifferences(this.comparisonResult.differences);
|
|
|
|
this.differencesTable.updateProperties({
|
|
data: data,
|
|
columns: [
|
|
{
|
|
value: loc.type,
|
|
cssClass: 'align-with-header',
|
|
width: 50
|
|
},
|
|
{
|
|
value: loc.sourceName,
|
|
cssClass: 'align-with-header',
|
|
width: 90
|
|
},
|
|
<azdata.CheckboxColumn>
|
|
{
|
|
value: loc.include,
|
|
cssClass: 'align-with-header',
|
|
width: 60,
|
|
type: azdata.ColumnType.checkBox,
|
|
action: azdata.ActionOnCellCheckboxCheck.customAction
|
|
},
|
|
{
|
|
value: loc.action,
|
|
cssClass: 'align-with-header',
|
|
width: 30
|
|
},
|
|
{
|
|
value: loc.targetName,
|
|
cssClass: 'align-with-header',
|
|
width: 150
|
|
}
|
|
],
|
|
CSSStyles: { 'left': '15px' },
|
|
width: '98%'
|
|
});
|
|
|
|
this.flexModel.removeItem(this.loader);
|
|
this.flexModel.removeItem(this.waitText);
|
|
this.resetButtons(ResetButtonState.afterCompareComplete);
|
|
|
|
if (this.comparisonResult.differences.length > 0) {
|
|
this.flexModel.addItem(this.splitView);
|
|
|
|
this.splitView.addItem(this.differencesTable);
|
|
this.splitView.addItem(this.diffEditor);
|
|
this.splitView.setLayout({
|
|
orientation: 'vertical',
|
|
splitViewHeight: 800
|
|
});
|
|
|
|
// create a map of the differences to row numbers
|
|
for (let i = 0; i < data.length; ++i) {
|
|
this.diffEntryRowMap.set(this.createDiffEntryKey(this.comparisonResult.differences[i]), i);
|
|
}
|
|
|
|
// only enable generate script button if the target is a db
|
|
if (this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) {
|
|
this.generateScriptButton.enabled = true;
|
|
} else {
|
|
this.generateScriptButton.title = loc.generateScriptDisabled;
|
|
}
|
|
|
|
// only enable apply button if the target is a db or a project
|
|
if (this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database ||
|
|
this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Project) {
|
|
this.applyButton.enabled = true;
|
|
} else {
|
|
this.applyButton.title = loc.applyDisabled;
|
|
}
|
|
} else {
|
|
this.flexModel.addItem(this.noDifferencesLabel, { CSSStyles: { 'margin': 'auto' } });
|
|
}
|
|
|
|
// explicitly exclude things that were excluded in previous compare
|
|
const thingsToExclude = this.sourceTargetSwitched ? this.originalTargetExcludes : this.originalSourceExcludes;
|
|
if (thingsToExclude) {
|
|
thingsToExclude.forEach(async (item) => {
|
|
await service.schemaCompareIncludeExcludeNode(this.comparisonResult.operationId, item, false, azdata.TaskExecutionMode.execute);
|
|
});
|
|
|
|
// disable apply and generate script buttons if no changes are included
|
|
if (thingsToExclude.size === this.comparisonResult.differences.length) {
|
|
this.setButtonStatesForNoChanges(false);
|
|
}
|
|
}
|
|
|
|
let sourceText = '';
|
|
let targetText = '';
|
|
this.tablelistenersToDispose.push(this.differencesTable.onRowSelected(() => {
|
|
let difference = this.comparisonResult.differences[this.differencesTable.selectedRows[0]];
|
|
if (difference !== undefined) {
|
|
sourceText = this.getFormattedScript(difference, true);
|
|
targetText = this.getFormattedScript(difference, false);
|
|
|
|
this.diffEditor.updateProperties({
|
|
contentLeft: sourceText,
|
|
contentRight: targetText,
|
|
title: loc.diffEditorTitle
|
|
});
|
|
}
|
|
}));
|
|
this.tablelistenersToDispose.push(this.differencesTable.onCellAction(async (rowState) => {
|
|
let checkboxState = <azdata.ICheckboxCellActionEventArgs>rowState;
|
|
if (checkboxState) {
|
|
await this.applyIncludeExclude(checkboxState);
|
|
}
|
|
}));
|
|
}
|
|
|
|
public async applyIncludeExclude(checkboxState: azdata.ICheckboxCellActionEventArgs): Promise<void> {
|
|
const service = await this.getService();
|
|
// show an info notification the first time when trying to exclude to notify the user that it may take some time to calculate affected dependencies
|
|
if (this.showIncludeExcludeWaitingMessage) {
|
|
this.showIncludeExcludeWaitingMessage = false;
|
|
vscode.window.showInformationMessage(loc.includeExcludeInfoMessage);
|
|
}
|
|
|
|
let diff = this.comparisonResult.differences[checkboxState.row];
|
|
const result = await service.schemaCompareIncludeExcludeNode(this.comparisonResult.operationId, diff, checkboxState.checked, azdata.TaskExecutionMode.execute);
|
|
let checkboxesToChange = [];
|
|
if (result.success) {
|
|
this.saveExcludeState(checkboxState);
|
|
|
|
// dependencies could have been included or excluded as a result, so save their exclude states
|
|
result.affectedDependencies.forEach(difference => {
|
|
// find the row of the difference and set its checkbox
|
|
const diffEntryKey = this.createDiffEntryKey(difference);
|
|
if (this.diffEntryRowMap.has(diffEntryKey)) {
|
|
const row = this.diffEntryRowMap.get(diffEntryKey);
|
|
checkboxesToChange.push({ row: row, column: 2, columnName: 'Include', checked: difference.included });
|
|
const dependencyCheckBoxState: azdata.ICheckboxCellActionEventArgs = {
|
|
checked: difference.included,
|
|
row: row,
|
|
column: 2,
|
|
columnName: undefined
|
|
};
|
|
this.saveExcludeState(dependencyCheckBoxState);
|
|
}
|
|
});
|
|
} else {
|
|
// failed because of dependencies
|
|
if (result.blockingDependencies) {
|
|
// show the first dependent that caused this to fail in the warning message
|
|
const diffEntryName = this.createName(diff.sourceValue ? diff.sourceValue : diff.targetValue);
|
|
const firstDependentName = this.createName(result.blockingDependencies[0].sourceValue ? result.blockingDependencies[0].sourceValue : result.blockingDependencies[0].targetValue);
|
|
let cannotExcludeMessage: string;
|
|
let cannotIncludeMessage: string;
|
|
if (firstDependentName) {
|
|
cannotExcludeMessage = loc.cannotExcludeMessageDependent(diffEntryName, firstDependentName);
|
|
cannotIncludeMessage = loc.cannotIncludeMessageDependent(diffEntryName, firstDependentName);
|
|
} else {
|
|
cannotExcludeMessage = loc.cannotExcludeMessage(diffEntryName);
|
|
cannotIncludeMessage = loc.cannotIncludeMessage(diffEntryName);
|
|
}
|
|
vscode.window.showWarningMessage(checkboxState.checked ? cannotIncludeMessage : cannotExcludeMessage);
|
|
} else {
|
|
vscode.window.showWarningMessage(result.errorMessage);
|
|
}
|
|
|
|
// set checkbox back to previous state
|
|
checkboxesToChange.push({ row: checkboxState.row, column: checkboxState.column, columnName: 'Include', checked: !checkboxState.checked });
|
|
}
|
|
|
|
if (checkboxesToChange.length > 0) {
|
|
this.differencesTable.updateCells = checkboxesToChange;
|
|
}
|
|
}
|
|
|
|
// save state based on source name if present otherwise target name (parity with SSDT)
|
|
private saveExcludeState(rowState: azdata.ICheckboxCellActionEventArgs) {
|
|
if (rowState) {
|
|
if (this.differencesTable.data[rowState.row]?.length > 2) {
|
|
this.differencesTable.data[rowState.row][2] = rowState.checked;
|
|
}
|
|
let diff = this.comparisonResult.differences[rowState.row];
|
|
let key = (diff.sourceValue && diff.sourceValue.length > 0) ? this.createName(diff.sourceValue) : this.createName(diff.targetValue);
|
|
if (key) {
|
|
if (!this.sourceTargetSwitched) {
|
|
this.originalSourceExcludes.delete(key);
|
|
this.removeExcludeEntry(this.scmpSourceExcludes, key);
|
|
|
|
if (!rowState.checked) {
|
|
this.originalSourceExcludes.set(key, diff);
|
|
if (this.originalSourceExcludes.size === this.comparisonResult.differences.length) {
|
|
this.setButtonStatesForNoChanges(false);
|
|
}
|
|
}
|
|
else {
|
|
this.setButtonStatesForNoChanges(true);
|
|
}
|
|
}
|
|
else {
|
|
this.originalTargetExcludes.delete(key);
|
|
this.removeExcludeEntry(this.scmpTargetExcludes, key);
|
|
|
|
if (!rowState.checked) {
|
|
this.originalTargetExcludes.set(key, diff);
|
|
if (this.originalTargetExcludes.size === this.comparisonResult.differences.length) {
|
|
this.setButtonStatesForNoChanges(false);
|
|
}
|
|
else {
|
|
this.setButtonStatesForNoChanges(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private shouldDiffBeIncluded(diff: mssql.DiffEntry): boolean {
|
|
let key = (diff.sourceValue && diff.sourceValue.length > 0) ? this.createName(diff.sourceValue) : this.createName(diff.targetValue);
|
|
if (key) {
|
|
if (this.sourceTargetSwitched === true
|
|
&& (this.originalTargetExcludes.has(key) || this.hasExcludeEntry(this.scmpTargetExcludes, key))) {
|
|
this.originalTargetExcludes.set(key, diff);
|
|
return false;
|
|
}
|
|
if (this.sourceTargetSwitched === false
|
|
&& (this.originalSourceExcludes.has(key) || this.hasExcludeEntry(this.scmpSourceExcludes, key))) {
|
|
this.originalSourceExcludes.set(key, diff);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private hasExcludeEntry(collection: mssql.SchemaCompareObjectId[], entryName: string): boolean {
|
|
let found = false;
|
|
if (collection) {
|
|
const index = collection.findIndex(e => this.createName(e.nameParts) === entryName);
|
|
found = index !== -1;
|
|
}
|
|
return found;
|
|
}
|
|
|
|
private removeExcludeEntry(collection: mssql.SchemaCompareObjectId[], entryName: string) {
|
|
if (collection) {
|
|
const index = collection.findIndex(e => this.createName(e.nameParts) === entryName);
|
|
collection.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
private getAllDifferences(differences: mssql.DiffEntry[]): string[][] {
|
|
let data = [];
|
|
let finalDifferences: mssql.DiffEntry[] = [];
|
|
if (differences) {
|
|
differences.forEach(difference => {
|
|
if (difference.differenceType === mssql.SchemaDifferenceType.Object) {
|
|
if ((difference.sourceValue !== null && difference.sourceValue.length > 0) || (difference.targetValue !== null && difference.targetValue.length > 0)) {
|
|
finalDifferences.push(difference); // Add only non-null changes to ensure index does not mismatch between dictionay and UI - #6234
|
|
let state: boolean = this.shouldDiffBeIncluded(difference);
|
|
data.push([difference.name, this.createName(difference.sourceValue), state, this.SchemaCompareActionMap[difference.updateAction], this.createName(difference.targetValue)]);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
this.comparisonResult.differences = finalDifferences;
|
|
return data;
|
|
}
|
|
|
|
private createName(nameParts: string[]): string {
|
|
if (isNullOrUndefined(nameParts) || nameParts.length === 0) {
|
|
return '';
|
|
}
|
|
return nameParts.join('.');
|
|
}
|
|
|
|
private createDiffEntryKey(entry: mssql.DiffEntry): string {
|
|
return `${this.createName(entry.sourceValue)}_${this.createName(entry.targetValue)}_${entry.updateAction}_${entry.name}`;
|
|
}
|
|
|
|
private getFormattedScript(diffEntry: mssql.DiffEntry, getSourceScript: boolean): string {
|
|
// if there is no entry, the script has to be \n because an empty string shows up as a difference but \n doesn't
|
|
if ((getSourceScript && diffEntry.sourceScript === null)
|
|
|| (!getSourceScript && diffEntry.targetScript === null)) {
|
|
return '\n';
|
|
}
|
|
|
|
let script = this.getAggregatedScript(diffEntry, getSourceScript);
|
|
return script;
|
|
}
|
|
|
|
private getAggregatedScript(diffEntry: mssql.DiffEntry, getSourceScript: boolean): string {
|
|
let script = '';
|
|
if (diffEntry !== null) {
|
|
let diffEntryScript = getSourceScript ? diffEntry.sourceScript : diffEntry.targetScript;
|
|
if (diffEntryScript) {
|
|
// add a blank line between each statement
|
|
script += diffEntryScript + '\n\n';
|
|
}
|
|
|
|
diffEntry.children.forEach(child => {
|
|
let childScript = this.getAggregatedScript(child, getSourceScript);
|
|
script += childScript;
|
|
});
|
|
}
|
|
return script;
|
|
}
|
|
|
|
public async startCompare(): Promise<void> {
|
|
this.flexModel.removeItem(this.splitView);
|
|
this.flexModel.removeItem(this.noDifferencesLabel);
|
|
this.flexModel.removeItem(this.startText);
|
|
this.flexModel.addItem(this.loader, { CSSStyles: { 'margin-top': '30px' } });
|
|
this.flexModel.addItem(this.waitText, { CSSStyles: { 'margin-top': '30px', 'align-self': 'center' } });
|
|
this.showIncludeExcludeWaitingMessage = true;
|
|
this.diffEditor.updateProperties({
|
|
contentLeft: os.EOL,
|
|
contentRight: os.EOL,
|
|
title: loc.diffEditorTitle
|
|
});
|
|
|
|
this.differencesTable.selectedRows = null;
|
|
if (this.tablelistenersToDispose) {
|
|
this.tablelistenersToDispose.forEach(x => x.dispose());
|
|
}
|
|
this.resetButtons(ResetButtonState.comparing);
|
|
this.promise = this.execute();
|
|
await this.promise;
|
|
}
|
|
|
|
private createCompareButton(): void {
|
|
this.compareButton = this.view.modelBuilder.button().withProps({
|
|
label: loc.compare,
|
|
iconPath: {
|
|
light: path.join(this.extensionContext.extensionPath, 'media', 'compare.svg'),
|
|
dark: path.join(this.extensionContext.extensionPath, 'media', 'compare-inverse.svg')
|
|
},
|
|
title: loc.compare
|
|
}).component();
|
|
|
|
this.compareButton.onDidClick(async (_click) => {
|
|
await this.startCompare();
|
|
});
|
|
}
|
|
|
|
private createCancelButton(): void {
|
|
this.cancelCompareButton = this.view.modelBuilder.button().withProps({
|
|
label: loc.stop,
|
|
iconPath: {
|
|
light: path.join(this.extensionContext.extensionPath, 'media', 'stop.svg'),
|
|
dark: path.join(this.extensionContext.extensionPath, 'media', 'stop-inverse.svg')
|
|
},
|
|
title: loc.stop
|
|
}).component();
|
|
|
|
this.cancelCompareButton.onDidClick(async (_click) => {
|
|
await this.cancelCompare();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Resets state of buttons and text to initial state before a comparison is started/completed
|
|
*/
|
|
public resetWindow(): void {
|
|
// clean the pane
|
|
this.flexModel.removeItem(this.loader);
|
|
this.flexModel.removeItem(this.waitText);
|
|
this.flexModel.addItem(this.startText, { CSSStyles: { 'margin': 'auto' } });
|
|
this.resetButtons(ResetButtonState.beforeCompareStart);
|
|
}
|
|
|
|
public async cancelCompare(): Promise<void> {
|
|
|
|
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareCancelStarted')
|
|
.withAdditionalProperties({
|
|
'startTime': Date.now().toString(),
|
|
'operationId': this.operationId
|
|
}).send();
|
|
|
|
this.resetWindow();
|
|
|
|
// cancel compare
|
|
if (this.operationId) {
|
|
const service = await this.getService();
|
|
const result = await service.schemaCompareCancel(this.operationId);
|
|
|
|
if (!result || !result.success) {
|
|
TelemetryReporter.createErrorEvent2(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareCancelFailed', undefined, undefined, getTelemetryErrorType(result.errorMessage))
|
|
.withAdditionalProperties({
|
|
'operationId': this.operationId
|
|
}).send();
|
|
vscode.window.showErrorMessage(loc.cancelErrorMessage(result.errorMessage));
|
|
}
|
|
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareCancelEnded')
|
|
.withAdditionalProperties({
|
|
'endTime': Date.now().toString(),
|
|
'operationId': this.operationId
|
|
}).send();
|
|
}
|
|
}
|
|
|
|
private createGenerateScriptButton(): void {
|
|
this.generateScriptButton = this.view.modelBuilder.button().withProps({
|
|
label: loc.generateScript,
|
|
iconPath: {
|
|
light: path.join(this.extensionContext.extensionPath, 'media', 'generate-script.svg'),
|
|
dark: path.join(this.extensionContext.extensionPath, 'media', 'generate-script-inverse.svg')
|
|
},
|
|
}).component();
|
|
|
|
this.generateScriptButton.onDidClick(async (_click) => {
|
|
await this.generateScript();
|
|
});
|
|
}
|
|
|
|
public async generateScript(): Promise<void> {
|
|
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareGenerateScriptStarted')
|
|
.withAdditionalProperties({
|
|
'startTime': Date.now().toString(),
|
|
'operationId': this.comparisonResult.operationId
|
|
}).send();
|
|
const service = await this.getService();
|
|
const result = await service.schemaCompareGenerateScript(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.script);
|
|
if (!result || !result.success) {
|
|
TelemetryReporter.createErrorEvent2(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareGenerateScriptFailed', undefined, undefined, getTelemetryErrorType(result.errorMessage))
|
|
.withAdditionalProperties({
|
|
'operationId': this.comparisonResult.operationId
|
|
}).send();
|
|
vscode.window.showErrorMessage(loc.generateScriptErrorMessage(result.errorMessage));
|
|
}
|
|
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareGenerateScriptEnded')
|
|
.withAdditionalProperties({
|
|
'endTime': Date.now().toString(),
|
|
'operationId': this.comparisonResult.operationId
|
|
}).send();
|
|
}
|
|
|
|
private createOptionsButton() {
|
|
this.optionsButton = this.view.modelBuilder.button().withProps({
|
|
label: loc.options,
|
|
iconPath: {
|
|
light: path.join(this.extensionContext.extensionPath, 'media', 'options.svg'),
|
|
dark: path.join(this.extensionContext.extensionPath, 'media', 'options-inverse.svg')
|
|
},
|
|
title: loc.options
|
|
}).component();
|
|
|
|
this.optionsButton.onDidClick(async (_click) => {
|
|
TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareOptionsOpened');
|
|
// create fresh every time
|
|
this.schemaCompareOptionDialog = new SchemaCompareOptionsDialog(this.deploymentOptions, this);
|
|
this.schemaCompareOptionDialog.openDialog();
|
|
});
|
|
}
|
|
|
|
private createApplyButton() {
|
|
|
|
this.applyButton = this.view.modelBuilder.button().withProps({
|
|
label: loc.apply,
|
|
iconPath: {
|
|
light: path.join(this.extensionContext.extensionPath, 'media', 'start.svg'),
|
|
dark: path.join(this.extensionContext.extensionPath, 'media', 'start-inverse.svg')
|
|
},
|
|
}).component();
|
|
|
|
this.applyButton.onDidClick(async (_click) => {
|
|
await this.publishChanges();
|
|
});
|
|
}
|
|
|
|
public async publishChanges(): Promise<void> {
|
|
// need only yes button - since the modal dialog has a default cancel
|
|
const yesString = loc.YesButtonText;
|
|
await vscode.window.showWarningMessage(loc.applyConfirmation, { modal: true }, yesString).then(async (result) => {
|
|
if (result === yesString) {
|
|
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyStarted')
|
|
.withAdditionalProperties({
|
|
'startTime': Date.now().toString(),
|
|
'operationId': this.comparisonResult.operationId,
|
|
'targetType': getSchemaCompareEndpointString(this.targetEndpointInfo.endpointType)
|
|
}).send();
|
|
|
|
// disable apply and generate script buttons because the results are no longer valid after applying the changes
|
|
this.setButtonsForRecompare();
|
|
|
|
const service: mssql.ISchemaCompareService = await this.getService();
|
|
let result: azdata.ResultStatus | undefined = undefined;
|
|
|
|
switch (this.targetEndpointInfo.endpointType) {
|
|
case mssql.SchemaCompareEndpointType.Database:
|
|
result = await service.schemaComparePublishDatabaseChanges(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.execute);
|
|
break;
|
|
case mssql.SchemaCompareEndpointType.Project:
|
|
result = await vscode.commands.executeCommand(loc.sqlDatabaseProjectsPublishChanges, this.comparisonResult.operationId, this.targetEndpointInfo.projectFilePath, this.targetEndpointInfo.extractTarget);
|
|
if (!result.success) {
|
|
void vscode.window.showErrorMessage(loc.applyError);
|
|
}
|
|
break;
|
|
case mssql.SchemaCompareEndpointType.Dacpac: // Dacpac is an invalid publish target
|
|
default:
|
|
throw new Error(`Unsupported SchemaCompareEndpointType: ${getSchemaCompareEndpointString(this.targetEndpointInfo.endpointType)}`);
|
|
}
|
|
|
|
if (!result || !result.success || result.errorMessage) {
|
|
TelemetryReporter.createErrorEvent2(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyFailed', undefined, undefined, getTelemetryErrorType(result?.errorMessage))
|
|
.withAdditionalProperties({
|
|
'operationId': this.comparisonResult.operationId,
|
|
'targetType': getSchemaCompareEndpointString(this.targetEndpointInfo.endpointType)
|
|
}).send();
|
|
vscode.window.showErrorMessage(loc.applyErrorMessage(result?.errorMessage));
|
|
|
|
// reenable generate script and apply buttons if apply failed
|
|
this.generateScriptButton.enabled = true;
|
|
this.generateScriptButton.title = loc.generateScriptEnabledMessage;
|
|
this.applyButton.enabled = true;
|
|
this.applyButton.title = loc.applyEnabledMessage;
|
|
} else if (this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Project) {
|
|
const workspaceApi = getDataWorkspaceExtensionApi();
|
|
workspaceApi.showProjectsView();
|
|
workspaceApi.refreshProjectsTree();
|
|
|
|
void vscode.window.showInformationMessage(loc.applySuccess);
|
|
}
|
|
|
|
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyEnded')
|
|
.withAdditionalProperties({
|
|
'endTime': Date.now().toString(),
|
|
'operationId': this.comparisonResult.operationId,
|
|
'targetType': getSchemaCompareEndpointString(this.targetEndpointInfo.endpointType)
|
|
}).send();
|
|
}
|
|
});
|
|
}
|
|
|
|
private resetButtons(resetButtonState: ResetButtonState): void {
|
|
switch (resetButtonState) {
|
|
case (ResetButtonState.noSourceTarget): {
|
|
this.compareButton.enabled = false;
|
|
this.optionsButton.enabled = false;
|
|
this.switchButton.enabled = ((this.sourceName && this.sourceName !== ' ') || (this.targetName && this.targetName !== ' ')) ? true : false; // allows switching if the source or target name is set
|
|
this.openScmpButton.enabled = true;
|
|
this.cancelCompareButton.enabled = false;
|
|
this.selectSourceButton.enabled = true;
|
|
this.selectTargetButton.enabled = true;
|
|
break;
|
|
}
|
|
// Before start and after complete are same functionally. Adding two enum values for clarity.
|
|
case (ResetButtonState.beforeCompareStart):
|
|
case (ResetButtonState.afterCompareComplete): {
|
|
this.compareButton.enabled = true;
|
|
this.optionsButton.enabled = true;
|
|
this.switchButton.enabled = true;
|
|
this.openScmpButton.enabled = true;
|
|
this.saveScmpButton.enabled = true;
|
|
this.cancelCompareButton.enabled = false;
|
|
this.selectSourceButton.enabled = true;
|
|
this.selectTargetButton.enabled = true;
|
|
break;
|
|
}
|
|
case (ResetButtonState.comparing): {
|
|
this.compareButton.enabled = false;
|
|
this.optionsButton.enabled = false;
|
|
this.switchButton.enabled = false;
|
|
this.openScmpButton.enabled = false;
|
|
this.cancelCompareButton.enabled = true;
|
|
this.selectSourceButton.enabled = false;
|
|
this.selectTargetButton.enabled = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Set generate script and apply to false because specific values depend on result and are set separately
|
|
this.generateScriptButton.enabled = false;
|
|
this.applyButton.enabled = false;
|
|
this.generateScriptButton.title = loc.generateScriptEnabledMessage;
|
|
this.applyButton.title = loc.applyEnabledMessage;
|
|
}
|
|
|
|
public setButtonsForRecompare(): void {
|
|
this.generateScriptButton.enabled = false;
|
|
this.applyButton.enabled = false;
|
|
this.generateScriptButton.title = loc.reCompareToRefeshMessage;
|
|
this.applyButton.title = loc.reCompareToRefeshMessage;
|
|
}
|
|
|
|
// reset state afer loading an scmp
|
|
private resetForNewCompare(): void {
|
|
this.flexModel.removeItem(this.splitView);
|
|
this.flexModel.removeItem(this.noDifferencesLabel);
|
|
this.flexModel.addItem(this.startText, { CSSStyles: { 'margin': 'auto' } });
|
|
}
|
|
|
|
private createSwitchButton(): void {
|
|
this.switchButton = this.view.modelBuilder.button().withProps({
|
|
label: loc.switchDirection,
|
|
iconPath: {
|
|
light: path.join(this.extensionContext.extensionPath, 'media', 'switch-directions.svg'),
|
|
dark: path.join(this.extensionContext.extensionPath, 'media', 'switch-directions-inverse.svg')
|
|
},
|
|
title: loc.switchDirectionDescription
|
|
}).component();
|
|
|
|
this.switchButton.onDidClick(async (_click) => {
|
|
TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSwitch');
|
|
// switch source and target
|
|
[this.sourceEndpointInfo, this.targetEndpointInfo] = [this.targetEndpointInfo, this.sourceEndpointInfo];
|
|
[this.sourceName, this.targetName] = [this.targetName, this.sourceName];
|
|
|
|
this.sourceNameComponent.updateProperties({
|
|
value: this.sourceName,
|
|
title: this.sourceName
|
|
});
|
|
|
|
this.targetNameComponent.updateProperties({
|
|
value: this.targetName,
|
|
title: this.targetName
|
|
});
|
|
|
|
// remember that source target have been toggled
|
|
this.sourceTargetSwitched = this.sourceTargetSwitched ? false : true;
|
|
|
|
// only compare if both source and target are set
|
|
if (this.sourceEndpointInfo && this.targetEndpointInfo) {
|
|
await this.startCompare();
|
|
}
|
|
});
|
|
}
|
|
|
|
private createSourceAndTargetButtons(): void {
|
|
this.selectSourceButton = this.view.modelBuilder.button().withProps({
|
|
label: '•••',
|
|
title: loc.selectSource,
|
|
ariaLabel: loc.selectSource,
|
|
secondary: true
|
|
}).component();
|
|
|
|
this.selectSourceButton.onDidClick(async (_click) => {
|
|
TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSelectSource');
|
|
this.schemaCompareDialog = new SchemaCompareDialog(this, undefined, this.extensionContext);
|
|
this.promise = this.schemaCompareDialog.openDialog();
|
|
await this.promise;
|
|
});
|
|
|
|
this.selectTargetButton = this.view.modelBuilder.button().withProps({
|
|
label: '•••',
|
|
title: loc.selectTarget,
|
|
ariaLabel: loc.selectTarget,
|
|
secondary: true
|
|
}).component();
|
|
|
|
this.selectTargetButton.onDidClick(async (_click) => {
|
|
TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSelectTarget');
|
|
this.schemaCompareDialog = new SchemaCompareDialog(this, undefined, this.extensionContext);
|
|
this.promise = await this.schemaCompareDialog.openDialog();
|
|
await this.promise;
|
|
});
|
|
}
|
|
|
|
private createOpenScmpButton() {
|
|
this.openScmpButton = this.view.modelBuilder.button().withProps({
|
|
label: loc.openScmp,
|
|
iconPath: {
|
|
light: path.join(this.extensionContext.extensionPath, 'media', 'open-scmp.svg'),
|
|
dark: path.join(this.extensionContext.extensionPath, 'media', 'open-scmp-inverse.svg')
|
|
},
|
|
title: loc.openScmpDescription
|
|
}).component();
|
|
|
|
this.openScmpButton.onDidClick(async (_click) => {
|
|
await this.openScmp();
|
|
});
|
|
}
|
|
|
|
public async openScmp(): Promise<void> {
|
|
TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareOpenScmpStarted');
|
|
const rootPath = getRootPath();
|
|
let fileUris = await vscode.window.showOpenDialog(
|
|
{
|
|
canSelectFiles: true,
|
|
canSelectFolders: false,
|
|
canSelectMany: false,
|
|
defaultUri: vscode.Uri.file(rootPath),
|
|
openLabel: loc.open,
|
|
filters: {
|
|
'scmp Files': ['scmp'],
|
|
}
|
|
}
|
|
);
|
|
|
|
if (!fileUris || fileUris.length === 0) {
|
|
return;
|
|
}
|
|
|
|
let fileUri = fileUris[0];
|
|
this.openScmpFile(fileUri, true);
|
|
}
|
|
|
|
/**
|
|
* Primary functional entrypoint for opening the schema comparison window with the scmp file Uri provided.
|
|
* @param fileUri .scmp file URI to open Schema Compare extension with
|
|
* @param callFromWithinSC is the call from openScmp? False by default, since it is one of the direct entry points.
|
|
*/
|
|
public async openScmpFile(fileUri: vscode.Uri, callFromWithinSC: boolean = false): Promise<void> {
|
|
if (!callFromWithinSC) {
|
|
//Instantiate and open schema compare window if called from "Open in Schema Compare"
|
|
await this.launch(undefined, undefined, false, undefined);
|
|
}
|
|
const service = await this.getService();
|
|
let startTime = Date.now();
|
|
const result = await service.schemaCompareOpenScmp(fileUri.fsPath);
|
|
if (!result || !result.success) {
|
|
TelemetryReporter.sendErrorEvent2(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareOpenScmpFailed', undefined, undefined, getTelemetryErrorType(result.errorMessage));
|
|
vscode.window.showErrorMessage(loc.openScmpErrorMessage(result.errorMessage));
|
|
return;
|
|
}
|
|
|
|
this.sourceEndpointInfo = await this.constructEndpointInfo(result.sourceEndpointInfo, loc.sourceTitle);
|
|
this.targetEndpointInfo = await this.constructEndpointInfo(result.targetEndpointInfo, loc.targetTitle);
|
|
|
|
this.updateSourceAndTarget();
|
|
this.setDeploymentOptions(result.deploymentOptions);
|
|
this.scmpSourceExcludes = result.excludedSourceElements;
|
|
this.scmpTargetExcludes = result.excludedTargetElements;
|
|
this.sourceTargetSwitched = result.originalTargetName !== this.targetEndpointInfo.databaseName;
|
|
|
|
// clear out any old results
|
|
this.resetForNewCompare();
|
|
|
|
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareOpenScmpEnded')
|
|
.withAdditionalProperties({
|
|
elapsedTime: (Date.now() - startTime).toString()
|
|
}).send();
|
|
}
|
|
|
|
private async constructEndpointInfo(endpoint: mssql.SchemaCompareEndpointInfo, caller: string): Promise<mssql.SchemaCompareEndpointInfo> {
|
|
let ownerUri;
|
|
let endpointInfo;
|
|
if (endpoint && endpoint.endpointType === mssql.SchemaCompareEndpointType.Database) {
|
|
// only set endpoint info if able to connect to the database
|
|
ownerUri = await verifyConnectionAndGetOwnerUri(endpoint, caller);
|
|
}
|
|
if (ownerUri) {
|
|
endpointInfo = endpoint;
|
|
endpointInfo.ownerUri = ownerUri;
|
|
} else if (endpoint.endpointType === mssql.SchemaCompareEndpointType.Project) {
|
|
endpointInfo = {
|
|
endpointType: endpoint.endpointType,
|
|
packageFilePath: '',
|
|
serverDisplayName: '',
|
|
serverName: '',
|
|
databaseName: '',
|
|
ownerUri: '',
|
|
connectionDetails: undefined,
|
|
projectFilePath: endpoint.projectFilePath,
|
|
targetScripts: [],
|
|
dataSchemaProvider: endpoint.dataSchemaProvider,
|
|
extractTarget: endpoint.extractTarget
|
|
};
|
|
} else {
|
|
// need to do this instead of just setting it to the endpoint because some fields are null which will cause an error when sending the compare request
|
|
endpointInfo = {
|
|
endpointType: endpoint.endpointType === mssql.SchemaCompareEndpointType.Database ? mssql.SchemaCompareEndpointType.Database : mssql.SchemaCompareEndpointType.Dacpac,
|
|
serverDisplayName: '',
|
|
serverName: '',
|
|
databaseName: '',
|
|
ownerUri: '',
|
|
packageFilePath: endpoint.packageFilePath,
|
|
connectionDetails: undefined
|
|
};
|
|
}
|
|
return endpointInfo;
|
|
}
|
|
|
|
private createSaveScmpButton(): void {
|
|
this.saveScmpButton = this.view.modelBuilder.button().withProps({
|
|
label: loc.saveScmp,
|
|
iconPath: {
|
|
light: path.join(this.extensionContext.extensionPath, 'media', 'save-scmp.svg'),
|
|
dark: path.join(this.extensionContext.extensionPath, 'media', 'save-scmp-inverse.svg')
|
|
},
|
|
title: loc.saveScmpDescription,
|
|
enabled: false
|
|
}).component();
|
|
|
|
this.saveScmpButton.onDidClick(async (_click) => {
|
|
await this.saveScmp();
|
|
});
|
|
}
|
|
|
|
public async saveScmp(): Promise<void> {
|
|
const rootPath = getRootPath();
|
|
const filePath = await vscode.window.showSaveDialog(
|
|
{
|
|
defaultUri: vscode.Uri.file(rootPath),
|
|
saveLabel: loc.save,
|
|
filters: {
|
|
'scmp Files': ['scmp'],
|
|
}
|
|
}
|
|
);
|
|
|
|
if (!filePath) {
|
|
return;
|
|
}
|
|
|
|
// convert include/exclude maps to arrays of object ids
|
|
let sourceExcludes: mssql.SchemaCompareObjectId[] = this.convertExcludesToObjectIds(this.originalSourceExcludes);
|
|
let targetExcludes: mssql.SchemaCompareObjectId[] = this.convertExcludesToObjectIds(this.originalTargetExcludes);
|
|
|
|
let startTime = Date.now();
|
|
TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSaveScmp');
|
|
const service = await this.getService();
|
|
const result = await service.schemaCompareSaveScmp(this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions, filePath.fsPath, sourceExcludes, targetExcludes);
|
|
if (!result || !result.success) {
|
|
TelemetryReporter.createErrorEvent2(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSaveScmpFailed', undefined, undefined, getTelemetryErrorType(result.errorMessage))
|
|
.withAdditionalProperties({
|
|
operationId: this.comparisonResult.operationId
|
|
}).send();
|
|
vscode.window.showErrorMessage(loc.saveScmpErrorMessage(result.errorMessage));
|
|
}
|
|
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSaveScmpEnded')
|
|
.withAdditionalProperties({
|
|
elapsedTime: (Date.now() - startTime).toString(),
|
|
operationId: this.comparisonResult.operationId
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Converts excluded diff entries into object ids which are needed to save them in an scmp
|
|
*/
|
|
private convertExcludesToObjectIds(excludedDiffEntries: Map<string, mssql.DiffEntry>): mssql.SchemaCompareObjectId[] {
|
|
let result = [];
|
|
excludedDiffEntries.forEach((value: mssql.DiffEntry) => {
|
|
result.push({
|
|
nameParts: value.sourceValue ? value.sourceValue : value.targetValue,
|
|
sqlObjectType: `Microsoft.Data.Tools.Schema.Sql.SchemaModel.${value.name}`
|
|
});
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
private setButtonStatesForNoChanges(enableButtons: boolean): void {
|
|
// generate script and apply can only be enabled if the target is a database or project
|
|
if (this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database ||
|
|
this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Project) {
|
|
this.applyButton.enabled = enableButtons;
|
|
this.applyButton.title = enableButtons ? loc.applyEnabledMessage : loc.applyNoChangesMessage;
|
|
}
|
|
|
|
if (this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) {
|
|
this.generateScriptButton.enabled = enableButtons;
|
|
this.generateScriptButton.title = enableButtons ? loc.generateScriptEnabledMessage : loc.generateScriptNoChangesMessage;
|
|
}
|
|
}
|
|
|
|
private async getService(): Promise<mssql.ISchemaCompareService> {
|
|
if (this.schemaCompareService === null || this.schemaCompareService === undefined) {
|
|
this.schemaCompareService = (await vscode.extensions.getExtension(mssql.extension.name).exports as mssql.IExtension).schemaCompare;
|
|
}
|
|
return this.schemaCompareService;
|
|
}
|
|
|
|
private async GetDefaultDeploymentOptions(): Promise<void> {
|
|
// Same as dacfx default options
|
|
const service = await this.getService();
|
|
let result = await service.schemaCompareGetDefaultOptions();
|
|
this.setDeploymentOptions(result.defaultDeploymentOptions);
|
|
}
|
|
}
|
|
|
|
// Borrowed as is from other extensions
|
|
// TODO : figure out if any inbuilt alternative is available
|
|
export function generateGuid(): string {
|
|
let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
|
|
// c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
|
|
let oct: string = '';
|
|
let tmp: number;
|
|
/* tslint:disable:no-bitwise */
|
|
for (let a: number = 0; a < 4; a++) {
|
|
tmp = (4294967296 * Math.random()) | 0;
|
|
oct += hexValues[tmp & 0xF] +
|
|
hexValues[tmp >> 4 & 0xF] +
|
|
hexValues[tmp >> 8 & 0xF] +
|
|
hexValues[tmp >> 12 & 0xF] +
|
|
hexValues[tmp >> 16 & 0xF] +
|
|
hexValues[tmp >> 20 & 0xF] +
|
|
hexValues[tmp >> 24 & 0xF] +
|
|
hexValues[tmp >> 28 & 0xF];
|
|
}
|
|
|
|
// 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively'
|
|
let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0];
|
|
return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12);
|
|
/* tslint:enable:no-bitwise */
|
|
}
|