feat(SIP-95): catalogs in SQL Lab and datasets (#28376)

This commit is contained in:
Beto Dealmeida
2024-05-08 17:19:36 -04:00
committed by GitHub
parent 07cd1d89d0
commit ce668d46cc
71 changed files with 842 additions and 100 deletions

View File

@@ -74,6 +74,7 @@ const AceEditorWrapper = ({
'id',
'dbId',
'sql',
'catalog',
'schema',
'templateParams',
'cursorPosition',
@@ -161,6 +162,7 @@ const AceEditorWrapper = ({
const { data: annotations } = useAnnotations({
dbId: queryEditor.dbId,
catalog: queryEditor.catalog,
schema: queryEditor.schema,
sql: currentSql,
templateParams: queryEditor.templateParams,
@@ -170,6 +172,7 @@ const AceEditorWrapper = ({
{
queryEditorId,
dbId: queryEditor.dbId,
catalog: queryEditor.catalog,
schema: queryEditor.schema,
},
!autocomplete,

View File

@@ -189,7 +189,12 @@ test('returns column keywords among selected tables', async () => {
storeWithSqlLab.dispatch(
tableApiUtil.upsertQueryData(
'tableMetadata',
{ dbId: expectDbId, schema: expectSchema, table: expectTable },
{
dbId: expectDbId,
catalog: null,
schema: expectSchema,
table: expectTable,
},
{
name: expectTable,
columns: [
@@ -205,7 +210,12 @@ test('returns column keywords among selected tables', async () => {
storeWithSqlLab.dispatch(
tableApiUtil.upsertQueryData(
'tableMetadata',
{ dbId: expectDbId, schema: expectSchema, table: unexpectedTable },
{
dbId: expectDbId,
catalog: null,
schema: expectSchema,
table: unexpectedTable,
},
{
name: unexpectedTable,
columns: [
@@ -227,6 +237,7 @@ test('returns column keywords among selected tables', async () => {
useKeywords({
queryEditorId: expectQueryEditorId,
dbId: expectDbId,
catalog: null,
schema: expectSchema,
}),
{

View File

@@ -42,6 +42,7 @@ import { SqlLabRootState } from 'src/SqlLab/types';
type Params = {
queryEditorId: string | number;
dbId?: string | number;
catalog?: string | null;
schema?: string;
};
@@ -58,7 +59,7 @@ const getHelperText = (value: string) =>
const extensionsRegistry = getExtensionsRegistry();
export function useKeywords(
{ queryEditorId, dbId, schema }: Params,
{ queryEditorId, dbId, catalog, schema }: Params,
skip = false,
) {
const useCustomKeywords = extensionsRegistry.get(
@@ -68,6 +69,7 @@ export function useKeywords(
const customKeywords = useCustomKeywords?.({
queryEditorId: String(queryEditorId),
dbId,
catalog,
schema,
});
const dispatch = useDispatch();
@@ -78,6 +80,7 @@ export function useKeywords(
const { data: schemaOptions } = useSchemasQueryState(
{
dbId,
catalog: catalog || undefined,
forceRefresh: false,
},
{ skip: skipFetch || !dbId },
@@ -85,6 +88,7 @@ export function useKeywords(
const { data: tableData } = useTablesQueryState(
{
dbId,
catalog,
schema,
forceRefresh: false,
},
@@ -125,6 +129,7 @@ export function useKeywords(
dbId && schema
? {
dbId,
catalog,
schema,
table,
}
@@ -137,7 +142,7 @@ export function useKeywords(
});
});
return [...columns];
}, [dbId, schema, apiState, tablesForColumnMetadata]);
}, [dbId, catalog, schema, apiState, tablesForColumnMetadata]);
const insertMatch = useEffectEvent((editor: Editor, data: any) => {
if (data.meta === 'table') {

View File

@@ -210,6 +210,38 @@ describe('SaveDatasetModal', () => {
expect(createDatasource).toHaveBeenCalledWith({
datasourceName: 'my dataset',
dbId: 1,
catalog: null,
schema: 'main',
sql: 'SELECT *',
templateParams: undefined,
});
});
it('sends the catalog when creating the dataset', async () => {
const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user });
render(
<SaveDatasetModal
{...mockedProps}
datasource={{ ...mockedProps.datasource, catalog: 'public' }}
/>,
{ useRedux: true },
);
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
const saveConfirmationBtn = screen.getByRole('button', {
name: /save/i,
});
userEvent.click(saveConfirmationBtn);
expect(createDatasource).toHaveBeenCalledWith({
datasourceName: 'my dataset',
dbId: 1,
catalog: 'public',
schema: 'main',
sql: 'SELECT *',
templateParams: undefined,

View File

@@ -77,6 +77,7 @@ export interface ISaveableDatasource {
dbId: number;
sql: string;
templateParams?: string | object | null;
catalog?: string | null;
schema?: string | null;
database?: Database;
}
@@ -292,6 +293,7 @@ export const SaveDatasetModal = ({
createDatasource({
sql: datasource.sql,
dbId: datasource.dbId || datasource?.database?.id,
catalog: datasource?.catalog,
schema: datasource?.schema,
templateParams,
datasourceName: datasetName,

View File

@@ -42,6 +42,7 @@ const mockState = {
{
id: mockedProps.queryEditorId,
dbId: 1,
catalog: null,
schema: 'main',
sql: 'SELECT * FROM t',
},

View File

@@ -48,7 +48,7 @@ export type QueryPayload = {
description?: string;
id?: string;
remoteId?: number;
} & Pick<QueryEditor, 'dbId' | 'schema' | 'sql'>;
} & Pick<QueryEditor, 'dbId' | 'catalog' | 'schema' | 'sql'>;
const Styles = styled.span`
span[role='img'] {
@@ -78,6 +78,7 @@ const SaveQuery = ({
'dbId',
'latestQueryId',
'queryLimit',
'catalog',
'schema',
'selectedText',
'sql',
@@ -115,6 +116,7 @@ const SaveQuery = ({
description,
dbId: query.dbId ?? 0,
sql: query.sql,
catalog: query.catalog,
schema: query.schema,
templateParams: query.templateParams,
remoteId: query?.remoteId || undefined,

View File

@@ -44,6 +44,10 @@ const mockedProps = {
beforeEach(() => {
fetchMock.get('glob:*/api/v1/database/?*', { result: [] });
fetchMock.get('glob:*/api/v1/database/*/catalogs/?*', {
count: 0,
result: [],
});
fetchMock.get('glob:*/api/v1/database/*/schemas/?*', {
count: 2,
result: ['main', 'new_schema'],
@@ -103,11 +107,14 @@ test('renders a TableElement', async () => {
});
test('table should be visible when expanded is true', async () => {
const { container, getByText, getByRole, queryAllByText } =
await renderAndWait(mockedProps, undefined, {
const { container, getByText, getByRole } = await renderAndWait(
mockedProps,
undefined,
{
...initialState,
sqlLab: { ...initialState.sqlLab, tables: [table] },
});
},
);
const dbSelect = getByRole('combobox', {
name: 'Select database or type to search databases',
@@ -115,14 +122,56 @@ test('table should be visible when expanded is true', async () => {
const schemaSelect = getByRole('combobox', {
name: 'Select schema or type to search schemas',
});
const dropdown = getByText(/Table/i);
const abUser = queryAllByText(/ab_user/i);
const dropdown = getByText(/Select table/i);
const abUser = getByText(/ab_user/i);
expect(getByText(/Database/i)).toBeInTheDocument();
expect(dbSelect).toBeInTheDocument();
expect(schemaSelect).toBeInTheDocument();
expect(dropdown).toBeInTheDocument();
expect(abUser).toHaveLength(2);
expect(abUser).toBeInTheDocument();
expect(
container.querySelector('.ant-collapse-content-active'),
).toBeInTheDocument();
table.columns.forEach(({ name }) => {
expect(getByText(name)).toBeInTheDocument();
});
});
test('catalog selector should be visible when enabled in the database', async () => {
const { container, getByText, getByRole } = await renderAndWait(
{
...mockedProps,
database: {
...mockedProps.database,
allow_multi_catalog: true,
},
},
undefined,
{
...initialState,
sqlLab: { ...initialState.sqlLab, tables: [table] },
},
);
const dbSelect = getByRole('combobox', {
name: 'Select database or type to search databases',
});
const catalogSelect = getByRole('combobox', {
name: 'Select catalog or type to search catalogs',
});
const schemaSelect = getByRole('combobox', {
name: 'Select schema or type to search schemas',
});
const dropdown = getByText(/Select table/i);
const abUser = getByText(/ab_user/i);
expect(getByText(/Database/i)).toBeInTheDocument();
expect(dbSelect).toBeInTheDocument();
expect(catalogSelect).toBeInTheDocument();
expect(schemaSelect).toBeInTheDocument();
expect(dropdown).toBeInTheDocument();
expect(abUser).toBeInTheDocument();
expect(
container.querySelector('.ant-collapse-content-active'),
).toBeInTheDocument();

View File

@@ -34,6 +34,7 @@ import {
removeTables,
collapseTable,
expandTable,
queryEditorSetCatalog,
queryEditorSetSchema,
setDatabases,
addDangerToast,
@@ -115,13 +116,17 @@ const SqlEditorLeftBar = ({
shallowEqual,
);
const dispatch = useDispatch();
const queryEditor = useQueryEditor(queryEditorId, ['dbId', 'schema']);
const queryEditor = useQueryEditor(queryEditorId, [
'dbId',
'catalog',
'schema',
]);
const [emptyResultsWithSearch, setEmptyResultsWithSearch] = useState(false);
const [userSelectedDb, setUserSelected] = useState<DatabaseObject | null>(
null,
);
const { schema } = queryEditor;
const { catalog, schema } = queryEditor;
useEffect(() => {
const bool = querystring.parse(window.location.search).db;
@@ -138,9 +143,9 @@ const SqlEditorLeftBar = ({
}
}, [database]);
const onEmptyResults = (searchText?: string) => {
const onEmptyResults = useCallback((searchText?: string) => {
setEmptyResultsWithSearch(!!searchText);
};
}, []);
const onDbChange = ({ id: dbId }: { id: number }) => {
setEmptyState?.(false);
@@ -152,7 +157,11 @@ const SqlEditorLeftBar = ({
[tables],
);
const onTablesChange = (tableNames: string[], schemaName: string) => {
const onTablesChange = (
tableNames: string[],
catalogName: string | null,
schemaName: string,
) => {
if (!schemaName) {
return;
}
@@ -169,7 +178,7 @@ const SqlEditorLeftBar = ({
});
tablesToAdd.forEach(tableName => {
dispatch(addTable(queryEditor, tableName, schemaName));
dispatch(addTable(queryEditor, tableName, catalogName, schemaName));
});
dispatch(removeTables(currentTables));
@@ -210,6 +219,15 @@ const SqlEditorLeftBar = ({
const shouldShowReset = window.location.search === '?reset=1';
const tableMetaDataHeight = height - 130; // 130 is the height of the selects above
const handleCatalogChange = useCallback(
(catalog: string | null) => {
if (queryEditor) {
dispatch(queryEditorSetCatalog(queryEditor, catalog));
}
},
[dispatch, queryEditor],
);
const handleSchemaChange = useCallback(
(schema: string) => {
if (queryEditor) {
@@ -246,9 +264,11 @@ const SqlEditorLeftBar = ({
getDbList={handleDbList}
handleError={handleError}
onDbChange={onDbChange}
onCatalogChange={handleCatalogChange}
catalog={catalog}
onSchemaChange={handleSchemaChange}
onTableSelectChange={onTablesChange}
schema={schema}
onTableSelectChange={onTablesChange}
tableValue={selectedTableNames}
sqlLabMode
/>

View File

@@ -111,6 +111,7 @@ class TabbedSqlEditors extends React.PureComponent<TabbedSqlEditorsProps> {
queryId,
dbid,
dbname,
catalog,
schema,
autorun,
new: isNewQuery,
@@ -149,6 +150,7 @@ class TabbedSqlEditors extends React.PureComponent<TabbedSqlEditorsProps> {
const newQueryEditor = {
name,
dbId: databaseId,
catalog,
schema,
autorun,
sql,

View File

@@ -101,7 +101,7 @@ const StyledCollapsePanel = styled(Collapse.Panel)`
`;
const TableElement = ({ table, ...props }: TableElementProps) => {
const { dbId, schema, name, expanded } = table;
const { dbId, catalog, schema, name, expanded } = table;
const theme = useTheme();
const dispatch = useDispatch();
const {
@@ -112,6 +112,7 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
} = useTableMetadataQuery(
{
dbId,
catalog,
schema,
table: name,
},
@@ -125,6 +126,7 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
} = useTableExtendedMetadataQuery(
{
dbId,
catalog,
schema,
table: name,
},