From e5aa7527405da7145c0e9c8bbc0cc581b64a674a Mon Sep 17 00:00:00 2001 From: Cory Rivera Date: Tue, 27 Jun 2023 12:32:09 -0700 Subject: [PATCH] Add Detach Database option to database context menu (#23480) --- extensions/mssql/config.json | 2 +- extensions/mssql/package.json | 10 ++++ extensions/mssql/package.nls.json | 3 +- extensions/mssql/src/contracts.ts | 12 ++++ extensions/mssql/src/mssql.d.ts | 10 ++++ .../mssql/src/objectManagement/commands.ts | 33 +++++++++++ .../mssql/src/objectManagement/constants.ts | 4 +- .../mssql/src/objectManagement/interfaces.ts | 8 +++ .../objectManagement/localizedConstants.ts | 16 ++++++ .../objectManagementService.ts | 9 +++ .../ui/detachDatabaseDialog.ts | 57 +++++++++++++++++++ .../ui/objectManagementDialogBase.ts | 22 ++++--- 12 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 extensions/mssql/src/objectManagement/ui/detachDatabaseDialog.ts diff --git a/extensions/mssql/config.json b/extensions/mssql/config.json index a18ab9a289..aa11f53e1e 100644 --- a/extensions/mssql/config.json +++ b/extensions/mssql/config.json @@ -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", diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 91255db2f3..99a5dc4753 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -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" diff --git a/extensions/mssql/package.nls.json b/extensions/mssql/package.nls.json index a8bea713d9..1d59535f9e 100644 --- a/extensions/mssql/package.nls.json +++ b/extensions/mssql/package.nls.json @@ -187,5 +187,6 @@ "title.newObject": "New", "title.objectProperties": "Properties (Preview)", "title.deleteObject": "Delete", - "title.renameObject": "Rename" + "title.renameObject": "Rename", + "title.detachDatabase": "Detach" } diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 33567ccbe9..455609012b 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -1628,6 +1628,18 @@ export namespace SearchObjectRequest { export const type = new RequestType('objectManagement/search'); } +export interface DetachDatabaseRequestParams { + connectionUri: string; + objectUrn: string; + dropConnections: boolean; + updateStatistics: boolean; + generateScript: boolean; +} + +export namespace DetachDatabaseRequest { + export const type = new RequestType('objectManagement/detachDatabase'); +} + // ------------------------------- < Object Management > ------------------------------------ // ------------------------------- < Encryption IV/KEY updation Event > ------------------------------------ diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index fdc1f1b1e9..412a979b02 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -974,6 +974,16 @@ declare module 'mssql' { * @param schema Schema to search in. */ search(contextId: string, objectTypes: string[], searchText?: string, schema?: string): Thenable; + /** + * 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; } // Object Management - End. } diff --git a/extensions/mssql/src/objectManagement/commands.ts b/extensions/mssql/src/objectManagement/commands.ts index a6285ffd12..8c5e15ca65 100644 --- a/extensions/mssql/src/objectManagement/commands.ts +++ b/extensions/mssql/src/objectManagement/commands.ts @@ -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 { + 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> { switch (dialogOptions.objectType) { case ObjectManagement.NodeType.ApplicationRole: diff --git a/extensions/mssql/src/objectManagement/constants.ts b/extensions/mssql/src/objectManagement/constants.ts index 52fe8abcf9..2b078b8da6 100644 --- a/extensions/mssql/src/objectManagement/constants.ts +++ b/extensions/mssql/src/objectManagement/constants.ts @@ -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'; diff --git a/extensions/mssql/src/objectManagement/interfaces.ts b/extensions/mssql/src/objectManagement/interfaces.ts index 36331346a7..f72593d9ae 100644 --- a/extensions/mssql/src/objectManagement/interfaces.ts +++ b/extensions/mssql/src/objectManagement/interfaces.ts @@ -451,6 +451,7 @@ export interface DatabaseViewInfo extends ObjectManagement.ObjectViewInfo { } + +export interface DatabaseFile { + name: string; + type: string; + path: string; + fileGroup: string; +} diff --git a/extensions/mssql/src/objectManagement/localizedConstants.ts b/extensions/mssql/src/objectManagement/localizedConstants.ts index fa6ee056db..eceb0d7a3d 100644 --- a/extensions/mssql/src/objectManagement/localizedConstants.ts +++ b/extensions/mssql/src/objectManagement/localizedConstants.ts @@ -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?"); diff --git a/extensions/mssql/src/objectManagement/objectManagementService.ts b/extensions/mssql/src/objectManagement/objectManagementService.ts index 4258c73dd9..751f6aa0bb 100644 --- a/extensions/mssql/src/objectManagement/objectManagementService.ts +++ b/extensions/mssql/src/objectManagement/objectManagementService.ts @@ -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 { + 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 { + 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++) { diff --git a/extensions/mssql/src/objectManagement/ui/detachDatabaseDialog.ts b/extensions/mssql/src/objectManagement/ui/detachDatabaseDialog.ts new file mode 100644 index 0000000000..e54866dc57 --- /dev/null +++ b/extensions/mssql/src/objectManagement/ui/detachDatabaseDialog.ts @@ -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 { + 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 { + 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 { + await this.objectManagementService.detachDatabase(this.options.connectionUri, this.options.objectUrn, this._dropConnections, this._updateStatistics, false); + } + + protected override async generateScript(): Promise { + return await this.objectManagementService.detachDatabase(this.options.connectionUri, this.options.objectUrn, this._dropConnections, this._updateStatistics, true); + } + + protected override async validateInput(): Promise { + return []; + } +} diff --git a/extensions/mssql/src/objectManagement/ui/objectManagementDialogBase.ts b/extensions/mssql/src/objectManagement/ui/objectManagementDialogBase.ts index f78313faa6..a2cec1a35c 100644 --- a/extensions/mssql/src/objectManagement/ui/objectManagementDialogBase.ts +++ b/extensions/mssql/src/objectManagement/ui/objectManagementDialogBase.ts @@ -35,12 +35,16 @@ export abstract class ObjectManagementDialogBase { + await this.objectManagementService.save(this._contextId, this.objectInfo); + } + protected override async initialize(): Promise { await super.initialize(); const typeDisplayName = localizedConstants.getNodeTypeDisplayName(this.options.objectType); @@ -67,7 +75,7 @@ export abstract class ObjectManagementDialogBase