Răsfoiți Sursa

HUE-4002 [editor] Add parser support for all generic SQL test cases from the old autocompleter

Johan Ahlen 9 ani în urmă
părinte
comite
ded47313a0

+ 437 - 71
desktop/core/src/desktop/static/desktop/js/autocomplete/sql.jison

@@ -15,18 +15,37 @@
 // limitations under the License.
 
 %lex
+%options case-insensitive
 %%
 
-\s+                       { /* skip whitespace */ }
-'|CURSOR|'                { return '|CURSOR|'; }
-[Ss][Ee][Ll][Ee][Cc][Tt]  { return 'SELECT'; }
-[Uu][Ss][Ee]              { return 'USE'; }
-[Ff][Rr][Oo][Mm]          { return 'FROM'; }
-[a-zA-Z0-9_]*\b           { return 'STRING_IDENTIFIER'; }
-','                       { return ','; }
-'*'                       { return '*'; }
-';'                       { return ';'; }
-<<EOF>>                   { return 'EOF'; }
+[ \t\n]                             { /* skip whitespace */ }
+'--'.*                              { /* skip comments */ }
+[/][*][^*]*[*]+([^/*][^*]*[*]+)*[/] { /* skip comments */ }
+
+'|CURSOR|'                          { parser.yy.cursorFound = true; return 'CURSOR'; }
+'|PARTIAL_CURSOR|'                  { parser.yy.cursorFound = true; return 'PARTIAL_CURSOR'; }
+
+'AND'                               { return 'AND'; }
+'BY'                                { return 'BY'; }
+'FROM'                              { return 'FROM'; }
+'GROUP'                             { return 'GROUP'; }
+'IS'                                { return 'IS'; }
+'JOIN'                              { return 'JOIN'; }
+'NOT'                               { return 'NOT'; }
+'ON'                                { return 'ON'; }
+'OR'                                { return 'OR'; }
+'ORDER'                             { return 'ORDER'; }
+'SELECT'                            { determineCase(yytext); return 'SELECT'; }
+'USE'                               { determineCase(yytext); return 'USE'; }
+'WHERE'                             { return 'WHERE'; }
+
+[0-9]+                              { return 'UNSIGNED_INTEGER'; }
+[A-Za-z][A-Za-z0-9_]*               { return 'REGULAR_IDENTIFIER'; }
+
+[-+&~|^/%*(),.;!]                   { return yytext; }
+[=<>]                               { return yytext; }
+
+<<EOF>>                             { return 'EOF'; }
 
 /lex
 
@@ -34,8 +53,39 @@
 
 %%
 
+AnyCursor
+ : 'CURSOR'
+ | 'PARTIAL_CURSOR'
+ ;
+
+InitResults
+ : /* empty */
+   {
+     parser.yy.result = {};
+     parser.yy.cursorFound = false;
+     delete parser.yy.latestTableReferences;
+     parser.parseError = function (message, error) {
+       if (typeof parser.yy.result.suggestColumns !== 'undefined') {
+         completeSuggestColumns();
+       }
+       prioritizeSuggestions();
+       parser.yy.result.error = error;
+       return message;
+     }
+   }
+ ;
+
 Sql
- : SqlStatements EOF
+ : InitResults SqlStatements ';' EOF
+   {
+     prioritizeSuggestions();
+     return parser.yy.result;
+   }
+ | InitResults SqlStatements EOF
+   {
+     prioritizeSuggestions();
+     return parser.yy.result;
+   }
  ;
 
 SqlStatements
@@ -44,106 +94,422 @@ SqlStatements
  ;
 
 SqlStatement
- : SelectStatement
- | UseStatement
- | STRING_IDENTIFIER '|CURSOR|'
+ : UseStatement
+ | QueryExpression
+ | 'REGULAR_IDENTIFIER' AnyCursor 'REGULAR_IDENTIFIER'
+ | 'REGULAR_IDENTIFIER' AnyCursor
    {
-     return filterStartsWith(adjustKeywordCase(isLowerCase($1), suggestions.statements), $1);
+     suggestKeywords(['SELECT', 'USE']);
    }
- | '|CURSOR|'
+ | AnyCursor
    {
-     return adjustKeywordCase(false, suggestions.statements);
+     suggestKeywords(['SELECT', 'USE']);
    }
  ;
 
 UseStatement
-  : 'USE' STRING_IDENTIFIER ';'
-  ;
+ : 'USE' 'REGULAR_IDENTIFIER' AnyCursor
+   {
+     suggestDatabases();
+   }
+ | 'USE' 'REGULAR_IDENTIFIER'
+   {
+     if (! parser.yy.cursorFound) {
+       parser.yy.result.useDatabase = $2;
+     }
+   }
+ | 'USE' AnyCursor
+   {
+     suggestDatabases();
+   }
+ ;
 
-SelectStatement
- : 'SELECT' SelectExpression 'FROM' TableReference ';'
- | 'SELECT' '|CURSOR|'
+QueryExpression
+ : 'SELECT' SelectList TableExpression
    {
-     var tables = parser.yy.callbacks.tableHandler({
-       prependQuestionMark: true,
-       prependFrom: true,
-       lowerCase: isLowerCase($1)
-     });
+     completeSuggestColumns();
+   }
+ | 'SELECT' SelectList
+ ;
+
+TableExpression
+ : FromClause
+ | FromClause 'PARTIAL_CURSOR'
+   {
+     suggestTables();
+     suggestDatabases({ appendDot: true });
+   }
+ | FromClause SelectConditionList
+ ;
+
+FromClause
+ : 'FROM' TableReferenceList
+ | 'FROM' AnyCursor
+   {
+     suggestTables();
+     suggestDatabases({ appendDot: true });
+   }
+ ;
+
+SelectConditionList
+ : SelectCondition
+ | SelectConditionList SelectCondition
+ ;
+
+SelectCondition
+ : WhereClause
+ | GroupByClause
+   {
+     delete parser.yy.result.suggestStar;
+   }
+ | OrderByClause
+   {
+     delete parser.yy.result.suggestStar;
+   }
+ | LimitClause
+   {
+     delete parser.yy.result.suggestStar;
+   }
+ | 'CURSOR'
+   {
+     suggestKeywords(['WHERE', 'GROUP BY', 'LIMIT']);
+   }
+ ;
+
+WhereClause
+ : 'WHERE' SearchCondition
+ | 'WHERE' 'CURSOR'
+   {
+     suggestColumns();
+   }
+ ;
+
+SearchCondition
+ : BooleanValueExpression
+ ;
+
+BooleanValueExpression
+ : BooleanTerm
+ | BooleanValueExpression 'OR' BooleanTerm
+ ;
+
+BooleanTerm
+ : BooleanFactor
+ | BooleanFactor 'AND' 'CURSOR'
+   {
+     // TODO: Fix issue when EOF after 'CURSOR' in started parenthesized expression it now throws a parser error
+     suggestColumns();
+   }
+ | BooleanFactor 'AND' BooleanTerm
+ ;
+
+BooleanFactor
+ : 'NOT' BooleanTest
+ | BooleanTest
+ ;
+
+BooleanTest
+ : Predicate
+ | Predicate CompOp Predicate
+ | Predicate 'IS' TruthValue
+ | Predicate 'IS' 'NOT' TruthValue
+ ;
+
+Predicate
+ : ParenthesizedBooleanValueExpression
+ | NonParenthesizedValueExpressionPrimary
+ ;
+
+CompOp
+ : '='
+ | '<>'
+ | '<'
+ | '>'
+ | '<='
+ | '>='
+ ;
+
+ParenthesizedBooleanValueExpression
+ : '(' BooleanValueExpression ')'
+ | '(' AnyCursor
+   {
+     // For '...FROM tableA JOIN tableB ON (|', might need an ON flag
+     suggestColumns();
+   }
+ ;
+
+NonParenthesizedValueExpressionPrimary
+ : ColumnReference // TODO: Expand with more choices
+ ;
+
+ColumnReference
+ : BasicIdentifierChain
+ ;
+
+BasicIdentifierChain
+ : InitIdentifierChain IdentifierChain
+   {
+     delete parser.yy.identifierChain;
+   }
+ ;
+
+InitIdentifierChain
+ : /* empty */
+   {
+     parser.yy.identifierChain = [];
+   }
+ ;
 
-     var databases = parser.yy.callbacks.databaseHandler({
-       prependQuestionMark: true,
-       prependFrom: true,
-       lowerCase: isLowerCase($1)
+IdentifierChain
+ : Identifier
+ | IdentifierChain '.' 'PARTIAL_CURSOR'
+   {
+     suggestColumns({
+       identifierChain: parser.yy.identifierChain
      });
+   }
+ | IdentifierChain '.' Identifier
+ ;
 
-     return tables.concat(databases);
+Identifier
+ : 'REGULAR_IDENTIFIER'
+   {
+     parser.yy.identifierChain.push($1);
+   }
+ | '"' 'REGULAR_IDENTIFIER' '"'
+   {
+     parser.yy.identifierChain.push($2);
    }
  ;
 
-SelectExpression
- : SelectExpressionList
- | '*' '|CURSOR|'
+GroupByClause
+ : 'GROUP' 'BY' ColumnList
+ | 'GROUP' 'CURSOR'
    {
-      var tables = parser.yy.callbacks.tableHandler({
-        prependFrom: true,
-        lowerCase: isLowerCase($1)
-      });
+     suggestKeywords(['BY']);
+   }
+ ;
 
-      var databases = parser.yy.callbacks.databaseHandler({
-        prependFrom: true,
-        lowerCase: isLowerCase($1)
-      });
+OrderByClause
+ : 'ORDER' 'BY' ColumnList
+ | 'ORDER' 'CURSOR'
+   {
+     suggestKeywords(['BY']);
+   }
+ ;
 
-      return tables.concat(databases);
+LimitClause
+ : 'LIMIT' 'UNSIGNED_INTEGER'
+ | 'LIMIT' 'CURSOR'
+   {
+     suggestNumbers([5, 10, 15]);
+   }
+ ;
+
+SelectList
+ : ColumnList
+ | '*' 'REGULAR_IDENTIFIER' 'PARTIAL_CURSOR' // TODO: Support partials differently, for instance remove before parsing
+   {
+     suggestTables({ prependFrom: true });
+     suggestDatabases({ prependFrom: true, appendDot: true });
+   }
+ | '*' 'CURSOR'
+   {
+     suggestTables({ prependFrom: true });
+     suggestDatabases({ prependFrom: true, appendDot: true });
    }
  | '*'
  ;
 
-SelectExpressionList
+ColumnList
  : DerivedColumn
- | SelectExpressionList ',' DerivedColumn
+ | ColumnList ',' DerivedColumn
  ;
 
+// Needs revising REGULAR_IDENTIFIER should be CHARACTER_PRIMARY
 DerivedColumn
- : STRING_IDENTIFIER
+ : 'REGULAR_IDENTIFIER' '.' 'REGULAR_IDENTIFIER'
+ | 'REGULAR_IDENTIFIER' '.' '*'
+ | 'REGULAR_IDENTIFIER' '.' AnyCursor
+   {
+     parser.yy.result.suggestStar = true;
+     suggestColumns({
+       identifierChain: [ $1 ]
+     });
+   }
+ | 'REGULAR_IDENTIFIER' 'PARTIAL_CURSOR'
+   {
+     suggestColumns();
+     suggestTables({ prependQuestionMark: true, prependFrom: true });
+     suggestDatabases({ prependQuestionMark: true, prependFrom: true, appendDot: true });
+   }
+ | 'REGULAR_IDENTIFIER'
+ | 'CURSOR'
+   {
+     parser.yy.result.suggestStar = true;
+     suggestColumns();
+     suggestTables({ prependQuestionMark: true, prependFrom: true });
+     suggestDatabases({ prependQuestionMark: true, prependFrom: true, appendDot: true });
+   }
+ ;
+
+TableReferenceList
+ : TableReference
+ | TableReferenceList ',' TableReference
  ;
 
 TableReference
- : STRING_IDENTIFIER
+ : TablePrimaryOrJoinedTable
+ ;
+
+TablePrimaryOrJoinedTable
+ : TablePrimary
+ | JoinedTable
+ ;
+
+TablePrimary
+ : 'REGULAR_IDENTIFIER'
+   {
+     addTableReference({ table: $1 });
+   }
+ | 'REGULAR_IDENTIFIER' 'REGULAR_IDENTIFIER'
+   {
+     addTableReference({ table: $1, alias: $2 });
+   }
+ | 'REGULAR_IDENTIFIER' '.' 'REGULAR_IDENTIFIER'
+   {
+     addTableReference({ database: $1, table: $3 });
+   }
+ | 'REGULAR_IDENTIFIER' '.' 'REGULAR_IDENTIFIER' 'REGULAR_IDENTIFIER'
+   {
+     addTableReference({ database: $1, table: $3, alias: $4 });
+   }
+ | 'REGULAR_IDENTIFIER' '.' AnyCursor
+   {
+     suggestTables({ database: $1 });
+   }
+ ;
+
+JoinedTable
+ : TableReference 'JOIN' TableReference JoinSpecification
+ | TableReference 'JOIN' AnyCursor
+   {
+     suggestTables({});
+     suggestDatabases({ appendDot: true });
+   }
+ ;
+
+JoinSpecification
+ : JoinCondition
+ ;
+
+JoinCondition
+ : 'ON' SearchCondition
  ;
 
 %%
 
-var suggestions = {
-  statements: [{ value: 'SELECT', meta: 'keyword' }, { value: 'USE', meta: 'keyword' }]
+var prioritizeSuggestions = function () {
+   parser.yy.result.lowerCase = parser.yy.lowerCase || false;
+   if (typeof parser.yy.result.suggestIdentifiers !== 'undefined' &&  parser.yy.result.suggestIdentifiers.length > 0) {
+     delete parser.yy.result.suggestColumns;
+     delete parser.yy.result.suggestTables;
+     delete parser.yy.result.suggestDatabases;
+     return;
+   }
+   if (typeof parser.yy.result.suggestColumns !== 'undefined') {
+     if (typeof parser.yy.result.suggestColumns.table === 'undefined') {
+       delete parser.yy.result.suggestColumns;
+     } else {
+       delete parser.yy.result.suggestTables;
+       delete parser.yy.result.suggestDatabases;
+     }
+     return;
+   }
 }
 
-var filterStartsWith = function (suggestions, start) {
-  var startLower = start.toLowerCase();
-  return suggestions.filter(function (suggestion) {
-    return suggestion.value.toLowerCase().indexOf(startLower) === 0;
-  });
+var completeSuggestColumns = function () {
+   if (parser.yy.cursorFound &&
+       typeof parser.yy.result.suggestColumns !== 'undefined') {
+     var identifierChain = parser.yy.result.suggestColumns.identifierChain;
+     delete parser.yy.result.suggestColumns.identifierChain;
+     var tableReferences = parser.yy.latestTableReferences;
+
+     // IdentifierChain contains a possibly started identifier or empty, example: a.b.c = ['a', 'b', 'c']
+     if (identifierChain.length > 0) {
+       var foundTable = tableReferences.filter(function (tableRef) {
+         return identifierChain[0] === tableRef.alias || identifierChain[0] === tableRef.table;
+       })
+       if (foundTable.length === 1) {
+         tableReferences = foundTable;
+       }
+     }
+
+     if (tableReferences.length === 1) {
+       parser.yy.result.suggestColumns.table = tableReferences[0].table;
+       if (typeof tableReferences[0].database !== 'undefined') {
+         parser.yy.result.suggestColumns.database = tableReferences[0].database;
+       }
+     } else if (tableReferences.length > 1) {
+       // Table identifier is required for column completion
+       delete parser.yy.result.suggestColumns;
+       parser.yy.result.suggestIdentifiers = [];
+       tableReferences.forEach(function (tableRef) {
+         parser.yy.result.suggestIdentifiers.push((tableRef.alias || tableRef.table) + '.');
+       });
+     }
+   }
 }
 
-var adjustKeywordCase = function (lowerCase, suggestions) {
-  if (lowerCase) {
-    suggestions.forEach(function (suggestion) {
-      if (suggestion.meta === 'keyword') {
-        suggestion.value = suggestion.value.toLowerCase();
-      }
-    });
-  } else {
-    suggestions.forEach(function (suggestion) {
-      if (suggestion.meta === 'keyword') {
-        suggestion.value = suggestion.value.toUpperCase();
-      }
-    });
+var addTableReference = function (ref) {
+  if (typeof parser.yy.latestTableReferences === 'undefined') {
+    parser.yy.latestTableReferences = [];
   }
-  return suggestions;
+  parser.yy.latestTableReferences.push(ref);
+}
+
+var suggestNumbers = function (numbers) {
+  parser.yy.result.suggestNumbers = numbers;
+}
+
+var suggestKeywords = function (keywords) {
+  parser.yy.result.suggestKeywords = keywords;
 }
 
-var isLowerCase = function (text) {
-  return text.toLowerCase() === text;
+var suggestColumns = function (details) {
+  parser.yy.result.suggestColumns = details || { identifierChain: [] };
+}
+
+var suggestTables = function (details) {
+  parser.yy.result.suggestTables = details || {};
+}
+
+var suggestDatabases = function (details) {
+  parser.yy.result.suggestDatabases = details || {};
+}
+
+var determineCase = function (text) {
+  parser.yy.lowerCase = text.toLowerCase() === text;
+};
+
+/**
+ * Main parser function
+ */
+parser.parseSql = function(beforeCursor, afterCursor, dialect) {
+  var result;
+  parser.yy.dialect = dialect;
+  try {
+    // Add |CURSOR| or |PARTIAL_CURSOR| to represent the different cursor states in the lexer
+    result = parser.parse(beforeCursor + (beforeCursor.length == 0 || beforeCursor.indexOf(' ', beforeCursor.length - 1) !== -1 ? ' |CURSOR| ' : '|PARTIAL_CURSOR|') + afterCursor);
+  } catch (err) {
+    // On any error try to at least return any existing result
+    if (typeof parser.yy.result === 'undefined') {
+      throw err;
+    }
+    result = parser.yy.result;
+  }
+
+  return result;
 }
 
 /*

Fișier diff suprimat deoarece este prea mare
+ 159 - 30
desktop/core/src/desktop/static/desktop/js/autocomplete/sql.js


Fișier diff suprimat deoarece este prea mare
+ 409 - 339
desktop/core/src/desktop/static/desktop/spec/autocomplete/sqlSpec.js


+ 17 - 0
desktop/core/src/desktop/static/desktop/spec/autocompleterTestUtils.js

@@ -34,6 +34,23 @@ define([
           }
         }
       }
+    },
+    testDefinitionMatcher: {
+      toEqualDefinition : function() {
+        return {
+          compare: function(actualResponse, testDefinition) {
+            if (testDefinition.ignoreErrors) {
+              delete actualResponse.error;
+            }
+            return {
+              pass: jasmine.matchersUtil.equals(actualResponse, testDefinition.expectedResult),
+              message: '\n        Statement: ' + testDefinition.beforeCursor + '|' + testDefinition.afterCursor + '\n' +
+                       'Expected response: ' + JSON.stringify(testDefinition.expectedResult) + '\n' +
+                       '  Parser response: ' + JSON.stringify(actualResponse) +   '\n'
+            };
+          }
+        }
+      }
     }
   }
 });

+ 0 - 1
desktop/core/src/desktop/templates/jasmine.mako

@@ -15,6 +15,5 @@
 ## limitations under the License.
 ##
 
-<<<<<<< HEAD
 <%include file="jasmineRunner.html"/>
 

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff