diff --git a/extensions/mssql/l10n/bundle.l10n.json b/extensions/mssql/l10n/bundle.l10n.json index 91dafa1caf..3879228cc6 100644 --- a/extensions/mssql/l10n/bundle.l10n.json +++ b/extensions/mssql/l10n/bundle.l10n.json @@ -725,6 +725,9 @@ "Exclude Object Types": "Exclude Object Types", "SQLCMD Variables": "SQLCMD Variables", "Revert values to project defaults": "Revert values to project defaults", + "SqlPackage Command": "SqlPackage Command", + "Generate sqlpackage command": "Generate sqlpackage command", + "Copy command to clipboard": "Copy command to clipboard", "Create New Connection Group": "Create New Connection Group", "Edit Connection Group: {0}/{0} is the name of the connection group being edited": { "message": "Edit Connection Group: {0}", @@ -1714,6 +1717,7 @@ "message": "Profile loaded, but the connection could not be automatically established. Please create a connection to {0} then try again.", "comment": ["{0} is the server name"] }, + "Failed to generate SqlPackage command: {0}": "Failed to generate SqlPackage command: {0}", "Schema Compare": "Schema Compare", "Options have changed. Recompare to see the comparison?": "Options have changed. Recompare to see the comparison?", "Failed to generate script: '{0}'/{0} is the error message returned from the generate script operation": { diff --git a/extensions/mssql/src/constants/locConstants.ts b/extensions/mssql/src/constants/locConstants.ts index 8534e209b0..dd25791ec6 100644 --- a/extensions/mssql/src/constants/locConstants.ts +++ b/extensions/mssql/src/constants/locConstants.ts @@ -1414,6 +1414,9 @@ export class PublishProject { args: [serverName], comment: ["{0} is the server name"], }); + public static FailedToGenerateSqlPackageCommand(errorMessage: string) { + return l10n.t("Failed to generate SqlPackage command: {0}", errorMessage); + } } export class SchemaCompare { diff --git a/extensions/mssql/src/controllers/mainController.ts b/extensions/mssql/src/controllers/mainController.ts index 0ea3e032a9..81e5160fd0 100644 --- a/extensions/mssql/src/controllers/mainController.ts +++ b/extensions/mssql/src/controllers/mainController.ts @@ -34,6 +34,7 @@ import { AzureAccountService } from "../services/azureAccountService"; import { AzureResourceService } from "../services/azureResourceService"; import { DacFxService } from "../services/dacFxService"; import { SqlProjectsService } from "../services/sqlProjectsService"; +import { SqlPackageService } from "../languageservice/sqlPackageService"; import { SchemaCompareService } from "../services/schemaCompareService"; import { SqlTasksService } from "../services/sqlTasksService"; import StatusView from "../views/statusView"; @@ -126,6 +127,7 @@ export default class MainController implements vscode.Disposable { public sqlTasksService: SqlTasksService; public dacFxService: DacFxService; public schemaCompareService: SchemaCompareService; + public sqlPackageService: SqlPackageService; public tableExplorerService: ITableExplorerService; public sqlProjectsService: SqlProjectsService; public azureAccountService: AzureAccountService; @@ -575,6 +577,7 @@ export default class MainController implements vscode.Disposable { this.dacFxService = new DacFxService(SqlToolsServerClient.instance); this.sqlProjectsService = new SqlProjectsService(SqlToolsServerClient.instance); this.schemaCompareService = new SchemaCompareService(SqlToolsServerClient.instance); + this.sqlPackageService = new SqlPackageService(SqlToolsServerClient.instance); this.tableExplorerService = new TableExplorerService(SqlToolsServerClient.instance); const azureResourceController = new AzureResourceController(); this.azureAccountService = new AzureAccountService( @@ -2651,6 +2654,7 @@ export default class MainController implements vscode.Disposable { this, this.sqlProjectsService, this.dacFxService, + this.sqlPackageService, deploymentOptions.defaultDeploymentOptions, ); diff --git a/extensions/mssql/src/languageservice/sqlPackageService.ts b/extensions/mssql/src/languageservice/sqlPackageService.ts new file mode 100644 index 0000000000..c371051deb --- /dev/null +++ b/extensions/mssql/src/languageservice/sqlPackageService.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import SqlToolsServiceClient from "../languageservice/serviceclient"; +import * as sqlPackageContracts from "../models/contracts/sqlPackage/sqlPackageContracts"; +import * as mssql from "vscode-mssql"; + +/** + * Service for SqlPackage operations + */ +export class SqlPackageService { + constructor(private _client: SqlToolsServiceClient) {} + + /** + * Generate a SqlPackage command based on the provided parameters + */ + public async generateSqlPackageCommand( + params: mssql.SqlPackageCommandParams, + ): Promise { + return this._client.sendRequest( + sqlPackageContracts.GenerateSqlPackageCommandRequest.type, + params, + ); + } +} diff --git a/extensions/mssql/src/models/contracts/sqlPackage/sqlPackageContracts.ts b/extensions/mssql/src/models/contracts/sqlPackage/sqlPackageContracts.ts new file mode 100644 index 0000000000..66057c98d5 --- /dev/null +++ b/extensions/mssql/src/models/contracts/sqlPackage/sqlPackageContracts.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RequestType } from "vscode-languageclient"; +import * as mssql from "vscode-mssql"; + +/** + * Request to generate SqlPackage command string + */ +export namespace GenerateSqlPackageCommandRequest { + export const type = new RequestType< + mssql.SqlPackageCommandParams, + mssql.SqlPackageCommandResult, + void, + void + >("sqlpackage/generateCommand"); +} diff --git a/extensions/mssql/src/publishProject/publishProjectWebViewController.ts b/extensions/mssql/src/publishProject/publishProjectWebViewController.ts index 9a4477cfe8..79ad7b389e 100644 --- a/extensions/mssql/src/publishProject/publishProjectWebViewController.ts +++ b/extensions/mssql/src/publishProject/publishProjectWebViewController.ts @@ -20,7 +20,9 @@ import { PublishFormContainerFields, PublishDialogState, PublishTarget, + GenerateSqlPackageCommandRequest, } from "../sharedInterfaces/publishDialog"; +import { SqlPackageService } from "../languageservice/sqlPackageService"; import { sendActionEvent, sendErrorEvent } from "../telemetry/telemetry"; import { generatePublishFormComponents } from "./formComponentHelpers"; import { @@ -59,6 +61,7 @@ export class PublishProjectWebViewController extends FormWebviewController< public readonly initialized: Deferred = new Deferred(); private readonly _sqlProjectsService?: SqlProjectsService; private readonly _dacFxService?: mssql.IDacFxService; + private readonly _sqlPackageService?: SqlPackageService; private readonly _connectionManager: ConnectionManager; private readonly _projectController: ProjectController; private readonly _mainController: MainController; @@ -72,6 +75,7 @@ export class PublishProjectWebViewController extends FormWebviewController< mainController: MainController, sqlProjectsService?: SqlProjectsService, dacFxService?: mssql.IDacFxService, + sqlPackageService?: SqlPackageService, deploymentOptions?: mssql.DeploymentOptions, ) { super( @@ -123,6 +127,7 @@ export class PublishProjectWebViewController extends FormWebviewController< this._sqlProjectsService = sqlProjectsService; this._dacFxService = dacFxService; + this._sqlPackageService = sqlPackageService; this._connectionManager = connectionManager; this._projectController = new ProjectController(); this._mainController = mainController; @@ -807,6 +812,8 @@ export class PublishProjectWebViewController extends FormWebviewController< // Update connection fields after background connection completes this._connectionUri = connectionResult.connectionUri || this._connectionUri; + this._connectionString = + connectionResult.connectionString || this._connectionString; if (connectionResult.errorMessage) { this.state.formMessage = { message: Loc.ProfileLoadedConnectionFailed( @@ -952,16 +959,74 @@ export class PublishProjectWebViewController extends FormWebviewController< } }, ); + + // Request handler to generate sqlpackage command string + this.onRequest(GenerateSqlPackageCommandRequest.type, async () => { + try { + const dacpacPath = this.state.projectProperties?.dacpacOutputPath; + + if (!dacpacPath) { + throw new Error("DACPAC path not found. Please build the project first."); + } + + // Build arguments object matching CommandLineArguments structure expected by backend + const commandLineArguments: { [key: string]: string } = { + SourceFile: dacpacPath, + }; + + // Pass connection string if available, otherwise pass server and database name + if (this._connectionString) { + commandLineArguments.TargetConnectionString = this._connectionString; + } else { + // Fallback to server and database name when connection string is not yet available + // (e.g., when profile is loading connection in background) + if (this.state.formState.serverName) { + commandLineArguments.TargetServerName = this.state.formState.serverName; + } + if (this.state.formState.databaseName) { + commandLineArguments.TargetDatabaseName = this.state.formState.databaseName; + } + } + + // Pass publish profile path if available + if (this.state.formState.publishProfilePath) { + commandLineArguments.Profile = this.state.formState.publishProfilePath; + } + + // Serialize arguments as JSON (backend deserializes with PropertyNameCaseInsensitive) + const serializedArguments = JSON.stringify(commandLineArguments); + + // Call SQL Tools Service to generate the command + // Backend will handle all formatting, quoting, and command construction + const result = await this._sqlPackageService.generateSqlPackageCommand({ + action: "Publish" as mssql.CommandLineToolAction, + arguments: serializedArguments, + deploymentOptions: this.state.deploymentOptions, + variables: this.state.formState.sqlCmdVariables, + }); + + if (!result.success) { + // Return error message instead of throwing, so it can be displayed in the dialog + return Loc.FailedToGenerateSqlPackageCommand(result.errorMessage); + } + + return result.command || ""; + } catch (error) { + // Return error message for unexpected errors + return Loc.FailedToGenerateSqlPackageCommand(getErrorMessage(error)); + } + }); } /** * Connects to SQL Server using a connection string and populates the database dropdown. * This happens in the background when loading a publish profile. * @param connectionString The connection string from the publish profile - * @returns Object containing connectionUri if successful, or errorMessage if failed + * @returns Object containing connectionUri and connectionString if successful, or errorMessage if failed */ private async connectAndPopulateDatabases(connectionString: string): Promise<{ connectionUri?: string; + connectionString?: string; errorMessage?: string; }> { const fileUri = `mssql://publish-profile-${Utils.generateGuid()}`; @@ -999,7 +1064,14 @@ export class PublishProjectWebViewController extends FormWebviewController< })); } - return { connectionUri: fileUri }; + // Get connection string for SqlPackage command generation and saving to publish profile + const retrievedConnectionString = await this._connectionManager.getConnectionString( + fileUri, + true, // includePassword + true, // includeApplicationName + ); + + return { connectionUri: fileUri, connectionString: retrievedConnectionString }; } catch (error) { return { errorMessage: getErrorMessage(error) }; } diff --git a/extensions/mssql/src/reactviews/common/locConstants.ts b/extensions/mssql/src/reactviews/common/locConstants.ts index a386888e85..cfc79b50f2 100644 --- a/extensions/mssql/src/reactviews/common/locConstants.ts +++ b/extensions/mssql/src/reactviews/common/locConstants.ts @@ -1066,6 +1066,10 @@ export class LocConstants { SqlCmdVariableNameColumn: l10n.t("Name"), SqlCmdVariableValueColumn: l10n.t("Value"), RevertSqlCmdVariablesToDefaults: l10n.t("Revert values to project defaults"), + SqlPackageCommand: l10n.t("SqlPackage Command"), + GenerateSqlPackageCommand: l10n.t("Generate sqlpackage command"), + SqlPackageCommandTitle: l10n.t("SqlPackage Command"), + copySqlPackageCommandToClipboard: l10n.t("Copy command to clipboard"), }; } diff --git a/extensions/mssql/src/reactviews/pages/PublishProject/components/sqlPackageCommandDialog.tsx b/extensions/mssql/src/reactviews/pages/PublishProject/components/sqlPackageCommandDialog.tsx new file mode 100644 index 0000000000..fbed67d7d8 --- /dev/null +++ b/extensions/mssql/src/reactviews/pages/PublishProject/components/sqlPackageCommandDialog.tsx @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + Button, + Dialog, + DialogActions, + DialogBody, + DialogContent, + DialogSurface, + DialogTitle, + Textarea, +} from "@fluentui/react-components"; +import { Copy24Regular } from "@fluentui/react-icons"; +import { LocConstants } from "../../../common/locConstants"; + +interface SqlPackageCommandDialogProps { + isOpen: boolean; + onClose: () => void; + sqlPackageCommand: string; +} + +export const SqlPackageCommandDialog: React.FC = ({ + isOpen, + onClose, + sqlPackageCommand, +}) => { + const loc = LocConstants.getInstance().publishProject; + const commonLoc = LocConstants.getInstance().common; + + const handleCopySqlPackageCommand = async () => { + await navigator.clipboard.writeText(sqlPackageCommand); + }; + + return ( + + + + + {loc.SqlPackageCommandTitle} +