diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4fe7f19c..eb7602fb 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6459,7 +6459,7 @@ dependencies = [ [[package]] name = "tabularis" -version = "0.18.0" +version = "0.19.0" dependencies = [ "aes-gcm", "argon2", diff --git a/src/contexts/DatabaseProvider.tsx b/src/contexts/DatabaseProvider.tsx index 5a3a0506..dee90a71 100644 --- a/src/contexts/DatabaseProvider.tsx +++ b/src/contexts/DatabaseProvider.tsx @@ -678,7 +678,13 @@ export const DatabaseProvider = ({ children }: { children: ReactNode }) => { // comes from the server on every connect, so databases created/dropped // outside the app show up without editing the connection. const allDatabasesMode = isMultiDatabaseCapable(capabilities) && savedDbList.length === 0; - let isMultiDb = savedDbList.length > 1; + // `>= 1`, not `> 1`: a connection saved with a single database is still a + // database *selection*, and `usesMultiDatabaseLayout` already accepts one. + // Gating here at `> 1` left `selectedDatabases` empty for that case, so + // the layout function's `>= 1` was unreachable and the sidebar lost the + // manage/refresh controls — leaving no way to pick another database + // without reopening the connection settings. + let isMultiDb = savedDbList.length >= 1; let dbList = isMultiDb ? savedDbList : []; if (allDatabasesMode) { @@ -713,7 +719,7 @@ export const DatabaseProvider = ({ children }: { children: ReactNode }) => { const { selection, removed } = reconcileDatabaseSelection(dbList, available); if (removed.length > 0) { dbList = selection; - isMultiDb = selection.length > 1; + isMultiDb = selection.length >= 1; invoke('set_selected_databases', { connectionId, databases: selection, @@ -769,6 +775,11 @@ export const DatabaseProvider = ({ children }: { children: ReactNode }) => { databaseDataMap: initialDbMap, allDatabasesMode, ...(allDatabasesMode ? { databaseName: firstDb } : {}), + // A lone database has nothing to choose between, so mark it active: + // the tree expands the active database, and leaving it collapsed + // would put its tables one click further away than the flat + // single-database layout this replaces. + ...(dbList.length === 1 ? { activeSchema: firstDb } : {}), isLoadingTables: false, isLoadingViews: false, isLoadingRoutines: false, diff --git a/tests/contexts/DatabaseProvider.test.tsx b/tests/contexts/DatabaseProvider.test.tsx index 2d8b7feb..70cf2cfe 100644 --- a/tests/contexts/DatabaseProvider.test.tsx +++ b/tests/contexts/DatabaseProvider.test.tsx @@ -45,6 +45,24 @@ const mockPostgresManifest = { }, }; +const mockMysqlManifest = { + id: 'mysql', + name: 'MySQL', + version: '1.0.0', + description: '', + default_port: 3306, + capabilities: { + schemas: false, + single_database: false, + file_based: false, + folder_based: false, + no_connection_required: false, + views: true, + routines: true, + identifier_quote: '`', + }, +}; + describe('DatabaseProvider', () => { const mockConnections = [ { @@ -175,6 +193,90 @@ describe('DatabaseProvider', () => { expect(invoke).toHaveBeenCalledWith('get_views', { connectionId: 'conn-123' }); }); + it('exposes a single saved database as a selection, not as a flat connection', async () => { + // A connection saved with one database is still a database *selection*: + // `usesMultiDatabaseLayout` accepts one, so `selectedDatabases` must be + // populated for it, otherwise the sidebar drops the manage/refresh controls + // and there is no way to pick another database without editing the + // connection. + const singleDbConnection = [ + { + id: 'conn-single', + name: 'Single DB MySQL', + params: { driver: 'mysql', host: 'localhost', database: 'onlydb' }, + }, + ]; + + vi.mocked(invoke).mockImplementation((cmd: string) => { + if (cmd === 'get_connections') return Promise.resolve(singleDbConnection); + if (cmd === 'get_driver_manifest') return Promise.resolve(mockMysqlManifest); + if (cmd === 'test_connection') return Promise.resolve('Connection successful!'); + if (cmd === 'register_active_connection') return Promise.resolve(undefined); + if (cmd === 'get_available_databases') return Promise.resolve(['onlydb', 'otherdb']); + if (cmd === 'get_tables') return Promise.resolve(mockTables); + if (cmd === 'get_views') return Promise.resolve(mockViews); + if (cmd === 'get_routines') return Promise.resolve(mockRoutines); + if (cmd === 'get_triggers') return Promise.resolve([]); + if (cmd === 'set_window_title') return Promise.resolve(undefined); + return Promise.reject(new Error(`Unexpected command: ${cmd}`)); + }); + + const wrapper = ({ children }: { children: React.ReactNode }) => + React.createElement(DatabaseProvider, null, children); + + const { result } = renderHook(() => useDatabase(), { wrapper }); + + await act(async () => { + await result.current.connect('conn-single'); + }); + + await waitFor(() => { + expect(result.current.selectedDatabases).toEqual(['onlydb']); + // Marked active so the tree expands it: a lone database should not sit + // collapsed behind an extra click. + expect(result.current.activeSchema).toBe('onlydb'); + }); + }); + + it('leaves no database active when several are selected', async () => { + // With a choice to make, none is picked for the user. + const twoDbConnection = [ + { + id: 'conn-two', + name: 'Two DB MySQL', + params: { driver: 'mysql', host: 'localhost', database: ['firstdb', 'seconddb'] }, + }, + ]; + + vi.mocked(invoke).mockImplementation((cmd: string) => { + if (cmd === 'get_connections') return Promise.resolve(twoDbConnection); + if (cmd === 'get_driver_manifest') return Promise.resolve(mockMysqlManifest); + if (cmd === 'test_connection') return Promise.resolve('Connection successful!'); + if (cmd === 'register_active_connection') return Promise.resolve(undefined); + if (cmd === 'get_available_databases') return Promise.resolve(['firstdb', 'seconddb']); + if (cmd === 'get_tables') return Promise.resolve(mockTables); + if (cmd === 'get_views') return Promise.resolve(mockViews); + if (cmd === 'get_routines') return Promise.resolve(mockRoutines); + if (cmd === 'get_triggers') return Promise.resolve([]); + if (cmd === 'set_window_title') return Promise.resolve(undefined); + return Promise.reject(new Error(`Unexpected command: ${cmd}`)); + }); + + const wrapper = ({ children }: { children: React.ReactNode }) => + React.createElement(DatabaseProvider, null, children); + + const { result } = renderHook(() => useDatabase(), { wrapper }); + + await act(async () => { + await result.current.connect('conn-two'); + }); + + await waitFor(() => { + expect(result.current.selectedDatabases).toEqual(['firstdb', 'seconddb']); + expect(result.current.activeSchema).toBeNull(); + }); + }); + it('should handle connection failure', async () => { vi.mocked(invoke).mockRejectedValue(new Error('Connection failed'));