소스 검색

HUE-7738 [editor] Add support for UDF keyword suggestions in the autocompleter

Johan Ahlen 5 년 전
부모
커밋
762d1762c8

+ 5 - 1
desktop/core/src/desktop/js/ko/components/contextPopover/functionContext.js

@@ -45,7 +45,11 @@ class FunctionContextTabs {
     this.hasErrors = ko.observable(false);
 
     findUdf(connector, data.function)
-      .then(this.details)
+      .then(udfs => {
+        // TODO: Support showing multiple UDFs with the same name but different category in the context popover.
+        // For instance, trunc appears both for dates with one description and for numbers with another description.
+        this.details = udfs.length ? udfs[0] : undefined;
+      })
       .catch(() => {
         this.hasErrors(true);
       })

+ 48 - 21
desktop/core/src/desktop/js/sql/autocompleteResults.js

@@ -30,7 +30,7 @@ import { cancelActiveRequest } from 'api/apiUtils';
 import { findBrowserConnector, getRootFilePath } from 'utils/hueConfig';
 import {
   findUdf,
-  getArgumentTypesForUdf,
+  getArgumentDetailsForUdf,
   getUdfsWithReturnTypes,
   getReturnTypesForUdf,
   getSetOptions
@@ -508,24 +508,50 @@ class AutocompleteResults {
 
   async adjustForUdfArgument() {
     return new Promise(async resolve => {
-      const foundArgumentTypes = (await getArgumentTypesForUdf(
+      const foundArgumentDetails = (await getArgumentDetailsForUdf(
         this.snippet.connector(),
         this.parseResult.udfArgument.name,
         this.parseResult.udfArgument.position
-      )) || ['T'];
-      if (foundArgumentTypes.length === 0 && this.parseResult.suggestColumns) {
+      )) || [{ type: 'T' }];
+      if (foundArgumentDetails.length === 0 && this.parseResult.suggestColumns) {
         delete this.parseResult.suggestColumns;
         delete this.parseResult.suggestKeyValues;
         delete this.parseResult.suggestValues;
         delete this.parseResult.suggestFunctions;
         delete this.parseResult.suggestIdentifiers;
         delete this.parseResult.suggestKeywords;
-      } else if (foundArgumentTypes[0] !== 'BOOLEAN') {
+      } else if (foundArgumentDetails[0].type !== 'BOOLEAN') {
         if (this.parseResult.suggestFunctions && !this.parseResult.suggestFunctions.types) {
-          this.parseResult.suggestFunctions.types = foundArgumentTypes;
+          this.parseResult.suggestFunctions.types = foundArgumentDetails.map(
+            details => details.type
+          );
         }
         if (this.parseResult.suggestColumns && !this.parseResult.suggestColumns.types) {
-          this.parseResult.suggestColumns.types = foundArgumentTypes;
+          this.parseResult.suggestColumns.types = foundArgumentDetails.map(details => details.type);
+        }
+      }
+      if (foundArgumentDetails.length) {
+        const keywords = [];
+        foundArgumentDetails.forEach(details => {
+          if (details.keywords) {
+            keywords.push(...details.keywords);
+          }
+        });
+        if (keywords.length) {
+          if (!this.parseResult.suggestKeywords) {
+            this.parseResult.suggestKeywords = [];
+          }
+          this.parseResult.suggestKeywords.push(
+            ...keywords.map(keyword => {
+              if (typeof keyword === 'object') {
+                return keyword;
+              }
+              return {
+                value: keyword,
+                weight: 10000 // Bubble up units etc on top
+              };
+            })
+          );
         }
       }
       resolve();
@@ -787,18 +813,17 @@ class AutocompleteResults {
 
           const firstType = types[0].toUpperCase();
 
-          Object.keys(functionsToSuggest).forEach(name => {
+          functionsToSuggest.forEach(udf => {
             functionSuggestions.push({
               category: CATEGORIES.UDF,
-              value: name + '()',
-              meta: functionsToSuggest[name].returnTypes.join('|'),
+              value: udf.name + '()',
+              meta: udf.returnTypes.join('|'),
               weightAdjust:
-                firstType !== 'T' &&
-                functionsToSuggest[name].returnTypes.some(otherType => otherType === firstType)
+                firstType !== 'T' && udf.returnTypes.some(otherType => otherType === firstType)
                   ? 1
                   : 0,
               popular: ko.observable(false),
-              details: functionsToSuggest[name]
+              details: udf
             });
           });
 
@@ -829,20 +854,18 @@ class AutocompleteResults {
           self.parseResult.suggestAnalyticFunctions || false
         )
           .then(functionsToSuggest => {
-            Object.keys(functionsToSuggest).forEach(name => {
+            functionsToSuggest.forEach(udf => {
               functionSuggestions.push({
                 category: CATEGORIES.UDF,
-                value: name + '()',
-                meta: functionsToSuggest[name].returnTypes.join('|'),
+                value: udf.name + '()',
+                meta: udf.returnTypes.join('|'),
                 weightAdjust:
                   types[0].toUpperCase() !== 'T' &&
-                  functionsToSuggest[name].returnTypes.some(otherType => {
-                    return otherType === types[0].toUpperCase();
-                  })
+                  udf.returnTypes.some(otherType => otherType === types[0].toUpperCase())
                     ? 1
                     : 0,
                 popular: ko.observable(false),
-                details: functionsToSuggest[name]
+                details: udf
               });
             });
             self.appendEntries(functionSuggestions);
@@ -1804,11 +1827,15 @@ class AutocompleteResults {
                         clean = clean.replace(substitution.replace, substitution.with);
                       });
 
-                      value.function = await findUdf(
+                      const foundUdfs = await findUdf(
                         self.snippet.connector(),
                         value.aggregateFunction
                       );
 
+                      // TODO: Support showing multiple UDFs with the same name but different category in the autocomplete details.
+                      // For instance, trunc appears both for dates with one description and for numbers with another description.
+                      value.function = foundUdfs.length ? foundUdfs[0] : undefined;
+
                       aggregateFunctionsSuggestions.push({
                         value: clean,
                         meta: value.function.returnTypes.join('|'),

+ 30 - 3
desktop/core/src/desktop/js/sql/autocompleteResults.test.js

@@ -348,15 +348,16 @@ describe('AutocompleteResults.js', () => {
     subject.entries([]);
 
     const spy = spyOn(sqlReferenceRepository, 'getUdfsWithReturnTypes').and.callFake(async () =>
-      Promise.resolve({
-        count: {
+      Promise.resolve([
+        {
+          name: 'count',
           returnTypes: ['BIGINT'],
           arguments: [[{ type: 'T' }]],
           signature: 'count(col)',
           draggable: 'count()',
           description: 'some desc'
         }
-      })
+      ])
     );
 
     expect(subject.filtered().length).toBe(0);
@@ -376,6 +377,32 @@ describe('AutocompleteResults.js', () => {
     expect(subject.filtered()[0].details.description).toBeDefined();
   });
 
+  it('should handle parse results with udf argument keywords', async () => {
+    subject.entries([]);
+
+    const spy = spyOn(sqlReferenceRepository, 'getArgumentDetailsForUdf').and.callFake(async () =>
+      Promise.resolve([{ type: 'T', keywords: ['a', 'b'] }])
+    );
+
+    expect(subject.filtered().length).toBe(0);
+
+    await subject.update({
+      lowerCase: false,
+      udfArgument: {
+        name: 'someudf',
+        position: 1
+      }
+    });
+
+    await defer();
+
+    expect(spy).toHaveBeenCalled();
+
+    expect(subject.filtered().length).toEqual(2);
+    expect(subject.filtered()[0].value).toEqual('a');
+    expect(subject.filtered()[1].value).toEqual('b');
+  });
+
   it('should handle parse results set options', async () => {
     subject.entries([]);
 

+ 40 - 33
desktop/core/src/desktop/js/sql/reference/sqlReferenceRepository.js

@@ -106,7 +106,11 @@ export const getUdfCategories = async (connector, database) => {
         }
       }
       await mergeWithApiUdfs(categories, connector, database);
-
+      categories.forEach(category => {
+        Object.keys(category.functions).forEach(udfName => {
+          category.functions[udfName].name = udfName;
+        });
+      });
       resolve(categories);
     });
   }
@@ -116,11 +120,10 @@ export const getUdfCategories = async (connector, database) => {
 
 export const findUdf = async (connector, functionName) => {
   const categories = await getUdfCategories(connector);
-  let found = undefined;
-  categories.some(category => {
+  const found = [];
+  categories.forEach(category => {
     if (category.functions[functionName]) {
-      found = category.functions[functionName];
-      return true;
+      found.push(category.functions[functionName]);
     }
   });
   return found;
@@ -130,11 +133,22 @@ export const getReturnTypesForUdf = async (connector, functionName) => {
   if (!functionName) {
     return ['T'];
   }
-  const udf = await findUdf(connector, functionName);
-  if (!udf || !udf.returnTypes) {
-    return ['T'];
+  const udfs = await findUdf(connector, functionName);
+  if (!udfs.length) {
+    let returnTypesPresent = false;
+    const returnTypes = new Set();
+    udfs.forEach(udf => {
+      if (udf.returnTypes) {
+        returnTypesPresent = true;
+        udf.returnTypes.forEach(type => returnTypes.add(type));
+      }
+    });
+    if (returnTypesPresent) {
+      return Array.from(returnTypes.entries());
+    }
   }
-  return udf.returnTypes;
+
+  return ['T'];
 };
 
 export const getUdfsWithReturnTypes = async (
@@ -144,7 +158,7 @@ export const getUdfsWithReturnTypes = async (
   includeAnalytic
 ) => {
   const categories = await getUdfCategories(connector);
-  const result = {};
+  const result = [];
   categories.forEach(category => {
     if (
       (!category.isAnalytic && !category.isAggregate) ||
@@ -154,38 +168,31 @@ export const getUdfsWithReturnTypes = async (
       Object.keys(category.functions).forEach(udfName => {
         const udf = category.functions[udfName];
         if (!returnTypes || matchesType(connector, returnTypes, udf.returnTypes)) {
-          result[udfName] = udf;
+          result.push(udf);
         }
       });
     }
   });
+  result.sort((a, b) => a.name.localeCompare(b.name));
   return result;
 };
 
-export const getArgumentTypesForUdf = async (connector, functionName, argumentPosition) => {
-  const foundFunction = await findUdf(connector, functionName);
-  if (!foundFunction) {
-    return ['T'];
+export const getArgumentDetailsForUdf = async (connector, functionName, argumentPosition) => {
+  const foundFunctions = await findUdf(connector, functionName);
+  if (!foundFunctions.length) {
+    return [{ type: 'T' }];
   }
-  const args = foundFunction.arguments;
-  if (argumentPosition > args.length) {
-    const multiples = args[args.length - 1].filter(type => {
-      return type.multiple;
-    });
-    if (multiples.length > 0) {
-      return multiples
-        .map(argument => {
-          return argument.type;
-        })
-        .sort();
+
+  const possibleArguments = [];
+  foundFunctions.forEach(foundFunction => {
+    const args = foundFunction.arguments;
+    if (argumentPosition > args.length) {
+      possibleArguments.push(...args[args.length - 1].filter(type => type.multiple));
+    } else {
+      possibleArguments.push(...args[argumentPosition - 1]);
     }
-    return [];
-  }
-  return args[argumentPosition - 1]
-    .map(argument => {
-      return argument.type;
-    })
-    .sort();
+  });
+  return possibleArguments;
 };
 
 export const getSetOptions = async connector => {

+ 69 - 25
desktop/core/src/desktop/js/sql/reference/sqlReferenceRepository.test.js

@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { getArgumentTypesForUdf } from './sqlReferenceRepository';
+import { getArgumentDetailsForUdf } from './sqlReferenceRepository';
 import * as apiUtils from 'sql/reference/apiUtils';
 
 describe('sqlReferenceRepository.js', () => {
@@ -87,44 +87,88 @@ describe('sqlReferenceRepository.js', () => {
 
   jest.spyOn(apiUtils, 'fetchUdfs').mockImplementation(() => Promise.resolve([]));
 
+  const extractType = details => details.type;
+
   it('should give the expected argument types at a specific position', async () => {
-    expect(await getArgumentTypesForUdf(hiveConn, 'cos', 1)).toEqual(['DECIMAL', 'DOUBLE']);
-    expect(await getArgumentTypesForUdf(hiveConn, 'cos', 2)).toEqual([]);
-    expect(await getArgumentTypesForUdf(impalaConn, 'cos', 1)).toEqual(['DOUBLE']);
-    expect(await getArgumentTypesForUdf(impalaConn, 'cos', 2)).toEqual([]);
+    expect((await getArgumentDetailsForUdf(hiveConn, 'cos', 1)).map(extractType)).toEqual([
+      'DECIMAL',
+      'DOUBLE'
+    ]);
+    expect((await getArgumentDetailsForUdf(hiveConn, 'cos', 2)).map(extractType)).toEqual([]);
+    expect((await getArgumentDetailsForUdf(impalaConn, 'cos', 1)).map(extractType)).toEqual([
+      'DOUBLE'
+    ]);
+    expect((await getArgumentDetailsForUdf(impalaConn, 'cos', 2)).map(extractType)).toEqual([]);
 
-    expect(await getArgumentTypesForUdf(hiveConn, 'concat', 10)).toEqual(['BINARY', 'STRING']);
-    expect(await getArgumentTypesForUdf(impalaConn, 'concat', 10)).toEqual(['STRING']);
+    expect((await getArgumentDetailsForUdf(hiveConn, 'concat', 10)).map(extractType)).toEqual([
+      'STRING',
+      'BINARY'
+    ]);
+    expect((await getArgumentDetailsForUdf(impalaConn, 'concat', 10)).map(extractType)).toEqual([
+      'STRING'
+    ]);
   });
 
   it('should handle functions with different type of arguments', async () => {
-    expect(await getArgumentTypesForUdf(hiveConn, 'reflect', 1)).toEqual(['STRING']);
-    expect(await getArgumentTypesForUdf(hiveConn, 'reflect', 2)).toEqual(['STRING']);
-    expect(await getArgumentTypesForUdf(hiveConn, 'reflect', 3)).toEqual(['T']);
-    expect(await getArgumentTypesForUdf(hiveConn, 'reflect', 200)).toEqual(['T']);
+    expect((await getArgumentDetailsForUdf(hiveConn, 'reflect', 1)).map(extractType)).toEqual([
+      'STRING'
+    ]);
+    expect((await getArgumentDetailsForUdf(hiveConn, 'reflect', 2)).map(extractType)).toEqual([
+      'STRING'
+    ]);
+    expect((await getArgumentDetailsForUdf(hiveConn, 'reflect', 3)).map(extractType)).toEqual([
+      'T'
+    ]);
+    expect((await getArgumentDetailsForUdf(hiveConn, 'reflect', 200)).map(extractType)).toEqual([
+      'T'
+    ]);
   });
 
   it('should handle functions with an infinite amount of arguments', async () => {
-    expect(await getArgumentTypesForUdf(impalaConn, 'greatest', 1)).toEqual(['T']);
-    expect(await getArgumentTypesForUdf(impalaConn, 'greatest', 200)).toEqual(['T']);
+    expect((await getArgumentDetailsForUdf(impalaConn, 'greatest', 1)).map(extractType)).toEqual([
+      'T'
+    ]);
+    expect((await getArgumentDetailsForUdf(impalaConn, 'greatest', 200)).map(extractType)).toEqual([
+      'T'
+    ]);
 
-    expect(await getArgumentTypesForUdf(hiveConn, 'strleft', 1)).toEqual(['T']);
-    expect(await getArgumentTypesForUdf(hiveConn, 'strleft', 2)).toEqual(['T']);
-    expect(await getArgumentTypesForUdf(hiveConn, 'strleft', 3)).toEqual(['T']);
-    expect(await getArgumentTypesForUdf(hiveConn, 'strleft', 200)).toEqual(['T']);
+    expect((await getArgumentDetailsForUdf(hiveConn, 'strleft', 1)).map(extractType)).toEqual([
+      'T'
+    ]);
+    expect((await getArgumentDetailsForUdf(hiveConn, 'strleft', 2)).map(extractType)).toEqual([
+      'T'
+    ]);
+    expect((await getArgumentDetailsForUdf(hiveConn, 'strleft', 3)).map(extractType)).toEqual([
+      'T'
+    ]);
+    expect((await getArgumentDetailsForUdf(hiveConn, 'strleft', 200)).map(extractType)).toEqual([
+      'T'
+    ]);
   });
 
   it('should not return types for arguments out of bounds', async () => {
-    expect(await getArgumentTypesForUdf(impalaConn, 'strleft', 1)).toEqual(['STRING']);
-    expect(await getArgumentTypesForUdf(impalaConn, 'strleft', 2)).toEqual(['INT']);
-    expect(await getArgumentTypesForUdf(impalaConn, 'strleft', 3)).toEqual([]);
-    expect(await getArgumentTypesForUdf(impalaConn, 'strleft', 200)).toEqual([]);
+    expect((await getArgumentDetailsForUdf(impalaConn, 'strleft', 1)).map(extractType)).toEqual([
+      'STRING'
+    ]);
+    expect((await getArgumentDetailsForUdf(impalaConn, 'strleft', 2)).map(extractType)).toEqual([
+      'INT'
+    ]);
+    expect((await getArgumentDetailsForUdf(impalaConn, 'strleft', 3)).map(extractType)).toEqual([]);
+    expect((await getArgumentDetailsForUdf(impalaConn, 'strleft', 200)).map(extractType)).toEqual(
+      []
+    );
   });
 
   it("should return T for any argument if the udf isn't found", async () => {
-    expect(await getArgumentTypesForUdf(hiveConn, 'blabla', 2)).toEqual(['T']);
-    expect(await getArgumentTypesForUdf(hiveConn, 'blabla', 200)).toEqual(['T']);
-    expect(await getArgumentTypesForUdf(impalaConn, 'blabla', 2)).toEqual(['T']);
-    expect(await getArgumentTypesForUdf(impalaConn, 'blabla', 200)).toEqual(['T']);
+    expect((await getArgumentDetailsForUdf(hiveConn, 'blabla', 2)).map(extractType)).toEqual(['T']);
+    expect((await getArgumentDetailsForUdf(hiveConn, 'blabla', 200)).map(extractType)).toEqual([
+      'T'
+    ]);
+    expect((await getArgumentDetailsForUdf(impalaConn, 'blabla', 2)).map(extractType)).toEqual([
+      'T'
+    ]);
+    expect((await getArgumentDetailsForUdf(impalaConn, 'blabla', 200)).map(extractType)).toEqual([
+      'T'
+    ]);
   });
 });