Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Detach Database option to database context menu #23480

Merged
merged 26 commits into from
Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5469ca3
Add scaffolding for attach and detach database
corivera Jun 12, 2023
4c1a187
Add params
corivera Jun 12, 2023
b08c627
Exclude attach and detach for azure servers
corivera Jun 12, 2023
6709b2d
Update arguments
corivera Jun 12, 2023
ea92f67
Update args
corivera Jun 12, 2023
7765d0f
Update string
corivera Jun 12, 2023
af135dc
Merge branch 'main' into corivera/attachDetach
corivera Jun 13, 2023
da19d5d
Merge branch 'main' into corivera/attachDetach
corivera Jun 14, 2023
bc6eb61
Merge branch 'main' into corivera/attachDetach
corivera Jun 16, 2023
93948aa
Merge branch 'main' into corivera/attachDetach
corivera Jun 19, 2023
f7dec8f
Add detach database dialog
corivera Jun 19, 2023
367107a
Remove attach database changes
corivera Jun 19, 2023
80b5cb2
Add UI
corivera Jun 19, 2023
60edf50
Enable buttons by default
corivera Jun 19, 2023
a251027
Skip validate
corivera Jun 19, 2023
739fbcc
Set boolean initial values
corivera Jun 19, 2023
1e36857
Set URI correctly
corivera Jun 19, 2023
9983d44
Populate file info
corivera Jun 20, 2023
1033f50
Add fwlink for detach DB docs
corivera Jun 20, 2023
50dbaf1
Change RPC method name
corivera Jun 21, 2023
ed08760
Combine scripting method
corivera Jun 21, 2023
b4bef23
Merge branch 'main' into corivera/attachDetach
corivera Jun 22, 2023
972be8a
Merge branch 'main' into corivera/attachDetach
corivera Jun 26, 2023
d169dc3
Bump STS version
corivera Jun 26, 2023
f034473
Remove unnecessary strings
corivera Jun 26, 2023
8b86392
Remove column styling
corivera Jun 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion extensions/mssql/config.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
10 changes: 10 additions & 0 deletions extensions/mssql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@
"category": "MSSQL",
"title": "%title.renameObject%"
},
{
"command": "mssql.detachDatabase",
"category": "MSSQL",
"title": "%title.detachDatabase%"
},
{
"command": "mssql.enableGroupBySchema",
"category": "MSSQL",
Expand Down Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion extensions/mssql/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,5 +187,6 @@
"title.newObject": "New",
"title.objectProperties": "Properties (Preview)",
"title.deleteObject": "Delete",
"title.renameObject": "Rename"
"title.renameObject": "Rename",
"title.detachDatabase": "Detach"
}
12 changes: 12 additions & 0 deletions extensions/mssql/src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 > ------------------------------------
Expand Down
10 changes: 10 additions & 0 deletions extensions/mssql/src/mssql.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
33 changes: 33 additions & 0 deletions extensions/mssql/src/objectManagement/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
alanrenmsft marked this conversation as resolved.
Show resolved Hide resolved
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:
Expand Down
4 changes: 3 additions & 1 deletion extensions/mssql/src/objectManagement/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -37,7 +38,8 @@ export const enum TelemetryActions {
OpenNewObjectDialog = 'OpenNewObjectDialog',
OpenPropertiesDialog = 'OpenPropertiesDialog',
RenameObject = 'RenameObject',
UpdateObject = 'UpdateObject'
UpdateObject = 'UpdateObject',
OpenDetachDatabaseDialog = 'OpenDetachDatabaseDialog'
}

export const ObjectManagementViewName = 'ObjectManagement';
Expand Down
8 changes: 8 additions & 0 deletions extensions/mssql/src/objectManagement/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ export interface DatabaseViewInfo extends ObjectManagement.ObjectViewInfo<Databa
compatibilityLevels: string[];
containmentTypes: string[];
recoveryModels: string[];
files: DatabaseFile[];

isAzureDB: boolean;
azureBackupRedundancyLevels: string[];
Expand Down Expand Up @@ -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;
}
16 changes: 16 additions & 0 deletions extensions/mssql/src/objectManagement/localizedConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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?");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand Down Expand Up @@ -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++) {
Expand Down
57 changes: 57 additions & 0 deletions extensions/mssql/src/objectManagement/ui/detachDatabaseDialog.ts
Original file line number Diff line number Diff line change
@@ -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 [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand All @@ -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);
Expand All @@ -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);
Expand Down
Loading