diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.test.ts b/superset-frontend/src/SqlLab/reducers/sqlLab.test.ts index 3166a902e84..11e6c199678 100644 --- a/superset-frontend/src/SqlLab/reducers/sqlLab.test.ts +++ b/superset-frontend/src/SqlLab/reducers/sqlLab.test.ts @@ -61,6 +61,53 @@ describe('sqlLabReducer', () => { }); }); + test('should default extra_json to an empty object when extra is unset', () => { + // `extra` is nullable in the metadata database, and JSON.parse(extra || '') + // is guaranteed to throw because '' is never valid JSON, so one such row + // took down the whole reducer. + const incomingDb = { + ...databases.result[0], + extra: null, + }; + const incomingDbId = Number(incomingDb.id); + + const action = actions.setDatabases([incomingDb] as any); + + const newState = sqlLabReducer(initialState, action); + + expect(newState.databases[incomingDbId]).toEqual({ + ...incomingDb, + extra_json: {}, + }); + }); + + test('defaults extra_json when a database has malformed extra', () => { + const incomingDb = { ...databases.result[0], extra: '{not json' }; + + const newState = sqlLabReducer( + initialState, + actions.setDatabases([incomingDb] as any), + ); + + expect(newState.databases[Number(incomingDb.id)].extra_json).toEqual({}); + }); + + test('keeps a valid extra payload', () => { + const incomingDb = { + ...databases.result[0], + extra: '{"engine_params": {"pool_size": 5}}', + }; + + const newState = sqlLabReducer( + initialState, + actions.setDatabases([incomingDb] as any), + ); + + expect(newState.databases[Number(incomingDb.id)].extra_json).toEqual({ + engine_params: { pool_size: 5 }, + }); + }); + // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('Query editors actions', () => { let newState: SqlLabState; diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.ts b/superset-frontend/src/SqlLab/reducers/sqlLab.ts index ac3b7a96d6f..874ff94e6ef 100644 --- a/superset-frontend/src/SqlLab/reducers/sqlLab.ts +++ b/superset-frontend/src/SqlLab/reducers/sqlLab.ts @@ -36,6 +36,27 @@ import { type SqlLabState = SqlLabRootState['sqlLab']; +/** + * A database's `extra` column is free-form and frequently empty: it is nullable + * in the metadata database and the API returns it verbatim. `JSON.parse` cannot + * represent that, and `JSON.parse(extra || '')` is guaranteed to throw, since + * the empty string is never valid JSON — so a single database row with no + * `extra` took down the whole SET_DATABASES reducer and with it SQL Lab. + * Malformed JSON is treated the same way: one bad row must not cost the user + * every other database. + */ +function parseDatabaseExtra(extra: unknown): Record { + if (typeof extra !== 'string' || extra.trim() === '') { + return {}; + } + try { + const parsed = JSON.parse(extra); + return parsed && typeof parsed === 'object' ? parsed : {}; + } catch { + return {}; + } +} + function alterUnsavedQueryEditorState( state: SqlLabState, updatedState: Partial, @@ -727,7 +748,7 @@ export default function sqlLabReducer( (action.databases as any[])!.forEach((db: any) => { databases[db.id] = { ...db, - extra_json: JSON.parse(db.extra || ''), + extra_json: parseDatabaseExtra(db.extra), }; }); return {