Add Detach Database option to database context menu (#23480)

This commit is contained in:
Cory Rivera
2023-06-27 12:32:09 -07:00
committed by GitHub
parent fcb56da720
commit e5aa752740
12 changed files with 176 additions and 10 deletions

View File

@@ -1,6 +1,6 @@
{
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
"version": "4.8.0.29",
"version": "4.8.0.31",
"downloadFileNames": {
"Windows_86": "win-x86-net7.0.zip",
"Windows_64": "win-x64-net7.0.zip",

View File

@@ -101,6 +101,11 @@
"category": "MSSQL",
"title": "%title.renameObject%"
},
{
"command": "mssql.detachDatabase",
"category": "MSSQL",
"title": "%title.detachDatabase%"
},
{
"command": "mssql.enableGroupBySchema",
"category": "MSSQL",
@@ -542,6 +547,11 @@
"when": "connectionProvider == MSSQL && nodeType == Column && config.workbench.enablePreviewFeatures && nodePath =~ /^.*\\/Tables\\/.*\\/Columns\\/.*$/",
"group": "0_query@3"
},
{
"command": "mssql.detachDatabase",
"when": "connectionProvider == MSSQL && nodeType == Database && !isCloud && config.workbench.enablePreviewFeatures",
"group": "0_query@4"
},
{
"command": "mssql.enableGroupBySchema",
"when": "connectionProvider == MSSQL && nodeType && nodeType =~ /^(Server|Database)$/ && !config.mssql.objectExplorer.groupBySchema"

View File

@@ -187,5 +187,6 @@
"title.newObject": "New",
"title.objectProperties": "Properties (Preview)",
"title.deleteObject": "Delete",
"title.renameObject": "Rename"
"title.renameObject": "Rename",
"title.detachDatabase": "Detach"
}

View File

@@ -1628,6 +1628,18 @@ export namespace SearchObjectRequest {
export const type = new RequestType<SearchObjectRequestParams, mssql.ObjectManagement.SearchResultItem[], void, void>('objectManagement/search');
}
export interface DetachDatabaseRequestParams {
connectionUri: string;
objectUrn: string;
dropConnections: boolean;
updateStatistics: boolean;
generateScript: boolean;
}
export namespace DetachDatabaseRequest {
export const type = new RequestType<DetachDatabaseRequestParams, string, void, void>('objectManagement/detachDatabase');
}
// ------------------------------- < Object Management > ------------------------------------
// ------------------------------- < Encryption IV/KEY updation Event > ------------------------------------

View File

@@ -974,6 +974,16 @@ declare module 'mssql' {
* @param schema Schema to search in.
*/
search(contextId: string, objectTypes: string[], searchText?: string, schema?: string): Thenable<ObjectManagement.SearchResultItem[]>;
/**
* Detach a database.
* @param connectionUri The URI of the server connection.
* @param objectUrn SMO Urn of the database to be detached. More information: https://learn.microsoft.com/sql/relational-databases/server-management-objects-smo/overview-smo
* @param dropConnections Whether to drop active connections to this database.
* @param updateStatistics Whether to update the optimization statistics related to this database.
* @param generateScript Whether to generate a TSQL script for the operation instead of detaching the database.
* @returns A string value representing the generated TSQL query if generateScript was set to true, and an empty string otherwise.
*/
detachDatabase(connectionUri: string, objectUrn: string, dropConnections: boolean, updateStatistics: boolean, generateScript: boolean): Thenable<string>;
}
// Object Management - End.
}

View File

@@ -23,6 +23,7 @@ import { DatabaseRoleDialog } from './ui/databaseRoleDialog';
import { ApplicationRoleDialog } from './ui/applicationRoleDialog';
import { DatabaseDialog } from './ui/databaseDialog';
import { ServerPropertiesDialog } from './ui/serverPropertiesDialog';
import { DetachDatabaseDialog } from './ui/detachDatabaseDialog';
export function registerObjectManagementCommands(appContext: AppContext) {
// Notes: Change the second parameter to false to use the actual object management service.
@@ -39,6 +40,9 @@ export function registerObjectManagementCommands(appContext: AppContext) {
appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.renameObject', async (context: azdata.ObjectExplorerContext) => {
await handleRenameObjectCommand(context, service);
}));
appContext.extensionContext.subscriptions.push(vscode.commands.registerCommand('mssql.detachDatabase', async (context: azdata.ObjectExplorerContext) => {
await handleDetachDatabase(context, service);
}));
}
function getObjectManagementService(appContext: AppContext, useTestService: boolean): IObjectManagementService {
@@ -237,6 +241,35 @@ async function handleRenameObjectCommand(context: azdata.ObjectExplorerContext,
});
}
async function handleDetachDatabase(context: azdata.ObjectExplorerContext, service: IObjectManagementService): Promise<void> {
const connectionUri = await getConnectionUri(context);
if (!connectionUri) {
return;
}
try {
const parentUrn = await getParentUrn(context);
const options: ObjectManagementDialogOptions = {
connectionUri: connectionUri,
isNewObject: false,
database: context.connectionProfile!.databaseName!,
objectType: context.nodeInfo.nodeType as ObjectManagement.NodeType,
objectName: context.nodeInfo.label,
parentUrn: parentUrn,
objectUrn: context.nodeInfo!.metadata!.urn,
objectExplorerContext: context
};
const dialog = new DetachDatabaseDialog(service, options);
await dialog.open();
}
catch (err) {
TelemetryReporter.createErrorEvent2(ObjectManagementViewName, TelemetryActions.OpenDetachDatabaseDialog, err).withAdditionalProperties({
objectType: context.nodeInfo!.nodeType
}).send();
console.error(err);
await vscode.window.showErrorMessage(objectManagementLoc.OpenDetachDatabaseDialogError(getErrorMessage(err)));
}
}
function getDialog(service: IObjectManagementService, dialogOptions: ObjectManagementDialogOptions): ObjectManagementDialogBase<ObjectManagement.SqlObject, ObjectManagement.ObjectViewInfo<ObjectManagement.SqlObject>> {
switch (dialogOptions.objectType) {
case ObjectManagement.NodeType.ApplicationRole:

View File

@@ -29,6 +29,7 @@ export const CreateDatabaseRoleDocUrl = 'https://learn.microsoft.com/sql/t-sql/s
export const AlterDatabaseRoleDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/alter-role-transact-sql';
export const CreateDatabaseDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/create-database-transact-sql';
export const ViewServerPropertiesDocUrl = 'https://learn.microsoft.com/sql/t-sql/functions/serverproperty-transact-sql';
export const DetachDatabaseDocUrl = 'https://go.microsoft.com/fwlink/?linkid=2240322';
export const DatabasePropertiesDocUrl = 'https://learn.microsoft.com/sql/relational-databases/databases/database-properties-general-page';
export const enum TelemetryActions {
@@ -37,7 +38,8 @@ export const enum TelemetryActions {
OpenNewObjectDialog = 'OpenNewObjectDialog',
OpenPropertiesDialog = 'OpenPropertiesDialog',
RenameObject = 'RenameObject',
UpdateObject = 'UpdateObject'
UpdateObject = 'UpdateObject',
OpenDetachDatabaseDialog = 'OpenDetachDatabaseDialog'
}
export const ObjectManagementViewName = 'ObjectManagement';

View File

@@ -451,6 +451,7 @@ export interface DatabaseViewInfo extends ObjectManagement.ObjectViewInfo<Databa
compatibilityLevels: string[];
containmentTypes: string[];
recoveryModels: string[];
files: DatabaseFile[];
isAzureDB: boolean;
azureBackupRedundancyLevels: string[];
@@ -488,3 +489,10 @@ export interface Server extends ObjectManagement.SqlObject {
export interface ServerViewInfo extends ObjectManagement.ObjectViewInfo<Server> {
}
export interface DatabaseFile {
name: string;
type: string;
path: string;
fileGroup: string;
}

View File

@@ -92,6 +92,13 @@ export function DeleteObjectError(objectType: string, objectName: string, error:
}, "An error occurred while deleting the {0}: {1}. {2}", objectType, objectName, error);
}
export function OpenDetachDatabaseDialogError(error: string): string {
return localize({
key: 'objectManagement.openDetachDatabaseDialogError',
comment: ['{0}: error message.']
}, "An error occurred while opening the detach database dialog. {0}", error);
}
export function OpenObjectPropertiesDialogError(objectType: string, objectName: string, error: string): string {
return localize({
key: 'objectManagement.openObjectPropertiesDialogError',
@@ -162,6 +169,15 @@ export const CurrentSLOText = localize('objectManagement.currentSLOLabel', "Curr
export const EditionText = localize('objectManagement.editionLabel', "Edition");
export const MaxSizeText = localize('objectManagement.maxSizeLabel', "Max Size");
export const AzurePricingLinkText = localize('objectManagement.azurePricingLink', "Azure SQL Database pricing calculator");
export const DetachDatabaseDialogTitle = (dbName: string) => localize('objectManagement.detachDatabaseDialogTitle', "Detach Database - {0} (Preview)", dbName);
export const DetachDropConnections = localize('objectManagement.detachDropConnections', "Drop connnections");
export const DetachUpdateStatistics = localize('objectManagement.detachUpdateStatistics', "Update statistics");
export const DatabaseFilesLabel = localize('objectManagement.databaseFiles', "Database Files");
export const DatabaseFileNameLabel = localize('objectManagement.databaseFileName', "Name");
export const DatabaseFileTypeLabel = localize('objectManagement.databaseFileType', "Type");
export const DatabaseFilePathLabel = localize('objectManagement.databaseFilePath', "Path");
export const DatabaseFileGroupLabel = localize('objectManagement.databaseFileGroup', "File Group");
export const DetachDatabaseOptions = localize('objectManagement.detachDatabaseOptions', "Detach Database Options");
// Login
export const BlankPasswordConfirmationText: string = localize('objectManagement.blankPasswordConfirmation', "Creating a login with a blank password is a security risk. Are you sure you want to continue?");

View File

@@ -65,6 +65,11 @@ export class ObjectManagementService extends BaseService implements IObjectManag
const params: contracts.SearchObjectRequestParams = { contextId, searchText, objectTypes, schema };
return this.runWithErrorHandling(contracts.SearchObjectRequest.type, params);
}
async detachDatabase(connectionUri: string, objectUrn: string, dropConnections: boolean, updateStatistics: boolean, generateScript: boolean): Promise<string> {
const params: contracts.DetachDatabaseRequestParams = { connectionUri, objectUrn, dropConnections, updateStatistics, generateScript };
return this.runWithErrorHandling(contracts.DetachDatabaseRequest.type, params);
}
}
const ServerLevelSecurableTypes: SecurableTypeMetadata[] = [
@@ -232,6 +237,10 @@ export class TestObjectManagementService implements IObjectManagementService {
return this.delayAndResolve(items);
}
async detachDatabase(connectionUri: string, objectUrn: string, dropConnections: boolean, updateStatistics: boolean, generateScript: boolean): Promise<string> {
return this.delayAndResolve('');
}
private generateSearchResult(objectType: ObjectManagement.NodeType, schema: string | undefined, count: number): ObjectManagement.SearchResultItem[] {
let items: ObjectManagement.SearchResultItem[] = [];
for (let i = 0; i < count; i++) {

View File

@@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './objectManagementDialogBase';
import { IObjectManagementService, ObjectManagement } from 'mssql';
import { Database, DatabaseViewInfo } from '../interfaces';
import { DetachDatabaseDocUrl } from '../constants';
import { DatabaseFileGroupLabel, DatabaseFileNameLabel, DatabaseFilePathLabel, DatabaseFileTypeLabel, DatabaseFilesLabel, DetachDatabaseDialogTitle, DetachDatabaseOptions, DetachDropConnections, DetachUpdateStatistics } from '../localizedConstants';
export class DetachDatabaseDialog extends ObjectManagementDialogBase<Database, DatabaseViewInfo> {
private _dropConnections = false;
private _updateStatistics = false;
constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) {
super(objectManagementService, options, DetachDatabaseDialogTitle(options.database), 'DetachDatabase');
}
protected override get isDirty(): boolean {
return true;
}
protected async initializeUI(): Promise<void> {
let tableData = this.viewInfo.files.map(file => [file.name, file.type, file.fileGroup, file.path]);
let columnNames = [DatabaseFileNameLabel, DatabaseFileTypeLabel, DatabaseFileGroupLabel, DatabaseFilePathLabel];
let fileTable = this.createTable(DatabaseFilesLabel, columnNames, tableData);
let tableGroup = this.createGroup(DatabaseFilesLabel, [fileTable], false);
let connCheckbox = this.createCheckbox(DetachDropConnections, async checked => {
this._dropConnections = checked;
});
let updateCheckbox = this.createCheckbox(DetachUpdateStatistics, async checked => {
this._updateStatistics = checked;
});
let checkboxGroup = this.createGroup(DetachDatabaseOptions, [connCheckbox, updateCheckbox], false);
let components = [tableGroup, checkboxGroup];
this.formContainer.addItems(components);
}
protected override get helpUrl(): string {
return DetachDatabaseDocUrl;
}
protected override async saveChanges(contextId: string, object: ObjectManagement.SqlObject): Promise<void> {
await this.objectManagementService.detachDatabase(this.options.connectionUri, this.options.objectUrn, this._dropConnections, this._updateStatistics, false);
}
protected override async generateScript(): Promise<string> {
return await this.objectManagementService.detachDatabase(this.options.connectionUri, this.options.objectUrn, this._dropConnections, this._updateStatistics, true);
}
protected override async validateInput(): Promise<string[]> {
return [];
}
}

View File

@@ -35,12 +35,16 @@ export abstract class ObjectManagementDialogBase<ObjectInfoType extends ObjectMa
private _viewInfo: ViewInfoType;
private _originalObjectInfo: ObjectInfoType;
constructor(protected readonly objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) {
super(options.isNewObject ? localizedConstants.NewObjectDialogTitle(localizedConstants.getNodeTypeDisplayName(options.objectType, true)) :
localizedConstants.ObjectPropertiesDialogTitle(localizedConstants.getNodeTypeDisplayName(options.objectType, true), options.objectName),
getDialogName(options.objectType, options.isNewObject),
options
);
constructor(protected readonly objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions, dialogTitle?: string, dialogName?: string) {
if (!dialogTitle) {
dialogTitle = options.isNewObject
? localizedConstants.NewObjectDialogTitle(localizedConstants.getNodeTypeDisplayName(options.objectType, true))
: localizedConstants.ObjectPropertiesDialogTitle(localizedConstants.getNodeTypeDisplayName(options.objectType, true), options.objectName);
}
if (!dialogName) {
dialogName = getDialogName(options.objectType, options.isNewObject);
}
super(dialogTitle, dialogName, options);
this._contextId = generateUuid();
}
@@ -54,6 +58,10 @@ export abstract class ObjectManagementDialogBase<ObjectInfoType extends ObjectMa
return errors;
}
protected async saveChanges(contextId: string, object: ObjectManagement.SqlObject): Promise<void> {
await this.objectManagementService.save(this._contextId, this.objectInfo);
}
protected override async initialize(): Promise<void> {
await super.initialize();
const typeDisplayName = localizedConstants.getNodeTypeDisplayName(this.options.objectType);
@@ -67,7 +75,7 @@ export abstract class ObjectManagementDialogBase<ObjectInfoType extends ObjectMa
try {
if (this.isDirty) {
const startTime = Date.now();
await this.objectManagementService.save(this._contextId, this.objectInfo);
await this.saveChanges(this._contextId, this.objectInfo);
if (this.options.objectExplorerContext) {
if (this.options.isNewObject) {
await refreshNode(this.options.objectExplorerContext);