diff --git a/extensions/mssql/l10n/bundle.l10n.json b/extensions/mssql/l10n/bundle.l10n.json index 57f10979a5..a57b1ea099 100644 --- a/extensions/mssql/l10n/bundle.l10n.json +++ b/extensions/mssql/l10n/bundle.l10n.json @@ -1194,6 +1194,7 @@ "Operation failed": "Operation failed", "An unexpected error occurred": "An unexpected error occurred", "Failed to load databases": "Failed to load databases", + "Loading databases...": "Loading databases...", "DACPAC deployed successfully": "DACPAC deployed successfully", "DACPAC extracted successfully": "DACPAC extracted successfully", "BACPAC imported successfully": "BACPAC imported successfully", diff --git a/extensions/mssql/src/controllers/dacpacDialogWebviewController.ts b/extensions/mssql/src/controllers/dacpacDialogWebviewController.ts index ff5d43c211..e27c891623 100644 --- a/extensions/mssql/src/controllers/dacpacDialogWebviewController.ts +++ b/extensions/mssql/src/controllers/dacpacDialogWebviewController.ts @@ -596,24 +596,47 @@ export class DacpacDialogWebviewController extends ReactWebviewPanelController< /** * Lists databases on the connected server */ - private async listDatabases(ownerUri: string): Promise<{ databases: string[] }> { + private async listDatabases( + ownerUri: string, + ): Promise<{ databases: string[]; errorMessage?: string }> { + const systemDatabases = ["master", "tempdb", "model", "msdb"]; + const stateDatabaseName = this.state.databaseName; + + let errorMessage: string | undefined; try { - const result = await this.connectionManager.client.sendRequest( - ListDatabasesRequest.type, - { ownerUri: ownerUri }, - ); + const databaseNames = await this.connectionManager.listDatabases(ownerUri); // Filter out system databases - const systemDatabases = ["master", "tempdb", "model", "msdb"]; - const userDatabases = (result.databaseNames || []).filter( + const userDatabases = (databaseNames || []).filter( (db) => !systemDatabases.includes(db.toLowerCase()), ); - return { databases: userDatabases }; + if (userDatabases.length > 0) { + // Ensure the state database is in the list if set + if ( + stateDatabaseName && + !systemDatabases.includes(stateDatabaseName.toLowerCase()) && + !userDatabases.includes(stateDatabaseName) + ) { + userDatabases.unshift(stateDatabaseName); + } + return { databases: userDatabases }; + } } catch (error) { this.logger.error(`Failed to list databases: ${error}`); - return { databases: [] }; + errorMessage = error instanceof Error ? error.message : "Failed to list databases"; } + + // Fallback: if the database list is empty or the request failed, + // use the database name from the initial state (set via ObjectExplorerUtils.getDatabaseName) + if (stateDatabaseName && !systemDatabases.includes(stateDatabaseName.toLowerCase())) { + return { + databases: [stateDatabaseName], + errorMessage, + }; + } + + return { databases: [], errorMessage }; } /** @@ -803,6 +826,7 @@ export class DacpacDialogWebviewController extends ReactWebviewPanelController< isConnected: boolean; errorMessage?: string; isFabric?: boolean; + databaseName?: string; }> { try { // Find the profile in saved connections @@ -824,6 +848,7 @@ export class DacpacDialogWebviewController extends ReactWebviewPanelController< // Check if this is a Fabric connection const isFabric = this.isFabricConnection(profile as IConnectionDialogProfile); + const databaseName = profile.database || ""; // Check if already connected and the connection is valid let ownerUri = this.connectionManager.getUriForConnection(profile); @@ -833,6 +858,7 @@ export class DacpacDialogWebviewController extends ReactWebviewPanelController< ownerUri, isConnected: true, isFabric, + databaseName, }; } @@ -848,6 +874,7 @@ export class DacpacDialogWebviewController extends ReactWebviewPanelController< ownerUri, isConnected: true, isFabric, + databaseName, }; } else { // Check if connection failed due to error or if it was never initiated @@ -862,6 +889,7 @@ export class DacpacDialogWebviewController extends ReactWebviewPanelController< isConnected: false, errorMessage, isFabric, + databaseName, }; } } catch (error) { @@ -920,6 +948,13 @@ export class DacpacDialogWebviewController extends ReactWebviewPanelController< return { isValid: false, errorMessage }; } + // Check if the database matches the one from the active connection. + // If the user lacks permissions to list databases but is connected to a specific database, + // we should trust the connection's database and skip the server-side existence check. + const isConnectionDatabase = + this.state.databaseName && + databaseName.toLowerCase() === this.state.databaseName.toLowerCase(); + // Check if database exists try { const result = await this.connectionManager.client.sendRequest( @@ -951,6 +986,11 @@ export class DacpacDialogWebviewController extends ReactWebviewPanelController< // For Extract/Export operations, database must exist if (!shouldNotExist && !exists) { + // If the database matches the connection's database, trust it even if + // the list is incomplete (user may lack permissions to list all databases) + if (isConnectionDatabase) { + return { isValid: true }; + } return { isValid: false, errorMessage: LocConstants.DacpacDialog.DatabaseNotFound, @@ -959,6 +999,11 @@ export class DacpacDialogWebviewController extends ReactWebviewPanelController< return { isValid: true }; } catch (error) { + // If listing databases failed but the database matches the connection's database, + // allow the operation since the user is already connected to this database + if (isConnectionDatabase) { + return { isValid: true }; + } const errorMessage = error instanceof Error ? `Failed to validate database name: ${error.message}` diff --git a/extensions/mssql/src/reactviews/common/locConstants.ts b/extensions/mssql/src/reactviews/common/locConstants.ts index ff796f98c5..27e4ab4586 100644 --- a/extensions/mssql/src/reactviews/common/locConstants.ts +++ b/extensions/mssql/src/reactviews/common/locConstants.ts @@ -1827,6 +1827,7 @@ export class LocConstants { operationFailed: l10n.t("Operation failed"), unexpectedError: l10n.t("An unexpected error occurred"), failedToLoadDatabases: l10n.t("Failed to load databases"), + loadingDatabases: l10n.t("Loading databases..."), deploySuccess: l10n.t("DACPAC deployed successfully"), extractSuccess: l10n.t("DACPAC extracted successfully"), importSuccess: l10n.t("BACPAC imported successfully"), diff --git a/extensions/mssql/src/reactviews/pages/DacpacDialog/SourceDatabaseSection.tsx b/extensions/mssql/src/reactviews/pages/DacpacDialog/SourceDatabaseSection.tsx index 1f0139b135..35715f4d7b 100644 --- a/extensions/mssql/src/reactviews/pages/DacpacDialog/SourceDatabaseSection.tsx +++ b/extensions/mssql/src/reactviews/pages/DacpacDialog/SourceDatabaseSection.tsx @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Dropdown, Field, Input, makeStyles, Option } from "@fluentui/react-components"; +import { Dropdown, Field, Input, makeStyles, Option, Spinner } from "@fluentui/react-components"; import { locConstants } from "../../common/locConstants"; /** @@ -19,11 +19,13 @@ interface SourceDatabaseSectionProps { setDatabaseName: (value: string) => void; availableDatabases: string[]; isOperationInProgress: boolean; + isLoadingDatabases: boolean; ownerUri: string; validationMessages: Record; showDatabaseSource: boolean; showNewDatabase: boolean; isFabric?: boolean; + onDropdownOpen?: () => void; } const useStyles = makeStyles({ @@ -39,11 +41,13 @@ export const SourceDatabaseSection = ({ setDatabaseName, availableDatabases, isOperationInProgress, + isLoadingDatabases, ownerUri, validationMessages, showDatabaseSource, showNewDatabase, isFabric = false, + onDropdownOpen, }: SourceDatabaseSectionProps) => { const classes = useStyles(); @@ -67,14 +71,30 @@ export const SourceDatabaseSection = ({ placeholder={locConstants.dacpacDialog.selectDatabase} value={databaseName} selectedOptions={[databaseName]} - onOptionSelect={(_, data) => setDatabaseName(data.optionText || "")} + onOptionSelect={(_, data) => { + if (!isLoadingDatabases) { + setDatabaseName(data.optionText || ""); + } + }} + onOpenChange={(_, data) => { + if (data.open && onDropdownOpen) { + onDropdownOpen(); + } + }} disabled={isOperationInProgress || !ownerUri || isFabric} aria-label={locConstants.dacpacDialog.sourceDatabaseLabel}> - {availableDatabases.map((db) => ( - - ))} + ) : ( + availableDatabases.map((db) => ( + + )) + )} ) : ( diff --git a/extensions/mssql/src/reactviews/pages/DacpacDialog/TargetDatabaseSection.tsx b/extensions/mssql/src/reactviews/pages/DacpacDialog/TargetDatabaseSection.tsx index 2b00ce5d61..8955104154 100644 --- a/extensions/mssql/src/reactviews/pages/DacpacDialog/TargetDatabaseSection.tsx +++ b/extensions/mssql/src/reactviews/pages/DacpacDialog/TargetDatabaseSection.tsx @@ -12,6 +12,7 @@ import { Option, Radio, RadioGroup, + Spinner, } from "@fluentui/react-components"; import { locConstants } from "../../common/locConstants"; @@ -30,9 +31,11 @@ interface TargetDatabaseSectionProps { setIsNewDatabase: (value: boolean) => void; availableDatabases: string[]; isOperationInProgress: boolean; + isLoadingDatabases: boolean; ownerUri: string; validationMessages: Record; isFabric?: boolean; + onDropdownOpen?: () => void; } const useStyles = makeStyles({ @@ -55,9 +58,11 @@ export const TargetDatabaseSection = ({ setIsNewDatabase, availableDatabases, isOperationInProgress, + isLoadingDatabases, ownerUri, validationMessages, isFabric = false, + onDropdownOpen, }: TargetDatabaseSectionProps) => { const classes = useStyles(); @@ -117,14 +122,30 @@ export const TargetDatabaseSection = ({ placeholder={locConstants.dacpacDialog.selectDatabase} value={databaseName} selectedOptions={[databaseName]} - onOptionSelect={(_, data) => setDatabaseName(data.optionText || "")} + onOptionSelect={(_, data) => { + if (!isLoadingDatabases) { + setDatabaseName(data.optionText || ""); + } + }} + onOpenChange={(_, data) => { + if (data.open && onDropdownOpen) { + onDropdownOpen(); + } + }} disabled={isOperationInProgress || !ownerUri} aria-label={locConstants.dacpacDialog.databaseNameLabel}> - {availableDatabases.map((db) => ( - - ))} + ) : ( + availableDatabases.map((db) => ( + + )) + )} )} diff --git a/extensions/mssql/src/reactviews/pages/DacpacDialog/dacpacDialogForm.tsx b/extensions/mssql/src/reactviews/pages/DacpacDialog/dacpacDialogForm.tsx index 8b14922331..c878ce95f4 100644 --- a/extensions/mssql/src/reactviews/pages/DacpacDialog/dacpacDialogForm.tsx +++ b/extensions/mssql/src/reactviews/pages/DacpacDialog/dacpacDialogForm.tsx @@ -5,7 +5,7 @@ import { Button, Link, makeStyles, tokens } from "@fluentui/react-components"; import { DatabaseArrowRight20Regular } from "@fluentui/react-icons"; -import { useState, useEffect, useContext } from "react"; +import { useState, useEffect, useContext, useRef } from "react"; import * as dacpacDialog from "../../../sharedInterfaces/dacpacDialog"; import { IConnectionDialogProfile } from "../../../sharedInterfaces/connectionDialog"; import { dataTierApplicationsDocumentationUrl } from "../../common/constants"; @@ -67,8 +67,15 @@ export const DacpacDialogForm = () => { ); const [ownerUri, setOwnerUri] = useState(initialOwnerUri || ""); const [isConnecting, setIsConnecting] = useState(false); + const [isLoadingDatabases, setIsLoadingDatabases] = useState(false); const [isFabric, setIsFabric] = useState(false); + // Track the current connection's database name (updated on each server connection) + const connectionDatabaseNameRef = useRef(initialDatabaseName || ""); + + // Track whether the full database list has been fetched for the current connection + const databasesLoadedRef = useRef(false); + // Load available connections when component mounts useEffect(() => { void loadConnections(); @@ -81,18 +88,22 @@ export const DacpacDialogForm = () => { }; }, []); - // Load available databases when server or operation changes + // When server or operation changes, reset to the connection's default database + // The full database list will only be fetched when the user opens the dropdown useEffect(() => { - if ( - ownerUri && - (operationType === dacpacDialog.DacPacDialogOperationType.Deploy || - operationType === dacpacDialog.DacPacDialogOperationType.Extract || - operationType === dacpacDialog.DacPacDialogOperationType.Export || - (operationType === dacpacDialog.DacPacDialogOperationType.Import && isFabric)) - ) { - void loadDatabases(); + if (ownerUri && !isConnecting) { + const connDbName = connectionDatabaseNameRef.current; + if (connDbName) { + setDatabaseName(connDbName); + setAvailableDatabases([connDbName]); + setIsNewDatabase(false); + } else { + setDatabaseName(""); + setAvailableDatabases([]); + } + databasesLoadedRef.current = false; } - }, [operationType, ownerUri, isFabric]); + }, [ownerUri, isConnecting]); // Update file path suggestion when database or operation type changes for Export/Extract useEffect(() => { @@ -118,19 +129,15 @@ export const DacpacDialogForm = () => { void updateSuggestedPath(); }, [databaseName, operationType, context]); - // Clear state when switching operations (Deploy <-> Extract <-> Export <-> Import) + // Clear file/app state when switching operations (Deploy <-> Extract <-> Export <-> Import) useEffect(() => { - setDatabaseName(""); setFilePath(""); setApplicationName(""); setApplicationVersion(DEFAULT_APPLICATION_VERSION); + // Mark databases as not loaded so next dropdown open re-fetches + databasesLoadedRef.current = false; }, [operationType]); - // Clear the selected database if the server is changed - useEffect(() => { - setDatabaseName(""); - }, [selectedProfileId]); - const loadConnections = async () => { try { setIsConnecting(true); @@ -150,6 +157,11 @@ export const DacpacDialogForm = () => { if (result.selectedConnection) { setSelectedProfileId(result.selectedConnection.id!); + // Track the connection's database name (prefer connection profile, fall back to OE node) + const connDbName = + result.selectedConnection.database || initialDatabaseName || ""; + connectionDatabaseNameRef.current = connDbName; + // If we have an ownerUri (either provided or from auto-connect) if (result.ownerUri) { setOwnerUri(result.ownerUri); @@ -158,7 +170,11 @@ export const DacpacDialogForm = () => { // Check if this is a Fabric connection if (result.isFabric) { setIsFabric(true); - // For Fabric, default to existing database + } + + // If there is an initial database name (from Object Explorer or connection profile), + // default to existing database mode + if (result.isFabric || connDbName) { setIsNewDatabase(false); } @@ -206,6 +222,10 @@ export const DacpacDialogForm = () => { if (result?.isConnected && result.ownerUri) { setOwnerUri(result.ownerUri); + // Track the connection's database name + if (result.databaseName) { + connectionDatabaseNameRef.current = result.databaseName; + } // Check if this is a Fabric connection if (result.isFabric) { setIsFabric(true); @@ -213,6 +233,10 @@ export const DacpacDialogForm = () => { setIsNewDatabase(false); } else { setIsFabric(false); + // If the connection has a database, default to existing database mode + if (result.databaseName) { + setIsNewDatabase(false); + } } // Databases will be loaded automatically via useEffect } else { @@ -243,18 +267,43 @@ export const DacpacDialogForm = () => { } }; + /** + * Called when the user opens the database dropdown. + * Fetches the full database list from the server on first open. + */ + const handleDatabaseDropdownOpen = () => { + if (!databasesLoadedRef.current && ownerUri) { + void loadDatabases(); + } + }; + const loadDatabases = async () => { + const connDbName = connectionDatabaseNameRef.current; + setIsLoadingDatabases(true); try { const result = await context?.listDatabases({ ownerUri: ownerUri || "" }); if (result?.databases) { setAvailableDatabases(result.databases); - // Auto-select database if: - // 1. Fabric connection (always select first database) - if (isFabric && result.databases.length > 0) { - // For Fabric, always select the first database - setDatabaseName(result.databases[0]); + databasesLoadedRef.current = true; + // Only auto-select if no database is currently selected + if (!databaseName) { + if (isFabric && result.databases.length > 0) { + setDatabaseName(result.databases[0]); + } else if (connDbName && result.databases.includes(connDbName)) { + setDatabaseName(connDbName); + } } } + // Show error from backend if database listing failed + if (result?.errorMessage) { + setValidationMessages((prev) => ({ + ...prev, + database: { + message: `${locConstants.dacpacDialog.failedToLoadDatabases}: ${result.errorMessage}`, + severity: "error", + }, + })); + } } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); setValidationMessages((prev) => ({ @@ -264,6 +313,8 @@ export const DacpacDialogForm = () => { severity: "error", }, })); + } finally { + setIsLoadingDatabases(false); } }; @@ -395,11 +446,19 @@ export const DacpacDialogForm = () => { const clearForm = () => { setFilePath(""); - setDatabaseName(""); setApplicationName(""); setApplicationVersion(DEFAULT_APPLICATION_VERSION); setValidationMessages({}); - setIsNewDatabase(true); + + // Restore the connection's default database instead of clearing it + const connDbName = connectionDatabaseNameRef.current; + if (connDbName) { + setDatabaseName(connDbName); + setIsNewDatabase(false); + } else { + setDatabaseName(""); + setIsNewDatabase(true); + } }; /** @@ -697,11 +756,13 @@ export const DacpacDialogForm = () => { setDatabaseName={setDatabaseName} availableDatabases={availableDatabases} isOperationInProgress={isOperationInProgress} + isLoadingDatabases={isLoadingDatabases} ownerUri={ownerUri} validationMessages={validationMessages} showDatabaseSource={showDatabaseSource} showNewDatabase={false} isFabric={isFabric} + onDropdownOpen={handleDatabaseDropdownOpen} /> )} @@ -724,9 +785,11 @@ export const DacpacDialogForm = () => { setIsNewDatabase={setIsNewDatabase} availableDatabases={availableDatabases} isOperationInProgress={isOperationInProgress} + isLoadingDatabases={isLoadingDatabases} ownerUri={ownerUri} validationMessages={validationMessages} isFabric={isFabric} + onDropdownOpen={handleDatabaseDropdownOpen} /> )} @@ -737,11 +800,13 @@ export const DacpacDialogForm = () => { setDatabaseName={setDatabaseName} availableDatabases={availableDatabases} isOperationInProgress={isOperationInProgress} + isLoadingDatabases={isLoadingDatabases} ownerUri={ownerUri} validationMessages={validationMessages} showDatabaseSource={false} showNewDatabase={showNewDatabase} isFabric={isFabric} + onDropdownOpen={handleDatabaseDropdownOpen} /> )} diff --git a/extensions/mssql/src/reactviews/pages/DacpacDialog/dacpacDialogStateProvider.tsx b/extensions/mssql/src/reactviews/pages/DacpacDialog/dacpacDialogStateProvider.tsx index 0784ee0228..89fde0fa6f 100644 --- a/extensions/mssql/src/reactviews/pages/DacpacDialog/dacpacDialogStateProvider.tsx +++ b/extensions/mssql/src/reactviews/pages/DacpacDialog/dacpacDialogStateProvider.tsx @@ -56,13 +56,19 @@ export interface DacpacDialogRpcMethods { } | undefined >; - connectToServer: (params: { - profileId: string; - }) => Promise< - | { ownerUri: string; isConnected: boolean; errorMessage?: string; isFabric?: boolean } + connectToServer: (params: { profileId: string }) => Promise< + | { + ownerUri: string; + isConnected: boolean; + errorMessage?: string; + isFabric?: boolean; + databaseName?: string; + } | undefined >; - listDatabases: (params: { ownerUri: string }) => Promise<{ databases: string[] } | undefined>; + listDatabases: (params: { + ownerUri: string; + }) => Promise<{ databases: string[]; errorMessage?: string } | undefined>; // File browsing methods browseInputFile: (params: { diff --git a/extensions/mssql/src/sharedInterfaces/dacpacDialog.ts b/extensions/mssql/src/sharedInterfaces/dacpacDialog.ts index 9304cd651c..832e314973 100644 --- a/extensions/mssql/src/sharedInterfaces/dacpacDialog.ts +++ b/extensions/mssql/src/sharedInterfaces/dacpacDialog.ts @@ -177,9 +177,11 @@ export namespace ValidateFilePathWebviewRequest { * Request to list databases on a server from the webview */ export namespace ListDatabasesWebviewRequest { - export const type = new RequestType<{ ownerUri: string }, { databases: string[] }, void>( - "dacpacDialog/listDatabases", - ); + export const type = new RequestType< + { ownerUri: string }, + { databases: string[]; errorMessage?: string }, + void + >("dacpacDialog/listDatabases"); } /** @@ -237,7 +239,13 @@ export namespace InitializeConnectionWebviewRequest { export namespace ConnectToServerWebviewRequest { export const type = new RequestType< { profileId: string }, - { ownerUri: string; isConnected: boolean; errorMessage?: string; isFabric?: boolean }, + { + ownerUri: string; + isConnected: boolean; + errorMessage?: string; + isFabric?: boolean; + databaseName?: string; + }, void >("dacpacDialog/connectToServer"); } diff --git a/extensions/mssql/test/unit/dacpacDialogWebviewController.test.ts b/extensions/mssql/test/unit/dacpacDialogWebviewController.test.ts index 8aca56e335..7ab8eb04f3 100644 --- a/extensions/mssql/test/unit/dacpacDialogWebviewController.test.ts +++ b/extensions/mssql/test/unit/dacpacDialogWebviewController.test.ts @@ -127,6 +127,19 @@ suite("DacpacDialogWebviewController", () => { ); return controller; } + function createControllerWithState( + state: Record, + ): DacpacDialogWebviewController { + controller = new DacpacDialogWebviewController( + mockContext, + vscodeWrapperStub, + connectionManagerStub, + dacFxServiceStub, + { ...initialState, ...state }, + ownerUri, + ); + return controller; + } suite("Deployment Operations", () => { test("Publish DACPAC succeeds for new database", async () => { const mockResult: DacFxResult = { @@ -778,31 +791,66 @@ suite("DacpacDialogWebviewController", () => { }); suite("Database Operations", () => { test("lists databases successfully", async () => { - const mockDatabases = { - databaseNames: ["master", "tempdb", "model", "msdb", "TestDB"], - }; - sqlToolsClientStub.sendRequest - .withArgs(ListDatabasesRequest.type, sinon.match.any) - .resolves(mockDatabases); + connectionManagerStub.listDatabases + .withArgs(ownerUri) + .resolves(["master", "tempdb", "model", "msdb", "TestDB"]); createController(); const requestHandler = requestHandlers.get(ListDatabasesWebviewRequest.type.method); expect(requestHandler, "Request handler was not registered").to.be.a("function"); const response = await requestHandler!({ ownerUri: ownerUri }); // System databases are filtered out, only user databases are returned expect(response.databases).to.deep.equal(["TestDB"]); - expect(sqlToolsClientStub.sendRequest).to.have.been.calledWith( - ListDatabasesRequest.type, - { ownerUri: ownerUri }, - ); + expect(response.errorMessage).to.be.undefined; + expect(connectionManagerStub.listDatabases).to.have.been.calledWith(ownerUri); }); - test("returns empty array when list databases fails", async () => { - sqlToolsClientStub.sendRequest - .withArgs(ListDatabasesRequest.type, sinon.match.any) + test("returns empty array and error when list databases fails and no state database", async () => { + connectionManagerStub.listDatabases + .withArgs(ownerUri) .rejects(new Error("Connection failed")); createController(); const requestHandler = requestHandlers.get(ListDatabasesWebviewRequest.type.method); const response = await requestHandler!({ ownerUri: ownerUri }); expect(response.databases).to.be.an("array").that.is.empty; + expect(response.errorMessage).to.equal("Connection failed"); + }); + test("falls back to state database when list databases fails", async () => { + connectionManagerStub.listDatabases + .withArgs(ownerUri) + .rejects(new Error("Connection failed")); + createControllerWithState({ databaseName: "MyDatabase" }); + const requestHandler = requestHandlers.get(ListDatabasesWebviewRequest.type.method); + const response = await requestHandler!({ ownerUri: ownerUri }); + expect(response.databases).to.deep.equal(["MyDatabase"]); + expect(response.errorMessage).to.equal("Connection failed"); + }); + test("falls back to state database when list returns only system databases", async () => { + connectionManagerStub.listDatabases + .withArgs(ownerUri) + .resolves(["master", "tempdb", "model", "msdb"]); + createControllerWithState({ databaseName: "MyDatabase" }); + const requestHandler = requestHandlers.get(ListDatabasesWebviewRequest.type.method); + const response = await requestHandler!({ ownerUri: ownerUri }); + expect(response.databases).to.deep.equal(["MyDatabase"]); + }); + test("does not fall back to system database from state", async () => { + connectionManagerStub.listDatabases + .withArgs(ownerUri) + .rejects(new Error("Connection failed")); + createControllerWithState({ databaseName: "master" }); + const requestHandler = requestHandlers.get(ListDatabasesWebviewRequest.type.method); + const response = await requestHandler!({ ownerUri: ownerUri }); + expect(response.databases).to.be.an("array").that.is.empty; + expect(response.errorMessage).to.equal("Connection failed"); + }); + test("includes state database in list when not already present", async () => { + connectionManagerStub.listDatabases + .withArgs(ownerUri) + .resolves(["master", "tempdb", "model", "msdb", "OtherDB"]); + createControllerWithState({ databaseName: "MyDatabase" }); + const requestHandler = requestHandlers.get(ListDatabasesWebviewRequest.type.method); + const response = await requestHandler!({ ownerUri: ownerUri }); + expect(response.databases).to.deep.equal(["MyDatabase", "OtherDB"]); + expect(response.errorMessage).to.be.undefined; }); }); suite("Database Name Validation", () => { @@ -957,6 +1005,79 @@ suite("DacpacDialogWebviewController", () => { expect(response.errorMessage).to.include("Failed to validate database name"); expect(response.errorMessage).to.include("Network error"); }); + test("allows connection database when listing databases returns empty list", async () => { + const mockDatabases = { + databaseNames: [], + }; + sqlToolsClientStub.sendRequest + .withArgs(ListDatabasesRequest.type, sinon.match.any) + .resolves(mockDatabases); + createControllerWithState({ databaseName: "MyDB" }); + const requestHandler = requestHandlers.get( + ValidateDatabaseNameWebviewRequest.type.method, + ); + const response = await requestHandler!({ + databaseName: "MyDB", + ownerUri: ownerUri, + shouldNotExist: false, + }); + expect(response.isValid).to.be.true; + expect(response.errorMessage).to.be.undefined; + }); + test("allows connection database case-insensitively when listing databases returns empty list", async () => { + const mockDatabases = { + databaseNames: [], + }; + sqlToolsClientStub.sendRequest + .withArgs(ListDatabasesRequest.type, sinon.match.any) + .resolves(mockDatabases); + createControllerWithState({ databaseName: "MyDB" }); + const requestHandler = requestHandlers.get( + ValidateDatabaseNameWebviewRequest.type.method, + ); + const response = await requestHandler!({ + databaseName: "mydb", + ownerUri: ownerUri, + shouldNotExist: false, + }); + expect(response.isValid).to.be.true; + expect(response.errorMessage).to.be.undefined; + }); + test("allows connection database when listing databases fails with error", async () => { + sqlToolsClientStub.sendRequest + .withArgs(ListDatabasesRequest.type, sinon.match.any) + .rejects(new Error("Insufficient permissions")); + createControllerWithState({ databaseName: "MyDB" }); + const requestHandler = requestHandlers.get( + ValidateDatabaseNameWebviewRequest.type.method, + ); + const response = await requestHandler!({ + databaseName: "MyDB", + ownerUri: ownerUri, + shouldNotExist: false, + }); + expect(response.isValid).to.be.true; + expect(response.errorMessage).to.be.undefined; + }); + test("rejects non-connection database when listing databases returns empty list", async () => { + const mockDatabases = { + databaseNames: [], + }; + sqlToolsClientStub.sendRequest + .withArgs(ListDatabasesRequest.type, sinon.match.any) + .resolves(mockDatabases); + createControllerWithState({ databaseName: "MyDB" }); + const requestHandler = requestHandlers.get( + ValidateDatabaseNameWebviewRequest.type.method, + ); + const response = await requestHandler!({ + databaseName: "OtherDB", + ownerUri: ownerUri, + shouldNotExist: false, + }); + expect(response.isValid).to.be.false; + expect(response.errorMessage).to.equal(LocConstants.DacpacDialog.DatabaseNotFound); + }); }); suite("Cancel Operation", () => { test("cancel notification resolves dialog with undefined and disposes panel", async () => { @@ -1165,6 +1286,7 @@ suite("DacpacDialogWebviewController", () => { const result = await handler!({ profileId: "conn1" }); expect(result).to.exist; expect(result.ownerUri).to.equal("new-owner-uri"); + expect(result.databaseName).to.equal("db1"); expect(result.errorMessage).to.be.undefined; // Called twice: once to check if connected, once after connecting to get the URI expect(connectionManagerStub.getUriForConnection).to.have.been.calledTwice; @@ -1191,6 +1313,7 @@ suite("DacpacDialogWebviewController", () => { const result = await handler!({ profileId: "conn1" }); expect(result).to.exist; expect(result.ownerUri).to.equal("generated-owner-uri-123"); + expect(result.databaseName).to.equal("db1"); expect(result.errorMessage).to.be.undefined; // Verify the sequence of calls expect(connectionManagerStub.getUriForConnection).to.have.been.calledTwice; @@ -1218,6 +1341,7 @@ suite("DacpacDialogWebviewController", () => { const handler = requestHandlers.get(ConnectToServerWebviewRequest.type.method); const result = await handler!({ profileId: "conn1" }); expect(result.ownerUri).to.equal("existing-owner-uri"); + expect(result.databaseName).to.equal("db1"); expect(result.errorMessage).to.be.undefined; // Should not call connect since already connected expect(connectionManagerStub.connect).to.not.have.been.called; diff --git a/localization/xliff/vscode-mssql.xlf b/localization/xliff/vscode-mssql.xlf index 260874bfa5..b940d3d77b 100644 --- a/localization/xliff/vscode-mssql.xlf +++ b/localization/xliff/vscode-mssql.xlf @@ -3547,6 +3547,9 @@ Loading databases in selected workspace... + + Loading databases... + Loading deployment