michael-s-molina commented on code in PR #24677:
URL: https://github.com/apache/superset/pull/24677#discussion_r1268198490


##########
superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts:
##########
@@ -0,0 +1,200 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useEffect, useMemo, useRef } from 'react';
+import { useSelector, useDispatch, shallowEqual, useStore } from 'react-redux';
+import { t } from '@superset-ui/core';
+
+import { Editor } from 'src/components/AsyncAceEditor';
+import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
+import { addTable, addDangerToast } from 'src/SqlLab/actions/sqlLab';
+import {
+  SCHEMA_AUTOCOMPLETE_SCORE,
+  TABLE_AUTOCOMPLETE_SCORE,
+  COLUMN_AUTOCOMPLETE_SCORE,
+  SQL_FUNCTIONS_AUTOCOMPLETE_SCORE,
+} from 'src/SqlLab/constants';
+import {
+  useSchemas,
+  useTables,
+  tableEndpoints,
+  skipToken,
+} from 'src/hooks/apiResources';
+import { api } from 'src/hooks/apiResources/queryApi';
+import { useDatabaseFunctionsQuery } from 
'src/hooks/apiResources/databaseFunctions';
+import useEffectEvent from 'src/hooks/useEffectEvent';
+import { SqlLabRootState } from 'src/SqlLab/types';
+
+type Params = {
+  queryEditorId: string | number;
+  dbId?: string | number;
+  schema?: string;
+};
+
+const EMPTY_LIST = [] as typeof sqlKeywords;
+
+export function useKeywords(
+  { queryEditorId, dbId, schema }: Params,
+  skip = false,
+) {
+  const dispatch = useDispatch();
+  const hasFetchedKeywords = useRef(false);
+  // skipFetch is used to prevent re-evaluating memoized keywords
+  // due to updated api results by skip flag
+  const skipFetch = hasFetchedKeywords && skip;
+  const { data: schemaOptions } = useSchemas({
+    ...(!skipFetch && { dbId }),
+  });
+  const { data: tableData } = useTables({
+    ...(!skipFetch && {
+      dbId,
+      schema,
+    }),
+  });
+
+  const { data: functionNames, isError } = useDatabaseFunctionsQuery(
+    { dbId },
+    { skip: skipFetch || !dbId },
+  );
+
+  useEffect(() => {

Review Comment:
   We need to account for all resources when handling errors (tables, schemas). 



##########
superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts:
##########
@@ -0,0 +1,200 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useEffect, useMemo, useRef } from 'react';
+import { useSelector, useDispatch, shallowEqual, useStore } from 'react-redux';
+import { t } from '@superset-ui/core';
+
+import { Editor } from 'src/components/AsyncAceEditor';
+import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
+import { addTable, addDangerToast } from 'src/SqlLab/actions/sqlLab';
+import {
+  SCHEMA_AUTOCOMPLETE_SCORE,
+  TABLE_AUTOCOMPLETE_SCORE,
+  COLUMN_AUTOCOMPLETE_SCORE,
+  SQL_FUNCTIONS_AUTOCOMPLETE_SCORE,
+} from 'src/SqlLab/constants';
+import {
+  useSchemas,
+  useTables,
+  tableEndpoints,
+  skipToken,
+} from 'src/hooks/apiResources';
+import { api } from 'src/hooks/apiResources/queryApi';
+import { useDatabaseFunctionsQuery } from 
'src/hooks/apiResources/databaseFunctions';
+import useEffectEvent from 'src/hooks/useEffectEvent';
+import { SqlLabRootState } from 'src/SqlLab/types';
+
+type Params = {
+  queryEditorId: string | number;
+  dbId?: string | number;
+  schema?: string;
+};
+
+const EMPTY_LIST = [] as typeof sqlKeywords;
+
+export function useKeywords(
+  { queryEditorId, dbId, schema }: Params,
+  skip = false,
+) {
+  const dispatch = useDispatch();
+  const hasFetchedKeywords = useRef(false);
+  // skipFetch is used to prevent re-evaluating memoized keywords
+  // due to updated api results by skip flag
+  const skipFetch = hasFetchedKeywords && skip;
+  const { data: schemaOptions } = useSchemas({

Review Comment:
   We should pass the `onError` callback and deal with errors when fetching 
schemas and tables.



##########
superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts:
##########
@@ -0,0 +1,256 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import fetchMock from 'fetch-mock';
+import { act, renderHook } from '@testing-library/react-hooks';
+import {
+  createWrapper,
+  defaultStore as store,
+  createStore,
+} from 'spec/helpers/testing-library';
+import { api } from 'src/hooks/apiResources/queryApi';
+import { tableApiUtil } from 'src/hooks/apiResources/tables';
+import { addTable } from 'src/SqlLab/actions/sqlLab';
+import { initialState } from 'src/SqlLab/fixtures';
+import { reducers } from 'src/SqlLab/reducers';
+import {
+  SCHEMA_AUTOCOMPLETE_SCORE,
+  TABLE_AUTOCOMPLETE_SCORE,
+  COLUMN_AUTOCOMPLETE_SCORE,
+  SQL_FUNCTIONS_AUTOCOMPLETE_SCORE,
+} from 'src/SqlLab/constants';
+import { useKeywords } from './useKeywords';
+
+const fakeSchemaApiResult = ['schema1', 'schema2'];
+const fakeTableApiResult = {
+  count: 2,
+  result: [
+    {
+      id: 1,
+      value: 'fake api result1',
+      label: 'fake api label1',
+    },
+    {
+      id: 2,
+      value: 'fake api result2',
+      label: 'fake api label2',
+    },
+  ],
+};
+const fakeFunctionNamesApiResult = {
+  function_names: ['abs', 'avg', 'sum'],
+};
+
+afterEach(() => {
+  fetchMock.reset();
+  act(() => {
+    store.dispatch(api.util.resetApiState());
+  });
+});
+
+test('returns keywords including fetched data', async () => {
+  const expectDbId = 1;
+  const expectSchema = 'schema1';
+
+  const schemaApiRoute = `glob:*/api/v1/database/${expectDbId}/schemas/*`;
+  const tableApiRoute = `glob:*/api/v1/database/${expectDbId}/tables/?q=*`;
+  const dbFunctionNamesApiRoute = 
`glob:*/api/v1/database/${expectDbId}/function_names/`;
+
+  fetchMock.get(schemaApiRoute, {
+    result: fakeSchemaApiResult,
+  });
+  fetchMock.get(tableApiRoute, fakeTableApiResult);
+  fetchMock.get(dbFunctionNamesApiRoute, fakeFunctionNamesApiResult);
+
+  const { result, waitFor } = renderHook(
+    () =>
+      useKeywords({
+        queryEditorId: 'testqueryid',
+        dbId: expectDbId,
+        schema: expectSchema,
+      }),
+    {
+      wrapper: createWrapper({
+        useRedux: true,
+        store,
+      }),
+    },
+  );
+
+  await waitFor(() => expect(fetchMock.calls(schemaApiRoute).length).toBe(1));
+  await waitFor(() => expect(fetchMock.calls(tableApiRoute).length).toBe(1));
+  await waitFor(() =>
+    expect(fetchMock.calls(dbFunctionNamesApiRoute).length).toBe(1),
+  );
+  fakeSchemaApiResult.forEach(schema => {
+    expect(result.current).toContainEqual(
+      expect.objectContaining({
+        name: schema,
+        score: SCHEMA_AUTOCOMPLETE_SCORE,
+        meta: 'schema',
+      }),
+    );
+  });
+  fakeTableApiResult.result.forEach(({ value }) => {
+    expect(result.current).toContainEqual(
+      expect.objectContaining({
+        value,
+        score: TABLE_AUTOCOMPLETE_SCORE,
+        meta: 'table',
+      }),
+    );
+  });
+  fakeFunctionNamesApiResult.function_names.forEach(func => {
+    expect(result.current).toContainEqual(
+      expect.objectContaining({
+        name: func,
+        value: func,
+        meta: 'function',
+        score: SQL_FUNCTIONS_AUTOCOMPLETE_SCORE,
+      }),
+    );
+  });
+});
+
+test('skip fetching if autocomplete skipped', () => {
+  const expectDbId = 1;
+  const expectSchema = 'schema1';
+  const { result } = renderHook(
+    () =>
+      useKeywords(
+        {
+          queryEditorId: 'testqueryid',
+          dbId: expectDbId,
+          schema: expectSchema,
+        },
+        true,
+      ),
+    {
+      wrapper: createWrapper({
+        useRedux: true,
+        store,
+      }),
+    },
+  );
+  expect(result.current).toEqual([]);
+  expect(fetchMock.calls()).toEqual([]);
+});
+
+test('returns column keywords among selected tables', async () => {
+  const expectDbId = 1;

Review Comment:
   Could extract the constants that are reusable by the tests?



##########
superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts:
##########
@@ -0,0 +1,200 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useEffect, useMemo, useRef } from 'react';
+import { useSelector, useDispatch, shallowEqual, useStore } from 'react-redux';
+import { t } from '@superset-ui/core';
+
+import { Editor } from 'src/components/AsyncAceEditor';
+import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
+import { addTable, addDangerToast } from 'src/SqlLab/actions/sqlLab';
+import {
+  SCHEMA_AUTOCOMPLETE_SCORE,
+  TABLE_AUTOCOMPLETE_SCORE,
+  COLUMN_AUTOCOMPLETE_SCORE,
+  SQL_FUNCTIONS_AUTOCOMPLETE_SCORE,
+} from 'src/SqlLab/constants';
+import {
+  useSchemas,
+  useTables,
+  tableEndpoints,
+  skipToken,
+} from 'src/hooks/apiResources';
+import { api } from 'src/hooks/apiResources/queryApi';
+import { useDatabaseFunctionsQuery } from 
'src/hooks/apiResources/databaseFunctions';
+import useEffectEvent from 'src/hooks/useEffectEvent';
+import { SqlLabRootState } from 'src/SqlLab/types';
+
+type Params = {
+  queryEditorId: string | number;
+  dbId?: string | number;
+  schema?: string;
+};
+
+const EMPTY_LIST = [] as typeof sqlKeywords;
+
+export function useKeywords(
+  { queryEditorId, dbId, schema }: Params,
+  skip = false,
+) {
+  const dispatch = useDispatch();
+  const hasFetchedKeywords = useRef(false);
+  // skipFetch is used to prevent re-evaluating memoized keywords
+  // due to updated api results by skip flag
+  const skipFetch = hasFetchedKeywords && skip;
+  const { data: schemaOptions } = useSchemas({
+    ...(!skipFetch && { dbId }),
+  });
+  const { data: tableData } = useTables({
+    ...(!skipFetch && {
+      dbId,
+      schema,
+    }),
+  });
+
+  const { data: functionNames, isError } = useDatabaseFunctionsQuery(

Review Comment:
   It would be good if these hooks follow the same pattern for error handling. 
Tables and Schemas have an `onError` callback. Could you make 
`useDatabaseFunctionsQuery` follow the same pattern?



##########
superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts:
##########
@@ -0,0 +1,200 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useEffect, useMemo, useRef } from 'react';
+import { useSelector, useDispatch, shallowEqual, useStore } from 'react-redux';
+import { t } from '@superset-ui/core';
+
+import { Editor } from 'src/components/AsyncAceEditor';
+import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
+import { addTable, addDangerToast } from 'src/SqlLab/actions/sqlLab';
+import {
+  SCHEMA_AUTOCOMPLETE_SCORE,
+  TABLE_AUTOCOMPLETE_SCORE,
+  COLUMN_AUTOCOMPLETE_SCORE,
+  SQL_FUNCTIONS_AUTOCOMPLETE_SCORE,
+} from 'src/SqlLab/constants';
+import {
+  useSchemas,
+  useTables,
+  tableEndpoints,
+  skipToken,
+} from 'src/hooks/apiResources';
+import { api } from 'src/hooks/apiResources/queryApi';
+import { useDatabaseFunctionsQuery } from 
'src/hooks/apiResources/databaseFunctions';
+import useEffectEvent from 'src/hooks/useEffectEvent';
+import { SqlLabRootState } from 'src/SqlLab/types';
+
+type Params = {
+  queryEditorId: string | number;
+  dbId?: string | number;
+  schema?: string;
+};
+
+const EMPTY_LIST = [] as typeof sqlKeywords;
+
+export function useKeywords(
+  { queryEditorId, dbId, schema }: Params,
+  skip = false,
+) {
+  const dispatch = useDispatch();
+  const hasFetchedKeywords = useRef(false);
+  // skipFetch is used to prevent re-evaluating memoized keywords
+  // due to updated api results by skip flag
+  const skipFetch = hasFetchedKeywords && skip;
+  const { data: schemaOptions } = useSchemas({
+    ...(!skipFetch && { dbId }),
+  });
+  const { data: tableData } = useTables({
+    ...(!skipFetch && {
+      dbId,
+      schema,
+    }),
+  });
+
+  const { data: functionNames, isError } = useDatabaseFunctionsQuery(
+    { dbId },
+    { skip: skipFetch || !dbId },
+  );
+
+  useEffect(() => {
+    if (isError) {
+      dispatch(
+        addDangerToast(t('An error occurred while fetching function names.')),
+      );
+    }
+  }, [dispatch, isError]);
+
+  const tablesForColumnMetadata = useSelector<SqlLabRootState, string[]>(
+    ({ sqlLab }) =>
+      skip
+        ? []
+        : (sqlLab?.tables ?? [])
+            .filter(table => table.queryEditorId === queryEditorId)
+            .map(table => table.name),
+    shallowEqual,
+  );
+
+  const store = useStore();
+  const apiState = store.getState()[api.reducerPath];
+
+  const allColumns = useMemo(() => {
+    const columns = new Set<string>();
+    tablesForColumnMetadata.forEach(table => {
+      tableEndpoints.tableMetadata
+        .select(
+          dbId && schema
+            ? {
+                dbId,
+                schema,
+                table,
+              }
+            : skipToken,
+        )({
+          [api.reducerPath]: apiState,
+        })
+        .data?.columns?.forEach(({ name }) => {
+          columns.add(name);
+        });
+    });
+    return [...columns];
+  }, [dbId, schema, apiState, tablesForColumnMetadata]);
+
+  const insertMatch = useEffectEvent((editor: Editor, data: any) => {
+    if (data.meta === 'table') {
+      dispatch(addTable({ id: queryEditorId, dbId }, data.value, schema));
+    }
+
+    let { caption } = data;
+    if (data.meta === 'table' && caption.includes(' ')) {
+      caption = `"${caption}"`;
+    }
+
+    // executing 
https://github.com/thlorenz/brace/blob/3a00c5d59777f9d826841178e1eb36694177f5e6/ext/language_tools.js#L1448
+    editor.completer.insertMatch(
+      `${caption}${['function', 'schema'].includes(data.meta) ? '' : ' '}`,
+    );
+  });
+
+  const schemaKeywords = useMemo(
+    () =>
+      (schemaOptions ?? []).map(s => ({
+        name: s.label,
+        value: s.value,
+        score: SCHEMA_AUTOCOMPLETE_SCORE,
+        meta: 'schema',
+        completer: {
+          insertMatch,
+        },
+      })),
+    [schemaOptions, insertMatch],
+  );
+
+  const tableKeywords = useMemo(
+    () =>
+      (tableData?.options ?? []).map(({ value, label }) => ({
+        name: label,
+        value,
+        score: TABLE_AUTOCOMPLETE_SCORE,
+        meta: 'table',
+        completer: {
+          insertMatch,
+        },
+      })),
+    [tableData?.options, insertMatch],
+  );
+
+  const columnKeywords = useMemo(
+    () =>
+      allColumns.map(col => ({
+        name: col,
+        value: col,
+        score: COLUMN_AUTOCOMPLETE_SCORE,
+        meta: 'column',
+      })),
+    [allColumns],
+  );
+
+  const functionKeywords = useMemo(
+    () =>
+      (functionNames ?? []).map(func => ({
+        name: func,
+        value: func,
+        score: SQL_FUNCTIONS_AUTOCOMPLETE_SCORE,
+        meta: 'function',
+        completer: {
+          insertMatch,
+        },
+      })),
+    [functionNames, insertMatch],
+  );
+
+  const keywords = useMemo(
+    () =>
+      columnKeywords
+        .concat(schemaKeywords)
+        .concat(tableKeywords)
+        .concat(functionKeywords)
+        .concat(sqlKeywords),
+    [schemaKeywords, tableKeywords, columnKeywords, functionKeywords],
+  );
+
+  hasFetchedKeywords.current = !skip;
+
+  return skip ? EMPTY_LIST : keywords;

Review Comment:
   Can you move the `skip` check to the beginning of the function to avoid 
everything else in case it should return an empty list?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to