Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions extensions/mssql/l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down Expand Up @@ -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": {
Expand Down
3 changes: 3 additions & 0 deletions extensions/mssql/src/constants/locConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions extensions/mssql/src/controllers/mainController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -2651,6 +2654,7 @@ export default class MainController implements vscode.Disposable {
this,
this.sqlProjectsService,
this.dacFxService,
this.sqlPackageService,
deploymentOptions.defaultDeploymentOptions,
);

Expand Down
27 changes: 27 additions & 0 deletions extensions/mssql/src/languageservice/sqlPackageService.ts
Original file line number Diff line number Diff line change
@@ -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<mssql.SqlPackageCommandResult> {
return this._client.sendRequest(
sqlPackageContracts.GenerateSqlPackageCommandRequest.type,
params,
);
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -59,6 +61,7 @@ export class PublishProjectWebViewController extends FormWebviewController<
public readonly initialized: Deferred<void> = new Deferred<void>();
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;
Expand All @@ -72,6 +75,7 @@ export class PublishProjectWebViewController extends FormWebviewController<
mainController: MainController,
sqlProjectsService?: SqlProjectsService,
dacFxService?: mssql.IDacFxService,
sqlPackageService?: SqlPackageService,
deploymentOptions?: mssql.DeploymentOptions,
) {
super(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()}`;
Expand Down Expand Up @@ -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) };
}
Expand Down
4 changes: 4 additions & 0 deletions extensions/mssql/src/reactviews/common/locConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<SqlPackageCommandDialogProps> = ({
isOpen,
onClose,
sqlPackageCommand,
}) => {
const loc = LocConstants.getInstance().publishProject;
const commonLoc = LocConstants.getInstance().common;

const handleCopySqlPackageCommand = async () => {
await navigator.clipboard.writeText(sqlPackageCommand);
};

return (
<Dialog open={isOpen}>
<DialogSurface>
<DialogBody>
<DialogTitle
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<span>{loc.SqlPackageCommandTitle}</span>
<Button
appearance="transparent"
size="small"
icon={<Copy24Regular />}
onClick={handleCopySqlPackageCommand}
title={loc.copySqlPackageCommandToClipboard}
/>
</DialogTitle>
<DialogContent>
<div
style={{
display: "flex",
flexDirection: "column",
marginTop: "10px",
}}>
<Textarea
value={sqlPackageCommand}
readOnly
resize="none"
style={{
height: "200px",
fontFamily: "var(--vscode-editor-font-family, monospace)",
fontSize: "var(--vscode-editor-font-size, 13px)",
}}
aria-label={loc.SqlPackageCommandTitle}
/>
</div>
</DialogContent>
<DialogActions>
<Button appearance="secondary" onClick={onClose}>
{commonLoc.close}
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
);
};
Loading
Loading