Browse Source

HUE-9084 [editor] Adding Apache Druid parser skeleton

Romain 6 years ago
parent
commit
3d0d9ec1a3
17 changed files with 11166 additions and 0 deletions
  1. 3 0
      .eslintignore
  2. 91 0
      desktop/core/src/desktop/js/parse/sql/druid/druidAutocompleteParser.js
  3. 91 0
      desktop/core/src/desktop/js/parse/sql/druid/druidSyntaxParser.js
  4. 372 0
      desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParserSpec.js
  5. 157 0
      desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Alter_Spec.js
  6. 277 0
      desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Create_Spec.js
  7. 290 0
      desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Drop_Spec.js
  8. 138 0
      desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Error_Spec.js
  9. 139 0
      desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Insert_Spec.js
  10. 423 0
      desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Locations_Spec.js
  11. 6174 0
      desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Select_Spec.js
  12. 50 0
      desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Set_Spec.js
  13. 384 0
      desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Update_Spec.js
  14. 146 0
      desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Use_Spec.js
  15. 208 0
      desktop/core/src/desktop/js/parse/sql/druid/spec/druidSyntaxParserSpec.js
  16. 2221 0
      desktop/core/src/desktop/js/parse/sql/druid/sqlParseSupport.js
  17. 2 0
      desktop/core/src/desktop/js/parse/sql/sqlParserRepository.js

+ 3 - 0
.eslintignore

@@ -17,8 +17,11 @@
 /desktop/core/src/desktop/js/parse/sql/ksql/ksqlSyntaxParser.js
 /desktop/core/src/desktop/js/parse/sql/calcite/calciteAutocompleteParser.js
 /desktop/core/src/desktop/js/parse/sql/calcite/calciteSyntaxParser.js
+/desktop/core/src/desktop/js/parse/sql/druid/druidAutocompleteParser.js
+/desktop/core/src/desktop/js/parse/sql/druid/druidSyntaxParser.js
 /desktop/core/src/desktop/js/parse/sql/generic/spec/genericAutocompleteParser_Locations_Spec.js
 /desktop/core/src/desktop/js/parse/sql/impala/spec/impalaAutocompleteParser_Locations_Spec.js
 /desktop/core/src/desktop/js/parse/sql/hive/spec/hiveAutocompleteParser_Locations_Spec.js
 /desktop/core/src/desktop/js/parse/sql/ksql/spec/ksqlAutocompleteParser_Locations_Spec.js
 /desktop/core/src/desktop/js/parse/sql/calcite/spec/calciteAutocompleteParser_Locations_Spec.js
+/desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Locations_Spec.js

File diff suppressed because it is too large
+ 91 - 0
desktop/core/src/desktop/js/parse/sql/druid/druidAutocompleteParser.js


File diff suppressed because it is too large
+ 91 - 0
desktop/core/src/desktop/js/parse/sql/druid/druidSyntaxParser.js


+ 372 - 0
desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParserSpec.js

@@ -0,0 +1,372 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import druidAutocompleteParser from '../druidAutocompleteParser';
+
+describe('druidAutocompleteParser.js', () => {
+  beforeAll(() => {
+    druidAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      druidAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  it('should suggest keywords for ";;|"', () => {
+    assertAutoComplete({
+      beforeCursor: ';;',
+      afterCursor: '',
+      containsKeywords: ['SELECT', 'WITH'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest keywords for ";|;"', () => {
+    assertAutoComplete({
+      beforeCursor: ';',
+      afterCursor: ';',
+      containsKeywords: ['SELECT'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest keywords for "|;;;;', () => {
+    assertAutoComplete({
+      beforeCursor: '',
+      afterCursor: ';;;;',
+      containsKeywords: ['SELECT'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest keywords for "foo|bar"', () => {
+    assertAutoComplete({
+      beforeCursor: 'foo',
+      afterCursor: 'bar',
+      containsKeywords: ['SELECT'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  describe('Error Handling', () => {
+    it('should suggest keywords for "bla; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'bla; ',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "bla bla bla;bla; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'bla bla bla;bla; ',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "Åäö; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'Åäö; ',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "bla bla bla;bla;\\n|;bladiblaa blaa"', () => {
+      assertAutoComplete({
+        beforeCursor: 'bla bla bla;bla;\n',
+        afterCursor: ';bladiblaa blaa',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "FROM; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'FROM; ',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "INTO USE; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INTO USE; ',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "INTO SELECT; OR FROM FROM; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INTO SELECT; OR FROM FROM;',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "INTO SELECT; OR FROM FROM; |;BLAAA; AND;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INTO SELECT; OR FROM FROM;',
+        afterCursor: ';BLAAA; AND;',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "INTO bla bla;AND booo; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INTO bla bla;AND booo;',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "|; SELECT LIMIT 10"', () => {
+      assertAutoComplete({
+        beforeCursor: '',
+        afterCursor: '; SELECT LIMIT 10',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "| * FROM boo; SELECT LIMIT 10"', () => {
+      assertAutoComplete({
+        beforeCursor: '',
+        afterCursor: ' * FROM boo; SELECT LIMIT 10',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "bla| * FROM boo; SELECT LIMIT 10"', () => {
+      assertAutoComplete({
+        beforeCursor: 'bla',
+        afterCursor: ' * FROM boo; SELECT LIMIT 10',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+  });
+
+  describe('SET', () => {
+    it('should handle "set bla.bla="ble";|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'set bla.bla="ble";',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should handle "set bla.bla=\'ble\';|"', () => {
+      assertAutoComplete({
+        beforeCursor: "set bla.bla='ble';",
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should handle "set mem_limit=64g;|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'set mem_limit=64g;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should handle "set DISABLE_UNSAFE_SPILLS=true;|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'set DISABLE_UNSAFE_SPILLS=true;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should handle "set RESERVATION_REQUEST_TIMEOUT=900000;|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'set RESERVATION_REQUEST_TIMEOUT=900000;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: true
+        }
+      });
+    });
+  });
+
+  describe('partial removal', () => {
+    it('should identify part lengths', () => {
+      const limitChars = [
+        ' ',
+        '\n',
+        '\t',
+        '&',
+        '~',
+        '%',
+        '!',
+        '.',
+        ',',
+        '+',
+        '-',
+        '*',
+        '/',
+        '=',
+        '<',
+        '>',
+        ')',
+        '[',
+        ']',
+        ';'
+      ];
+
+      expect(druidAutocompleteParser.identifyPartials('', '')).toEqual({ left: 0, right: 0 });
+      expect(druidAutocompleteParser.identifyPartials('foo', '')).toEqual({ left: 3, right: 0 });
+      expect(druidAutocompleteParser.identifyPartials(' foo', '')).toEqual({ left: 3, right: 0 });
+      expect(druidAutocompleteParser.identifyPartials('asdf 1234', '')).toEqual({
+        left: 4,
+        right: 0
+      });
+
+      expect(druidAutocompleteParser.identifyPartials('foo', 'bar')).toEqual({
+        left: 3,
+        right: 3
+      });
+
+      expect(druidAutocompleteParser.identifyPartials('fo', 'o()')).toEqual({
+        left: 2,
+        right: 3
+      });
+
+      expect(druidAutocompleteParser.identifyPartials('fo', 'o(')).toEqual({ left: 2, right: 2 });
+      expect(druidAutocompleteParser.identifyPartials('fo', 'o(bla bla)')).toEqual({
+        left: 2,
+        right: 10
+      });
+
+      expect(druidAutocompleteParser.identifyPartials('foo ', '')).toEqual({ left: 0, right: 0 });
+      expect(druidAutocompleteParser.identifyPartials("foo '", "'")).toEqual({
+        left: 0,
+        right: 0
+      });
+
+      expect(druidAutocompleteParser.identifyPartials('foo "', '"')).toEqual({
+        left: 0,
+        right: 0
+      });
+      limitChars.forEach(char => {
+        expect(druidAutocompleteParser.identifyPartials('bar foo' + char, '')).toEqual({
+          left: 0,
+          right: 0
+        });
+
+        expect(druidAutocompleteParser.identifyPartials('bar foo' + char + 'foofoo', '')).toEqual({
+          left: 6,
+          right: 0
+        });
+
+        expect(druidAutocompleteParser.identifyPartials('bar foo' + char + 'foofoo ', '')).toEqual({
+          left: 0,
+          right: 0
+        });
+
+        expect(druidAutocompleteParser.identifyPartials('', char + 'foo bar')).toEqual({
+          left: 0,
+          right: 0
+        });
+
+        expect(druidAutocompleteParser.identifyPartials('', 'foofoo' + char)).toEqual({
+          left: 0,
+          right: 6
+        });
+
+        expect(druidAutocompleteParser.identifyPartials('', ' foofoo' + char)).toEqual({
+          left: 0,
+          right: 0
+        });
+      });
+    });
+  });
+});

+ 157 - 0
desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Alter_Spec.js

@@ -0,0 +1,157 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import druidAutocompleteParser from '../druidAutocompleteParser';
+
+describe('druidAutocompleteParser.js ALTER statements', () => {
+  beforeAll(() => {
+    druidAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      druidAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  describe('ALTER TABLE', () => {
+    it('should suggest keywords for "ALTER |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER ',
+        afterCursor: '',
+        containsKeywords: ['TABLE'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest tables for "ALTER TABLE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER TABLE ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { onlyTables: true },
+          suggestDatabases: { appendDot: true }
+        }
+      });
+    });
+
+    it('should suggest tables for "ALTER TABLE foo.|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER TABLE foo.',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { identifierChain: [{ name: 'foo' }], onlyTables: true }
+        }
+      });
+    });
+  });
+
+  describe('ALTER VIEW', () => {
+    it('should handle "ALTER VIEW baa.boo AS SELECT * FROM bla;|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER VIEW baa.boo AS SELECT * FROM bla;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "ALTER |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER ',
+        afterCursor: '',
+        containsKeywords: ['VIEW'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest views for "ALTER VIEW |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER VIEW ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { onlyViews: true },
+          suggestDatabases: { appendDot: true }
+        }
+      });
+    });
+
+    it('should suggest views for "ALTER VIEW boo.|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER VIEW boo.',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { identifierChain: [{ name: 'boo' }], onlyViews: true }
+        }
+      });
+    });
+
+    it('should suggest keywords for "ALTER VIEW boo |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER VIEW boo ',
+        afterCursor: '',
+        containsKeywords: ['AS'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "ALTER VIEW baa.boo AS |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER VIEW baa.boo AS ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['SELECT']
+        }
+      });
+    });
+
+    it('should suggest databases for "ALTER VIEW baa.boo AS SELECT * FROM |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'ALTER VIEW baa.boo AS SELECT * FROM ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {},
+          suggestDatabases: { appendDot: true }
+        }
+      });
+    });
+  });
+});

+ 277 - 0
desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Create_Spec.js

@@ -0,0 +1,277 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import druidAutocompleteParser from '../druidAutocompleteParser';
+
+describe('druidAutocompleteParser.js CREATE statements', () => {
+  beforeAll(() => {
+    druidAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      druidAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  it('should suggest keywords for "|"', () => {
+    assertAutoComplete({
+      beforeCursor: '',
+      afterCursor: '',
+      containsKeywords: ['CREATE'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest keywords for "CREATE |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'CREATE ',
+      afterCursor: '',
+      containsKeywords: ['DATABASE', 'ROLE', 'SCHEMA', 'TABLE', 'VIEW'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  describe('CREATE DATABASE', () => {
+    it('should suggest keywords for "CREATE DATABASE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE DATABASE ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['IF NOT EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE DATABASE IF |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE DATABASE IF ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['NOT EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE SCHEMA |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE SCHEMA ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['IF NOT EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE DATABASE | bla;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE DATABASE ',
+        afterCursor: ' bla;',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['IF NOT EXISTS']
+        }
+      });
+    });
+  });
+
+  describe('CREATE ROLE', () => {
+    it('should handle "CREATE ROLE boo; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE ROLE boo; ',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+  });
+
+  describe('CREATE TABLE', () => {
+    it('should suggest keywords for "CREATE TABLE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE TABLE ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['IF NOT EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE TABLE IF |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE TABLE IF ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['NOT EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE TABLE IF NOT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE TABLE IF NOT ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['EXISTS']
+        }
+      });
+    });
+
+    it('should handle for "CREATE TABLE foo (id INT);|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE TABLE foo (id INT);',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE TABLE foo (id |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE TABLE foo (id ',
+        afterCursor: '',
+        containsKeywords: ['BOOLEAN'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE TABLE foo (id INT, `some` FLOAT, bar |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE TABLE foo (id INT, `some` FLOAT, bar ',
+        afterCursor: '',
+        containsKeywords: ['BOOLEAN'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+  });
+
+  describe('CREATE VIEW', () => {
+    it('should handle "CREATE VIEW foo AS SELECT a, | FROM tableOne"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE VIEW foo AS SELECT a, ',
+        afterCursor: ' FROM tableOne',
+        hasLocations: true,
+        containsKeywords: ['*', 'CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'tableOne' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'tableOne' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE VIEW |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE VIEW ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['IF NOT EXISTS'],
+          suggestDatabases: { appendDot: true }
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE VIEW | boo AS select * from baa;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE VIEW ',
+        afterCursor: ' boo AS SELECT * FROM baa;',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['IF NOT EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE VIEW IF |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE VIEW IF ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['NOT EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE VIEW IF NOT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE VIEW IF NOT ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE VIEW boo AS |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE VIEW boo AS ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['SELECT']
+        }
+      });
+    });
+
+    it('should suggest keywords for "CREATE VIEW IF NOT EXISTS boo AS |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'CREATE VIEW IF NOT EXISTS boo AS ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['SELECT']
+        }
+      });
+    });
+  });
+});

+ 290 - 0
desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Drop_Spec.js

@@ -0,0 +1,290 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import druidAutocompleteParser from '../druidAutocompleteParser';
+
+describe('druidAutocompleteParser.js DROP statements', () => {
+  beforeAll(() => {
+    druidAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      druidAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  it('should suggest keywords for "DROP |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'DROP ',
+      afterCursor: '',
+      containsKeywords: ['DATABASE', 'ROLE', 'SCHEMA', 'TABLE', 'VIEW'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  describe('DROP DATABASE', () => {
+    it('should suggest databases for "DROP DATABASE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP DATABASE ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestDatabases: {},
+          suggestKeywords: ['IF EXISTS']
+        }
+      });
+    });
+
+    it('should suggest databases for "DROP SCHEMA |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP SCHEMA ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestDatabases: {},
+          suggestKeywords: ['IF EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "DROP DATABASE IF |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP DATABASE IF ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['EXISTS']
+        }
+      });
+    });
+
+    it('should suggest databases for "DROP DATABASE IF EXISTS |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP DATABASE IF EXISTS ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestDatabases: {}
+        }
+      });
+    });
+
+    it('should suggest keywords for "DROP DATABASE foo |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP DATABASE foo ',
+        afterCursor: '',
+        containsKeywords: ['CASCADE'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+  });
+
+  describe('DROP ROLE', () => {
+    it('should handle "DROP ROLE boo;|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP ROLE boo;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+  });
+
+  describe('DROP TABLE', () => {
+    it('should handle "DROP TABLE db.tbl PURGE;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP TABLE db.tbl PURGE;',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest tables for "DROP TABLE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP TABLE ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { onlyTables: true },
+          suggestKeywords: ['IF EXISTS'],
+          suggestDatabases: {
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "DROP TABLE db.|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP TABLE db.',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { identifierChain: [{ name: 'db' }], onlyTables: true }
+        }
+      });
+    });
+
+    it('should suggest keywords for "DROP TABLE IF |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP TABLE IF ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['EXISTS']
+        }
+      });
+    });
+
+    it('should suggest tables for "DROP TABLE IF EXISTS |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP TABLE IF EXISTS ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { onlyTables: true },
+          suggestDatabases: {
+            appendDot: true
+          }
+        }
+      });
+    });
+  });
+
+  describe('DROP VIEW', () => {
+    it('should handle "DROP VIEW boo;|', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP VIEW boo;',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "DROP VIEW IF EXISTS baa.boo;|', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP VIEW IF EXISTS baa.boo;',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest views for "DROP VIEW |', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP VIEW ',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { onlyViews: true },
+          suggestDatabases: { appendDot: true },
+          suggestKeywords: ['IF EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "DROP VIEW IF |', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP VIEW IF ',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['EXISTS']
+        }
+      });
+    });
+
+    it('should suggest views for "DROP VIEW boo.|', () => {
+      assertAutoComplete({
+        beforeCursor: 'DROP VIEW boo.',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { identifierChain: [{ name: 'boo' }], onlyViews: true }
+        }
+      });
+    });
+  });
+
+  describe('TRUNCATE TABLE', () => {
+    it('should handle "TRUNCATE TABLE baa.boo;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'TRUNCATE TABLE baa.boo;',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "TRUNCATE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'truncate ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: true,
+          suggestKeywords: ['TABLE']
+        }
+      });
+    });
+
+    it('should suggest tables for "TRUNCATE TABLE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'TRUNCATE TABLE ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {},
+          suggestDatabases: { appendDot: true },
+          suggestKeywords: ['IF EXISTS']
+        }
+      });
+    });
+  });
+});

+ 138 - 0
desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Error_Spec.js

@@ -0,0 +1,138 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import druidAutocompleteParser from '../druidAutocompleteParser';
+
+describe('druidAutocompleteParser.js Error statements', () => {
+  beforeAll(() => {
+    druidAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      druidAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  it('should suggest columns for "SELECT BAABO BOOAA BLARGH, | FROM testTable"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT BAABO BOOAA BLARGH, ',
+      afterCursor: ' FROM testTable',
+      expectedResult: {
+        lowerCase: false,
+        suggestFunctions: {},
+        suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+        suggestAnalyticFunctions: true,
+        suggestColumns: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+      }
+    });
+  });
+
+  it('should suggest columns for "SELECT foo, bar, SELECT, | FROM testTable"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT foo, bar, SELECT, ',
+      afterCursor: ' FROM testTable',
+      expectedResult: {
+        lowerCase: false,
+        suggestFunctions: {},
+        suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+        suggestAnalyticFunctions: true,
+        suggestColumns: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+      }
+    });
+  });
+
+  it('should suggest columns for "SELECT foo, baa baa baa baa, SELECT, | FROM testTable"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT foo, baa baa baa baa, SELECT, ',
+      afterCursor: ' FROM testTable',
+      expectedResult: {
+        lowerCase: false,
+        suggestFunctions: {},
+        suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+        suggestAnalyticFunctions: true,
+        suggestColumns: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+      }
+    });
+  });
+
+  it('should suggest columns for "SELECT * FROM testTable WHERE baa baaa booo |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM testTable WHERE baa baaa boo',
+      afterCursor: '',
+      containsKeywords: ['GROUP BY', 'ORDER BY'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest columns for "SELECT * FROM testTable WHERE baa baaa booo GROUP |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM testTable WHERE baa baaa boo GROUP ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestKeywords: ['BY'],
+        suggestGroupBys: { prefix: 'BY', tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+      }
+    });
+  });
+
+  it('should suggest columns for "SELECT * FROM testTable WHERE baa baaa booo ORDER |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM testTable WHERE baa baaa boo ORDER ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestKeywords: ['BY'],
+        suggestOrderBys: { prefix: 'BY', tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+      }
+    });
+  });
+
+  it('should suggest columns for "SELECT * FROM testTable ORDER BY bla bla bla boo |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM testTable ORDER BY bla bla bla boo ',
+      afterCursor: '',
+      containsKeywords: ['LIMIT', 'UNION'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest columns for "SELECT * FROM testTable GROUP BY boo hoo hoo ORDER BY bla bla bla boo |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM testTable ORDER BY bla bla bla boo ',
+      afterCursor: '',
+      containsKeywords: ['LIMIT', 'UNION'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+});

+ 139 - 0
desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Insert_Spec.js

@@ -0,0 +1,139 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import druidAutocompleteParser from '../druidAutocompleteParser';
+
+describe('druidAutocompleteParser.js INSERT statements', () => {
+  beforeAll(() => {
+    druidAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      druidAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  describe('INSERT', () => {
+    it('should handle "INSERT INTO bla.boo VALUES (1, 2, \'a\', 3); |"', () => {
+      assertAutoComplete({
+        beforeCursor: "INSERT INTO bla.boo VALUES (1, 2, 'a', 3); ",
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "|"', () => {
+      assertAutoComplete({
+        beforeCursor: '',
+        afterCursor: '',
+        containsKeywords: ['INSERT'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "INSERT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INSERT ',
+        afterCursor: '',
+        containsKeywords: ['INTO'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest tables for "INSERT INTO |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INSERT INTO ',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {},
+          suggestDatabases: { appendDot: true },
+          suggestKeywords: ['TABLE']
+        }
+      });
+    });
+
+    it('should suggest tables for "INSERT INTO baa.|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INSERT INTO baa.',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { identifierChain: [{ name: 'baa' }] }
+        }
+      });
+    });
+
+    it('should suggest tables for "INSERT INTO TABLE baa.|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INSERT INTO TABLE baa.',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { identifierChain: [{ name: 'baa' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "INSERT INTO baa |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INSERT INTO baa ',
+        afterCursor: '',
+        containsKeywords: ['VALUES'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "INSERT INTO TABLE baa |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'INSERT INTO TABLE baa ',
+        afterCursor: '',
+        containsKeywords: ['VALUES'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+  });
+});

+ 423 - 0
desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Locations_Spec.js

@@ -0,0 +1,423 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import druidAutocompleteParser from '../druidAutocompleteParser';
+
+// prettier-ignore-start
+describe('druidAutocompleteParser.js locations', () => {
+  beforeAll(() => {
+    druidAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = true;
+    expect(
+      druidAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  const assertLocations = function(options) {
+    assertAutoComplete({
+      beforeCursor: options.beforeCursor,
+      afterCursor: options.afterCursor || '',
+      locationsOnly: true,
+      noErrors: true,
+      expectedLocations: options.expectedLocations,
+      expectedDefinitions: options.expectedDefinitions
+    });
+  };
+
+  it('should report locations for "select cos(1) as foo from customers order by foo;"', () => {
+    assertLocations({
+      beforeCursor: 'select cos(1) as foo from customers order by foo; ',
+      expectedLocations: [
+        { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 49 } },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 8, last_column: 21 } },
+        { type: 'function', location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 }, function: 'cos' },
+        { type: 'alias', source: 'column', alias: 'foo', location: { first_line: 1, last_line: 1, first_column: 18, last_column: 21 }, parentLocation: { first_line: 1, last_line: 1, first_column: 8, last_column: 14 } },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 27, last_column: 36 }, identifierChain: [{ name: 'customers' }] },
+        { type: 'whereClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 36, last_column: 36 } },
+        { type: 'alias', location: { first_line: 1, last_line: 1, first_column: 46, last_column: 49 }, alias: 'foo', source: 'column', parentLocation: { first_line: 1, last_line: 1, first_column: 8, last_column: 14 } },
+        { type: 'limitClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 49, last_column: 49 } }
+      ]
+    });
+  });
+
+  it('should report locations for "WITH boo AS (SELECT * FROM tbl) SELECT * FROM boo; |"', () => {
+    assertLocations({
+      beforeCursor: 'WITH boo AS (SELECT * FROM tbl) SELECT * FROM boo; ',
+      expectedLocations: [
+        { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 50 } },
+        { type: 'alias', source: 'cte', alias: 'boo', location: { first_line: 1, last_line: 1, first_column: 6, last_column: 9 } },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 21, last_column: 22 }, subquery: true },
+        { type: 'asterisk', location: { first_line: 1, last_line: 1, first_column: 21, last_column: 22 }, tables: [{ identifierChain: [{ name: 'tbl' }] }] },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 28, last_column: 31 }, identifierChain: [{ name: 'tbl' }] },
+        { type: 'whereClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 31, last_column: 31 }, subquery: true },
+        { type: 'limitClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 31, last_column: 31 }, subquery: true },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 40, last_column: 41 } },
+        { type: 'asterisk', location: { first_line: 1, last_line: 1, first_column: 40, last_column: 41 }, tables: [{ identifierChain: [{ name: 'boo' }] }] },
+        { type: 'alias', target: 'cte', alias: 'boo', location: { first_line: 1, last_line: 1, first_column: 47, last_column: 50 } },
+        { type: 'whereClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 50, last_column: 50 } },
+        { type: 'limitClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 50, last_column: 50 } }
+      ]
+    });
+  });
+
+  it('should report locations for "SELECT * FROM testTable1 JOIN db1.table2; |"', () => {
+    assertLocations({
+      beforeCursor: 'SELECT * FROM testTable1 JOIN db1.table2; ',
+      expectedLocations: [
+        { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 41 } },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 } },
+        { type: 'asterisk', location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 }, tables: [
+            { identifierChain: [{ name: 'testTable1' }] },
+            { identifierChain: [{ name: 'db1' }, { name: 'table2' }] }
+          ] },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 15, last_column: 25 }, identifierChain: [{ name: 'testTable1' }] },
+        { type: 'database', location: { first_line: 1, last_line: 1, first_column: 31, last_column: 34 }, identifierChain: [{ name: 'db1' }] },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 35, last_column: 41 }, identifierChain: [{ name: 'db1' }, { name: 'table2' }] },
+        { type: 'whereClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 41, last_column: 41 } },
+        { type: 'limitClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 41, last_column: 41 } }
+      ]
+    });
+  });
+
+  it('should report locations for "SELECT a.col FROM db.tbl a; |"', () => {
+    assertLocations({
+      beforeCursor: 'SELECT a.col FROM db.tbl a; ',
+      expectedLocations: [
+        { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 27 } },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 8, last_column: 13 } },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 }, identifierChain: [{ name: 'db' }, { name: 'tbl' }] },
+        { type: 'column', location: { first_line: 1, last_line: 1, first_column: 10, last_column: 13 }, identifierChain: [{ name: 'col' }], tables: [{ identifierChain: [{ name: 'db' }, { name: 'tbl' }], alias: 'a' }], qualified: true },
+        { type: 'database', location: { first_line: 1, last_line: 1, first_column: 19, last_column: 21 }, identifierChain: [{ name: 'db' }] },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 22, last_column: 25 }, identifierChain: [{ name: 'db' }, { name: 'tbl' }] },
+        { type: 'alias', source: 'table', alias: 'a', location: { first_line: 1, last_line: 1, first_column: 26, last_column: 27 }, identifierChain: [{ name: 'db' }, { name: 'tbl' }] },
+        { type: 'whereClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 27, last_column: 27 } },
+        { type: 'limitClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 27, last_column: 27 } }
+      ]
+    });
+  });
+
+  it('should report locations for "SELECT tbl.col FROM db.tbl a; |"', () => {
+    assertLocations({
+      beforeCursor: 'SELECT tbl.col FROM db.tbl a; ',
+      expectedLocations: [
+        { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 29 } },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 8, last_column: 15 } },
+        { type: 'column', location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 }, identifierChain: [{ name: 'tbl' }], tables: [{ identifierChain: [{ name: 'db' }, { name: 'tbl' }], alias: 'a' }], qualified: false },
+        { type: 'complex', location: { first_line: 1, last_line: 1, first_column: 12, last_column: 15 }, identifierChain: [{ name: 'tbl' }, { name: 'col' }], tables: [{ identifierChain: [{ name: 'db' }, { name: 'tbl' }], alias: 'a' }], qualified: true },
+        { type: 'database', location: { first_line: 1, last_line: 1, first_column: 21, last_column: 23 }, identifierChain: [{ name: 'db' }] },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 24, last_column: 27 }, identifierChain: [{ name: 'db' }, { name: 'tbl' }] },
+        { type: 'alias', source: 'table', alias: 'a', location: { first_line: 1, last_line: 1, first_column: 28, last_column: 29 }, identifierChain: [{ name: 'db' }, { name: 'tbl' }] },
+        { type: 'whereClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 29, last_column: 29 } },
+        { type: 'limitClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 29, last_column: 29 } }
+      ]
+    });
+  });
+
+  it('should report locations for "select x from x;"', () => {
+    assertLocations({
+      beforeCursor: 'select x from x;',
+      expectedLocations: [
+        { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 16 } },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 } },
+        { type: 'column', location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 }, identifierChain: [{ name: 'x' }], tables: [{ identifierChain: [{ name: 'x' }] }], qualified: false },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 15, last_column: 16 }, identifierChain: [{ name: 'x' }] },
+        { type: 'whereClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 16, last_column: 16 } },
+        { type: 'limitClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 16, last_column: 16 } }
+      ]
+    });
+  });
+
+  it('should report locations for "select x from x;select y from y;"', () => {
+    assertLocations({
+      beforeCursor: 'select x from x;select y from y;',
+      expectedLocations: [
+        { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 16 } },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 } },
+        { type: 'column', location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 }, identifierChain: [{ name: 'x' }], tables: [{ identifierChain: [{ name: 'x' }] }], qualified: false },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 15, last_column: 16 }, identifierChain: [{ name: 'x' }] },
+        { type: 'whereClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 16, last_column: 16 } },
+        { type: 'limitClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 16, last_column: 16 } },
+        { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 17, last_column: 32 } },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 24, last_column: 25 } },
+        { type: 'column', location: { first_line: 1, last_line: 1, first_column: 24, last_column: 25 }, identifierChain: [{ name: 'y' }], tables: [{ identifierChain: [{ name: 'y' }] }], qualified: false },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 31, last_column: 32 }, identifierChain: [{ name: 'y' }] },
+        { type: 'whereClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 32, last_column: 32 } },
+        { type: 'limitClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 32, last_column: 32 } }
+      ]
+    });
+  });
+
+  it('should report locations for "-- comment\nselect x from x;\n\n\nselect y from y;"', () => {
+    assertLocations({
+      beforeCursor: '-- comment\nselect x from x;\n\n\nselect y from y;',
+      expectedLocations: [
+        { type: 'statement', location: { first_line: 1, last_line: 2, first_column: 1, last_column: 16 } },
+        { type: 'selectList', missing: false, location: { first_line: 2, last_line: 2, first_column: 8, last_column: 9 } },
+        { type: 'column', location: { first_line: 2, last_line: 2, first_column: 8, last_column: 9 }, identifierChain: [{ name: 'x' }], tables: [{ identifierChain: [{ name: 'x' }] }], qualified: false },
+        { type: 'table', location: { first_line: 2, last_line: 2, first_column: 15, last_column: 16 }, identifierChain: [{ name: 'x' }] },
+        { type: 'whereClause', missing: true, location: { first_line: 2, last_line: 2, first_column: 16, last_column: 16 } },
+        { type: 'limitClause', missing: true, location: { first_line: 2, last_line: 2, first_column: 16, last_column: 16 } },
+        { type: 'statement', location: { first_line: 2, last_line: 5, first_column: 17, last_column: 16 } },
+        { type: 'selectList', missing: false, location: { first_line: 5, last_line: 5, first_column: 8, last_column: 9 } },
+        { type: 'column', location: { first_line: 5, last_line: 5, first_column: 8, last_column: 9 }, identifierChain: [{ name: 'y' }], tables: [{ identifierChain: [{ name: 'y' }] }], qualified: false },
+        { type: 'table', location: { first_line: 5, last_line: 5, first_column: 15, last_column: 16 }, identifierChain: [{ name: 'y' }] },
+        { type: 'whereClause', missing: true, location: { first_line: 5, last_line: 5, first_column: 16, last_column: 16 } },
+        { type: 'limitClause', missing: true, location: { first_line: 5, last_line: 5, first_column: 16, last_column: 16 } }
+      ]
+    });
+  });
+
+  it('should report locations for "select x from x, y;"', () => {
+    assertLocations({
+      beforeCursor: 'select x from x, y;',
+      expectedLocations: [
+        { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 19 } },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 } },
+        { type: 'column', location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 }, identifierChain: [{ name: 'x' }], tables: [{ identifierChain: [{ name: 'x' }] }, { identifierChain: [{ name: 'y' }] }], qualified: false },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 15, last_column: 16 }, identifierChain: [{ name: 'x' }] },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 18, last_column: 19 }, identifierChain: [{ name: 'y' }] },
+        { type: 'whereClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 19, last_column: 19 } },
+        { type: 'limitClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 19, last_column: 19 } }
+      ]
+    });
+  });
+
+  it('should report locations for "SELECT t3.id, id FROM testTable1, db.testTable2, testTable3 t3;|"', () => {
+    assertLocations({
+      beforeCursor: 'SELECT t3.id, id FROM testTable1, db.testTable2, testTable3 t3;',
+      expectedLocations: [
+        { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 63 } },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 8, last_column: 17 } },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 }, identifierChain: [{ name: 'testTable3' }] },
+        { type: 'column', location: { first_line: 1, last_line: 1, first_column: 11, last_column: 13 }, identifierChain: [{ name: 'id' }], tables: [{ identifierChain: [{ name: 'testTable3' }], alias: 't3' }], qualified: true },
+        { type: 'column', location: { first_line: 1, last_line: 1, first_column: 15, last_column: 17 }, identifierChain: [{ name: 'id' }], tables: [
+            { identifierChain: [{ name: 'testTable1' }] },
+            { identifierChain: [{ name: 'db' }, { name: 'testTable2' }] },
+            { identifierChain: [{ name: 'testTable3' }], alias: 't3' }
+          ], qualified: false },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 23, last_column: 33 }, identifierChain: [{ name: 'testTable1' }] },
+        { type: 'database', location: { first_line: 1, last_line: 1, first_column: 35, last_column: 37 }, identifierChain: [{ name: 'db' }] },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 38, last_column: 48 }, identifierChain: [{ name: 'db' }, { name: 'testTable2' }] },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 50, last_column: 60 }, identifierChain: [{ name: 'testTable3' }] },
+        { type: 'alias', source: 'table', alias: 't3', location: { first_line: 1, last_line: 1, first_column: 61, last_column: 63 }, identifierChain: [{ name: 'testTable3' }] },
+        { type: 'whereClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 63, last_column: 63 } },
+        { type: 'limitClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 63, last_column: 63 } }
+      ]
+    });
+  });
+
+  it('should report locations for "SELECT * FROM foo WHERE bar IN (1+1, 2+2);|"', () => {
+    assertLocations({
+      beforeCursor: 'SELECT * FROM foo WHERE bar IN (1+1, 2+2);',
+      expectedLocations: [
+        { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 42 } },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 } },
+        { type: 'asterisk', location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 }, tables: [{ identifierChain: [{ name: 'foo' }] }] },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 15, last_column: 18 }, identifierChain: [{ name: 'foo' }] },
+        { type: 'whereClause', missing: false, location: { first_line: 1, last_line: 1, first_column: 19, last_column: 42 } },
+        { type: 'column', location: { first_line: 1, last_line: 1, first_column: 25, last_column: 28 }, identifierChain: [{ name: 'bar' }], tables: [{ identifierChain: [{ name: 'foo' }] }], qualified: false },
+        { type: 'limitClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 42, last_column: 42 } }
+      ]
+    });
+  });
+
+  it('should report locations for "SELECT * FROM foo WHERE bar IN (id+1-1, id+1-2);|"', () => {
+    assertLocations({
+      beforeCursor: 'SELECT * FROM foo WHERE bar IN (id+1-1, id+1-2);',
+      expectedLocations: [
+        { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 48 } },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 } },
+        { type: 'asterisk', location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 }, tables: [{ identifierChain: [{ name: 'foo' }] }] },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 15, last_column: 18 }, identifierChain: [{ name: 'foo' }] },
+        { type: 'whereClause', missing: false, location: { first_line: 1, last_line: 1, first_column: 19, last_column: 48 } },
+        { type: 'column', location: { first_line: 1, last_line: 1, first_column: 25, last_column: 28 }, identifierChain: [{ name: 'bar' }], tables: [{ identifierChain: [{ name: 'foo' }] }], qualified: false },
+        { type: 'column', location: { first_line: 1, last_line: 1, first_column: 33, last_column: 35 }, identifierChain: [{ name: 'id' }], tables: [{ identifierChain: [{ name: 'foo' }] }], qualified: false },
+        { type: 'column', location: { first_line: 1, last_line: 1, first_column: 41, last_column: 43 }, identifierChain: [{ name: 'id' }], tables: [{ identifierChain: [{ name: 'foo' }] }], qualified: false },
+        { type: 'limitClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 48, last_column: 48 } }
+      ]
+    });
+  });
+
+  it(
+    'should report locations for "SELECT s07.description, s07.salary, s08.salary,\\r\\n' +
+      '  s08.salary - s07.salary\\r\\n' +
+      'FROM\\r\\n' +
+      '  sample_07 s07 JOIN sample_08 s08\\r\\n' +
+      'ON ( s07.code = s08.code)\\r\\n' +
+      'WHERE\\r\\n' +
+      '  s07.salary < s08.salary\\r\\n' +
+      'ORDER BY s08.salary-s07.salary DESC\\r\\n' +
+      'LIMIT 1000;|"',
+    () => {
+      assertLocations({
+        beforeCursor:
+          'SELECT s07.description, s07.salary, s08.salary,\r\n' +
+          '  s08.salary - s07.salary\r\n' +
+          'FROM\r\n' +
+          '  sample_07 s07 JOIN sample_08 s08\r\n' +
+          'ON ( s07.code = s08.code)\r\n' +
+          'WHERE\r\n' +
+          '  s07.salary < s08.salary\r\n' +
+          'ORDER BY s08.salary-s07.salary DESC\r\n' +
+          'LIMIT 1000;',
+        expectedLocations: [
+          { type: 'statement', location: { first_line: 1, last_line: 9, first_column: 1, last_column: 11 } },
+          { type: 'selectList', missing: false, location: { first_line: 1, last_line: 2, first_column: 8, last_column: 26 } },
+          { type: 'table', location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 }, identifierChain: [{ name: 'sample_07' }] },
+          { type: 'column', location: { first_line: 1, last_line: 1, first_column: 12, last_column: 23 }, identifierChain: [{ name: 'description' }], tables: [{ identifierChain: [{ name: 'sample_07' }], alias: 's07' }], qualified: true },
+          { type: 'table', location: { first_line: 1, last_line: 1, first_column: 25, last_column: 28 }, identifierChain: [{ name: 'sample_07' }] },
+          { type: 'column', location: { first_line: 1, last_line: 1, first_column: 29, last_column: 35 }, identifierChain: [{ name: 'salary' }], tables: [{ identifierChain: [{ name: 'sample_07' }], alias: 's07' }], qualified: true },
+          { type: 'table', location: { first_line: 1, last_line: 1, first_column: 37, last_column: 40 }, identifierChain: [{ name: 'sample_08' }] },
+          { type: 'column', location: { first_line: 1, last_line: 1, first_column: 41, last_column: 47 }, identifierChain: [{ name: 'salary' }], tables: [{ identifierChain: [{ name: 'sample_08' }], alias: 's08' }], qualified: true },
+          { type: 'table', location: { first_line: 2, last_line: 2, first_column: 3, last_column: 6 }, identifierChain: [{ name: 'sample_08' }] },
+          { type: 'column', location: { first_line: 2, last_line: 2, first_column: 7, last_column: 13 }, identifierChain: [{ name: 'salary' }], tables: [{ identifierChain: [{ name: 'sample_08' }], alias: 's08' }], qualified: true },
+          { type: 'table', location: { first_line: 2, last_line: 2, first_column: 16, last_column: 19 }, identifierChain: [{ name: 'sample_07' }] },
+          { type: 'column', location: { first_line: 2, last_line: 2, first_column: 20, last_column: 26 }, identifierChain: [{ name: 'salary' }], tables: [{ identifierChain: [{ name: 'sample_07' }], alias: 's07' }], qualified: true },
+          { type: 'table', location: { first_line: 4, last_line: 4, first_column: 3, last_column: 12 }, identifierChain: [{ name: 'sample_07' }] },
+          { type: 'alias', source: 'table', alias: 's07', location: { first_line: 4, last_line: 4, first_column: 13, last_column: 16 }, identifierChain: [{ name: 'sample_07' }] },
+          { type: 'table', location: { first_line: 4, last_line: 4, first_column: 22, last_column: 31 }, identifierChain: [{ name: 'sample_08' }] },
+          { type: 'alias', source: 'table', alias: 's08', location: { first_line: 4, last_line: 4, first_column: 32, last_column: 35 }, identifierChain: [{ name: 'sample_08' }] },
+          { type: 'table', location: { first_line: 5, last_line: 5, first_column: 6, last_column: 9 }, identifierChain: [{ name: 'sample_07' }] },
+          { type: 'column', location: { first_line: 5, last_line: 5, first_column: 10, last_column: 14 }, identifierChain: [{ name: 'code' }], tables: [{ identifierChain: [{ name: 'sample_07' }], alias: 's07' }], qualified: true },
+          { type: 'table', location: { first_line: 5, last_line: 5, first_column: 17, last_column: 20 }, identifierChain: [{ name: 'sample_08' }] },
+          { type: 'column', location: { first_line: 5, last_line: 5, first_column: 21, last_column: 25 }, identifierChain: [{ name: 'code' }], tables: [{ identifierChain: [{ name: 'sample_08' }], alias: 's08' }], qualified: true },
+          { type: 'whereClause', missing: false, location: { first_line: 6, last_line: 7, first_column: 1, last_column: 26 } },
+          { type: 'table', location: { first_line: 7, last_line: 7, first_column: 3, last_column: 6 }, identifierChain: [{ name: 'sample_07' }] },
+          { type: 'column', location: { first_line: 7, last_line: 7, first_column: 7, last_column: 13 }, identifierChain: [{ name: 'salary' }], tables: [{ identifierChain: [{ name: 'sample_07' }], alias: 's07' }], qualified: true },
+          { type: 'table', location: { first_line: 7, last_line: 7, first_column: 16, last_column: 19 }, identifierChain: [{ name: 'sample_08' }] },
+          { type: 'column', location: { first_line: 7, last_line: 7, first_column: 20, last_column: 26 }, identifierChain: [{ name: 'salary' }], tables: [{ identifierChain: [{ name: 'sample_08' }], alias: 's08' }], qualified: true },
+          { type: 'table', location: { first_line: 8, last_line: 8, first_column: 10, last_column: 13 }, identifierChain: [{ name: 'sample_08' }] },
+          { type: 'column', location: { first_line: 8, last_line: 8, first_column: 14, last_column: 20 }, identifierChain: [{ name: 'salary' }], tables: [{ identifierChain: [{ name: 'sample_08' }], alias: 's08' }], qualified: true },
+          { type: 'table', location: { first_line: 8, last_line: 8, first_column: 21, last_column: 24 }, identifierChain: [{ name: 'sample_07' }] },
+          { type: 'column', location: { first_line: 8, last_line: 8, first_column: 25, last_column: 31 }, identifierChain: [{ name: 'salary' }], tables: [{ identifierChain: [{ name: 'sample_07' }], alias: 's07' }], qualified: true },
+          { type: 'limitClause', missing: false, location: { first_line: 9, last_line: 9, first_column: 1, last_column: 11 } }
+        ]
+      });
+    }
+  );
+
+  it('should handle "select bl from blablabla join (select * from blablabla) s1;', () => {
+    assertLocations({
+      beforeCursor: 'select bl from blablabla join (select * from blablabla) s1;',
+      afterCursor: '',
+      expectedLocations: [
+        { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 59 } },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 } },
+        { type: 'column', location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 }, identifierChain: [{ name: 'bl' }], tables: [{ identifierChain: [{ name: 'blablabla' }] }, { subQuery: 's1' }], qualified: false },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 16, last_column: 25 }, identifierChain: [{ name: 'blablabla' }] },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 39, last_column: 40 }, subquery: true },
+        { type: 'asterisk', location: { first_line: 1, last_line: 1, first_column: 39, last_column: 40 }, tables: [{ identifierChain: [{ name: 'blablabla' }] }] },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 46, last_column: 55 }, identifierChain: [{ name: 'blablabla' }] },
+        { type: 'whereClause', subquery: true, missing: true, location: { first_line: 1, last_line: 1, first_column: 55, last_column: 55 } },
+        { type: 'limitClause', subquery: true, missing: true, location: { first_line: 1, last_line: 1, first_column: 55, last_column: 55 } },
+        { type: 'alias', source: 'subquery', alias: 's1', location: { first_line: 1, last_line: 1, first_column: 57, last_column: 59 } },
+        { type: 'whereClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 59, last_column: 59 } },
+        { type: 'limitClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 59, last_column: 59 } }
+      ]
+    });
+  });
+
+  it(
+    'should report locations for "SELECT CASE cos(boo.a) > baa.boo \\n' +
+      '\\tWHEN baa.b THEN true \\n' +
+      '\\tWHEN boo.c THEN false \\n' +
+      '\\tWHEN baa.blue THEN boo.d \\n' +
+      '\\tELSE baa.e END \\n' +
+      '\\t FROM db1.foo boo, bar baa WHERE baa.bla IN (SELECT ble FROM bla);|"',
+    () => {
+      assertLocations({
+        beforeCursor:
+          'SELECT CASE cos(boo.a) > baa.boo \n\tWHEN baa.b THEN true \n\tWHEN boo.c THEN false \n\tWHEN baa.blue THEN boo.d \n\tELSE baa.e END \n\t FROM db1.foo boo, bar baa WHERE baa.bla IN (SELECT ble FROM bla);',
+        expectedLocations: [
+          { type: 'statement', location: { first_line: 1, last_line: 6, first_column: 1, last_column: 67 } },
+          { type: 'selectList', missing: false, location: { first_line: 1, last_line: 5, first_column: 8, last_column: 16 } },
+          { type: 'function', location: { first_line: 1, last_line: 1, first_column: 13, last_column: 15 }, function: 'cos' },
+          { type: 'table', location: { first_line: 1, last_line: 1, first_column: 17, last_column: 20 }, identifierChain: [{ name: 'db1' }, { name: 'foo' }] },
+          { type: 'column', location: { first_line: 1, last_line: 1, first_column: 21, last_column: 22 }, identifierChain: [{ name: 'a' }], tables: [{ identifierChain: [{ name: 'db1' }, { name: 'foo' }], alias: 'boo' }], qualified: true },
+          { type: 'table', location: { first_line: 1, last_line: 1, first_column: 26, last_column: 29 }, identifierChain: [{ name: 'bar' }] },
+          { type: 'column', location: { first_line: 1, last_line: 1, first_column: 30, last_column: 33 }, identifierChain: [{ name: 'boo' }], tables: [{ identifierChain: [{ name: 'bar' }], alias: 'baa' }], qualified: true },
+          { type: 'table', location: { first_line: 2, last_line: 2, first_column: 7, last_column: 10 }, identifierChain: [{ name: 'bar' }] },
+          { type: 'column', location: { first_line: 2, last_line: 2, first_column: 11, last_column: 12 }, identifierChain: [{ name: 'b' }], tables: [{ identifierChain: [{ name: 'bar' }], alias: 'baa' }], qualified: true },
+          { type: 'table', location: { first_line: 3, last_line: 3, first_column: 7, last_column: 10 }, identifierChain: [{ name: 'db1' }, { name: 'foo' }] },
+          { type: 'column', location: { first_line: 3, last_line: 3, first_column: 11, last_column: 12 }, identifierChain: [{ name: 'c' }], tables: [{ identifierChain: [{ name: 'db1' }, { name: 'foo' }], alias: 'boo' }], qualified: true },
+          { type: 'table', location: { first_line: 4, last_line: 4, first_column: 7, last_column: 10 }, identifierChain: [{ name: 'bar' }] },
+          { type: 'column', location: { first_line: 4, last_line: 4, first_column: 11, last_column: 15 }, identifierChain: [{ name: 'blue' }], tables: [{ identifierChain: [{ name: 'bar' }], alias: 'baa' }], qualified: true },
+          { type: 'table', location: { first_line: 4, last_line: 4, first_column: 21, last_column: 24 }, identifierChain: [{ name: 'db1' }, { name: 'foo' }] },
+          { type: 'column', location: { first_line: 4, last_line: 4, first_column: 25, last_column: 26 }, identifierChain: [{ name: 'd' }], tables: [{ identifierChain: [{ name: 'db1' }, { name: 'foo' }], alias: 'boo' }], qualified: true },
+          { type: 'table', location: { first_line: 5, last_line: 5, first_column: 7, last_column: 10 }, identifierChain: [{ name: 'bar' }] },
+          { type: 'column', location: { first_line: 5, last_line: 5, first_column: 11, last_column: 12 }, identifierChain: [{ name: 'e' }], tables: [{ identifierChain: [{ name: 'bar' }], alias: 'baa' }], qualified: true },
+          { type: 'database', location: { first_line: 6, last_line: 6, first_column: 8, last_column: 11 }, identifierChain: [{ name: 'db1' }] },
+          { type: 'table', location: { first_line: 6, last_line: 6, first_column: 12, last_column: 15 }, identifierChain: [{ name: 'db1' }, { name: 'foo' }] },
+          { type: 'alias', source: 'table', alias: 'boo', location: { first_line: 6, last_line: 6, first_column: 16, last_column: 19 }, identifierChain: [{ name: 'db1' }, { name: 'foo' }] },
+          { type: 'table', location: { first_line: 6, last_line: 6, first_column: 21, last_column: 24 }, identifierChain: [{ name: 'bar' }] },
+          { type: 'alias', source: 'table', alias: 'baa', location: { first_line: 6, last_line: 6, first_column: 25, last_column: 28 }, identifierChain: [{ name: 'bar' }] },
+          { type: 'whereClause', missing: false, location: { first_line: 6, last_line: 6, first_column: 29, last_column: 67 } },
+          { type: 'table', location: { first_line: 6, last_line: 6, first_column: 35, last_column: 38 }, identifierChain: [{ name: 'bar' }] },
+          { type: 'column', location: { first_line: 6, last_line: 6, first_column: 39, last_column: 42 }, identifierChain: [{ name: 'bla' }], tables: [{ identifierChain: [{ name: 'bar' }], alias: 'baa' }], qualified: true },
+          { type: 'selectList', missing: false, location: { first_line: 6, last_line: 6, first_column: 54, last_column: 57 }, subquery: true },
+          { type: 'column', location: { first_line: 6, last_line: 6, first_column: 54, last_column: 57 }, identifierChain: [{ name: 'ble' }], tables: [{ identifierChain: [{ name: 'bla' }] }], qualified: false },
+          { type: 'table', location: { first_line: 6, last_line: 6, first_column: 63, last_column: 66 }, identifierChain: [{ name: 'bla' }] },
+          { type: 'whereClause', subquery: true, missing: true, location: { first_line: 6, last_line: 6, first_column: 66, last_column: 66 } },
+          { type: 'limitClause', subquery: true, missing: true, location: { first_line: 6, last_line: 6, first_column: 66, last_column: 66 } },
+          { type: 'limitClause', missing: true, location: { first_line: 6, last_line: 6, first_column: 67, last_column: 67 } }
+        ]
+      });
+    }
+  );
+
+  it('should report locations for "SELECT tta.* FROM testTableA tta, testTableB; |"', () => {
+    assertLocations({
+      beforeCursor: 'SELECT tta.* FROM testTableA tta, testTableB; ',
+      expectedLocations: [
+        { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 45 } },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 8, last_column: 13 } },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 }, identifierChain: [{ name: 'testTableA' }] },
+        { type: 'asterisk', location: { first_line: 1, last_line: 1, first_column: 12, last_column: 13 }, tables: [{ alias: 'tta', identifierChain: [{ name: 'testTableA' }] }] },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 19, last_column: 29 }, identifierChain: [{ name: 'testTableA' }] },
+        { type: 'alias', source: 'table', alias: 'tta', location: { first_line: 1, last_line: 1, first_column: 30, last_column: 33 }, identifierChain: [{ name: 'testTableA' }] },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 35, last_column: 45 }, identifierChain: [{ name: 'testTableB' }] },
+        { type: 'whereClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 45, last_column: 45 } },
+        { type: 'limitClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 45, last_column: 45 } }
+      ]
+    });
+  });
+
+  it('should report locations for "SELECT COUNT(*) FROM testTable; |"', () => {
+    assertLocations({
+      beforeCursor: 'SELECT COUNT(*) FROM testTable;',
+      expectedLocations: [
+        { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 31 } },
+        { type: 'selectList', missing: false, location: { first_line: 1, last_line: 1, first_column: 8, last_column: 16 } },
+        { type: 'function', location: { first_line: 1, last_line: 1, first_column: 8, last_column: 12 }, function: 'count' },
+        { type: 'table', location: { first_line: 1, last_line: 1, first_column: 22, last_column: 31 }, identifierChain: [{ name: 'testTable' }] },
+        { type: 'whereClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 31, last_column: 31 } },
+        { type: 'limitClause', missing: true, location: { first_line: 1, last_line: 1, first_column: 31, last_column: 31 } }
+      ]
+    });
+  });
+});
+// prettier-ignore-end

+ 6174 - 0
desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Select_Spec.js

@@ -0,0 +1,6174 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import druidAutocompleteParser from '../druidAutocompleteParser';
+
+describe('druidAutocompleteParser.js SELECT statements', () => {
+  beforeAll(() => {
+    druidAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      druidAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  it('should suggest keywords for "|"', () => {
+    assertAutoComplete({
+      beforeCursor: '',
+      afterCursor: '',
+      containsKeywords: ['SELECT'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest tables and databases for "SELECT * |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * ',
+      afterCursor: '',
+      containsKeywords: ['FROM'],
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {
+          prependFrom: true
+        },
+        suggestDatabases: {
+          prependFrom: true,
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest tables and databases for "SELECT *\\r\\n |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT *\r\n',
+      afterCursor: '',
+      containsKeywords: ['FROM'],
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {
+          prependFrom: true
+        },
+        suggestDatabases: {
+          prependFrom: true,
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should not suggest anything for "SELECT u.|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT u.',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest keywords for "SELECT foo, bar |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT foo, bar ',
+      afterCursor: '',
+      containsKeywords: ['AS', '+', 'FROM', 'DIV'],
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {
+          prependFrom: true
+        },
+        suggestDatabases: {
+          prependFrom: true,
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest keywords for "SELECT foo AS a, bar |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT foo AS a, bar ',
+      afterCursor: '',
+      containsKeywords: ['AS', '+'],
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {
+          prependFrom: true
+        },
+        suggestDatabases: {
+          prependFrom: true,
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest keywords for "SELECT * FROM testTableA tta, testTableB |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM testTableA tta, testTableB ',
+      afterCursor: '',
+      expectedResult: {
+        suggestJoins: {
+          prependJoin: true,
+          tables: [{ identifierChain: [{ name: 'testTableB' }] }]
+        },
+        suggestFilters: {
+          prefix: 'WHERE',
+          tables: [
+            { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+            { identifierChain: [{ name: 'testTableB' }] }
+          ]
+        },
+        suggestGroupBys: {
+          prefix: 'GROUP BY',
+          tables: [
+            { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+            { identifierChain: [{ name: 'testTableB' }] }
+          ]
+        },
+        suggestOrderBys: {
+          prefix: 'ORDER BY',
+          tables: [
+            { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+            { identifierChain: [{ name: 'testTableB' }] }
+          ]
+        },
+        suggestKeywords: [
+          'AS',
+          'WHERE',
+          'GROUP BY',
+          'HAVING',
+          'ORDER BY',
+          'LIMIT',
+          'UNION',
+          'FULL JOIN',
+          'FULL OUTER JOIN',
+          'INNER JOIN',
+          'JOIN',
+          'LEFT JOIN',
+          'LEFT OUTER JOIN',
+          'RIGHT JOIN',
+          'RIGHT OUTER JOIN'
+        ],
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest databases or tables for "SELECT * fr|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * fr',
+      afterCursor: '',
+      containsKeywords: ['FROM'],
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {
+          prependFrom: true
+        },
+        suggestDatabases: {
+          prependFrom: true,
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest databases or tables for "SELECT * FROM |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {},
+        suggestDatabases: {
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest databases or tables for "SELECT * FROM tes|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM tes',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {},
+        suggestDatabases: {
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest databases or tables for "SELECT * FROM `tes|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM `tes',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {},
+        suggestDatabases: {
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest tables for "SELECT * FROM database_two.|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM database_two.',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: { identifierChain: [{ name: 'database_two' }] }
+      }
+    });
+  });
+
+  it('should suggest tables for "SELECT * FROM `database_two`.|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM `database_two`.',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: { identifierChain: [{ name: 'database_two' }] }
+      }
+    });
+  });
+
+  it('should suggest tables for "SELECT * FROM 33abc.|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM 33abc.',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: { identifierChain: [{ name: '33abc' }] }
+      }
+    });
+  });
+
+  it('should suggest tables for "SELECT * FROM `database_two`.`bla |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'SELECT * FROM `database_two`.`bla ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: { identifierChain: [{ name: 'database_two' }] }
+      }
+    });
+  });
+
+  describe('Complete Statements', () => {
+    it('should handle "SELECT 4 / 2; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT 4 / 2; ',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "SELECT 4 DIV 2; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT 4 DIV 2; ',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it("should handle \"SELECT bla NOT RLIKE 'ble', ble NOT REGEXP 'b' FROM tbl; |\"", () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT bla NOT RLIKE 'ble', ble NOT REGEXP 'b' FROM tbl; ",
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "SELECT * FROM tbl limit ${limit=20}; |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM tbl limit ${limit=20}; ',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns "SELECT IF(baa, boo, bee) AS b, | FROM testTable;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT IF(baa, boo, bee) AS b, ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['*', 'CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAnalyticFunctions: true,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns "SELECT IF(baa > 2, boo, bee) AS b, | FROM testTable;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT IF(baa > 2, boo, bee) AS b, ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['*', 'CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAnalyticFunctions: true,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+  });
+
+  describe('Select List Completion', () => {
+    it('should handle "select count(*), tst.count, avg (id), avg from autocomp_test tst;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select count(*), tst.count, avg (id), avg from autocomp_test tst;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 65 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 42 }
+            },
+            {
+              type: 'function',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 12 },
+              function: 'count'
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 18, last_column: 21 },
+              identifierChain: [{ name: 'autocomp_test' }]
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 22, last_column: 27 },
+              identifierChain: [{ name: 'count' }],
+              tables: [{ identifierChain: [{ name: 'autocomp_test' }], alias: 'tst' }],
+              qualified: true
+            },
+            {
+              type: 'function',
+              location: { first_line: 1, last_line: 1, first_column: 29, last_column: 32 },
+              function: 'avg'
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 34, last_column: 36 },
+              identifierChain: [{ name: 'id' }],
+              tables: [{ identifierChain: [{ name: 'autocomp_test' }], alias: 'tst' }],
+              qualified: false
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 39, last_column: 42 },
+              identifierChain: [{ name: 'avg' }],
+              tables: [{ identifierChain: [{ name: 'autocomp_test' }], alias: 'tst' }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 48, last_column: 61 },
+              identifierChain: [{ name: 'autocomp_test' }]
+            },
+            {
+              type: 'alias',
+              source: 'table',
+              alias: 'tst',
+              location: { first_line: 1, last_line: 1, first_column: 62, last_column: 65 },
+              identifierChain: [{ name: 'autocomp_test' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 65, last_column: 65 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 65, last_column: 65 }
+            }
+          ],
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: '',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT |;\n\nSELECT * FROM foo;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ';\n\nSELECT * FROM foo;',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM foo;\n\nSELECT |;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo;\n\nSELECT ',
+        afterCursor: ';',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT |;\n\nSELECT * FROM foo boo;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ';\n\nSELECT * FROM foo boo;',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM foo boo;\n\nSELECT |;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo boo;\n\nSELECT ',
+        afterCursor: ';',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest lowerCase for "select |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select ',
+        afterCursor: '',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: true,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT ALL |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ALL ',
+        afterCursor: '',
+        containsKeywords: ['*', 'CASE'],
+        doesNotContainKeywords: ['ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT DISTINCT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT DISTINCT ',
+        afterCursor: '',
+        containsKeywords: ['*'],
+        doesNotContainKeywords: ['ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT DISTINCT | a, b, c FROM tbl"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT DISTINCT ',
+        afterCursor: ' a, b, c FROM tbl',
+        containsKeywords: ['*'],
+        doesNotContainKeywords: ['ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'tbl' }] }] },
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 34 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 15, last_column: 25 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 18, last_column: 19 },
+              identifierChain: [{ name: 'a' }],
+              tables: [{ identifierChain: [{ name: 'tbl' }] }],
+              qualified: false
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 21, last_column: 22 },
+              identifierChain: [{ name: 'b' }],
+              tables: [{ identifierChain: [{ name: 'tbl' }] }],
+              qualified: false
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 24, last_column: 25 },
+              identifierChain: [{ name: 'c' }],
+              tables: [{ identifierChain: [{ name: 'tbl' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 31, last_column: 34 },
+              identifierChain: [{ name: 'tbl' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 34, last_column: 34 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 34, last_column: 34 }
+            }
+          ]
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT | FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM tableA;',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'tableA' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'tableA' }] }] }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT | AS boo FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' AS boo FROM tableA;',
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'tableA' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'tableA' }] }] }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT | boo FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' boo FROM tableA;',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'tableA' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'tableA' }] }] }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT bla| AS boo FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT bla',
+        afterCursor: ' AS boo FROM tableA;',
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'tableA' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'tableA' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | FROM testWHERE"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM testWHERE',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testWHERE' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testWHERE' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT (bl|a AND boo FROM testWHERE"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT (bl',
+        afterCursor: ' AND boo FROM testWHERE',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testWHERE' }] }]
+          }
+        }
+      });
+    });
+
+    // TODO: Parser can't handle multiple errors in a row, in this case 2 missing ')')
+    xit('should suggest columns for "SELECT ((| FROM testWHERE"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ((',
+        afterCursor: ' FROM testWHERE',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testWHERE' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT (bla| AND boo FROM testWHERE"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT (bla',
+        afterCursor: ' AND boo FROM testWHERE',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testWHERE' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | FROM testON"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM testON',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testON' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'testON' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | FROM transactions"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM transactions',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'transactions' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'transactions' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest aliases for "SELECT | FROM testTableA tta, testTableB"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM testTableA tta, testTableB',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: {
+            tables: [
+              { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+              { identifierChain: [{ name: 'testTableB' }] }
+            ]
+          },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [
+              { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+              { identifierChain: [{ name: 'testTableB' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'tta.', type: 'alias' },
+            { name: 'testTableB.', type: 'table' }
+          ]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT TTA.| FROM testTableA tta"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT TTA.',
+        afterCursor: ' FROM testTableA tta',
+        containsKeywords: ['*'],
+        expectedResult: {
+          lowerCase: false,
+          // TODO: add alias on table in suggestColumns (needs support in sqlAutocomplete3.js)
+          // Case is: select cu.| from customers
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTableA' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT tta.| FROM testTableA TTA"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT tta.',
+        afterCursor: ' FROM testTableA TTA',
+        containsKeywords: ['*'],
+        expectedResult: {
+          lowerCase: false,
+          // TODO: add alias on table in suggestColumns (needs support in sqlAutocomplete3.js)
+          // Case is: select cu.| from customers
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTableA' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | FROM db.tbl1, db.tbl2"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM db.tbl1, db.tbl2',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: {
+            tables: [
+              { identifierChain: [{ name: 'db' }, { name: 'tbl1' }] },
+              { identifierChain: [{ name: 'db' }, { name: 'tbl2' }] }
+            ]
+          },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [
+              { identifierChain: [{ name: 'db' }, { name: 'tbl1' }] },
+              { identifierChain: [{ name: 'db' }, { name: 'tbl2' }] }
+            ]
+          },
+          suggestIdentifiers: [{ name: 'tbl1.', type: 'table' }, { name: 'tbl2.', type: 'table' }]
+        }
+      });
+    });
+
+    it('should suggest columns for "select | from database_two.testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select ',
+        afterCursor: ' from database_two.testTable',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: true,
+          suggestAggregateFunctions: {
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "select | from `database one`.`test table`"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select ',
+        afterCursor: ' from `database one`.`test table`',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: true,
+          suggestAggregateFunctions: {
+            tables: [{ identifierChain: [{ name: 'database one' }, { name: 'test table' }] }]
+          },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'database one' }, { name: 'test table' }] }]
+          },
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 41 }
+            },
+            {
+              type: 'selectList',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 7, last_column: 7 }
+            },
+            {
+              type: 'database',
+              location: { first_line: 1, last_line: 1, first_column: 14, last_column: 28 },
+              identifierChain: [{ name: 'database one' }]
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 29, last_column: 41 },
+              identifierChain: [{ name: 'database one' }, { name: 'test table' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 41, last_column: 41 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 41, last_column: 41 }
+            }
+          ]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, | FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, ',
+        afterCursor: ' FROM tableA;',
+        containsKeywords: ['*', 'CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'tableA' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'tableA' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a,| FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a,',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['*', 'CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT *, | FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT *, ',
+        afterCursor: ' FROM tableA;',
+        containsKeywords: ['*', 'CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'tableA' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'tableA' }] }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT a | FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a ',
+        afterCursor: ' FROM tableA;',
+        containsKeywords: ['AS', '='],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'tableA' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT a |, FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a ',
+        afterCursor: ', FROM tableA;',
+        containsKeywords: ['AS', '='],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'tableA' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT a, b | FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b ',
+        afterCursor: ' FROM tableA;',
+        containsKeywords: ['AS', '='],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'tableA' }, { name: 'b' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT a |, b, c AS foo, d FROM tableA;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a ',
+        afterCursor: ', b, c AS foo, d FROM tableA;',
+        containsKeywords: ['AS', '='],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'tableA' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | a, cast(b as int), c, d FROM testTable WHERE a = \'US\' AND b >= 998 ORDER BY c DESC LIMIT 15"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor:
+          " a, cast(b as int), c, d FROM testTable WHERE a = 'US' AND b >= 998 ORDER BY c DESC LIMIT 15",
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, |,c, d FROM testTable WHERE a = \'US\' AND b >= 998 ORDER BY c DESC LIMIT 15"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, ',
+        afterCursor: ",c, d FROM testTable WHERE a = 'US' AND b >= 998 ORDER BY c DESC LIMIT 15",
+        containsKeywords: ['*', 'CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 87 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 22 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 9 },
+              identifierChain: [{ name: 'a' }],
+              tables: [{ identifierChain: [{ name: 'testTable' }] }],
+              qualified: false
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 11, last_column: 12 },
+              identifierChain: [{ name: 'b' }],
+              tables: [{ identifierChain: [{ name: 'testTable' }] }],
+              qualified: false
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 15, last_column: 16 },
+              identifierChain: [{ name: 'c' }],
+              tables: [{ identifierChain: [{ name: 'testTable' }] }],
+              qualified: false
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 18, last_column: 19 },
+              identifierChain: [{ name: 'd' }],
+              tables: [{ identifierChain: [{ name: 'testTable' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 25, last_column: 34 },
+              identifierChain: [{ name: 'testTable' }]
+            },
+            {
+              type: 'whereClause',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 35, last_column: 62 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 41, last_column: 42 },
+              identifierChain: [{ name: 'a' }],
+              tables: [{ identifierChain: [{ name: 'testTable' }] }],
+              qualified: false
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 54, last_column: 55 },
+              identifierChain: [{ name: 'b' }],
+              tables: [{ identifierChain: [{ name: 'testTable' }] }],
+              qualified: false
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 72, last_column: 73 },
+              identifierChain: [{ name: 'c' }],
+              tables: [{ identifierChain: [{ name: 'testTable' }] }],
+              qualified: false
+            },
+            {
+              type: 'limitClause',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 79, last_column: 87 }
+            }
+          ]
+        }
+      });
+    });
+  });
+
+  describe('Variable References', () => {
+    it('should suggest tables for "SELECT | FROM ${some_variable};"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM ${some_variable};',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: {
+            tables: [{ identifierChain: [{ name: '${some_variable}' }] }]
+          },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: '${some_variable}' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable WHERE ${some_variable} |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE ${some_variable} ',
+        afterCursor: '',
+        containsKeywords: ['<', 'BETWEEN'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestGroupBys: {
+            prefix: 'GROUP BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestOrderBys: {
+            prefix: 'ORDER BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: '${some_variable}' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE ${some_variable} + 1 = |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE ${some_variable} + 1 = ',
+        afterCursor: '',
+        containsKeywords: ['CASE', 'NULL'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+  });
+
+  describe('Window and analytic functions', () => {
+    it('should handle "SELECT row_number() OVER (PARTITION BY a) FROM testTable;|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY a) FROM testTable;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "SELECT COUNT(DISTINCT a) OVER (PARTITION by c) FROM testTable;|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT COUNT(DISTINCT a) OVER (PARTITION by c) FROM testTable;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest analytical functions for "SELECT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: '',
+        containsKeywords: ['*'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: { prependQuestionMark: true, prependFrom: true },
+          suggestDatabases: { prependQuestionMark: true, prependFrom: true, appendDot: true }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() ',
+        afterCursor: '',
+        containsKeywords: ['OVER'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['OVER'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() |, b, c FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() ',
+        afterCursor: ', b, c FROM testTable',
+        containsKeywords: ['OVER'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT count(DISTINCT a) |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT count(DISTINCT a) ',
+        afterCursor: '',
+        containsKeywords: ['OVER'],
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { prependFrom: true },
+          suggestDatabases: { prependFrom: true, appendDot: true }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT count(DISTINCT a) | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT count(DISTINCT a) ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['OVER'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT count(DISTINCT a) |, b, c FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT count(DISTINCT a) ',
+        afterCursor: ', b, c FROM testTable',
+        containsKeywords: ['OVER'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (| FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER ( ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['PARTITION BY'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['BY'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a, b ORDER | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY a, b ORDER ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['BY'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT row_number() OVER (ORDER BY | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (ORDER BY ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT row_number() OVER (ORDER BY |) FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (ORDER BY ',
+        afterCursor: ') FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          } // TODO: source: 'order by'
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT row_number() OVER (ORDER BY foo |) FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (ORDER BY a ',
+        afterCursor: ') FROM testTable',
+        containsKeywords: ['ASC', 'DESC'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT row_number() OVER (PARTITION BY | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT row_number() OVER (PARTITION BY a, | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY a, ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY a ORDER BY b ',
+        afterCursor: '',
+        containsKeywords: ['ASC', 'ROWS BETWEEN', 'RANGE BETWEEN'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['CURRENT ROW', 'UNBOUNDED PRECEDING']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN 1 |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN 1 ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['PRECEDING']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN UNBOUNDED |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN UNBOUNDED ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['PRECEDING']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN CURRENT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN CURRENT ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['ROW']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN 1 PRECEDING |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN 1 PRECEDING ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['AND']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN UNBOUNDED PRECEDING |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN UNBOUNDED PRECEDING ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['AND']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN CURRENT ROW |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN CURRENT ROW ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['AND']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN 1 PRECEDING AND |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN 1 PRECEDING AND ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['CURRENT ROW', 'UNBOUNDED FOLLOWING']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN UNBOUNDED PRECEDING AND |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN UNBOUNDED PRECEDING AND ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['CURRENT ROW', 'UNBOUNDED FOLLOWING']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN CURRENT ROW AND |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN CURRENT ROW AND ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['CURRENT ROW', 'UNBOUNDED FOLLOWING']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN 1 PRECEDING AND CURRENT |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN 1 PRECEDING AND CURRENT ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['ROW']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['FOLLOWING']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN CURRENT ROW AND 1 |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT row_number() OVER (PARTITION BY a ORDER BY b ROWS BETWEEN CURRENT ROW AND 1 ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['FOLLOWING']
+        }
+      });
+    });
+  });
+
+  describe('Functions', () => {
+    it('should suggest tables for "SELECT COUNT(*) |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT COUNT(*) ',
+        afterCursor: '',
+        containsKeywords: ['AS', '+'],
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT COUNT(foo |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT COUNT(foo ',
+        afterCursor: '',
+        containsKeywords: ['AND', '='],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT COUNT(foo, |) FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT COUNT(foo, ',
+        afterCursor: ') FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'bar' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT COUNT(foo, bl|, bla) FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT COUNT(foo, bl',
+        afterCursor: ',bla) FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'bar' }] }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT COUNT(foo, bla |, bar)"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT COUNT(foo ',
+        afterCursor: ', bar)',
+        containsKeywords: ['AND', '='],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns and values for "SELECT COUNT(foo, bl = |,bla) FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT COUNT(foo, bl = ',
+        afterCursor: ',bla) FROM bar;',
+        containsKeywords: ['CASE'],
+        hasErrors: false,
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'bar' }] }]
+          },
+          suggestValues: {},
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bl' }] }
+        }
+      });
+    });
+
+    it('should suggest columns and values for "SELECT bl = \'| FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT bl = '",
+        afterCursor: ' FROM bar;',
+        hasErrors: false,
+        expectedResult: {
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 23 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 15 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 },
+              identifierChain: [{ name: 'bl' }],
+              tables: [{ identifierChain: [{ name: 'bar' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 20, last_column: 23 },
+              identifierChain: [{ name: 'bar' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 23, last_column: 23 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 23, last_column: 23 }
+            }
+          ],
+          lowerCase: false,
+          suggestValues: { partialQuote: "'", missingEndQuote: true },
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bl' }] }
+        }
+      });
+    });
+
+    it('should suggest columns and values for "SELECT bl = \'|\' FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT bl = '",
+        afterCursor: "' FROM bar;",
+        hasErrors: false,
+        expectedResult: {
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 24 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 16 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 },
+              identifierChain: [{ name: 'bl' }],
+              tables: [{ identifierChain: [{ name: 'bar' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 21, last_column: 24 },
+              identifierChain: [{ name: 'bar' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 24, last_column: 24 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 24, last_column: 24 }
+            }
+          ],
+          lowerCase: false,
+          suggestValues: { partialQuote: "'", missingEndQuote: false },
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bl' }] }
+        }
+      });
+    });
+
+    it('should suggest columns and values for "SELECT bl = \'bl| bl\' FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT bl = 'bl",
+        afterCursor: " bl' FROM bar;",
+        hasErrors: false,
+        expectedResult: {
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 29 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 19 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 },
+              identifierChain: [{ name: 'bl' }],
+              tables: [{ identifierChain: [{ name: 'bar' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 26, last_column: 29 },
+              identifierChain: [{ name: 'bar' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 29, last_column: 29 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 29, last_column: 29 }
+            }
+          ],
+          lowerCase: false,
+          suggestValues: { partialQuote: "'", missingEndQuote: false },
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bl' }] }
+        }
+      });
+    });
+
+    it('should suggest columns and values for "SELECT bl = "| FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT bl = "',
+        afterCursor: ' FROM bar;',
+        hasErrors: false,
+        expectedResult: {
+          lowerCase: false,
+          suggestValues: { partialQuote: '"', missingEndQuote: true },
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bl' }] }
+        }
+      });
+    });
+
+    it('should suggest columns and values for "SELECT bl = "|" FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT bl = "',
+        afterCursor: '" FROM bar;',
+        hasErrors: false,
+        expectedResult: {
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 24 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 16 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 },
+              identifierChain: [{ name: 'bl' }],
+              tables: [{ identifierChain: [{ name: 'bar' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 21, last_column: 24 },
+              identifierChain: [{ name: 'bar' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 24, last_column: 24 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 24, last_column: 24 }
+            }
+          ],
+          lowerCase: false,
+          suggestValues: { partialQuote: '"', missingEndQuote: false },
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bl' }] }
+        }
+      });
+    });
+
+    it('should suggest columns and values for "SELECT bl = "bl| bl" FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT bl = "bl',
+        afterCursor: ' bl" FROM bar;',
+        hasErrors: false,
+        expectedResult: {
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 29 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 19 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 },
+              identifierChain: [{ name: 'bl' }],
+              tables: [{ identifierChain: [{ name: 'bar' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 26, last_column: 29 },
+              identifierChain: [{ name: 'bar' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 29, last_column: 29 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 29, last_column: 29 }
+            }
+          ],
+          lowerCase: false,
+          suggestValues: { partialQuote: '"', missingEndQuote: false },
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bl' }] }
+        }
+      });
+    });
+
+    it('should suggest functions for "SELECT CAST(|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {}
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CAST(| FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(',
+        afterCursor: ' FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'bar' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CAST(bla| FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(bla',
+        afterCursor: ' FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'bar' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CAST(| AS FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(',
+        afterCursor: ' AS FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'bar' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CAST(| AS INT FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(',
+        afterCursor: ' AS INT FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'bar' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CAST(| AS STRING) FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(',
+        afterCursor: ' AS STRING) FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'bar' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CAST(bla| AS STRING) FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(bla',
+        afterCursor: ' AS STRING) FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ name: 'bar' }] }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CAST(bla |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(bla ',
+        afterCursor: '',
+        containsKeywords: ['AS', 'AND'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CAST(bla | FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(bla ',
+        afterCursor: ' FROM bar;',
+        containsKeywords: ['AS', '='],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bla' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "select cast(bla as |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select cast(bla as ',
+        afterCursor: '',
+        containsKeywords: ['INT', 'STRING'],
+        expectedResult: {
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CAST(bla AS | FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(bla AS ',
+        afterCursor: ' FROM bar;',
+        containsKeywords: ['INT', 'STRING'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CAST(bla AS ST|) FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(bla AS ST',
+        afterCursor: ') FROM bar;',
+        containsKeywords: ['INT', 'STRING'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CAST(AS |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CAST(AS ',
+        afterCursor: '',
+        containsKeywords: ['INT', 'STRING'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest handle "SELECT db.customUdf(col) FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT db.customUdf(col) FROM bar;',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 34 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 25 }
+            },
+            {
+              type: 'database',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 10 },
+              identifierChain: [{ name: 'db' }]
+            },
+            {
+              type: 'function',
+              location: { first_line: 1, last_line: 1, first_column: 11, last_column: 19 },
+              identifierChain: [{ name: 'db' }, { name: 'customUdf' }],
+              function: 'customudf'
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 21, last_column: 24 },
+              identifierChain: [{ name: 'col' }],
+              tables: [{ identifierChain: [{ name: 'bar' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 31, last_column: 34 },
+              identifierChain: [{ name: 'bar' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 34, last_column: 34 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 34, last_column: 34 }
+            }
+          ]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT db.customUdf(| FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT db.customUdf(',
+        afterCursor: ' FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['T'] },
+          suggestColumns: {
+            types: ['T'],
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'bar' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT db.customUdf(1, | FROM bar;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT db.customUdf(1, ',
+        afterCursor: ' FROM bar;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['T'] },
+          suggestColumns: {
+            types: ['T'],
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'bar' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT <GeneralSetFunction>(|) FROM testTable"', () => {
+      const aggregateFunctions = [
+        { name: 'AVG', containsKeywords: ['DISTINCT'] },
+        { name: 'COUNT', containsKeywords: ['*', 'DISTINCT'] },
+        { name: 'STDDEV_POP', containsKeywords: ['DISTINCT'] },
+        { name: 'STDDEV_SAMP', containsKeywords: ['DISTINCT'] },
+        { name: 'SUM', containsKeywords: ['DISTINCT'] },
+        { name: 'MAX', containsKeywords: ['DISTINCT'] },
+        { name: 'MIN', containsKeywords: ['DISTINCT'] },
+        { name: 'VAR_POP', containsKeywords: ['DISTINCT'] },
+        { name: 'var_samp', containsKeywords: ['DISTINCT'] }
+      ];
+      aggregateFunctions.forEach(aggregateFunction => {
+        if (aggregateFunction.name === 'COUNT') {
+          assertAutoComplete({
+            beforeCursor: 'SELECT ' + aggregateFunction.name + '(',
+            afterCursor: ') FROM testTable',
+            containsKeywords: aggregateFunction.containsKeywords.concat(['*', 'CASE']),
+            expectedResult: {
+              lowerCase: false,
+              suggestFunctions: {},
+              suggestColumns: {
+                source: 'select',
+                tables: [{ identifierChain: [{ name: 'testTable' }] }]
+              }
+            }
+          });
+        } else {
+          const expectedResult = {
+            lowerCase: false,
+            suggestFunctions: { types: aggregateFunction.types || ['T'] },
+            suggestColumns: {
+              source: 'select',
+              types: aggregateFunction.types || ['T'],
+              tables: [{ identifierChain: [{ name: 'testTable' }] }]
+            }
+          };
+          assertAutoComplete({
+            beforeCursor: 'SELECT ' + aggregateFunction.name + '(',
+            afterCursor: ') FROM testTable',
+            containsKeywords: aggregateFunction.containsKeywords.concat(['CASE']),
+            expectedResult: expectedResult
+          });
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT id, SUM(a * | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT id, SUM(a * ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE ',
+        afterCursor: ' FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestKeywords: ['WHEN']
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN a = b AND | THEN FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN a = b AND ',
+        afterCursor: ' THEN FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = b AND | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = b AND ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CASE a = b | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = b ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['WHEN', 'AND', '<>'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN a = b OR | THEN boo FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN a = b OR ',
+        afterCursor: ' THEN boo FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN a = b OR c THEN boo OR | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN a = b OR c THEN boo OR ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a =| WHEN c THEN d END FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a =',
+        afterCursor: ' WHEN c THEN d END FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestValues: {},
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a =| WHEN c THEN d ELSE e END FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a =',
+        afterCursor: ' WHEN c THEN d ELSE e END FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestValues: {},
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = c WHEN c THEN d | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN d ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['END', '<>'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = c WHEN c THEN d=| ELSE FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN d=',
+        afterCursor: ' ELSE FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestValues: {},
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CASE a = c WHEN c THEN d=1 | bla=foo FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN d=1 ',
+        afterCursor: ' bla=foo FROM testTable',
+        containsKeywords: ['AND', 'WHEN', 'ELSE', 'END', '<'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CASE a = c WHEN c THEN d=1 | bla=foo END FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN d=1 ',
+        afterCursor: ' bla=foo FROM testTable',
+        containsKeywords: ['AND', 'WHEN', 'ELSE', '>'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = c WHEN c THEN d ELSE | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN d ELSE ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = c WHEN c THEN d ELSE e AND | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN d ELSE e AND ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE ELSE | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE ELSE ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE | ELSE a FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE ',
+        afterCursor: ' ELSE a FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestKeywords: ['WHEN']
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE | ELSE FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE ',
+        afterCursor: ' ELSE FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestKeywords: ['WHEN']
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = c WHEN c THEN d ELSE e | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN d ELSE e ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['END', '='],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'e' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN THEN boo OR | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN THEN boo OR ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CASE | a = b THEN FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE ',
+        afterCursor: ' a = b THEN FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['WHEN']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CASE | a = b THEN boo FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE ',
+        afterCursor: ' a = b THEN boo FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['WHEN']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CASE | THEN boo FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE ',
+        afterCursor: ' THEN boo FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['WHEN'],
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN | boo FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN ',
+        afterCursor: ' boo FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['THEN'],
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN bla| boo WHEN b THEN c END FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN bla',
+        afterCursor: ' boo WHEN b THEN c END FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['THEN'],
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a WHEN b THEN c WHEN | boo ELSE c FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a WHEN b THEN c WHEN ',
+        afterCursor: ' boo ELSE c FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['THEN'],
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a WHEN b THEN c WHEN | boo WHEN d THEN e END FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a WHEN b THEN c WHEN ',
+        afterCursor: ' boo WHEN d THEN e END FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['THEN'],
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CASE a WHEN b THEN c | WHEN d THEN e END FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a WHEN b THEN c ',
+        afterCursor: ' WHEN d THEN e END FROM testTable',
+        containsKeywords: ['WHEN', '<'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'c' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT CASE a WHEN b THEN c | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a WHEN b THEN c ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['WHEN', '>'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'c' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN | THEN FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN ',
+        afterCursor: ' THEN FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest values for "SELECT CASE WHEN | = a FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN ',
+        afterCursor: ' = a FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestValues: {},
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN ab| THEN bla ELSE foo FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN ab',
+        afterCursor: ' THEN bla ELSE foo FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE bla WHEN ab| THEN bla ELSE foo END FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE bla WHEN ab',
+        afterCursor: ' THEN bla ELSE foo END FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a WHEN | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a WHEN ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN a = | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN a = ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestValues: {},
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN a = b | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN a = b ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['AND', 'THEN', '<'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = c WHEN c | d FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c ',
+        afterCursor: ' d FROM testTable',
+        containsKeywords: ['THEN', '>'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'c' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = c WHEN c THEN | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE a = c WHEN c THEN | g FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE a = c WHEN c THEN ',
+        afterCursor: ' g FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN THEN | g FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN THEN ',
+        afterCursor: ' g FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT CASE WHEN THEN | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT CASE WHEN THEN ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+  });
+
+  describe('Value Expression Completion', () => {
+    it("should suggest functions for \"SELECT 'boo \\' baa' = |\"", () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT 'boo \\' baa' = ",
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['STRING'] }
+        }
+      });
+    });
+
+    it('should suggest functions for "SELECT "boo \\" baa" = |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT "boo \\" baa" = ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['STRING'] }
+        }
+      });
+    });
+
+    it('should suggest identifiers for "SELECT 1 = | OR false FROM tableOne boo, tableTwo baa;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT 1 = ',
+        afterCursor: ' OR false FROM tableOne boo, tableTwo baa;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['NUMBER'],
+            tables: [
+              { identifierChain: [{ name: 'tableOne' }], alias: 'boo' },
+              { identifierChain: [{ name: 'tableTwo' }], alias: 'baa' }
+            ]
+          },
+          suggestIdentifiers: [{ name: 'boo.', type: 'alias' }, { name: 'baa.', type: 'alias' }]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM tbl1, tbl2 atbl2, tbl3 WHERE id = atbl2.|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM tbl1, tbl2 atbl2, tbl3 WHERE id = atbl2.',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: {
+            source: 'where',
+            types: ['T'],
+            tables: [{ identifierChain: [{ name: 'tbl2' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM tbl1, tbl2 atbl2, tbl3 WHERE id = atbl2.|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM tbl1, tbl2 atbl2, tbl3 WHERE id = atbl2.',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: {
+            source: 'where',
+            types: ['T'],
+            tables: [{ identifierChain: [{ name: 'tbl2' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest values for "SELECT * FROM testTable WHERE id = |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE id =',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'id' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE -|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE -',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT -| FROM testTable WHERE id = 1;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT -',
+        afterCursor: ' FROM testTable WHERE id = 1;',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT 1 < | FROM testTable WHERE id = 1;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT 1 < ',
+        afterCursor: ' FROM testTable WHERE id = 1;',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "select foo from tbl where | % 2 = 0"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select foo from tbl where ',
+        afterCursor: ' % 2 = 0',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: true,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'tbl' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest values for "SELECT * FROM testTable WHERE -id = |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE -id = ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'id' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT greatest(1, 2, a, 4, | FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT greatest(1, 2, a, 4, ',
+        afterCursor: ' FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['T'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['T'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT greatest(1, |, a, 4) FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT greatest(1, ',
+        afterCursor: ', a, 4) FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['T'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['T'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | > id FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' > id FROM testTable',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestColumns: {
+            source: 'select',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestValues: {},
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'id' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT * FROM testTable WHERE | = id"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE ',
+        afterCursor: ' = id',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'id' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT a, b, c FROM testTable WHERE d = |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d >= ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT a, b, c FROM testTable WHERE d < |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d < ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT a, b, c FROM testTable WHERE d <= |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d <= ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT a, b, c FROM testTable WHERE d <=> |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d <=> ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT a, b, c FROM testTable WHERE d <> |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d <> ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT a, b, c FROM testTable WHERE d >= |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d >= ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT a, b, c FROM testTable WHERE d > |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d > ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest values and columns for "SELECT a, b, c FROM testTable WHERE d != |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d != ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d + 1 != |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d + 1 != ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE bla| + 1 != 3"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE bla',
+        afterCursor: ' + 1 != 3',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d + |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d + ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d - |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d - ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d * |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d * ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d / |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d / ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d % |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d % ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d | |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d | ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d & |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d & ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d ^ |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d ^ ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE ~|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE ~',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE -|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE -',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    // TODO: This one causes an unrecoverable error after the cursor, we should suggest group by etc.
+    it('should suggest columns for "SELECT a, b, c FROM testTable WHERE d | RLIKE \'bla bla\'"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c FROM testTable WHERE d ',
+        afterCursor: " RLIKE 'bla bla'",
+        containsKeywords: ['<', 'IN'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'd' }] },
+          suggestGroupBys: {
+            prefix: 'GROUP BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestOrderBys: {
+            prefix: 'ORDER BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT bar FROM foo WHERE id = 1 |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT bar FROM foo WHERE id = 1 ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestGroupBys: { prefix: 'GROUP BY', tables: [{ identifierChain: [{ name: 'foo' }] }] },
+          suggestOrderBys: { prefix: 'ORDER BY', tables: [{ identifierChain: [{ name: 'foo' }] }] },
+          suggestKeywords: [
+            'GROUP BY',
+            'HAVING',
+            'ORDER BY',
+            'LIMIT',
+            'UNION',
+            '<',
+            '<=',
+            '<=>',
+            '<>',
+            '=',
+            '>',
+            '>=',
+            'AND',
+            'BETWEEN',
+            'IN',
+            'IS FALSE',
+            'IS NOT FALSE',
+            'IS NOT NULL',
+            'IS NOT TRUE',
+            'IS NULL',
+            'IS TRUE',
+            'NOT BETWEEN',
+            'NOT IN',
+            'OR'
+          ]
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE id <=> 1 |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE id <=> 1 ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestGroupBys: { prefix: 'GROUP BY', tables: [{ identifierChain: [{ name: 'foo' }] }] },
+          suggestOrderBys: { prefix: 'ORDER BY', tables: [{ identifierChain: [{ name: 'foo' }] }] },
+          suggestKeywords: [
+            'GROUP BY',
+            'HAVING',
+            'ORDER BY',
+            'LIMIT',
+            'UNION',
+            '<',
+            '<=',
+            '<=>',
+            '<>',
+            '=',
+            '>',
+            '>=',
+            'AND',
+            'BETWEEN',
+            'IN',
+            'IS FALSE',
+            'IS NOT FALSE',
+            'IS NOT NULL',
+            'IS NOT TRUE',
+            'IS NULL',
+            'IS TRUE',
+            'NOT BETWEEN',
+            'NOT IN',
+            'OR'
+          ]
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE id IS |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE id IS ',
+        afterCursor: '',
+        containsKeywords: ['NOT NULL', 'NULL', 'NOT TRUE', 'TRUE', 'NOT FALSE', 'FALSE'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE id IS NOT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE id IS NOT ',
+        afterCursor: '',
+        containsKeywords: ['NULL', 'FALSE', 'TRUE'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE id IS | NULL"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE id IS ',
+        afterCursor: ' NULL',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['NOT']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE id IS | FALSE"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE id IS ',
+        afterCursor: ' FALSE',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['NOT']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE id IS | TRUE"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE id IS ',
+        afterCursor: ' TRUE',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['NOT']
+        }
+      });
+    });
+
+    // Fails because "NOT" is missing
+    xit('should suggest keywords for "SELECT * FROM foo WHERE id | LIKE \'bla bla\'"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE id ',
+        afterCursor: " LIKE 'bla bla'",
+        containsKeywords: ['<', 'IN', 'NOT'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'id' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM foo WHERE id LIKE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE id LIKE ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['STRING'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['STRING'],
+            tables: [{ identifierChain: [{ name: 'foo' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE id LIKE \'\' GROUP |"', () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT * FROM foo WHERE id LIKE '' GROUP ",
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestGroupBys: { prefix: 'BY', tables: [{ identifierChain: [{ name: 'foo' }] }] },
+          suggestKeywords: ['BY']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE id LIKE (\'bla bla\') |"', () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT * FROM foo WHERE id LIKE ('bla bla') ",
+        afterCursor: '',
+        containsKeywords: ['AND'],
+        expectedResult: {
+          lowerCase: false,
+          suggestGroupBys: { prefix: 'GROUP BY', tables: [{ identifierChain: [{ name: 'foo' }] }] },
+          suggestOrderBys: { prefix: 'ORDER BY', tables: [{ identifierChain: [{ name: 'foo' }] }] }
+        }
+      });
+    });
+
+    it('should suggest identifiers for "SELECT * FROM foo bla, bar WHERE id IS NULL AND |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo bla, bar WHERE id IS NULL AND ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestFilters: {
+            tables: [
+              { identifierChain: [{ name: 'foo' }], alias: 'bla' },
+              { identifierChain: [{ name: 'bar' }] }
+            ]
+          },
+          suggestColumns: {
+            source: 'where',
+            tables: [
+              { identifierChain: [{ name: 'foo' }], alias: 'bla' },
+              { identifierChain: [{ name: 'bar' }] }
+            ]
+          },
+          suggestIdentifiers: [{ name: 'bla.', type: 'alias' }, { name: 'bar.', type: 'table' }]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM foo AS bla WHERE id IS NULL && |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo AS bla WHERE id IS NULL && ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'foo' }], alias: 'bla' }] },
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'foo' }], alias: 'bla' }]
+          },
+          suggestIdentifiers: [{ name: 'bla.', type: 'alias' }]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM foo AS bla WHERE id IS NULL OR | AND 1 + 1 > 1"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo AS bla WHERE id IS NULL OR ',
+        afterCursor: ' AND 1 + 1 > 1',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'foo' }], alias: 'bla' }] },
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'foo' }], alias: 'bla' }]
+          },
+          suggestIdentifiers: [{ name: 'bla.', type: 'alias' }]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM foo AS bla WHERE id IS NULL || |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo AS bla WHERE id IS NULL || ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'foo' }], alias: 'bla' }] },
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'foo' }], alias: 'bla' }]
+          },
+          suggestIdentifiers: [{ name: 'bla.', type: 'alias' }]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM foo bar WHERE NOT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo bar WHERE NOT ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'foo' }], alias: 'bar' }]
+          },
+          suggestKeywords: ['EXISTS'],
+          suggestIdentifiers: [{ name: 'bar.', type: 'alias' }]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM foo bar WHERE ! |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo bar WHERE ! ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['BOOLEAN'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['BOOLEAN'],
+            tables: [{ identifierChain: [{ name: 'foo' }], alias: 'bar' }]
+          },
+          suggestIdentifiers: [{ name: 'bar.', type: 'alias' }]
+        }
+      });
+    });
+  });
+
+  describe('Field Completion', () => {
+    it('should suggest columns for "SELECT * FROM testTable WHERE |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestKeywords: ['EXISTS', 'NOT EXISTS'],
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE a|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE a',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestKeywords: ['EXISTS', 'NOT EXISTS'],
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE baa = 1 AND |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE baa = 1 AND ',
+        afterCursor: '',
+        containsKeywords: ['CASE', 'EXISTS', 'NOT', 'NULL'],
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestFunctions: {},
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE | AND baa = 1"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE ',
+        afterCursor: ' AND baa = 1',
+        containsKeywords: ['CASE', 'EXISTS', 'NOT', 'NULL'],
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestFunctions: {},
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE baa = 1 OR |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE baa = 1 OR ',
+        afterCursor: '',
+        containsKeywords: ['CASE', 'EXISTS', 'NOT', 'NULL'],
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestFunctions: {},
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE | OR baa = 1"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE ',
+        afterCursor: ' OR baa = 1',
+        containsKeywords: ['CASE', 'EXISTS', 'NOT', 'NULL'],
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestFunctions: {},
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable WHERE NOT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE NOT ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestKeywords: ['EXISTS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable WHERE foo = \'bar\' |"', () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT * FROM testTable WHERE foo = 'bar' ",
+        afterCursor: '',
+        containsKeywords: ['AND', '<'],
+        expectedResult: {
+          lowerCase: false,
+          suggestGroupBys: {
+            prefix: 'GROUP BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestOrderBys: {
+            prefix: 'ORDER BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT a, b, c, d, e FROM tableOne WHERE c >= 9998 an|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, c, d, e FROM tableOne WHERE c >= 9998 an',
+        afterCursor: '',
+        containsKeywords: ['AND', '='],
+        expectedResult: {
+          lowerCase: false,
+          suggestGroupBys: {
+            prefix: 'GROUP BY',
+            tables: [{ identifierChain: [{ name: 'tableOne' }] }]
+          },
+          suggestOrderBys: {
+            prefix: 'ORDER BY',
+            tables: [{ identifierChain: [{ name: 'tableOne' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE foo = \'bar\' AND |"', () => {
+      assertAutoComplete({
+        beforeCursor: "SELECT * FROM testTable WHERE foo = 'bar' AND ",
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestFilters: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestColumns: {
+            source: 'where',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a, b, \\nc,\\nd, |\\ng,\\nf\\nFROM testTable WHERE a > 1 AND b = \'b\' ORDER BY c;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a, b, \nc,\nd, ',
+        afterCursor: "\ng,\nf\nFROM testTable WHERE a > 1 AND b = 'b' ORDER BY c;",
+        containsKeywords: ['*', 'CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT a,b, | c FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a,b, ',
+        afterCursor: ' c FROM testTable',
+        containsKeywords: ['*'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | a, b, c FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' a, b, c FROM testTable',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'testTable' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT a |, b, c FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT a ',
+        afterCursor: ', b, c FROM testTable',
+        containsKeywords: ['AS', '>'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable WHERE | = \'bar\' AND "', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE ',
+        afterCursor: " = 'bar' AND ",
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['STRING'] },
+          suggestColumns: {
+            source: 'where',
+            types: ['STRING'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable WHERE a |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE a ',
+        afterCursor: '',
+        containsKeywords: ['BETWEEN', 'NOT BETWEEN'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'a' }] },
+          suggestGroupBys: {
+            prefix: 'GROUP BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestOrderBys: {
+            prefix: 'ORDER BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable WHERE a NOT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE a NOT ',
+        afterCursor: '',
+        containsKeywords: ['BETWEEN', 'EXISTS', 'IN', 'LIKE'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest values for "SELECT * FROM testTable WHERE a BETWEEN |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE a BETWEEN ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable WHERE a OR NOT EXISTS (|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable WHERE a OR NOT EXISTS (',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['SELECT']
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT t1.testTableColumn1, t2.testTableColumn3 FROM testTable1 t1 JOIN testTable2 t2 ON t1.|"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT t1.testTableColumn1, t2.testTableColumn3 FROM testTable1 t1 JOIN testTable2 t2 ON t1.',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: { tables: [{ identifierChain: [{ name: 'testTable1' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT t1.testTableColumn1, t2.testTableColumn3 FROM database_two.testTable1 t1 JOIN testTable2 t2 ON t1.|"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT t1.testTableColumn1, t2.testTableColumn3 FROM database_two.testTable1 t1 JOIN testTable2 t2 ON t1.',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: {
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable1' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT testTable.| FROM testTable"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT testTable.',
+        afterCursor: ' FROM testTable',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['*'],
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns "SELECT tt.| FROM testTable tt"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT tt.',
+        afterCursor: ' FROM testTable tt',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['*'],
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT tt.| FROM database_two.testTable tt"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT tt.',
+        afterCursor: ' FROM database_two.testTable tt',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['*'],
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT tta.| FROM testTableA tta, testTableB ttb"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT tta.',
+        afterCursor: ' FROM testTableA tta, testTableB ttb',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['*'],
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTableA' }] }]
+          }
+        }
+      });
+      assertAutoComplete({
+        beforeCursor: 'SELECT ttb.',
+        afterCursor: ' FROM testTableA tta, testTableB ttb',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['*'],
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'testTableB' }] }]
+          }
+        }
+      });
+    });
+  });
+
+  describe('ORDER BY Clause', () => {
+    it('should suggest keywords for "SELECT * FROM testTable GROUP BY a | LIMIT 10"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable GROUP BY a ',
+        afterCursor: ' LIMIT 10',
+        doesNotContainKeywords: ['LIMIT'],
+        containsKeywords: ['ORDER BY'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestOrderBys: {
+            prefix: 'ORDER BY',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'testTable' }, { name: 'a' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable ORDER |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable ORDER ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['BY'],
+          suggestOrderBys: { prefix: 'BY', tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable ORDER BY |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable ORDER BY ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAnalyticFunctions: true,
+          suggestColumns: {
+            source: 'order by',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestOrderBys: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM database_two.testTable ORDER BY |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          suggestColumns: {
+            source: 'order by',
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          },
+          suggestFunctions: {},
+          suggestAnalyticFunctions: true,
+          suggestOrderBys: {
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          },
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM database_two.testTable ORDER BY foo |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY foo ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['ASC', 'DESC', 'LIMIT', 'UNION']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM database_two.testTable ORDER BY foo + |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY foo + ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'order by',
+            types: ['NUMBER'],
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM database_two.testTable ORDER BY foo, |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY foo, ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAnalyticFunctions: true,
+          suggestColumns: {
+            source: 'order by',
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM database_two.testTable ORDER BY foo + baa ASC, |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY foo + baa ASC, ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAnalyticFunctions: true,
+          suggestColumns: {
+            source: 'order by',
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM database_two.testTable ORDER BY foo ASC, |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY foo ASC, ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestAnalyticFunctions: true,
+          suggestColumns: {
+            source: 'order by',
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM database_two.testTable ORDER BY foo DESC, bar |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY foo DESC, bar ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['ASC', 'DESC', 'LIMIT', 'UNION']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM database_two.testTable ORDER BY foo DESC, bar |, bla"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY foo DESC, bar ',
+        afterCursor: ', bla',
+        containsKeywords: ['ASC', 'DESC'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM database_two.testTable ORDER BY foo LIMIT 10 |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable ORDER BY foo LIMIT 10 ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['UNION']
+        }
+      });
+    });
+  });
+
+  describe('GROUP BY Clause', () => {
+    it('should suggest keywords for "SELECT * FROM testTable GROUP |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable GROUP ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['BY'],
+          suggestGroupBys: { prefix: 'BY', tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest identifiers for "SELECT * FROM testTableA tta, testTableB GROUP BY |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTableA tta, testTableB GROUP BY ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'group by',
+            tables: [
+              { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+              { identifierChain: [{ name: 'testTableB' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'tta.', type: 'alias' },
+            { name: 'testTableB.', type: 'table' }
+          ],
+          suggestGroupBys: {
+            tables: [
+              { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+              { identifierChain: [{ name: 'testTableB' }] }
+            ]
+          }
+        }
+      });
+    });
+
+    it('should suggest identifier for "SELECT * FROM testTableA tta, testTableB GROUP BY bla, |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTableA tta, testTableB GROUP BY bla, ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'group by',
+            tables: [
+              { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+              { identifierChain: [{ name: 'testTableB' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'tta.', type: 'alias' },
+            { name: 'testTableB.', type: 'table' }
+          ]
+        }
+      });
+    });
+
+    it('should suggest identifier for "SELECT * FROM testTableA tta, testTableB GROUP BY bla+|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTableA tta, testTableB GROUP BY bla+',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestColumns: {
+            source: 'group by',
+            types: ['NUMBER'],
+            tables: [
+              { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+              { identifierChain: [{ name: 'testTableB' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'tta.', type: 'alias' },
+            { name: 'testTableB.', type: 'table' }
+          ]
+        }
+      });
+    });
+
+    it('should suggest identifier for "SELECT * FROM testTableA tta, testTableB GROUP BY bla+foo, |, foo"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTableA tta, testTableB GROUP BY bla, ',
+        afterCursor: ', foo',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'group by',
+            tables: [
+              { identifierChain: [{ name: 'testTableA' }], alias: 'tta' },
+              { identifierChain: [{ name: 'testTableB' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'tta.', type: 'alias' },
+            { name: 'testTableB.', type: 'table' }
+          ]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM testTable GROUP BY |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable GROUP BY ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'group by',
+            tables: [{ identifierChain: [{ name: 'testTable' }] }]
+          },
+          suggestGroupBys: { tables: [{ identifierChain: [{ name: 'testTable' }] }] }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM database_two.testTable GROUP BY |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM database_two.testTable GROUP BY ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'group by',
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          },
+          suggestGroupBys: {
+            tables: [{ identifierChain: [{ name: 'database_two' }, { name: 'testTable' }] }]
+          }
+        }
+      });
+    });
+  });
+
+  describe('LIMIT clause', () => {
+    it('should not suggest anything for "SELECT COUNT(*) AS boo FROM testTable GROUP BY baa LIMIT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT COUNT(*) AS boo FROM testTable GROUP BY baa LIMIT ',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+  });
+
+  describe('UNION clause', () => {
+    // TODO: Fix locations
+    xit('should handle "SELECT * FROM (SELECT x FROM few_ints UNION ALL SELECT x FROM few_ints) AS t1 ORDER BY x;|', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT * FROM (SELECT x FROM few_ints UNION ALL SELECT x FROM few_ints) AS t1 ORDER BY x;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "SELECT key FROM (SELECT key FROM src ORDER BY key LIMIT 10)subq1 UNION SELECT key FROM (SELECT key FROM src1 ORDER BY key LIMIT 10)subq2;|', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT key FROM (SELECT key FROM src ORDER BY key LIMIT 10)subq1 UNION SELECT key FROM (SELECT key FROM src1 ORDER BY key LIMIT 10)subq2;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "SELECT * FROM t1 UNION DISTINCT SELECT * FROM t2;|', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM t1 UNION DISTINCT SELECT * FROM t2;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "SELECT * FROM t1 UNION SELECT * FROM t2;|', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM t1 UNION SELECT * FROM t2;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM t1 UNION |', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM t1 UNION ',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['ALL', 'DISTINCT', 'SELECT']
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM t1 UNION ALL SELECT |', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM t1 UNION ALL SELECT ',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+  });
+
+  describe('WITH clause', () => {
+    it('should handle "WITH q1 AS ( SELECT key FROM src WHERE something) SELECT * FROM q1;|', () => {
+      assertAutoComplete({
+        beforeCursor: 'WITH q1 AS ( SELECT key FROM src WHERE something) SELECT * FROM q1;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "WITH q1 AS (SELECT * FROM src WHERE something), q2 AS (SELECT * FROM src s2 WHERE something) SELECT * FROM q1 UNION ALL SELECT * FROM q2;|', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'WITH q1 AS (SELECT * FROM src WHERE something), q2 AS (SELECT * FROM src s2 WHERE something) SELECT * FROM q1 UNION ALL SELECT * FROM q2;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should handle "WITH t1 AS (SELECT 1) (WITH t2 AS (SELECT 2) SELECT * FROM t2) UNION ALL SELECT * FROM t1;|', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'WITH t1 AS (SELECT 1) (WITH t2 AS (SELECT 2) SELECT * FROM t2) UNION ALL SELECT * FROM t1;',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "WITH t1 |', () => {
+      assertAutoComplete({
+        beforeCursor: 'WITH t1 ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['AS']
+        }
+      });
+    });
+
+    it('should suggest keywords for "WITH t1 AS (|', () => {
+      assertAutoComplete({
+        beforeCursor: 'WITH t1 AS (',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['SELECT']
+        }
+      });
+    });
+
+    it('should suggest keywords for "WITH t1 AS (SELECT * FROM boo) |', () => {
+      assertAutoComplete({
+        beforeCursor: 'WITH t1 AS (SELECT * FROM boo) ',
+        afterCursor: '',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest identifiers for "WITH t1 AS (SELECT * FROM FOO) SELECT |', () => {
+      assertAutoComplete({
+        beforeCursor: 'WITH t1 AS (SELECT * FROM FOO) SELECT ',
+        afterCursor: '',
+        noErrors: true,
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: { prependQuestionMark: true, prependFrom: true },
+          suggestDatabases: { prependQuestionMark: true, prependFrom: true, appendDot: true },
+          suggestCommonTableExpressions: [
+            { name: 't1', prependFrom: true, prependQuestionMark: true }
+          ],
+          commonTableExpressions: [
+            { alias: 't1', columns: [{ tables: [{ identifierChain: [{ name: 'FOO' }] }] }] }
+          ]
+        }
+      });
+    });
+
+    it('should suggest identifiers for "WITH t1 AS (SELECT * FROM FOO), t2 AS (SELECT |', () => {
+      assertAutoComplete({
+        beforeCursor: 'WITH t1 AS (SELECT * FROM FOO), t2 AS (SELECT ',
+        afterCursor: '',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: { prependQuestionMark: true, prependFrom: true },
+          suggestDatabases: { prependQuestionMark: true, prependFrom: true, appendDot: true },
+          lowerCase: false,
+          suggestCommonTableExpressions: [
+            { name: 't1', prependFrom: true, prependQuestionMark: true }
+          ],
+          commonTableExpressions: [
+            { alias: 't1', columns: [{ tables: [{ identifierChain: [{ name: 'FOO' }] }] }] }
+          ]
+        }
+      });
+    });
+
+    it('should suggest identifiers for "WITH t1 AS (SELECT * FROM FOO) SELECT * FROM |', () => {
+      assertAutoComplete({
+        beforeCursor: 'WITH t1 AS (SELECT * FROM FOO) SELECT * FROM ',
+        afterCursor: '',
+        noErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: {},
+          suggestDatabases: { appendDot: true },
+          suggestCommonTableExpressions: [{ name: 't1' }],
+          commonTableExpressions: [
+            { alias: 't1', columns: [{ tables: [{ identifierChain: [{ name: 'FOO' }] }] }] }
+          ]
+        }
+      });
+    });
+
+    it('should suggest keywords for "with s as (select * from foo join bar) select * from |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'with s as (select * from foo join bar) select * from ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: true,
+          suggestTables: {},
+          suggestDatabases: { appendDot: true },
+          commonTableExpressions: [
+            {
+              columns: [
+                {
+                  tables: [
+                    { identifierChain: [{ name: 'foo' }] },
+                    { identifierChain: [{ name: 'bar' }] }
+                  ]
+                }
+              ],
+              alias: 's'
+            }
+          ],
+          suggestCommonTableExpressions: [{ name: 's' }]
+        }
+      });
+    });
+
+    it('should suggest keywords for "with s as (select * from foo join bar) select * from |;', () => {
+      assertAutoComplete({
+        beforeCursor: 'with s as (select * from foo join bar) select * from ',
+        afterCursor: ';',
+        expectedResult: {
+          lowerCase: true,
+          suggestTables: {},
+          suggestDatabases: { appendDot: true },
+          commonTableExpressions: [
+            {
+              columns: [
+                {
+                  tables: [
+                    { identifierChain: [{ name: 'foo' }] },
+                    { identifierChain: [{ name: 'bar' }] }
+                  ]
+                }
+              ],
+              alias: 's'
+            }
+          ],
+          suggestCommonTableExpressions: [{ name: 's' }]
+        }
+      });
+    });
+  });
+
+  describe('Joins', () => {
+    it('should suggest tables for "SELECT * FROM testTable1 JOIN |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 JOIN ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestJoins: {
+            prependJoin: false,
+            joinType: 'JOIN',
+            tables: [{ identifierChain: [{ name: 'testTable1' }] }]
+          },
+          suggestTables: {},
+          suggestDatabases: { appendDot: true }
+        }
+      });
+    });
+
+    it('should suggest joins for "SELECT * FROM testTable1 JOIN testTable2 JOIN |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 JOIN testTable2 JOIN ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestJoins: {
+            prependJoin: false,
+            joinType: 'JOIN',
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestTables: {},
+          suggestDatabases: { appendDot: true }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable1 INNER |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 INNER ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['JOIN']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable1 FULL |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 FULL ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['JOIN', 'OUTER JOIN']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable1 FULL OUTER |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 FULL OUTER ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['JOIN']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable1 LEFT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 LEFT ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['JOIN', 'OUTER JOIN']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable1 LEFT OUTER |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 LEFT OUTER ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['JOIN']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable1 RIGHT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 RIGHT ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['JOIN', 'OUTER JOIN']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM testTable1 RIGHT OUTER |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 RIGHT OUTER ',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['JOIN']
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM testTable1 JOIN db1.|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 JOIN db1.',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { identifierChain: [{ name: 'db1' }] }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM testTable1 JOIN db1.| JOIN foo"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 JOIN db1.',
+        afterCursor: ' JOIN foo',
+        expectedResult: {
+          lowerCase: false,
+          suggestTables: { identifierChain: [{ name: 'db1' }] }
+        }
+      });
+    });
+
+    it('should suggest join conditions for "SELECT testTable1.* FROM testTable1 JOIN testTable2 |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT testTable1.* FROM testTable1 JOIN testTable2 ',
+        afterCursor: '',
+        containsKeywords: ['ON'],
+        expectedResult: {
+          lowerCase: false,
+          suggestJoinConditions: {
+            prependOn: true,
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestFilters: {
+            prefix: 'WHERE',
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestGroupBys: {
+            prefix: 'GROUP BY',
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestOrderBys: {
+            prefix: 'ORDER BY',
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT testTable1.* FROM testTable1 JOIN testTable2 ON |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT testTable1.* FROM testTable1 JOIN testTable2 ON ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          suggestColumns: {
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestFunctions: {},
+          suggestJoinConditions: {
+            prependOn: false,
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'testTable1.', type: 'table' },
+            { name: 'testTable2.', type: 'table' }
+          ],
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'testTable1.', type: 'table' },
+            { name: 'testTable2.', type: 'table' }
+          ]
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (testTable1.testColumn1 = testTable2.testColumn3 AND |"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (testTable1.testColumn1 = testTable2.testColumn3 AND ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: false,
+          suggestFunctions: {},
+          suggestColumns: {
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'testTable1.', type: 'table' },
+            { name: 'testTable2.', type: 'table' }
+          ]
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (| AND testTable1.testColumn1 = testTable2.testColumn3"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (',
+        afterCursor: ' AND testTable1.testColumn1 = testTable2.testColumn3',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 109 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 20 }
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 18 },
+              identifierChain: [{ name: 'testTable1' }]
+            },
+            {
+              type: 'asterisk',
+              location: { first_line: 1, last_line: 1, first_column: 19, last_column: 20 },
+              tables: [{ identifierChain: [{ name: 'testTable1' }] }]
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 26, last_column: 36 },
+              identifierChain: [{ name: 'testTable1' }]
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 42, last_column: 52 },
+              identifierChain: [{ name: 'testTable2' }]
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 62, last_column: 72 },
+              identifierChain: [{ name: 'testTable1' }]
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 73, last_column: 84 },
+              identifierChain: [{ name: 'testColumn1' }],
+              tables: [{ identifierChain: [{ name: 'testTable1' }] }],
+              qualified: true
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 87, last_column: 97 },
+              identifierChain: [{ name: 'testTable2' }]
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 98, last_column: 109 },
+              identifierChain: [{ name: 'testColumn3' }],
+              tables: [{ identifierChain: [{ name: 'testTable2' }] }],
+              qualified: true
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 109, last_column: 109 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 109, last_column: 109 }
+            }
+          ],
+          suggestColumns: {
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'testTable1.', type: 'table' },
+            { name: 'testTable2.', type: 'table' }
+          ],
+          suggestFunctions: {},
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest identifiers for "select * from testTable1 join db.testTable2 on |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select * from testTable1 join db.testTable2 on ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: true,
+          suggestFunctions: {},
+          suggestJoinConditions: {
+            prependOn: false,
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'db' }, { name: 'testTable2' }] }
+            ]
+          },
+          suggestColumns: {
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'db' }, { name: 'testTable2' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'testTable1.', type: 'table' },
+            { name: 'testTable2.', type: 'table' }
+          ]
+        }
+      });
+    });
+
+    it('should suggest identifiers for "select * from testTable1 JOIN testTable2 on (testTable1.testColumn1 = |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select * from testTable1 JOIN testTable2 on (testTable1.testColumn1 = ',
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          suggestValues: {},
+          colRef: { identifierChain: [{ name: 'testTable1' }, { name: 'testColumn1' }] },
+          suggestFunctions: { types: ['COLREF'] },
+          suggestColumns: {
+            types: ['COLREF'],
+            tables: [
+              { identifierChain: [{ name: 'testTable1' }] },
+              { identifierChain: [{ name: 'testTable2' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'testTable1.', type: 'table' },
+            { name: 'testTable2.', type: 'table' }
+          ],
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should suggest columns for "select * from testTable1 JOIN testTable2 on (testTable1.testColumn1 = testTable2.|"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'select * from testTable1 JOIN testTable2 on (testTable1.testColumn1 = testTable2.',
+        afterCursor: '',
+        ignoreErrors: true,
+        expectedResult: {
+          lowerCase: true,
+          colRef: { identifierChain: [{ name: 'testTable1' }, { name: 'testColumn1' }] },
+          suggestColumns: {
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'testTable2' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (testTable1.testColumn1 = testTable2.testColumn3 AND testTable1.|"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT testTable1.* FROM testTable1 JOIN testTable2 ON (testTable1.testColumn1 = testTable2.testColumn3 AND testTable1.',
+        afterCursor: '',
+        ignoreErrors: true,
+        expectedResult: {
+          lowerCase: false,
+          suggestColumns: { tables: [{ identifierChain: [{ name: 'testTable1' }] }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT t1.* FROM table1 t1 | JOIN"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT t1.* FROM table1 t1 ',
+        afterCursor: ' JOIN',
+        expectedResult: {
+          lowerCase: false,
+          suggestJoins: {
+            prependJoin: true,
+            tables: [{ identifierChain: [{ name: 'table1' }], alias: 't1' }]
+          },
+          suggestKeywords: [
+            'FULL',
+            'FULL OUTER',
+            'INNER',
+            'LEFT',
+            'LEFT OUTER',
+            'RIGHT',
+            'RIGHT OUTER'
+          ]
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT t1.* FROM table1 t1 | JOIN table2 t2 ON t1.bla = t2.bla"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT t1.* FROM table1 t1 ',
+        afterCursor: ' JOIN table2 t2 ON t1.bla = t2.bla',
+        expectedResult: {
+          lowerCase: false,
+          suggestJoins: {
+            prependJoin: true,
+            tables: [{ identifierChain: [{ name: 'table1' }], alias: 't1' }]
+          },
+          suggestKeywords: [
+            'FULL',
+            'FULL OUTER',
+            'INNER',
+            'LEFT',
+            'LEFT OUTER',
+            'RIGHT',
+            'RIGHT OUTER'
+          ]
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT t1.* FROM table1 t1 JOIN table2 t2 | JOIN table3"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT t1.* FROM table1 t1 JOIN table2 t2 ',
+        afterCursor: ' JOIN table3',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: [
+            'ON',
+            'FULL',
+            'FULL OUTER',
+            'INNER',
+            'LEFT',
+            'LEFT OUTER',
+            'RIGHT',
+            'RIGHT OUTER'
+          ],
+          suggestJoinConditions: {
+            prependOn: true,
+            tables: [
+              { identifierChain: [{ name: 'table1' }], alias: 't1' },
+              { identifierChain: [{ name: 'table2' }], alias: 't2' }
+            ]
+          }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT t1.* FROM table1 t1 FULL | JOIN"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT t1.* FROM table1 t1 FULL ',
+        afterCursor: ' JOIN',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['OUTER']
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT t1.* FROM table1 t1 RIGHT | JOIN"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT t1.* FROM table1 t1 RIGHT ',
+        afterCursor: ' JOIN',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['OUTER']
+        }
+      });
+    });
+
+    it('should suggest joins for "SELECT * FROM testTable1 |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM testTable1 ',
+        afterCursor: '',
+        containsKeywords: ['JOIN'],
+        expectedResult: {
+          lowerCase: false,
+          suggestJoins: {
+            prependJoin: true,
+            tables: [{ identifierChain: [{ name: 'testTable1' }] }]
+          },
+          suggestFilters: {
+            prefix: 'WHERE',
+            tables: [{ identifierChain: [{ name: 'testTable1' }] }]
+          },
+          suggestGroupBys: {
+            prefix: 'GROUP BY',
+            tables: [{ identifierChain: [{ name: 'testTable1' }] }]
+          },
+          suggestOrderBys: {
+            prefix: 'ORDER BY',
+            tables: [{ identifierChain: [{ name: 'testTable1' }] }]
+          }
+        }
+      });
+    });
+  });
+
+  describe('SubQueries in WHERE Clause', () => {
+    it('should suggest keywords for "SELECT * FROM foo WHERE bar |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE bar ',
+        afterCursor: '',
+        containsKeywords: ['IN', 'NOT IN'],
+        containsColRefKeywords: true,
+        expectedResult: {
+          lowerCase: false,
+          colRef: { identifierChain: [{ name: 'foo' }, { name: 'bar' }] },
+          suggestGroupBys: { prefix: 'GROUP BY', tables: [{ identifierChain: [{ name: 'foo' }] }] },
+          suggestOrderBys: { prefix: 'ORDER BY', tables: [{ identifierChain: [{ name: 'foo' }] }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE bar NOT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE bar NOT ',
+        afterCursor: '',
+        containsKeywords: ['IN'],
+        expectedResult: {
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest keywords for "SELECT * FROM foo WHERE bar IN (|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE bar IN (',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['SELECT'],
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'foo' }] }]
+          },
+          colRef: { identifierChain: [{ name: 'foo' }, { name: 'bar' }] }
+        }
+      });
+    });
+
+    it('should suggest keywords for "select * from foo, bar where bar.bla in (|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select * from foo, bar where bar.bla in (',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: true,
+          suggestKeywords: ['SELECT'],
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'foo' }] }, { identifierChain: [{ name: 'bar' }] }]
+          },
+          suggestIdentifiers: [{ name: 'foo.', type: 'table' }, { name: 'bar.', type: 'table' }],
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bla' }] }
+        }
+      });
+    });
+
+    it('should suggest values for "select * from foo, bar where bar.bla in (\'a\', |"', () => {
+      assertAutoComplete({
+        beforeCursor: "select * from foo, bar where bar.bla in ('a', ",
+        afterCursor: '',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          lowerCase: true,
+          suggestFunctions: { types: ['COLREF'] },
+          suggestValues: {},
+          suggestColumns: {
+            source: 'where',
+            types: ['COLREF'],
+            tables: [{ identifierChain: [{ name: 'foo' }] }, { identifierChain: [{ name: 'bar' }] }]
+          },
+          suggestIdentifiers: [{ name: 'foo.', type: 'table' }, { name: 'bar.', type: 'table' }],
+          colRef: { identifierChain: [{ name: 'bar' }, { name: 'bla' }] }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM foo WHERE bar IN (SELECT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM foo WHERE bar IN (SELECT ',
+        afterCursor: '',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM bar WHERE foo NOT IN (SELECT |)"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM bar WHERE foo NOT IN (SELECT ',
+        afterCursor: ')',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+  });
+
+  describe('SubQueries in FROM Clause', () => {
+    it('should suggest keywords for "SELECT * FROM (|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM (',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: false,
+          suggestKeywords: ['SELECT']
+        }
+      });
+    });
+
+    it('should suggest keywords for "select * from (|"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select * from (',
+        afterCursor: '',
+        expectedResult: {
+          lowerCase: true,
+          suggestKeywords: ['SELECT']
+        }
+      });
+    });
+
+    it('should suggest keywords for "select foo.* from (|) foo"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select foo.* from (',
+        afterCursor: ') foo',
+        containsKeywords: ['SELECT'],
+        expectedResult: {
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM (SELECT |"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM (SELECT ',
+        afterCursor: '',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    // TODO: In this case the WHERE clause exists but isn't completely defined both for the query and the subquery
+    it('should suggest columns for "SELECT "contains an even number" FROM t1, t2 AS ta2 WHERE EXISTS (SELECT t3.foo FROM t3 WHERE | % 2 = 0"', () => {
+      assertAutoComplete({
+        beforeCursor:
+          'SELECT "contains an even number" FROM t1, t2 AS ta2 WHERE EXISTS (SELECT t3.foo FROM t3 WHERE ',
+        afterCursor: ' % 2 = 0',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          suggestColumns: {
+            types: ['NUMBER'],
+            source: 'where',
+            tables: [
+              { identifierChain: [{ name: 't1' }] },
+              { identifierChain: [{ name: 't2' }], alias: 'ta2' },
+              { identifierChain: [{ name: 't3' }] }
+            ]
+          },
+          suggestFunctions: { types: ['NUMBER'] },
+          suggestIdentifiers: [
+            { name: 't1.', type: 'table' },
+            { name: 'ta2.', type: 'alias' },
+            { name: 't3.', type: 'table' }
+          ],
+          lowerCase: false,
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 103 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 8, last_column: 33 }
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 39, last_column: 41 },
+              identifierChain: [{ name: 't1' }]
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 43, last_column: 45 },
+              identifierChain: [{ name: 't2' }]
+            },
+            {
+              type: 'alias',
+              source: 'table',
+              alias: 'ta2',
+              location: { first_line: 1, last_line: 1, first_column: 49, last_column: 52 },
+              identifierChain: [{ name: 't2' }]
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 52, last_column: 52 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 52, last_column: 52 }
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 74, last_column: 80 },
+              subquery: true
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 74, last_column: 76 },
+              identifierChain: [{ name: 't3' }]
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 77, last_column: 80 },
+              identifierChain: [{ name: 'foo' }],
+              tables: [{ identifierChain: [{ name: 't3' }] }],
+              qualified: true
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 86, last_column: 88 },
+              identifierChain: [{ name: 't3' }]
+            },
+            {
+              type: 'whereClause',
+              subquery: true,
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 88, last_column: 88 }
+            },
+            {
+              type: 'limitClause',
+              subquery: true,
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 88, last_column: 88 }
+            }
+          ]
+        }
+      });
+    });
+
+    it('should suggest identifiers for "SELECT | FROM testTable tt, (SELECT bla FROM abc WHERE foo > 1) bar"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM testTable tt, (SELECT bla FROM abc WHERE foo > 1) bar',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          locations: [
+            {
+              type: 'statement',
+              location: { first_line: 1, last_line: 1, first_column: 1, last_column: 67 }
+            },
+            {
+              type: 'selectList',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 7, last_column: 7 }
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 14, last_column: 23 },
+              identifierChain: [{ name: 'testTable' }]
+            },
+            {
+              type: 'alias',
+              source: 'table',
+              alias: 'tt',
+              location: { first_line: 1, last_line: 1, first_column: 24, last_column: 26 },
+              identifierChain: [{ name: 'testTable' }]
+            },
+            {
+              type: 'selectList',
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 36, last_column: 39 },
+              subquery: true
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 36, last_column: 39 },
+              identifierChain: [{ name: 'bla' }],
+              tables: [{ identifierChain: [{ name: 'abc' }] }],
+              qualified: false
+            },
+            {
+              type: 'table',
+              location: { first_line: 1, last_line: 1, first_column: 45, last_column: 48 },
+              identifierChain: [{ name: 'abc' }]
+            },
+            {
+              type: 'whereClause',
+              subquery: true,
+              missing: false,
+              location: { first_line: 1, last_line: 1, first_column: 49, last_column: 62 }
+            },
+            {
+              type: 'column',
+              location: { first_line: 1, last_line: 1, first_column: 55, last_column: 58 },
+              identifierChain: [{ name: 'foo' }],
+              tables: [{ identifierChain: [{ name: 'abc' }] }],
+              qualified: false
+            },
+            {
+              type: 'limitClause',
+              subquery: true,
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 62, last_column: 62 }
+            },
+            {
+              type: 'alias',
+              source: 'subquery',
+              alias: 'bar',
+              location: { first_line: 1, last_line: 1, first_column: 64, last_column: 67 }
+            },
+            {
+              type: 'whereClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 67, last_column: 67 }
+            },
+            {
+              type: 'limitClause',
+              missing: true,
+              location: { first_line: 1, last_line: 1, first_column: 67, last_column: 67 }
+            }
+          ],
+          suggestAggregateFunctions: {
+            tables: [{ identifierChain: [{ name: 'testTable' }], alias: 'tt' }]
+          },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [
+              { identifierChain: [{ name: 'testTable' }], alias: 'tt' },
+              { identifierChain: [{ subQuery: 'bar' }] }
+            ]
+          },
+          suggestIdentifiers: [{ name: 'tt.', type: 'alias' }, { name: 'bar.', type: 'sub-query' }],
+          subQueries: [
+            {
+              alias: 'bar',
+              columns: [{ identifierChain: [{ name: 'abc' }, { name: 'bla' }], type: 'COLREF' }]
+            }
+          ],
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns for "select | from (select id i, name as n, bla from foo) bar"', () => {
+      assertAutoComplete({
+        beforeCursor: 'select ',
+        afterCursor: ' from (select id i, name as n, bla from foo) bar',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ subQuery: 'bar' }] }]
+          },
+          subQueries: [
+            {
+              alias: 'bar',
+              columns: [
+                { alias: 'i', identifierChain: [{ name: 'foo' }, { name: 'id' }], type: 'COLREF' },
+                {
+                  alias: 'n',
+                  identifierChain: [{ name: 'foo' }, { name: 'name' }],
+                  type: 'COLREF'
+                },
+                { identifierChain: [{ name: 'foo' }, { name: 'bla' }], type: 'COLREF' }
+              ]
+            }
+          ],
+          suggestIdentifiers: [{ name: 'bar.', type: 'sub-query' }],
+          lowerCase: true
+        }
+      });
+    });
+
+    it('should suggest sub-query columns for "SELECT bar.| FROM (SELECT col1, col2, (col3 + 1) col3alias FROM foo) bar"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT bar.',
+        afterCursor: ' FROM (SELECT col1, col2, (col3 + 1) col3alias FROM foo) bar',
+        expectedResult: {
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ subQuery: 'bar' }] }]
+          },
+          suggestKeywords: ['*'],
+          lowerCase: false,
+          subQueries: [
+            {
+              alias: 'bar',
+              columns: [
+                { identifierChain: [{ name: 'foo' }, { name: 'col1' }], type: 'COLREF' },
+                { identifierChain: [{ name: 'foo' }, { name: 'col2' }], type: 'COLREF' },
+                { alias: 'col3alias', type: 'NUMBER' }
+              ]
+            }
+          ]
+        }
+      });
+    });
+
+    it('should suggest sub-query columns for "SELECT bar.| FROM (SELECT b FROM foo) boo, (SELECT a FROM bla) bar"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT bar.',
+        afterCursor: ' FROM (SELECT b FROM foo) boo, (SELECT a FROM bla) bar',
+        expectedResult: {
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ subQuery: 'bar' }] }]
+          },
+          suggestKeywords: ['*'],
+          subQueries: [
+            {
+              alias: 'boo',
+              columns: [{ identifierChain: [{ name: 'foo' }, { name: 'b' }], type: 'COLREF' }]
+            },
+            {
+              alias: 'bar',
+              columns: [{ identifierChain: [{ name: 'bla' }, { name: 'a' }], type: 'COLREF' }]
+            }
+          ],
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest identifiers for "SELECT cos(| FROM (SELECT b FROM foo) boo, (SELECT a FROM bla) bar"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT cos(',
+        afterCursor: ' FROM (SELECT b FROM foo) boo, (SELECT a FROM bla) bar',
+        containsKeywords: ['CASE'],
+        expectedResult: {
+          suggestColumns: {
+            source: 'select',
+            types: ['T'],
+            tables: [
+              { identifierChain: [{ subQuery: 'boo' }] },
+              { identifierChain: [{ subQuery: 'bar' }] }
+            ]
+          },
+          suggestIdentifiers: [
+            { name: 'boo.', type: 'sub-query' },
+            { name: 'bar.', type: 'sub-query' }
+          ],
+          suggestFunctions: { types: ['T'] },
+          subQueries: [
+            {
+              alias: 'boo',
+              columns: [{ identifierChain: [{ name: 'foo' }, { name: 'b' }], type: 'COLREF' }]
+            },
+            {
+              alias: 'bar',
+              columns: [{ identifierChain: [{ name: 'bla' }, { name: 'a' }], type: 'COLREF' }]
+            }
+          ],
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest tables for "SELECT * FROM (SELECT |)"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM (SELECT ',
+        afterCursor: ')',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestTables: {
+            prependQuestionMark: true,
+            prependFrom: true
+          },
+          suggestDatabases: {
+            prependQuestionMark: true,
+            prependFrom: true,
+            appendDot: true
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM (SELECT | FROM tableOne) subQueryOne, someDb.tableTwo talias, (SELECT * FROM t3 JOIN t4 ON t3.id = t4.id) AS subQueryTwo;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM (SELECT ',
+        afterCursor:
+          ' FROM tableOne) subQueryOne, someDb.tableTwo talias, (SELECT * FROM t3 JOIN t4 ON t3.id = t4.id) AS subQueryTwo;',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [{ identifierChain: [{ name: 'tableOne' }] }] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ name: 'tableOne' }] }]
+          }
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor: ' FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ subQuery: 'subQueryTwo' }] }]
+          },
+          suggestIdentifiers: [{ name: 'subQueryTwo.', type: 'sub-query' }],
+          subQueries: [
+            {
+              alias: 'subQueryTwo',
+              columns: [{ tables: [{ identifierChain: [{ subQuery: 'subQueryOne' }] }] }],
+              subQueries: [
+                {
+                  alias: 'subQueryOne',
+                  columns: [{ tables: [{ identifierChain: [{ name: 'tableOne' }] }] }]
+                }
+              ]
+            }
+          ],
+          lowerCase: false
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT | FROM (SELECT * FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT ',
+        afterCursor:
+          ' FROM (SELECT * FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ subQuery: 'subQueryThree' }] }]
+          },
+          suggestIdentifiers: [{ name: 'subQueryThree.', type: 'sub-query' }],
+          subQueries: [
+            {
+              alias: 'subQueryThree',
+              columns: [{ tables: [{ identifierChain: [{ subQuery: 'subQueryTwo' }] }] }],
+              subQueries: [
+                {
+                  alias: 'subQueryTwo',
+                  columns: [{ tables: [{ identifierChain: [{ subQuery: 'subQueryOne' }] }] }],
+                  subQueries: [
+                    {
+                      alias: 'subQueryOne',
+                      columns: [{ tables: [{ identifierChain: [{ name: 'tableOne' }] }] }]
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM (SELECT | FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM (SELECT ',
+        afterCursor:
+          ' FROM (SELECT * FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ subQuery: 'subQueryTwo' }] }]
+          },
+          suggestIdentifiers: [{ name: 'subQueryTwo.', type: 'sub-query' }],
+          subQueries: [
+            {
+              alias: 'subQueryTwo',
+              columns: [{ tables: [{ identifierChain: [{ subQuery: 'subQueryOne' }] }] }],
+              subQueries: [
+                {
+                  alias: 'subQueryOne',
+                  columns: [{ tables: [{ identifierChain: [{ name: 'tableOne' }] }] }]
+                }
+              ]
+            }
+          ]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT * FROM (SELECT * FROM (SELECT | FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT * FROM (SELECT * FROM (SELECT ',
+        afterCursor: ' FROM (SELECT * FROM tableOne) subQueryOne) subQueryTwo) subQueryThree',
+        containsKeywords: ['*', 'ALL', 'DISTINCT'],
+        expectedResult: {
+          lowerCase: false,
+          suggestAggregateFunctions: { tables: [] },
+          suggestAnalyticFunctions: true,
+          suggestFunctions: {},
+          suggestColumns: {
+            source: 'select',
+            tables: [{ identifierChain: [{ subQuery: 'subQueryOne' }] }]
+          },
+          suggestIdentifiers: [{ name: 'subQueryOne.', type: 'sub-query' }],
+          subQueries: [
+            {
+              alias: 'subQueryOne',
+              columns: [{ tables: [{ identifierChain: [{ name: 'tableOne' }] }] }]
+            }
+          ]
+        }
+      });
+    });
+
+    it('should suggest columns for "SELECT s2.| FROM (SELECT a, bla FROM (SELECT a, b, abs(1) as bla FROM testTable) s1) s2;"', () => {
+      assertAutoComplete({
+        beforeCursor: 'SELECT s2.',
+        afterCursor:
+          ' FROM (SELECT a, bla FROM (SELECT a, b, abs(1) as bla FROM testTable) s1) s2;',
+        expectedResult: {
+          suggestColumns: { source: 'select', tables: [{ identifierChain: [{ subQuery: 's2' }] }] },
+          suggestKeywords: ['*'],
+          subQueries: [
+            {
+              alias: 's2',
+              columns: [
+                { identifierChain: [{ subQuery: 's1' }, { name: 'a' }], type: 'COLREF' },
+                { identifierChain: [{ subQuery: 's1' }, { name: 'bla' }], type: 'COLREF' }
+              ],
+              subQueries: [
+                {
+                  alias: 's1',
+                  columns: [
+                    { identifierChain: [{ name: 'testTable' }, { name: 'a' }], type: 'COLREF' },
+                    { identifierChain: [{ name: 'testTable' }, { name: 'b' }], type: 'COLREF' },
+                    { alias: 'bla', type: 'T' }
+                  ]
+                }
+              ]
+            }
+          ],
+          lowerCase: false
+        }
+      });
+    });
+  });
+});

+ 50 - 0
desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Set_Spec.js

@@ -0,0 +1,50 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import druidAutocompleteParser from '../druidAutocompleteParser';
+
+describe('druidAutocompleteParser.js SET statements', () => {
+  beforeAll(() => {
+    druidAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      druidAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  it('should suggest keywords for "|"', () => {
+    assertAutoComplete({
+      beforeCursor: '',
+      afterCursor: '',
+      containsKeywords: ['SET'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+});

+ 384 - 0
desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Update_Spec.js

@@ -0,0 +1,384 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import druidAutocompleteParser from '../druidAutocompleteParser';
+
+describe('druidAutocompleteParser.js UPDATE statements', () => {
+  beforeAll(() => {
+    druidAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      druidAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  it('should suggest keywords for "|"', () => {
+    assertAutoComplete({
+      beforeCursor: '',
+      afterCursor: '',
+      containsKeywords: ['UPDATE'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest keywords for "UPDATE bar  |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'UPDATE bar  ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestKeywords: ['SET'],
+        locations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 1, first_column: 1, last_column: 12 }
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'bar' }]
+          }
+        ]
+      }
+    });
+  });
+
+  it('should suggest keywords for "UPDATE bar SET id=1, foo=2 |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'UPDATE bar SET id=1, foo=2 ',
+      afterCursor: '',
+      containsKeywords: ['WHERE'],
+      expectedResult: {
+        lowerCase: false,
+        locations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 1, first_column: 1, last_column: 27 }
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 16, last_column: 18 },
+            identifierChain: [{ name: 'id' }],
+            tables: [{ identifierChain: [{ name: 'bar' }] }],
+            qualified: false
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 22, last_column: 25 },
+            identifierChain: [{ name: 'foo' }],
+            tables: [{ identifierChain: [{ name: 'bar' }] }],
+            qualified: false
+          }
+        ]
+      }
+    });
+  });
+
+  it('should suggest keywords for "UPDATE bar SET id |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'UPDATE bar SET id ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestKeywords: ['='],
+        locations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 1, first_column: 1, last_column: 18 }
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 16, last_column: 18 },
+            identifierChain: [{ name: 'id' }],
+            tables: [{ identifierChain: [{ name: 'bar' }] }],
+            qualified: false
+          }
+        ]
+      }
+    });
+  });
+
+  it('should suggest tables for "UPDATE |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'UPDATE ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {},
+        suggestDatabases: {
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest tables for "UPDATE bla|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'UPDATE bla',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: {},
+        suggestDatabases: {
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should suggest tables for "UPDATE bar.|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'UPDATE bar.',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: { identifierChain: [{ name: 'bar' }] }
+      }
+    });
+  });
+
+  it('should suggest tables for "UPDATE bar.foo|"', () => {
+    assertAutoComplete({
+      beforeCursor: 'UPDATE bar.foo',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestTables: { identifierChain: [{ name: 'bar' }] }
+      }
+    });
+  });
+
+  it('should suggest columns for "UPDATE bar.foo SET |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'UPDATE bar.foo SET ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestColumns: { tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }] },
+        locations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 1, first_column: 1, last_column: 19 }
+          },
+          {
+            type: 'database',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 12, last_column: 15 },
+            identifierChain: [{ name: 'bar' }, { name: 'foo' }]
+          }
+        ]
+      }
+    });
+  });
+
+  it('should suggest columns for "UPDATE bar.foo SET id = 1, bla = \'foo\', |"', () => {
+    assertAutoComplete({
+      beforeCursor: "UPDATE bar.foo SET id = 1, bla = 'foo', ",
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestColumns: { tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }] },
+        locations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 1, first_column: 1, last_column: 40 }
+          },
+          {
+            type: 'database',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 12, last_column: 15 },
+            identifierChain: [{ name: 'bar' }, { name: 'foo' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 20, last_column: 22 },
+            identifierChain: [{ name: 'id' }],
+            tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }],
+            qualified: false
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 28, last_column: 31 },
+            identifierChain: [{ name: 'bla' }],
+            tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }],
+            qualified: false
+          }
+        ]
+      }
+    });
+  });
+
+  it('should suggest columns for "UPDATE bar.foo SET bla = \'foo\' WHERE |"', () => {
+    assertAutoComplete({
+      beforeCursor: "UPDATE bar.foo SET bla = 'foo' WHERE ",
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestFunctions: {},
+        suggestColumns: { tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }] },
+        suggestFilters: { tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }] },
+        suggestKeywords: ['EXISTS', 'NOT EXISTS'],
+        locations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 1, first_column: 1, last_column: 37 }
+          },
+          {
+            type: 'database',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 12, last_column: 15 },
+            identifierChain: [{ name: 'bar' }, { name: 'foo' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 20, last_column: 23 },
+            identifierChain: [{ name: 'bla' }],
+            tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }],
+            qualified: false
+          }
+        ]
+      }
+    });
+  });
+
+  it('should suggest values for "UPDATE bar.foo SET bla = \'foo\' WHERE id = |"', () => {
+    assertAutoComplete({
+      beforeCursor: "UPDATE bar.foo SET bla = 'foo' WHERE id = ",
+      afterCursor: '',
+      containsKeywords: ['CASE'],
+      expectedResult: {
+        locations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 1, first_column: 1, last_column: 42 }
+          },
+          {
+            type: 'database',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 12, last_column: 15 },
+            identifierChain: [{ name: 'bar' }, { name: 'foo' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 20, last_column: 23 },
+            identifierChain: [{ name: 'bla' }],
+            tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }],
+            qualified: false
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 38, last_column: 40 },
+            identifierChain: [{ name: 'id' }],
+            tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }],
+            qualified: false
+          }
+        ],
+        suggestFunctions: { types: ['COLREF'] },
+        suggestValues: {},
+        colRef: { identifierChain: [{ name: 'bar' }, { name: 'foo' }, { name: 'id' }] },
+        suggestColumns: {
+          types: ['COLREF'],
+          tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }]
+        },
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest columns for "UPDATE bar.foo SET bla = \'foo\' WHERE id = 1 AND |"', () => {
+    assertAutoComplete({
+      beforeCursor: "UPDATE bar.foo SET bla = 'foo' WHERE id = 1 AND ",
+      afterCursor: '',
+      containsKeywords: ['CASE'],
+      expectedResult: {
+        lowerCase: false,
+        suggestFunctions: {},
+        suggestColumns: { tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }] },
+        suggestFilters: { tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }] },
+        locations: [
+          {
+            type: 'statement',
+            location: { first_line: 1, last_line: 1, first_column: 1, last_column: 48 }
+          },
+          {
+            type: 'database',
+            location: { first_line: 1, last_line: 1, first_column: 8, last_column: 11 },
+            identifierChain: [{ name: 'bar' }]
+          },
+          {
+            type: 'table',
+            location: { first_line: 1, last_line: 1, first_column: 12, last_column: 15 },
+            identifierChain: [{ name: 'bar' }, { name: 'foo' }]
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 20, last_column: 23 },
+            identifierChain: [{ name: 'bla' }],
+            tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }],
+            qualified: false
+          },
+          {
+            type: 'column',
+            location: { first_line: 1, last_line: 1, first_column: 38, last_column: 40 },
+            identifierChain: [{ name: 'id' }],
+            tables: [{ identifierChain: [{ name: 'bar' }, { name: 'foo' }] }],
+            qualified: false
+          }
+        ]
+      }
+    });
+  });
+});

+ 146 - 0
desktop/core/src/desktop/js/parse/sql/druid/spec/druidAutocompleteParser_Use_Spec.js

@@ -0,0 +1,146 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 SqlTestUtils from 'parse/spec/sqlTestUtils';
+import druidAutocompleteParser from '../druidAutocompleteParser';
+
+describe('druidAutocompleteParser.js USE statements', () => {
+  beforeAll(() => {
+    druidAutocompleteParser.yy.parseError = function(msg) {
+      throw Error(msg);
+    };
+    jasmine.addMatchers(SqlTestUtils.testDefinitionMatcher);
+  });
+
+  const assertAutoComplete = testDefinition => {
+    const debug = false;
+
+    expect(
+      druidAutocompleteParser.parseSql(
+        testDefinition.beforeCursor,
+        testDefinition.afterCursor,
+        debug
+      )
+    ).toEqualDefinition(testDefinition);
+  };
+
+  it('should suggest keywords for "|"', () => {
+    assertAutoComplete({
+      beforeCursor: '',
+      afterCursor: '',
+      containsKeywords: ['USE'],
+      expectedResult: {
+        lowerCase: false
+      }
+    });
+  });
+
+  it('should suggest databases for "USE |"', () => {
+    assertAutoComplete({
+      serverResponses: {},
+      beforeCursor: 'USE ',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestDatabases: {}
+      }
+    });
+  });
+
+  it('should suggest databases for "USE bla|"', () => {
+    assertAutoComplete({
+      serverResponses: {},
+      beforeCursor: 'USE bla',
+      afterCursor: '',
+      expectedResult: {
+        lowerCase: false,
+        suggestDatabases: {}
+      }
+    });
+  });
+
+  it('should use a use statement for "use database_two; \\nselect |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'use database_two; \nSELECT ',
+      afterCursor: '',
+      containsKeywords: ['*', 'ALL', 'DISTINCT'],
+      expectedResult: {
+        useDatabase: 'database_two',
+        lowerCase: true,
+        suggestAggregateFunctions: { tables: [] },
+        suggestAnalyticFunctions: true,
+        suggestFunctions: {},
+        suggestTables: {
+          prependQuestionMark: true,
+          prependFrom: true
+        },
+        suggestDatabases: {
+          prependQuestionMark: true,
+          prependFrom: true,
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should use the last use statement for "USE other_db; USE closest_db; \\n\\tSELECT |"', () => {
+    assertAutoComplete({
+      beforeCursor: 'USE other_db; USE closest_db; \n\tSELECT ',
+      afterCursor: '',
+      containsKeywords: ['*', 'ALL', 'DISTINCT'],
+      expectedResult: {
+        useDatabase: 'closest_db',
+        lowerCase: false,
+        suggestAggregateFunctions: { tables: [] },
+        suggestAnalyticFunctions: true,
+        suggestFunctions: {},
+        suggestTables: {
+          prependQuestionMark: true,
+          prependFrom: true
+        },
+        suggestDatabases: {
+          prependQuestionMark: true,
+          prependFrom: true,
+          appendDot: true
+        }
+      }
+    });
+  });
+
+  it('should use the use statement for "USE other_db; USE closest_db; \\n\\tSELECT |; USE some_other_db;"', () => {
+    assertAutoComplete({
+      beforeCursor: 'USE other_db; USE closest_db; \n\tSELECT ',
+      afterCursor: '; USE some_other_db;',
+      containsKeywords: ['*', 'ALL', 'DISTINCT'],
+      expectedResult: {
+        useDatabase: 'closest_db',
+        lowerCase: false,
+        suggestAggregateFunctions: { tables: [] },
+        suggestAnalyticFunctions: true,
+        suggestFunctions: {},
+        suggestTables: {
+          prependQuestionMark: true,
+          prependFrom: true
+        },
+        suggestDatabases: {
+          prependQuestionMark: true,
+          prependFrom: true,
+          appendDot: true
+        }
+      }
+    });
+  });
+});

+ 208 - 0
desktop/core/src/desktop/js/parse/sql/druid/spec/druidSyntaxParserSpec.js

@@ -0,0 +1,208 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 druidSyntaxParser from '../druidSyntaxParser';
+
+describe('druidSyntaxParser.js', () => {
+  const expectedToStrings = function(expected) {
+    return expected.map(ex => ex.text);
+  };
+
+  it('should not find errors for ""', () => {
+    const result = druidSyntaxParser.parseSyntax('', '');
+
+    expect(result).toBeFalsy();
+  });
+
+  it('should report incomplete statement for "SEL"', () => {
+    const result = druidSyntaxParser.parseSyntax('SEL', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should report incomplete statement for "SELECT"', () => {
+    const result = druidSyntaxParser.parseSyntax('SELECT', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should report incomplete statement for "SELECT "', () => {
+    const result = druidSyntaxParser.parseSyntax('SELECT ', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should not report incomplete statement for "SELECT * FROM tbl"', () => {
+    const result = druidSyntaxParser.parseSyntax('SELECT * FROM tbl', '');
+
+    expect(result.incompleteStatement).toBeFalsy();
+  });
+
+  it('should not report incomplete statement for "SELECT * FROM tbl LIMIT 1"', () => {
+    const result = druidSyntaxParser.parseSyntax('SELECT * FROM tbl LIMIT 1', '');
+
+    expect(result.incompleteStatement).toBeFalsy();
+  });
+
+  it('should report incomplete statement for "SELECT * FROM tbl LIMIT "', () => {
+    const result = druidSyntaxParser.parseSyntax('SELECT * FROM tbl LIMIT ', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should report incomplete statement for "SELECT * FROM tbl GROUP"', () => {
+    const result = druidSyntaxParser.parseSyntax('SELECT * FROM tbl GROUP', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should not find errors for "SELECT *"', () => {
+    const result = druidSyntaxParser.parseSyntax('SELECT *', '');
+
+    expect(result).toBeFalsy();
+  });
+
+  it('should not report incomplete statement for "SELECT * FR"', () => {
+    const result = druidSyntaxParser.parseSyntax('SELECT * FR', '');
+
+    expect(result.incompleteStatement).toBeTruthy();
+  });
+
+  it('should find errors for "SLELECT "', () => {
+    const result = druidSyntaxParser.parseSyntax('SLELECT ', '');
+
+    expect(result).toBeTruthy();
+    expect(result.text).toEqual('SLELECT');
+    expect(result.expected.length).toBeGreaterThan(0);
+    expect(result.loc.first_column).toEqual(0);
+    expect(result.loc.last_column).toEqual(7);
+  });
+
+  it('should find errors for "alter tabel "', () => {
+    const result = druidSyntaxParser.parseSyntax('alter tabel ', '');
+
+    expect(result).toBeTruthy();
+    expect(result.text).toEqual('tabel');
+    expect(result.expected.length).toBeGreaterThan(0);
+    expect(result.loc.first_column).toEqual(6);
+    expect(result.loc.last_column).toEqual(11);
+  });
+
+  it('should find errors for "select *  form "', () => {
+    const result = druidSyntaxParser.parseSyntax('select *  form ', '');
+
+    expect(result).toBeTruthy();
+    expect(result.loc.first_column).toEqual(10);
+    expect(result.loc.last_column).toEqual(14);
+    expect(expectedToStrings(result.expected)).toEqual(['from', 'union']);
+  });
+
+  it('should find errors for "select * from customers c cultster by awasd asd afd;"', () => {
+    const result = druidSyntaxParser.parseSyntax(
+      'select * from customers c cultster by awasd asd afd;',
+      ''
+    );
+
+    expect(result).toBeTruthy();
+  });
+
+  it('should find errors for "select asdf wer qwer qewr   qwer"', () => {
+    const result = druidSyntaxParser.parseSyntax('select asdf wer qwer qewr   qwer', '');
+
+    expect(result).toBeTruthy();
+  });
+
+  it('should suggest expected words for "SLELECT "', () => {
+    const result = druidSyntaxParser.parseSyntax('SLELECT ', '');
+
+    expect(result).toBeTruthy();
+    expect(expectedToStrings(result.expected)).toEqual([
+      'SELECT',
+      'SET',
+      'ALTER',
+      'INSERT',
+      'CREATE',
+      'USE',
+      'DROP',
+      'TRUNCATE',
+      'UPDATE',
+      'WITH'
+    ]);
+  });
+
+  it('should suggest expected words for "slelect "', () => {
+    const result = druidSyntaxParser.parseSyntax('slelect ', '');
+
+    expect(result).toBeTruthy();
+    expect(expectedToStrings(result.expected)).toEqual([
+      'select',
+      'set',
+      'alter',
+      'insert',
+      'create',
+      'use',
+      'drop',
+      'truncate',
+      'update',
+      'with'
+    ]);
+  });
+
+  it('should suggest expected that the statement should end for "use somedb extrastuff "', () => {
+    const result = druidSyntaxParser.parseSyntax('use somedb extrastuff  ', '');
+
+    expect(result).toBeTruthy();
+    expect(result.expectedStatementEnd).toBeTruthy();
+  });
+
+  const expectEqualIds = function(beforeA, afterA, beforeB, afterB) {
+    const resultA = druidSyntaxParser.parseSyntax(beforeA, afterA);
+    const resultB = druidSyntaxParser.parseSyntax(beforeB, afterB);
+
+    expect(resultA).toBeTruthy('"' + beforeA + '|' + afterA + '" was not reported as an error');
+    expect(resultB).toBeTruthy('"' + beforeB + '|' + afterB + '" was not reported as an error');
+    expect(resultA.ruleId).toEqual(resultB.ruleId);
+  };
+
+  const expectNonEqualIds = function(beforeA, afterA, beforeB, afterB) {
+    const resultA = druidSyntaxParser.parseSyntax(beforeA, afterA);
+    const resultB = druidSyntaxParser.parseSyntax(beforeB, afterB);
+
+    expect(resultA).toBeTruthy('"' + beforeA + '|' + afterA + '" was not reported as an error');
+    expect(resultB).toBeTruthy('"' + beforeB + '|' + afterB + '" was not reported as an error');
+    expect(resultA.ruleId).not.toEqual(resultB.ruleId);
+  };
+
+  it('should have unique rule IDs when the same rule is failing in different locations', () => {
+    expectEqualIds('SLELECT ', '', 'dlrop ', '');
+    expectEqualIds('SELECT * FORM ', '', 'SELECT * bla ', '');
+    expectEqualIds('DROP TABLE b.bla ERRROROR ', '', 'DROP TABLE c.cla OTHERERRRRORRR ', '');
+    expectEqualIds(
+      'SELECT * FROM a WHERE id = 1, a b SELECT ',
+      '',
+      'SELECT id, foo FROM a WHERE a b SELECT',
+      ''
+    );
+    expectEqualIds(
+      'SELECT * FROM a WHERE id = 1, a b SELECT ',
+      '',
+      'SELECT id, foo FROM a WHERE a b SELECT',
+      ''
+    );
+
+    expectNonEqualIds('slelect ', '', 'select * form ', '');
+  });
+});

+ 2221 - 0
desktop/core/src/desktop/js/parse/sql/druid/sqlParseSupport.js

@@ -0,0 +1,2221 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 { SqlFunctions } from 'sql/sqlFunctions';
+import stringDistance from 'sql/stringDistance';
+
+const identifierEquals = (a, b) =>
+  a &&
+  b &&
+  a
+    .replace(/^\s*`/, '')
+    .replace(/`\s*$/, '')
+    .toLowerCase() ===
+    b
+      .replace(/^\s*`/, '')
+      .replace(/`\s*$/, '')
+      .toLowerCase();
+
+// endsWith polyfill from hue_utils.js, needed as workers live in their own js environment
+if (!String.prototype.endsWith) {
+  String.prototype.endsWith = function(searchString, position) {
+    const subjectString = this.toString();
+    if (
+      typeof position !== 'number' ||
+      !isFinite(position) ||
+      Math.floor(position) !== position ||
+      position > subjectString.length
+    ) {
+      position = subjectString.length;
+    }
+    position -= searchString.length;
+    const lastIndex = subjectString.lastIndexOf(searchString, position);
+    return lastIndex !== -1 && lastIndex === position;
+  };
+}
+
+const equalIgnoreCase = (a, b) => a && b && a.toLowerCase() === b.toLowerCase();
+
+const SIMPLE_TABLE_REF_SUGGESTIONS = [
+  'suggestJoinConditions',
+  'suggestAggregateFunctions',
+  'suggestFilters',
+  'suggestGroupBys',
+  'suggestOrderBys'
+];
+
+const initSqlParser = function(parser) {
+  parser.prepareNewStatement = function() {
+    linkTablePrimaries();
+    parser.commitLocations();
+
+    delete parser.yy.latestCommonTableExpressions;
+    delete parser.yy.correlatedSubQuery;
+    parser.yy.subQueries = [];
+    parser.yy.selectListAliases = [];
+    parser.yy.latestTablePrimaries = [];
+
+    prioritizeSuggestions();
+  };
+
+  parser.yy.parseError = function(message, error) {
+    parser.yy.errors.push(error);
+    return message;
+  };
+
+  parser.addCommonTableExpressions = function(identifiers) {
+    parser.yy.result.commonTableExpressions = identifiers;
+    parser.yy.latestCommonTableExpressions = identifiers;
+  };
+
+  parser.isInSubquery = function() {
+    return !!parser.yy.primariesStack.length;
+  };
+
+  parser.pushQueryState = function() {
+    parser.yy.resultStack.push(parser.yy.result);
+    parser.yy.locationsStack.push(parser.yy.locations);
+    parser.yy.selectListAliasesStack.push(parser.yy.selectListAliases);
+    parser.yy.primariesStack.push(parser.yy.latestTablePrimaries);
+    parser.yy.subQueriesStack.push(parser.yy.subQueries);
+
+    parser.yy.result = {};
+    parser.yy.locations = [];
+    parser.yy.selectListAliases = []; // Not allowed in correlated sub-queries
+
+    if (parser.yy.correlatedSubQuery) {
+      parser.yy.latestTablePrimaries = parser.yy.latestTablePrimaries.concat();
+      parser.yy.subQueries = parser.yy.subQueries.concat();
+    } else {
+      parser.yy.latestTablePrimaries = [];
+      parser.yy.subQueries = [];
+    }
+  };
+
+  parser.popQueryState = function(subQuery) {
+    linkTablePrimaries();
+    parser.commitLocations();
+
+    if (Object.keys(parser.yy.result).length === 0) {
+      parser.yy.result = parser.yy.resultStack.pop();
+    } else {
+      parser.yy.resultStack.pop();
+    }
+    const oldSubQueries = parser.yy.subQueries;
+    parser.yy.subQueries = parser.yy.subQueriesStack.pop();
+    if (subQuery) {
+      if (oldSubQueries.length > 0) {
+        subQuery.subQueries = oldSubQueries;
+      }
+      parser.yy.subQueries.push(subQuery);
+    }
+
+    parser.yy.latestTablePrimaries = parser.yy.primariesStack.pop();
+    parser.yy.locations = parser.yy.locationsStack.pop();
+    parser.yy.selectListAliases = parser.yy.selectListAliasesStack.pop();
+  };
+
+  parser.suggestSelectListAliases = function() {
+    if (
+      parser.yy.selectListAliases &&
+      parser.yy.selectListAliases.length > 0 &&
+      parser.yy.result.suggestColumns &&
+      (typeof parser.yy.result.suggestColumns.identifierChain === 'undefined' ||
+        parser.yy.result.suggestColumns.identifierChain.length === 0)
+    ) {
+      parser.yy.result.suggestColumnAliases = parser.yy.selectListAliases;
+    }
+  };
+
+  parser.mergeSuggestKeywords = function() {
+    let result = [];
+    Array.prototype.slice.call(arguments).forEach(suggestion => {
+      if (typeof suggestion !== 'undefined' && typeof suggestion.suggestKeywords !== 'undefined') {
+        result = result.concat(suggestion.suggestKeywords);
+      }
+    });
+    if (result.length > 0) {
+      return { suggestKeywords: result };
+    }
+    return {};
+  };
+
+  parser.suggestValueExpressionKeywords = function(valueExpression, extras) {
+    const expressionKeywords = parser.getValueExpressionKeywords(valueExpression, extras);
+    parser.suggestKeywords(expressionKeywords.suggestKeywords);
+    if (expressionKeywords.suggestColRefKeywords) {
+      parser.suggestColRefKeywords(expressionKeywords.suggestColRefKeywords);
+    }
+    if (valueExpression.lastType) {
+      parser.addColRefIfExists(valueExpression.lastType);
+    } else {
+      parser.addColRefIfExists(valueExpression);
+    }
+  };
+
+  parser.getSelectListKeywords = function(excludeAsterisk) {
+    const keywords = [{ value: 'CASE', weight: 450 }, 'FALSE', 'TRUE', 'NULL'];
+    if (!excludeAsterisk) {
+      keywords.push({ value: '*', weight: 10000 });
+    }
+    return keywords;
+  };
+
+  parser.getValueExpressionKeywords = function(valueExpression, extras) {
+    const types = valueExpression.lastType ? valueExpression.lastType.types : valueExpression.types;
+    // We could have valueExpression.columnReference to suggest based on column type
+    let keywords = [
+      '<',
+      '<=',
+      '<=>',
+      '<>',
+      '=',
+      '>',
+      '>=',
+      'BETWEEN',
+      'IN',
+      'IS NOT NULL',
+      'IS NULL',
+      'IS NOT TRUE',
+      'IS TRUE',
+      'IS NOT FALSE',
+      'IS FALSE',
+      'NOT BETWEEN',
+      'NOT IN'
+    ];
+    if (extras) {
+      keywords = keywords.concat(extras);
+    }
+    if (valueExpression.suggestKeywords) {
+      keywords = keywords.concat(valueExpression.suggestKeywords);
+    }
+    if (types.length === 1 && types[0] === 'COLREF') {
+      return {
+        suggestKeywords: keywords,
+        suggestColRefKeywords: {
+          BOOLEAN: ['AND', 'OR'],
+          NUMBER: ['+', '-', '*', '/', '%', 'DIV'],
+          STRING: ['LIKE', 'NOT LIKE', 'REGEXP', 'RLIKE']
+        }
+      };
+    }
+    if (
+      typeof SqlFunctions === 'undefined' ||
+      SqlFunctions.matchesType(parser.yy.activeDialect, ['BOOLEAN'], types)
+    ) {
+      keywords = keywords.concat(['AND', 'OR']);
+    }
+    if (
+      typeof SqlFunctions === 'undefined' ||
+      SqlFunctions.matchesType(parser.yy.activeDialect, ['NUMBER'], types)
+    ) {
+      keywords = keywords.concat(['+', '-', '*', '/', '%', 'DIV']);
+    }
+    if (
+      typeof SqlFunctions === 'undefined' ||
+      SqlFunctions.matchesType(parser.yy.activeDialect, ['STRING'], types)
+    ) {
+      keywords = keywords.concat(['LIKE', 'NOT LIKE', 'REGEXP', 'RLIKE']);
+    }
+    return { suggestKeywords: keywords };
+  };
+
+  parser.getTypeKeywords = function() {
+    return [
+      'BIGINT',
+      'BOOLEAN',
+      'CHAR',
+      'DECIMAL',
+      'DOUBLE',
+      'FLOAT',
+      'INT',
+      'SMALLINT',
+      'TIMESTAMP',
+      'STRING',
+      'TINYINT',
+      'VARCHAR'
+    ];
+  };
+
+  parser.getColumnDataTypeKeywords = function() {
+    return parser.getTypeKeywords();
+  };
+
+  parser.addColRefIfExists = function(valueExpression) {
+    if (valueExpression.columnReference) {
+      parser.yy.result.colRef = { identifierChain: valueExpression.columnReference };
+    }
+  };
+
+  parser.selectListNoTableSuggest = function(selectListEdit, hasDistinctOrAll) {
+    if (selectListEdit.cursorAtStart) {
+      let keywords = parser.getSelectListKeywords();
+      if (!hasDistinctOrAll) {
+        keywords = keywords.concat([{ value: 'ALL', weight: 2 }, { value: 'DISTINCT', weight: 2 }]);
+      }
+      parser.suggestKeywords(keywords);
+    } else {
+      parser.checkForKeywords(selectListEdit);
+    }
+    if (selectListEdit.suggestFunctions) {
+      parser.suggestFunctions();
+    }
+    if (selectListEdit.suggestColumns) {
+      parser.suggestColumns();
+    }
+    if (
+      selectListEdit.suggestAggregateFunctions &&
+      (!hasDistinctOrAll || hasDistinctOrAll === 'ALL')
+    ) {
+      parser.suggestAggregateFunctions();
+      parser.suggestAnalyticFunctions();
+    }
+  };
+
+  parser.suggestJoinConditions = function(details) {
+    parser.yy.result.suggestJoinConditions = details || {};
+    if (parser.yy.latestTablePrimaries && !parser.yy.result.suggestJoinConditions.tablePrimaries) {
+      parser.yy.result.suggestJoinConditions.tablePrimaries = parser.yy.latestTablePrimaries.concat();
+    }
+  };
+
+  parser.suggestJoins = function(details) {
+    parser.yy.result.suggestJoins = details || {};
+  };
+
+  parser.valueExpressionSuggest = function(oppositeValueExpression, operator) {
+    if (oppositeValueExpression && oppositeValueExpression.columnReference) {
+      parser.suggestValues();
+      parser.yy.result.colRef = { identifierChain: oppositeValueExpression.columnReference };
+    }
+    parser.suggestColumns();
+    parser.suggestFunctions();
+    let keywords = [
+      { value: 'CASE', weight: 450 },
+      { value: 'FALSE', weight: 450 },
+      { value: 'NULL', weight: 450 },
+      { value: 'TRUE', weight: 450 }
+    ];
+    if (typeof oppositeValueExpression === 'undefined' || typeof operator === 'undefined') {
+      keywords = keywords.concat(['EXISTS', 'NOT']);
+    }
+    if (oppositeValueExpression && oppositeValueExpression.types[0] === 'NUMBER') {
+      parser.applyTypeToSuggestions(['NUMBER']);
+    }
+    parser.suggestKeywords(keywords);
+  };
+
+  parser.applyTypeToSuggestions = function(types) {
+    if (types[0] === 'BOOLEAN') {
+      return;
+    }
+    if (parser.yy.result.suggestFunctions && !parser.yy.result.suggestFunctions.types) {
+      parser.yy.result.suggestFunctions.types = types;
+    }
+    if (parser.yy.result.suggestColumns && !parser.yy.result.suggestColumns.types) {
+      parser.yy.result.suggestColumns.types = types;
+    }
+  };
+
+  parser.findCaseType = function(whenThenList) {
+    const types = {};
+    whenThenList.caseTypes.forEach(valueExpression => {
+      valueExpression.types.forEach(type => {
+        types[type] = true;
+      });
+    });
+    if (Object.keys(types).length === 1) {
+      return { types: [Object.keys(types)[0]] };
+    }
+    return { types: ['T'] };
+  };
+
+  parser.findReturnTypes = function(functionName) {
+    return typeof SqlFunctions === 'undefined'
+      ? ['T']
+      : SqlFunctions.getReturnTypes(parser.yy.activeDialect, functionName.toLowerCase());
+  };
+
+  parser.applyArgumentTypesToSuggestions = function(functionName, position) {
+    const foundArguments =
+      typeof SqlFunctions === 'undefined'
+        ? ['T']
+        : SqlFunctions.getArgumentTypes(
+            parser.yy.activeDialect,
+            functionName.toLowerCase(),
+            position
+          );
+    if (foundArguments.length === 0 && parser.yy.result.suggestColumns) {
+      delete parser.yy.result.suggestColumns;
+      delete parser.yy.result.suggestKeyValues;
+      delete parser.yy.result.suggestValues;
+      delete parser.yy.result.suggestFunctions;
+      delete parser.yy.result.suggestIdentifiers;
+      delete parser.yy.result.suggestKeywords;
+    } else {
+      parser.applyTypeToSuggestions(foundArguments);
+    }
+  };
+
+  parser.commitLocations = function() {
+    if (parser.yy.locations.length === 0) {
+      return;
+    }
+
+    const tablePrimaries = parser.yy.latestTablePrimaries;
+
+    let i = parser.yy.locations.length;
+
+    while (i--) {
+      const location = parser.yy.locations[i];
+      if (location.type === 'variable' && location.colRef) {
+        parser.expandIdentifierChain({
+          wrapper: location.colRef,
+          tablePrimaries: tablePrimaries,
+          isColumnWrapper: true
+        });
+        delete location.colRef.linked;
+      }
+
+      if (location.type === 'unknown') {
+        if (
+          typeof location.identifierChain !== 'undefined' &&
+          location.identifierChain.length > 0 &&
+          location.identifierChain.length <= 2 &&
+          tablePrimaries
+        ) {
+          let found = tablePrimaries.filter(primary => {
+            return (
+              equalIgnoreCase(primary.alias, location.identifierChain[0].name) ||
+              (primary.identifierChain &&
+                equalIgnoreCase(primary.identifierChain[0].name, location.identifierChain[0].name))
+            );
+          });
+          if (!found.length && location.firstInChain) {
+            found = tablePrimaries.filter(primary => {
+              return (
+                !primary.alias &&
+                primary.identifierChain &&
+                equalIgnoreCase(
+                  primary.identifierChain[primary.identifierChain.length - 1].name,
+                  location.identifierChain[0].name
+                )
+              );
+            });
+          }
+
+          if (found.length) {
+            if (
+              found[0].identifierChain.length > 1 &&
+              location.identifierChain.length === 1 &&
+              equalIgnoreCase(found[0].identifierChain[0].name, location.identifierChain[0].name)
+            ) {
+              location.type = 'database';
+            } else if (
+              found[0].alias &&
+              equalIgnoreCase(location.identifierChain[0].name, found[0].alias) &&
+              location.identifierChain.length > 1
+            ) {
+              location.type = 'column';
+              parser.expandIdentifierChain({
+                tablePrimaries: tablePrimaries,
+                wrapper: location,
+                anyOwner: true
+              });
+            } else if (
+              !found[0].alias &&
+              found[0].identifierChain &&
+              equalIgnoreCase(
+                location.identifierChain[0].name,
+                found[0].identifierChain[found[0].identifierChain.length - 1].name
+              ) &&
+              location.identifierChain.length > 1
+            ) {
+              location.type = 'column';
+              parser.expandIdentifierChain({
+                tablePrimaries: tablePrimaries,
+                wrapper: location,
+                anyOwner: true
+              });
+            } else {
+              location.type = 'table';
+              parser.expandIdentifierChain({
+                tablePrimaries: tablePrimaries,
+                wrapper: location,
+                anyOwner: true
+              });
+            }
+          } else if (parser.yy.subQueries) {
+            found = parser.yy.subQueries.filter(subQuery => {
+              return equalIgnoreCase(subQuery.alias, location.identifierChain[0].name);
+            });
+            if (found.length > 0) {
+              location.type = 'subQuery';
+              location.identifierChain = [{ subQuery: found[0].alias }];
+            }
+          }
+        }
+      }
+
+      if (location.type === 'asterisk' && !location.linked) {
+        if (tablePrimaries && tablePrimaries.length > 0) {
+          location.tables = [];
+          location.linked = false;
+          if (!location.identifierChain) {
+            location.identifierChain = [{ asterisk: true }];
+          }
+          parser.expandIdentifierChain({
+            tablePrimaries: tablePrimaries,
+            wrapper: location,
+            anyOwner: false
+          });
+          if (location.tables.length === 0) {
+            parser.yy.locations.splice(i, 1);
+          }
+        } else {
+          parser.yy.locations.splice(i, 1);
+        }
+      }
+
+      if (
+        location.type === 'table' &&
+        typeof location.identifierChain !== 'undefined' &&
+        location.identifierChain.length === 1 &&
+        location.identifierChain[0].name
+      ) {
+        // Could be a cte reference
+        parser.yy.locations.some(otherLocation => {
+          if (
+            otherLocation.type === 'alias' &&
+            otherLocation.source === 'cte' &&
+            identifierEquals(otherLocation.alias, location.identifierChain[0].name)
+          ) {
+            // TODO: Possibly add the other location if we want to show the link in the future.
+            //       i.e. highlight select definition on hover over alias, also for subquery references.
+            location.type = 'alias';
+            location.target = 'cte';
+            location.alias = location.identifierChain[0].name;
+            delete location.identifierChain;
+            return true;
+          }
+        });
+      }
+
+      if (
+        location.type === 'table' &&
+        (typeof location.identifierChain === 'undefined' || location.identifierChain.length === 0)
+      ) {
+        parser.yy.locations.splice(i, 1);
+      }
+
+      if (location.type === 'unknown') {
+        location.type = 'column';
+      }
+
+      // A column location might refer to a previously defined alias, i.e. last 'foo' in "SELECT cast(id AS int) foo FROM tbl ORDER BY foo;"
+      if (location.type === 'column') {
+        for (let j = i - 1; j >= 0; j--) {
+          const otherLocation = parser.yy.locations[j];
+          if (
+            otherLocation.type === 'alias' &&
+            otherLocation.source === 'column' &&
+            location.identifierChain &&
+            location.identifierChain.length === 1 &&
+            location.identifierChain[0].name &&
+            otherLocation.alias &&
+            location.identifierChain[0].name.toLowerCase() === otherLocation.alias.toLowerCase()
+          ) {
+            location.type = 'alias';
+            location.source = 'column';
+            location.alias = location.identifierChain[0].name;
+            delete location.identifierChain;
+            location.parentLocation = otherLocation.parentLocation;
+            break;
+          }
+        }
+      }
+
+      if (location.type === 'column') {
+        const initialIdentifierChain = location.identifierChain
+          ? location.identifierChain.concat()
+          : undefined;
+
+        parser.expandIdentifierChain({
+          tablePrimaries: tablePrimaries,
+          wrapper: location,
+          anyOwner: true,
+          isColumnWrapper: true,
+          isColumnLocation: true
+        });
+
+        if (typeof location.identifierChain === 'undefined') {
+          parser.yy.locations.splice(i, 1);
+        } else if (
+          location.identifierChain.length === 0 &&
+          initialIdentifierChain &&
+          initialIdentifierChain.length === 1
+        ) {
+          // This is for the case "SELECT tblOrColName FROM db.tblOrColName";
+          location.identifierChain = initialIdentifierChain;
+        }
+      }
+      if (location.type === 'column' && location.identifierChain) {
+        if (location.identifierChain.length > 1 && location.tables && location.tables.length > 0) {
+          location.type = 'complex';
+        }
+      }
+      delete location.firstInChain;
+      if (location.type !== 'column' && location.type !== 'complex') {
+        delete location.qualified;
+      } else if (typeof location.qualified === 'undefined') {
+        location.qualified = false;
+      }
+    }
+
+    if (parser.yy.locations.length > 0) {
+      parser.yy.allLocations = parser.yy.allLocations.concat(parser.yy.locations);
+      parser.yy.locations = [];
+    }
+  };
+
+  const prioritizeSuggestions = function() {
+    parser.yy.result.lowerCase = parser.yy.lowerCase || false;
+
+    const cteIndex = {};
+
+    if (typeof parser.yy.latestCommonTableExpressions !== 'undefined') {
+      parser.yy.latestCommonTableExpressions.forEach(cte => {
+        cteIndex[cte.alias.toLowerCase()] = cte;
+      });
+    }
+
+    SIMPLE_TABLE_REF_SUGGESTIONS.forEach(suggestionType => {
+      if (
+        suggestionType !== 'suggestAggregateFunctions' &&
+        typeof parser.yy.result[suggestionType] !== 'undefined' &&
+        parser.yy.result[suggestionType].tables.length === 0
+      ) {
+        delete parser.yy.result[suggestionType];
+      } else if (
+        typeof parser.yy.result[suggestionType] !== 'undefined' &&
+        typeof parser.yy.result[suggestionType].tables !== 'undefined'
+      ) {
+        for (let i = parser.yy.result[suggestionType].tables.length - 1; i >= 0; i--) {
+          const table = parser.yy.result[suggestionType].tables[i];
+          if (
+            table.identifierChain.length === 1 &&
+            typeof table.identifierChain[0].name !== 'undefined' &&
+            typeof cteIndex[table.identifierChain[0].name.toLowerCase()] !== 'undefined'
+          ) {
+            parser.yy.result[suggestionType].tables.splice(i, 1);
+          }
+        }
+      }
+    });
+
+    if (typeof parser.yy.result.colRef !== 'undefined') {
+      if (
+        !parser.yy.result.colRef.linked ||
+        typeof parser.yy.result.colRef.identifierChain === 'undefined' ||
+        parser.yy.result.colRef.identifierChain.length === 0
+      ) {
+        delete parser.yy.result.colRef;
+        if (typeof parser.yy.result.suggestColRefKeywords !== 'undefined') {
+          Object.keys(parser.yy.result.suggestColRefKeywords).forEach(type => {
+            parser.yy.result.suggestKeywords = parser.yy.result.suggestKeywords.concat(
+              parser.createWeightedKeywords(parser.yy.result.suggestColRefKeywords[type], -1)
+            );
+          });
+          delete parser.yy.result.suggestColRefKeywords;
+        }
+        if (
+          parser.yy.result.suggestColumns &&
+          parser.yy.result.suggestColumns.types.length === 1 &&
+          parser.yy.result.suggestColumns.types[0] === 'COLREF'
+        ) {
+          parser.yy.result.suggestColumns.types = ['T'];
+        }
+        delete parser.yy.result.suggestValues;
+      }
+    }
+
+    if (typeof parser.yy.result.colRef !== 'undefined') {
+      if (
+        !parser.yy.result.suggestValues &&
+        !parser.yy.result.suggestColRefKeywords &&
+        (!parser.yy.result.suggestColumns || parser.yy.result.suggestColumns.types[0] !== 'COLREF')
+      ) {
+        delete parser.yy.result.colRef;
+      }
+    }
+    if (
+      typeof parser.yy.result.suggestIdentifiers !== 'undefined' &&
+      parser.yy.result.suggestIdentifiers.length > 0
+    ) {
+      delete parser.yy.result.suggestTables;
+      delete parser.yy.result.suggestDatabases;
+    }
+    if (typeof parser.yy.result.suggestColumns !== 'undefined') {
+      const suggestColumns = parser.yy.result.suggestColumns;
+      if (typeof suggestColumns.tables === 'undefined' || suggestColumns.tables.length === 0) {
+        delete parser.yy.result.suggestColumns;
+        delete parser.yy.result.subQueries;
+      } else {
+        delete parser.yy.result.suggestTables;
+        delete parser.yy.result.suggestDatabases;
+
+        suggestColumns.tables.forEach(table => {
+          if (
+            typeof table.identifierChain !== 'undefined' &&
+            table.identifierChain.length === 1 &&
+            typeof table.identifierChain[0].name !== 'undefined'
+          ) {
+            const cte = cteIndex[table.identifierChain[0].name.toLowerCase()];
+            if (typeof cte !== 'undefined') {
+              delete table.identifierChain[0].name;
+              table.identifierChain[0].cte = cte.alias;
+            }
+          } else if (typeof table.identifierChain === 'undefined' && table.subQuery) {
+            table.identifierChain = [{ subQuery: table.subQuery }];
+            delete table.subQuery;
+          }
+        });
+
+        if (
+          typeof suggestColumns.identifierChain !== 'undefined' &&
+          suggestColumns.identifierChain.length === 0
+        ) {
+          delete suggestColumns.identifierChain;
+        }
+      }
+    } else {
+      delete parser.yy.result.subQueries;
+    }
+
+    if (typeof parser.yy.result.suggestJoinConditions !== 'undefined') {
+      if (
+        typeof parser.yy.result.suggestJoinConditions.tables === 'undefined' ||
+        parser.yy.result.suggestJoinConditions.tables.length === 0
+      ) {
+        delete parser.yy.result.suggestJoinConditions;
+      }
+    }
+
+    if (
+      typeof parser.yy.result.suggestTables !== 'undefined' &&
+      typeof parser.yy.result.commonTableExpressions !== 'undefined'
+    ) {
+      const ctes = [];
+      parser.yy.result.commonTableExpressions.forEach(cte => {
+        const suggestion = { name: cte.alias };
+        if (parser.yy.result.suggestTables.prependFrom) {
+          suggestion.prependFrom = true;
+        }
+        if (parser.yy.result.suggestTables.prependQuestionMark) {
+          suggestion.prependQuestionMark = true;
+        }
+        ctes.push(suggestion);
+      });
+      if (ctes.length > 0) {
+        parser.yy.result.suggestCommonTableExpressions = ctes;
+      }
+    }
+  };
+
+  parser.identifyPartials = function(beforeCursor, afterCursor) {
+    const beforeMatch = beforeCursor.match(/[0-9a-zA-Z_]*$/);
+    const afterMatch = afterCursor.match(/^[0-9a-zA-Z_]*(?:\((?:[^)]*\))?)?/);
+    return {
+      left: beforeMatch ? beforeMatch[0].length : 0,
+      right: afterMatch ? afterMatch[0].length : 0
+    };
+  };
+
+  const addCleanTablePrimary = function(tables, tablePrimary) {
+    if (tablePrimary.alias) {
+      tables.push({ alias: tablePrimary.alias, identifierChain: tablePrimary.identifierChain });
+    } else {
+      tables.push({ identifierChain: tablePrimary.identifierChain });
+    }
+  };
+
+  parser.expandIdentifierChain = function(options) {
+    const wrapper = options.wrapper;
+    const anyOwner = options.anyOwner;
+    const isColumnWrapper = options.isColumnWrapper;
+    const isColumnLocation = options.isColumnLocation;
+    let tablePrimaries = options.tablePrimaries || parser.yy.latestTablePrimaries;
+
+    if (typeof wrapper.identifierChain === 'undefined' || typeof tablePrimaries === 'undefined') {
+      return;
+    }
+    let identifierChain = wrapper.identifierChain.concat();
+
+    if (tablePrimaries.length === 0) {
+      delete wrapper.identifierChain;
+      return;
+    }
+
+    if (!anyOwner) {
+      tablePrimaries = filterTablePrimariesForOwner(tablePrimaries, wrapper.owner);
+    }
+
+    if (identifierChain.length > 0 && identifierChain[identifierChain.length - 1].asterisk) {
+      const tables = [];
+      tablePrimaries.forEach(tablePrimary => {
+        if (identifierChain.length > 1 && !tablePrimary.subQueryAlias) {
+          if (
+            identifierChain.length === 2 &&
+            equalIgnoreCase(tablePrimary.alias, identifierChain[0].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          } else if (
+            identifierChain.length === 2 &&
+            equalIgnoreCase(tablePrimary.identifierChain[0].name, identifierChain[0].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          } else if (
+            identifierChain.length === 3 &&
+            tablePrimary.identifierChain.length > 1 &&
+            equalIgnoreCase(tablePrimary.identifierChain[0].name, identifierChain[0].name) &&
+            equalIgnoreCase(tablePrimary.identifierChain[1].name, identifierChain[1].name)
+          ) {
+            addCleanTablePrimary(tables, tablePrimary);
+          }
+        } else if (tablePrimary.subQueryAlias) {
+          tables.push({ identifierChain: [{ subQuery: tablePrimary.subQueryAlias }] });
+        } else {
+          addCleanTablePrimary(tables, tablePrimary);
+        }
+      });
+      // Possible Joins
+      if (tables.length > 0) {
+        wrapper.tables = tables;
+        delete wrapper.identifierChain;
+        return;
+      }
+    }
+
+    // IdentifierChain contains a possibly started identifier or empty, example: a.b.c = ['a', 'b', 'c']
+    // Reduce the tablePrimaries to the one that matches the first identifier if found
+    let foundPrimary;
+    let doubleMatch = false;
+    let aliasMatch = false;
+    if (identifierChain.length > 0) {
+      for (let i = 0; i < tablePrimaries.length; i++) {
+        if (tablePrimaries[i].subQueryAlias) {
+          if (equalIgnoreCase(tablePrimaries[i].subQueryAlias, identifierChain[0].name)) {
+            foundPrimary = tablePrimaries[i];
+          }
+        } else if (equalIgnoreCase(tablePrimaries[i].alias, identifierChain[0].name)) {
+          foundPrimary = tablePrimaries[i];
+          aliasMatch = true;
+          break;
+        } else if (
+          tablePrimaries[i].identifierChain.length > 1 &&
+          identifierChain.length > 1 &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[0].name, identifierChain[0].name) &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[1].name, identifierChain[1].name)
+        ) {
+          foundPrimary = tablePrimaries[i];
+          doubleMatch = true;
+          break;
+        } else if (
+          !foundPrimary &&
+          equalIgnoreCase(tablePrimaries[i].identifierChain[0].name, identifierChain[0].name) &&
+          identifierChain.length > (isColumnLocation ? 1 : 0)
+        ) {
+          foundPrimary = tablePrimaries[i];
+          // No break as first two can still match.
+        } else if (
+          !foundPrimary &&
+          tablePrimaries[i].identifierChain.length > 1 &&
+          !tablePrimaries[i].alias &&
+          equalIgnoreCase(
+            tablePrimaries[i].identifierChain[tablePrimaries[i].identifierChain.length - 1].name,
+            identifierChain[0].name
+          )
+        ) {
+          // This is for the case SELECT baa. FROM bla.baa, blo.boo;
+          foundPrimary = tablePrimaries[i];
+          break;
+        }
+      }
+    }
+
+    if (foundPrimary) {
+      identifierChain.shift();
+      if (doubleMatch) {
+        identifierChain.shift();
+      }
+    } else if (tablePrimaries.length === 1 && !isColumnWrapper) {
+      foundPrimary = tablePrimaries[0];
+    }
+
+    if (foundPrimary) {
+      if (isColumnWrapper) {
+        wrapper.identifierChain = identifierChain;
+        if (foundPrimary.subQueryAlias) {
+          wrapper.tables = [{ subQuery: foundPrimary.subQueryAlias }];
+        } else if (foundPrimary.alias) {
+          if (!isColumnLocation && isColumnWrapper && aliasMatch) {
+            // TODO: add alias on table in suggestColumns (needs support in sqlAutocomplete3.js)
+            // the case is: SELECT cu.| FROM customers cu;
+            // This prevents alias from being added automatically in sqlAutocompleter.js
+            wrapper.tables = [{ identifierChain: foundPrimary.identifierChain }];
+          } else {
+            wrapper.tables = [
+              { identifierChain: foundPrimary.identifierChain, alias: foundPrimary.alias }
+            ];
+          }
+        } else {
+          wrapper.tables = [{ identifierChain: foundPrimary.identifierChain }];
+        }
+      } else {
+        if (foundPrimary.subQueryAlias) {
+          identifierChain.unshift({ subQuery: foundPrimary.subQueryAlias });
+        } else {
+          identifierChain = foundPrimary.identifierChain.concat(identifierChain);
+        }
+        if (wrapper.tables) {
+          wrapper.tables.push({ identifierChain: identifierChain });
+          delete wrapper.identifierChain;
+        } else {
+          wrapper.identifierChain = identifierChain;
+        }
+      }
+    } else {
+      if (isColumnWrapper) {
+        wrapper.tables = [];
+      }
+      tablePrimaries.forEach(tablePrimary => {
+        const targetTable = tablePrimary.subQueryAlias
+          ? { subQuery: tablePrimary.subQueryAlias }
+          : { identifierChain: tablePrimary.identifierChain };
+        if (tablePrimary.alias) {
+          targetTable.alias = tablePrimary.alias;
+        }
+        if (wrapper.tables) {
+          wrapper.tables.push(targetTable);
+        }
+      });
+    }
+    delete wrapper.owner;
+    wrapper.linked = true;
+  };
+
+  const filterTablePrimariesForOwner = function(tablePrimaries, owner) {
+    const result = [];
+    tablePrimaries.forEach(primary => {
+      if (typeof owner === 'undefined' && typeof primary.owner === 'undefined') {
+        result.push(primary);
+      } else if (owner === primary.owner) {
+        result.push(primary);
+      }
+    });
+    return result;
+  };
+
+  const convertTablePrimariesToSuggestions = function(tablePrimaries) {
+    const tables = [];
+    const identifiers = [];
+    tablePrimaries.forEach(tablePrimary => {
+      if (tablePrimary.identifierChain && tablePrimary.identifierChain.length > 0) {
+        const table = { identifierChain: tablePrimary.identifierChain };
+        if (tablePrimary.alias) {
+          table.alias = tablePrimary.alias;
+          identifiers.push({ name: table.alias + '.', type: 'alias' });
+        } else {
+          const lastIdentifier =
+            tablePrimary.identifierChain[tablePrimary.identifierChain.length - 1];
+          if (typeof lastIdentifier.name !== 'undefined') {
+            identifiers.push({ name: lastIdentifier.name + '.', type: 'table' });
+          } else if (typeof lastIdentifier.subQuery !== 'undefined') {
+            identifiers.push({ name: lastIdentifier.subQuery + '.', type: 'sub-query' });
+          }
+        }
+        tables.push(table);
+      } else if (tablePrimary.subQueryAlias) {
+        identifiers.push({ name: tablePrimary.subQueryAlias + '.', type: 'sub-query' });
+        tables.push({ identifierChain: [{ subQuery: tablePrimary.subQueryAlias }] });
+      }
+    });
+    if (identifiers.length > 0) {
+      if (typeof parser.yy.result.suggestIdentifiers === 'undefined') {
+        parser.yy.result.suggestIdentifiers = identifiers;
+      } else {
+        parser.yy.result.suggestIdentifiers = identifiers.concat(
+          parser.yy.result.suggestIdentifiers
+        );
+      }
+    }
+    parser.yy.result.suggestColumns.tables = tables;
+    if (
+      parser.yy.result.suggestColumns.identifierChain &&
+      parser.yy.result.suggestColumns.identifierChain.length === 0
+    ) {
+      delete parser.yy.result.suggestColumns.identifierChain;
+    }
+    parser.yy.result.suggestColumns.linked = true;
+  };
+
+  const linkTablePrimaries = function() {
+    if (!parser.yy.cursorFound || typeof parser.yy.latestTablePrimaries === 'undefined') {
+      return;
+    }
+
+    SIMPLE_TABLE_REF_SUGGESTIONS.forEach(suggestionType => {
+      if (
+        typeof parser.yy.result[suggestionType] !== 'undefined' &&
+        parser.yy.result[suggestionType].tablePrimaries &&
+        !parser.yy.result[suggestionType].linked
+      ) {
+        parser.yy.result[suggestionType].tables = [];
+        parser.yy.result[suggestionType].tablePrimaries.forEach(tablePrimary => {
+          if (!tablePrimary.subQueryAlias) {
+            parser.yy.result[suggestionType].tables.push(
+              tablePrimary.alias
+                ? {
+                    identifierChain: tablePrimary.identifierChain.concat(),
+                    alias: tablePrimary.alias
+                  }
+                : { identifierChain: tablePrimary.identifierChain.concat() }
+            );
+          }
+        });
+        delete parser.yy.result[suggestionType].tablePrimaries;
+        parser.yy.result[suggestionType].linked = true;
+      }
+    });
+
+    if (
+      typeof parser.yy.result.suggestColumns !== 'undefined' &&
+      !parser.yy.result.suggestColumns.linked
+    ) {
+      const tablePrimaries = filterTablePrimariesForOwner(
+        parser.yy.latestTablePrimaries,
+        parser.yy.result.suggestColumns.owner
+      );
+      if (!parser.yy.result.suggestColumns.tables) {
+        parser.yy.result.suggestColumns.tables = [];
+      }
+      if (parser.yy.subQueries.length > 0) {
+        parser.yy.result.subQueries = parser.yy.subQueries;
+      }
+      if (
+        typeof parser.yy.result.suggestColumns.identifierChain === 'undefined' ||
+        parser.yy.result.suggestColumns.identifierChain.length === 0
+      ) {
+        if (tablePrimaries.length > 1) {
+          convertTablePrimariesToSuggestions(tablePrimaries);
+        } else {
+          if (
+            tablePrimaries.length === 1 &&
+            (tablePrimaries[0].alias || tablePrimaries[0].subQueryAlias)
+          ) {
+            convertTablePrimariesToSuggestions(tablePrimaries);
+          }
+          parser.expandIdentifierChain({
+            wrapper: parser.yy.result.suggestColumns,
+            anyOwner: false,
+            isColumnWrapper: true
+          });
+        }
+      } else {
+        parser.expandIdentifierChain({
+          wrapper: parser.yy.result.suggestColumns,
+          anyOwner: false,
+          isColumnWrapper: true
+        });
+      }
+    }
+
+    if (typeof parser.yy.result.colRef !== 'undefined' && !parser.yy.result.colRef.linked) {
+      parser.expandIdentifierChain({ wrapper: parser.yy.result.colRef });
+
+      const primaries = filterTablePrimariesForOwner(parser.yy.latestTablePrimaries);
+      if (
+        primaries.length === 0 ||
+        (primaries.length > 1 && parser.yy.result.colRef.identifierChain.length === 1)
+      ) {
+        parser.yy.result.colRef.identifierChain = [];
+      }
+    }
+    if (
+      typeof parser.yy.result.suggestKeyValues !== 'undefined' &&
+      !parser.yy.result.suggestKeyValues.linked
+    ) {
+      parser.expandIdentifierChain({ wrapper: parser.yy.result.suggestKeyValues });
+    }
+  };
+
+  parser.getSubQuery = function(cols) {
+    const columns = [];
+    cols.selectList.forEach(col => {
+      const result = {};
+      if (col.alias) {
+        result.alias = col.alias;
+      }
+      if (col.valueExpression && col.valueExpression.columnReference) {
+        result.identifierChain = col.valueExpression.columnReference;
+      } else if (col.asterisk) {
+        result.identifierChain = [{ asterisk: true }];
+      }
+      if (
+        col.valueExpression &&
+        col.valueExpression.types &&
+        col.valueExpression.types.length === 1
+      ) {
+        result.type = col.valueExpression.types[0];
+      }
+
+      columns.push(result);
+    });
+
+    return {
+      columns: columns
+    };
+  };
+
+  parser.addTablePrimary = function(ref) {
+    if (typeof parser.yy.latestTablePrimaries === 'undefined') {
+      parser.yy.latestTablePrimaries = [];
+    }
+    parser.yy.latestTablePrimaries.push(ref);
+  };
+
+  parser.suggestFileFormats = function() {
+    parser.suggestKeywords([
+      'AVRO',
+      'KUDU',
+      'ORC',
+      'PARQUET',
+      'RCFILE',
+      'SEQUENCEFILE',
+      'TEXTFILE'
+    ]);
+  };
+
+  parser.getKeywordsForOptionalsLR = function(optionals, keywords, override) {
+    let result = [];
+
+    for (let i = 0; i < optionals.length; i++) {
+      if (!optionals[i] && (typeof override === 'undefined' || override[i])) {
+        if (keywords[i] instanceof Array) {
+          result = result.concat(keywords[i]);
+        } else {
+          result.push(keywords[i]);
+        }
+      } else if (optionals[i]) {
+        break;
+      }
+    }
+    return result;
+  };
+
+  parser.suggestDdlAndDmlKeywords = function(extraKeywords) {
+    let keywords = [
+      'ALTER',
+      'CREATE',
+      'DESCRIBE',
+      'DROP',
+      'GRANT',
+      'INSERT',
+      'REVOKE',
+      'SELECT',
+      'SET',
+      'SHOW',
+      'TRUNCATE',
+      'UPDATE',
+      'USE',
+      'WITH'
+    ];
+
+    if (extraKeywords) {
+      keywords = keywords.concat(extraKeywords);
+    }
+
+    parser.suggestKeywords(keywords);
+  };
+
+  parser.checkForSelectListKeywords = function(selectList) {
+    if (selectList.length === 0) {
+      return;
+    }
+    const last = selectList[selectList.length - 1];
+    if (!last || !last.valueExpression) {
+      return;
+    }
+    const valueExpressionKeywords = parser.getValueExpressionKeywords(last.valueExpression);
+    let keywords = [];
+    if (last.suggestKeywords) {
+      keywords = keywords.concat(last.suggestKeywords);
+    }
+    if (valueExpressionKeywords.suggestKeywords) {
+      keywords = keywords.concat(valueExpressionKeywords.suggestKeywords);
+    }
+    if (valueExpressionKeywords.suggestColRefKeywords) {
+      parser.suggestColRefKeywords(valueExpressionKeywords.suggestColRefKeywords);
+      parser.addColRefIfExists(last.valueExpression);
+    }
+    if (!last.alias) {
+      keywords.push('AS');
+    }
+    if (keywords.length > 0) {
+      parser.suggestKeywords(keywords);
+    }
+  };
+
+  parser.checkForKeywords = function(expression) {
+    if (expression) {
+      if (expression.suggestKeywords && expression.suggestKeywords.length > 0) {
+        parser.suggestKeywords(expression.suggestKeywords);
+      }
+      if (expression.suggestColRefKeywords) {
+        parser.suggestColRefKeywords(expression.suggestColRefKeywords);
+        parser.addColRefIfExists(expression);
+      }
+    }
+  };
+
+  parser.createWeightedKeywords = function(keywords, weight) {
+    const result = [];
+    keywords.forEach(keyword => {
+      if (typeof keyword.weight !== 'undefined') {
+        keyword.weight = weight + keyword.weight / 10;
+        result.push(keyword);
+      } else {
+        result.push({ value: keyword, weight: weight });
+      }
+    });
+    return result;
+  };
+
+  parser.suggestKeywords = function(keywords) {
+    const weightedKeywords = [];
+    if (keywords.length === 0) {
+      return;
+    }
+    keywords.forEach(keyword => {
+      if (typeof keyword.weight !== 'undefined') {
+        weightedKeywords.push(keyword);
+      } else {
+        weightedKeywords.push({ value: keyword, weight: -1 });
+      }
+    });
+    weightedKeywords.sort((a, b) => {
+      if (a.weight !== b.weight) {
+        return b.weight - a.weight;
+      }
+      return a.value.localeCompare(b.value);
+    });
+    parser.yy.result.suggestKeywords = weightedKeywords;
+  };
+
+  parser.suggestColRefKeywords = function(colRefKeywords) {
+    parser.yy.result.suggestColRefKeywords = colRefKeywords;
+  };
+
+  parser.suggestTablesOrColumns = function(identifier) {
+    if (typeof parser.yy.latestTablePrimaries == 'undefined') {
+      parser.suggestTables({ identifierChain: [{ name: identifier }] });
+      return;
+    }
+    const tableRef = parser.yy.latestTablePrimaries.filter(tablePrimary => {
+      return equalIgnoreCase(tablePrimary.alias, identifier);
+    });
+    if (tableRef.length > 0) {
+      parser.suggestColumns({ identifierChain: [{ name: identifier }] });
+    } else {
+      parser.suggestTables({ identifierChain: [{ name: identifier }] });
+    }
+  };
+
+  parser.suggestFunctions = function(details) {
+    parser.yy.result.suggestFunctions = details || {};
+  };
+
+  parser.suggestAggregateFunctions = function() {
+    const primaries = [];
+    const aliases = {};
+    parser.yy.latestTablePrimaries.forEach(primary => {
+      if (typeof primary.alias !== 'undefined') {
+        aliases[primary.alias] = true;
+      }
+      // Drop if the first one refers to a table alias (...FROM tbl t, t.map tm ...)
+      if (
+        typeof primary.identifierChain !== 'undefined' &&
+        !aliases[primary.identifierChain[0].name] &&
+        typeof primary.owner === 'undefined'
+      ) {
+        primaries.push(primary);
+      }
+    });
+    parser.yy.result.suggestAggregateFunctions = { tablePrimaries: primaries };
+  };
+
+  parser.suggestAnalyticFunctions = function() {
+    parser.yy.result.suggestAnalyticFunctions = true;
+  };
+
+  parser.suggestSetOptions = function() {
+    parser.yy.result.suggestSetOptions = true;
+  };
+
+  parser.suggestIdentifiers = function(identifiers) {
+    parser.yy.result.suggestIdentifiers = identifiers;
+  };
+
+  parser.suggestColumns = function(details) {
+    if (typeof details === 'undefined') {
+      details = { identifierChain: [] };
+    } else if (typeof details.identifierChain === 'undefined') {
+      details.identifierChain = [];
+    }
+    parser.yy.result.suggestColumns = details;
+  };
+
+  parser.suggestGroupBys = function(details) {
+    parser.yy.result.suggestGroupBys = details || {};
+  };
+
+  parser.suggestOrderBys = function(details) {
+    parser.yy.result.suggestOrderBys = details || {};
+  };
+
+  parser.suggestFilters = function(details) {
+    parser.yy.result.suggestFilters = details || {};
+  };
+
+  parser.suggestKeyValues = function(details) {
+    parser.yy.result.suggestKeyValues = details || {};
+  };
+
+  parser.suggestTables = function(details) {
+    parser.yy.result.suggestTables = details || {};
+  };
+
+  const adjustLocationForCursor = function(location) {
+    // columns are 0-based and lines not, so add 1 to cols
+    const newLocation = {
+      first_line: location.first_line,
+      last_line: location.last_line,
+      first_column: location.first_column + 1,
+      last_column: location.last_column + 1
+    };
+    if (parser.yy.cursorFound) {
+      if (
+        parser.yy.cursorFound.first_line === newLocation.first_line &&
+        parser.yy.cursorFound.last_column <= newLocation.first_column
+      ) {
+        let additionalSpace = parser.yy.partialLengths.left + parser.yy.partialLengths.right;
+        additionalSpace -= parser.yy.partialCursor ? 1 : 3; // For some reason the normal cursor eats 3 positions.
+        newLocation.first_column = newLocation.first_column + additionalSpace;
+        newLocation.last_column = newLocation.last_column + additionalSpace;
+      }
+    }
+    return newLocation;
+  };
+
+  parser.addFunctionLocation = function(location, functionName) {
+    // Remove trailing '(' from location
+    const adjustedLocation = {
+      first_line: location.first_line,
+      last_line: location.last_line,
+      first_column: location.first_column,
+      last_column: location.last_column - 1
+    };
+    parser.yy.locations.push({
+      type: 'function',
+      location: adjustLocationForCursor(adjustedLocation),
+      function: functionName.toLowerCase()
+    });
+  };
+
+  parser.addStatementLocation = function(location) {
+    // Don't report lonely cursor as a statement
+    if (
+      location.first_line === location.last_line &&
+      Math.abs(location.last_column - location.first_column) === 1
+    ) {
+      return;
+    }
+    let adjustedLocation;
+    if (
+      parser.yy.cursorFound &&
+      parser.yy.cursorFound.last_line === location.last_line &&
+      parser.yy.cursorFound.first_column >= location.first_column &&
+      parser.yy.cursorFound.last_column <= location.last_column
+    ) {
+      const additionalSpace = parser.yy.partialLengths.left + parser.yy.partialLengths.right;
+      adjustedLocation = {
+        first_line: location.first_line,
+        last_line: location.last_line,
+        first_column: location.first_column + 1,
+        last_column: location.last_column + additionalSpace - (parser.yy.partialCursor ? 0 : 2)
+      };
+    } else {
+      adjustedLocation = {
+        first_line: location.first_line,
+        last_line: location.last_line,
+        first_column: location.first_column + 1,
+        last_column: location.last_column + 1
+      };
+    }
+
+    parser.yy.locations.push({
+      type: 'statement',
+      location: adjustedLocation
+    });
+  };
+
+  parser.firstDefined = function() {
+    for (let i = 0; i + 1 < arguments.length; i += 2) {
+      if (arguments[i]) {
+        return arguments[i + 1];
+      }
+    }
+  };
+
+  parser.addClauseLocation = function(type, precedingLocation, locationIfPresent, isCursor) {
+    let location;
+    if (isCursor) {
+      if (parser.yy.partialLengths.left === 0 && parser.yy.partialLengths.right === 0) {
+        location = {
+          type: type,
+          missing: true,
+          location: adjustLocationForCursor({
+            first_line: precedingLocation.last_line,
+            first_column: precedingLocation.last_column,
+            last_line: precedingLocation.last_line,
+            last_column: precedingLocation.last_column
+          })
+        };
+      } else {
+        location = {
+          type: type,
+          missing: false,
+          location: {
+            first_line: locationIfPresent.last_line,
+            first_column: locationIfPresent.last_column - 1,
+            last_line: locationIfPresent.last_line,
+            last_column:
+              locationIfPresent.last_column -
+              1 +
+              parser.yy.partialLengths.right +
+              parser.yy.partialLengths.left
+          }
+        };
+      }
+    } else {
+      location = {
+        type: type,
+        missing: !locationIfPresent,
+        location: adjustLocationForCursor(
+          locationIfPresent || {
+            first_line: precedingLocation.last_line,
+            first_column: precedingLocation.last_column,
+            last_line: precedingLocation.last_line,
+            last_column: precedingLocation.last_column
+          }
+        )
+      };
+    }
+    if (parser.isInSubquery()) {
+      location.subquery = true;
+    }
+    parser.yy.locations.push(location);
+  };
+
+  parser.addStatementTypeLocation = function(identifier, location, additionalText) {
+    // Don't add if already there except for SELECT
+    if (identifier !== 'SELECT' && parser.yy.allLocations) {
+      for (let i = parser.yy.allLocations.length - 1; i >= 0; i--) {
+        if (parser.yy.allLocations[i] && parser.yy.allLocations[i].type === 'statement') {
+          break;
+        }
+        if (parser.yy.allLocations[i] && parser.yy.allLocations[i].type === 'statementType') {
+          return;
+        }
+      }
+    }
+    const loc = {
+      type: 'statementType',
+      location: adjustLocationForCursor(location),
+      identifier: identifier
+    };
+    if (typeof additionalText !== 'undefined') {
+      switch (identifier) {
+        case 'ALTER':
+          if (/ALTER\s+VIEW/i.test(additionalText)) {
+            loc.identifier = 'ALTER VIEW';
+          } else {
+            loc.identifier = 'ALTER TABLE';
+          }
+          break;
+        case 'COMPUTE':
+          loc.identifier = 'COMPUTE STATS';
+          break;
+        case 'CREATE':
+          if (/CREATE\s+VIEW/i.test(additionalText)) {
+            loc.identifier = 'CREATE VIEW';
+          } else if (/CREATE\s+TABLE/i.test(additionalText)) {
+            loc.identifier = 'CREATE TABLE';
+          } else if (/CREATE\s+DATABASE/i.test(additionalText)) {
+            loc.identifier = 'CREATE DATABASE';
+          } else if (/CREATE\s+ROLE/i.test(additionalText)) {
+            loc.identifier = 'CREATE ROLE';
+          } else if (/CREATE\s+FUNCTION/i.test(additionalText)) {
+            loc.identifier = 'CREATE FUNCTION';
+          } else {
+            loc.identifier = 'CREATE TABLE';
+          }
+          break;
+        case 'DROP':
+          if (/DROP\s+VIEW/i.test(additionalText)) {
+            loc.identifier = 'DROP VIEW';
+          } else if (/DROP\s+TABLE/i.test(additionalText)) {
+            loc.identifier = 'DROP TABLE';
+          } else if (/DROP\s+DATABASE/i.test(additionalText)) {
+            loc.identifier = 'DROP DATABASE';
+          } else if (/DROP\s+ROLE/i.test(additionalText)) {
+            loc.identifier = 'DROP ROLE';
+          } else if (/DROP\s+STATS/i.test(additionalText)) {
+            loc.identifier = 'DROP STATS';
+          } else if (/DROP\s+FUNCTION/i.test(additionalText)) {
+            loc.identifier = 'DROP FUNCTION';
+          } else {
+            loc.identifier = 'DROP TABLE';
+          }
+          break;
+        case 'INVALIDATE':
+          loc.identifier = 'INVALIDATE METADATA';
+          break;
+        case 'LOAD':
+          loc.identifier = 'LOAD DATA';
+          break;
+        case 'TRUNCATE':
+          loc.identifier = 'TRUNCATE TABLE';
+          break;
+        default:
+      }
+    }
+    parser.yy.locations.push(loc);
+  };
+
+  parser.addFileLocation = function(location, path) {
+    parser.yy.locations.push({
+      type: 'file',
+      location: adjustLocationForCursor(location),
+      path: path
+    });
+  };
+
+  parser.addDatabaseLocation = function(location, identifierChain) {
+    parser.yy.locations.push({
+      type: 'database',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addTableLocation = function(location, identifierChain) {
+    parser.yy.locations.push({
+      type: 'table',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addColumnAliasLocation = function(location, alias, parentLocation) {
+    const aliasLocation = {
+      type: 'alias',
+      source: 'column',
+      alias: alias,
+      location: adjustLocationForCursor(location),
+      parentLocation: adjustLocationForCursor(parentLocation)
+    };
+    if (
+      parser.yy.locations.length &&
+      parser.yy.locations[parser.yy.locations.length - 1].type === 'column'
+    ) {
+      const closestColumn = parser.yy.locations[parser.yy.locations.length - 1];
+      if (
+        closestColumn.location.first_line === aliasLocation.parentLocation.first_line &&
+        closestColumn.location.last_line === aliasLocation.parentLocation.last_line &&
+        closestColumn.location.first_column === aliasLocation.parentLocation.first_column &&
+        closestColumn.location.last_column === aliasLocation.parentLocation.last_column
+      ) {
+        parser.yy.locations[parser.yy.locations.length - 1].alias = alias;
+      }
+    }
+    parser.yy.locations.push(aliasLocation);
+  };
+
+  parser.addTableAliasLocation = function(location, alias, identifierChain) {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'table',
+      alias: alias,
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addSubqueryAliasLocation = function(location, alias) {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'subquery',
+      alias: alias,
+      location: adjustLocationForCursor(location)
+    });
+  };
+
+  parser.addAsteriskLocation = function(location, identifierChain) {
+    parser.yy.locations.push({
+      type: 'asterisk',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addVariableLocation = function(location, value) {
+    if (/\${[^}]*}/.test(value)) {
+      parser.yy.locations.push({
+        type: 'variable',
+        location: adjustLocationForCursor(location),
+        value: value
+      });
+    }
+  };
+
+  parser.addColumnLocation = function(location, identifierChain) {
+    const isVariable =
+      identifierChain.length && /\${[^}]*}/.test(identifierChain[identifierChain.length - 1].name);
+    if (isVariable) {
+      parser.yy.locations.push({
+        type: 'variable',
+        location: adjustLocationForCursor(location),
+        value: identifierChain[identifierChain.length - 1].name
+      });
+    } else {
+      parser.yy.locations.push({
+        type: 'column',
+        location: adjustLocationForCursor(location),
+        identifierChain: identifierChain,
+        qualified: identifierChain.length > 1
+      });
+    }
+  };
+
+  parser.addCteAliasLocation = function(location, alias) {
+    parser.yy.locations.push({
+      type: 'alias',
+      source: 'cte',
+      alias: alias,
+      location: adjustLocationForCursor(location)
+    });
+  };
+
+  parser.addUnknownLocation = function(location, identifierChain) {
+    const isVariable =
+      identifierChain.length && /\${[^}]*}/.test(identifierChain[identifierChain.length - 1].name);
+    let loc;
+    if (isVariable) {
+      loc = {
+        type: 'variable',
+        location: adjustLocationForCursor(location),
+        value: identifierChain[identifierChain.length - 1].name
+      };
+    } else {
+      loc = {
+        type: 'unknown',
+        location: adjustLocationForCursor(location),
+        identifierChain: identifierChain,
+        qualified: identifierChain.length > 1
+      };
+    }
+    parser.yy.locations.push(loc);
+    return loc;
+  };
+
+  parser.addNewDatabaseLocation = function(location, identifierChain) {
+    parser.yy.definitions.push({
+      type: 'database',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain
+    });
+  };
+
+  parser.addNewTableLocation = function(location, identifierChain, colSpec) {
+    const columns = [];
+    if (colSpec) {
+      colSpec.forEach(col => {
+        columns.push({
+          identifierChain: [col.identifier], // TODO: Complex
+          type: col.type,
+          location: adjustLocationForCursor(col.location)
+        });
+      });
+    }
+    parser.yy.definitions.push({
+      type: 'table',
+      location: adjustLocationForCursor(location),
+      identifierChain: identifierChain,
+      columns: columns
+    });
+  };
+
+  parser.addColRefToVariableIfExists = function(left, right) {
+    if (
+      left &&
+      left.columnReference &&
+      left.columnReference.length &&
+      right &&
+      right.columnReference &&
+      right.columnReference.length &&
+      parser.yy.locations.length > 1
+    ) {
+      const addColRefToVariableLocation = function(variableValue, colRef) {
+        // See if colref is actually an alias
+        if (colRef.length === 1 && colRef[0].name) {
+          parser.yy.locations.some(location => {
+            if (location.type === 'column' && location.alias === colRef[0].name) {
+              colRef = location.identifierChain;
+              return true;
+            }
+          });
+        }
+
+        for (let i = parser.yy.locations.length - 1; i > 0; i--) {
+          const location = parser.yy.locations[i];
+          if (location.type === 'variable' && location.value === variableValue) {
+            location.colRef = { identifierChain: colRef };
+            break;
+          }
+        }
+      };
+
+      if (/\${[^}]*}/.test(left.columnReference[0].name)) {
+        // left is variable
+        addColRefToVariableLocation(left.columnReference[0].name, right.columnReference);
+      } else if (/\${[^}]*}/.test(right.columnReference[0].name)) {
+        // right is variable
+        addColRefToVariableLocation(right.columnReference[0].name, left.columnReference);
+      }
+    }
+  };
+
+  parser.suggestDatabases = function(details) {
+    parser.yy.result.suggestDatabases = details || {};
+  };
+
+  parser.suggestHdfs = function(details) {
+    parser.yy.result.suggestHdfs = details || {};
+  };
+
+  parser.suggestValues = function(details) {
+    parser.yy.result.suggestValues = details || {};
+  };
+
+  parser.determineCase = function(text) {
+    if (!parser.yy.caseDetermined) {
+      parser.yy.lowerCase = text.toLowerCase() === text;
+      parser.yy.caseDetermined = true;
+    }
+  };
+
+  parser.handleQuotedValueWithCursor = function(lexer, yytext, yylloc, quoteChar) {
+    if (yytext.indexOf('\u2020') !== -1 || yytext.indexOf('\u2021') !== -1) {
+      parser.yy.partialCursor = yytext.indexOf('\u2021') !== -1;
+      const cursorIndex = parser.yy.partialCursor
+        ? yytext.indexOf('\u2021')
+        : yytext.indexOf('\u2020');
+      parser.yy.cursorFound = {
+        first_line: yylloc.first_line,
+        last_line: yylloc.last_line,
+        first_column: yylloc.first_column + cursorIndex,
+        last_column: yylloc.first_column + cursorIndex + 1
+      };
+      const remainder = yytext.substring(cursorIndex + 1);
+      const remainingQuotes = (lexer.upcomingInput().match(new RegExp(quoteChar, 'g')) || [])
+        .length;
+      if (remainingQuotes > 0 && (remainingQuotes & 1) !== 0) {
+        parser.yy.missingEndQuote = false;
+        lexer.input();
+      } else {
+        parser.yy.missingEndQuote = true;
+        lexer.unput(remainder);
+      }
+      lexer.popState();
+      return true;
+    }
+    return false;
+  };
+
+  let lexerModified = false;
+
+  /**
+   * Main parser function
+   */
+  parser.parseSql = function(beforeCursor, afterCursor, debug) {
+    // Jison counts CRLF as two lines in the locations
+    beforeCursor = beforeCursor.replace(/\r\n|\n\r/gm, '\n');
+    afterCursor = afterCursor.replace(/\r\n|\n\r/gm, '\n');
+    parser.yy.result = { locations: [] };
+    parser.yy.lowerCase = false;
+    parser.yy.locations = [];
+    parser.yy.definitions = [];
+    parser.yy.allLocations = [];
+    parser.yy.subQueries = [];
+    parser.yy.errors = [];
+    parser.yy.selectListAliases = [];
+    parser.yy.activeDialect = 'generic';
+
+    parser.yy.locationsStack = [];
+    parser.yy.primariesStack = [];
+    parser.yy.subQueriesStack = [];
+    parser.yy.resultStack = [];
+    parser.yy.selectListAliasesStack = [];
+
+    delete parser.yy.caseDetermined;
+    delete parser.yy.cursorFound;
+    delete parser.yy.partialCursor;
+
+    // Fix for parser bug when switching lexer states
+    if (!lexerModified) {
+      const originalSetInput = parser.lexer.setInput;
+      parser.lexer.setInput = function(input, yy) {
+        return originalSetInput.bind(parser.lexer)(input, yy);
+      };
+      lexerModified = true;
+    }
+
+    parser.prepareNewStatement();
+
+    const REASONABLE_SURROUNDING_LENGTH = 150000; // About 3000 lines before and after
+
+    if (beforeCursor.length > REASONABLE_SURROUNDING_LENGTH) {
+      if (beforeCursor.length - beforeCursor.lastIndexOf(';') > REASONABLE_SURROUNDING_LENGTH) {
+        // Bail out if the last complete statement is more than 150000 chars before
+        return {};
+      }
+      // Cut it at the first statement found within 150000 chars before
+      const lastReasonableChunk = beforeCursor.substring(
+        beforeCursor.length - REASONABLE_SURROUNDING_LENGTH
+      );
+      beforeCursor = lastReasonableChunk.substring(lastReasonableChunk.indexOf(';') + 1);
+    }
+
+    if (afterCursor.length > REASONABLE_SURROUNDING_LENGTH) {
+      if (afterCursor.length - afterCursor.indexOf(';') > REASONABLE_SURROUNDING_LENGTH) {
+        // No need to bail out for what's comes after, we can still get keyword completion
+        afterCursor = '';
+      } else {
+        // Cut it at the last statement found within 150000 chars after
+        const firstReasonableChunk = afterCursor.substring(0, REASONABLE_SURROUNDING_LENGTH);
+        afterCursor = firstReasonableChunk.substring(0, firstReasonableChunk.lastIndexOf(';'));
+      }
+    }
+
+    parser.yy.partialLengths = parser.identifyPartials(beforeCursor, afterCursor);
+
+    if (parser.yy.partialLengths.left > 0) {
+      beforeCursor = beforeCursor.substring(0, beforeCursor.length - parser.yy.partialLengths.left);
+    }
+
+    if (parser.yy.partialLengths.right > 0) {
+      afterCursor = afterCursor.substring(parser.yy.partialLengths.right);
+    }
+
+    let result;
+    try {
+      // Add |CURSOR| or |PARTIAL_CURSOR| to represent the different cursor states in the lexer
+      result = parser.parse(
+        beforeCursor +
+          (beforeCursor.length === 0 || /[\s(]$/.test(beforeCursor) ? ' \u2020 ' : '\u2021') +
+          afterCursor
+      );
+    } catch (err) {
+      // On any error try to at least return any existing result
+      if (typeof parser.yy.result === 'undefined') {
+        throw err;
+      }
+      if (debug) {
+        console.warn(err);
+        console.warn(err.stack);
+      }
+      result = parser.yy.result;
+    }
+    if (parser.yy.errors.length > 0) {
+      parser.yy.result.errors = parser.yy.errors;
+      if (debug) {
+        console.warn(parser.yy.errors);
+      }
+    }
+    try {
+      linkTablePrimaries();
+      parser.commitLocations();
+      // Clean up and prioritize
+      prioritizeSuggestions();
+    } catch (err) {
+      if (debug) {
+        console.warn(err);
+        console.warn(err.stack);
+      }
+    }
+
+    parser.yy.allLocations.sort((a, b) => {
+      if (a.location.first_line !== b.location.first_line) {
+        return a.location.first_line - b.location.first_line;
+      }
+      if (a.location.first_column !== b.location.first_column) {
+        return a.location.first_column - b.location.first_column;
+      }
+      if (a.location.last_column !== b.location.last_column) {
+        return b.location.last_column - a.location.last_column;
+      }
+      return b.type.localeCompare(a.type);
+    });
+    parser.yy.result.locations = parser.yy.allLocations;
+    parser.yy.result.definitions = parser.yy.definitions;
+
+    parser.yy.result.locations.forEach(location => {
+      delete location.linked;
+    });
+    if (typeof parser.yy.result.suggestColumns !== 'undefined') {
+      delete parser.yy.result.suggestColumns.linked;
+    }
+
+    SIMPLE_TABLE_REF_SUGGESTIONS.forEach(suggestionType => {
+      if (typeof parser.yy.result[suggestionType] !== 'undefined') {
+        delete parser.yy.result[suggestionType].linked;
+      }
+    });
+
+    if (typeof parser.yy.result.colRef !== 'undefined') {
+      delete parser.yy.result.colRef.linked;
+    }
+    if (typeof parser.yy.result.suggestKeyValues !== 'undefined') {
+      delete parser.yy.result.suggestKeyValues.linked;
+    }
+
+    if (typeof result.error !== 'undefined' && typeof result.error.expected !== 'undefined') {
+      // Remove the cursor from expected tokens
+      result.error.expected = result.error.expected.filter(token => token.indexOf('CURSOR') === -1);
+    }
+
+    if (typeof result.error !== 'undefined' && result.error.recoverable) {
+      delete result.error;
+    }
+
+    // Adjust all the statement locations to include white space surrounding them
+    let lastStatementLocation = null;
+    result.locations.forEach(location => {
+      if (location.type === 'statement') {
+        if (lastStatementLocation === null) {
+          location.location.first_line = 1;
+          location.location.first_column = 1;
+        } else {
+          location.location.first_line = lastStatementLocation.location.last_line;
+          location.location.first_column = lastStatementLocation.location.last_column + 1;
+        }
+        lastStatementLocation = location;
+      }
+    });
+
+    return result;
+  };
+};
+
+const SYNTAX_PARSER_NOOP_FUNCTIONS = [
+  'addAsteriskLocation',
+  'addClauseLocation',
+  'addColRefIfExists',
+  'addColRefToVariableIfExists',
+  'addColumnAliasLocation',
+  'addColumnLocation',
+  'addCommonTableExpressions',
+  'addCteAliasLocation',
+  'addDatabaseLocation',
+  'addFileLocation',
+  'addFunctionLocation',
+  'addNewDatabaseLocation',
+  'addNewTableLocation',
+  'addStatementLocation',
+  'addStatementTypeLocation',
+  'addSubqueryAliasLocation',
+  'addTableAliasLocation',
+  'addTableLocation',
+  'addTablePrimary',
+  'addUnknownLocation',
+  'addVariableLocation',
+  'applyArgumentTypesToSuggestions',
+  'applyTypeToSuggestions',
+  'checkForKeywords',
+  'checkForSelectListKeywords',
+  'commitLocations',
+  'firstDefined',
+  'getSelectListKeywords',
+  'getSubQuery',
+  'getValueExpressionKeywords',
+  'identifyPartials',
+  'popQueryState',
+  'prepareNewStatement',
+  'pushQueryState',
+  'selectListNoTableSuggest',
+  'suggestAggregateFunctions',
+  'suggestAnalyticFunctions',
+  'suggestColRefKeywords',
+  'suggestColumns',
+  'suggestDatabases',
+  'suggestDdlAndDmlKeywords',
+  'suggestFileFormats',
+  'suggestFilters',
+  'suggestFunctions',
+  'suggestGroupBys',
+  'suggestHdfs',
+  'suggestIdentifiers',
+  'suggestJoinConditions',
+  'suggestJoins',
+  'suggestKeyValues',
+  'suggestKeywords',
+  'suggestOrderBys',
+  'suggestSelectListAliases',
+  'suggestTables',
+  'suggestTablesOrColumns',
+  'suggestValueExpressionKeywords',
+  'suggestValues',
+  'valueExpressionSuggest'
+];
+
+const SYNTAX_PARSER_NOOP = function() {};
+
+const initSyntaxParser = function(parser) {
+  // Noop functions for compatibility with the autocomplete parser as the grammar is shared
+  SYNTAX_PARSER_NOOP_FUNCTIONS.forEach(noopFn => {
+    parser[noopFn] = SYNTAX_PARSER_NOOP;
+  });
+
+  parser.yy.locations = [{}];
+
+  parser.determineCase = function(text) {
+    if (!parser.yy.caseDetermined) {
+      parser.yy.lowerCase = text.toLowerCase() === text;
+      parser.yy.caseDetermined = true;
+    }
+  };
+
+  parser.getKeywordsForOptionalsLR = function() {
+    return [];
+  };
+
+  parser.mergeSuggestKeywords = function() {
+    return {};
+  };
+
+  parser.getTypeKeywords = function() {
+    return [];
+  };
+
+  parser.getColumnDataTypeKeywords = function() {
+    return [];
+  };
+
+  parser.findCaseType = function() {
+    return { types: ['T'] };
+  };
+
+  parser.findReturnTypes = function() {
+    return ['T'];
+  };
+
+  parser.expandIdentifierChain = function() {
+    return [];
+  };
+
+  parser.createWeightedKeywords = function() {
+    return [];
+  };
+
+  parser.handleQuotedValueWithCursor = function(lexer, yytext, yylloc, quoteChar) {
+    if (yytext.indexOf('\u2020') !== -1 || yytext.indexOf('\u2021') !== -1) {
+      parser.yy.partialCursor = yytext.indexOf('\u2021') !== -1;
+      const cursorIndex = parser.yy.partialCursor
+        ? yytext.indexOf('\u2021')
+        : yytext.indexOf('\u2020');
+      parser.yy.cursorFound = {
+        first_line: yylloc.first_line,
+        last_line: yylloc.last_line,
+        first_column: yylloc.first_column + cursorIndex,
+        last_column: yylloc.first_column + cursorIndex + 1
+      };
+      const remainder = yytext.substring(cursorIndex + 1);
+      const remainingQuotes = (lexer.upcomingInput().match(new RegExp(quoteChar, 'g')) || [])
+        .length;
+      if (remainingQuotes > 0 && (remainingQuotes & 1) !== 0) {
+        parser.yy.missingEndQuote = false;
+        lexer.input();
+      } else {
+        parser.yy.missingEndQuote = true;
+        lexer.unput(remainder);
+      }
+      lexer.popState();
+      return true;
+    }
+    return false;
+  };
+
+  parser.yy.parseError = function(str, hash) {
+    parser.yy.error = hash;
+  };
+
+  const IGNORED_EXPECTED = {
+    ';': true,
+    '.': true,
+    EOF: true,
+    UNSIGNED_INTEGER: true,
+    UNSIGNED_INTEGER_E: true,
+    REGULAR_IDENTIFIER: true,
+    CURSOR: true,
+    PARTIAL_CURSOR: true,
+    HDFS_START_QUOTE: true,
+    HDFS_PATH: true,
+    HDFS_END_QUOTE: true,
+    COMPARISON_OPERATOR: true, // TODO: Expand in results when found
+    ARITHMETIC_OPERATOR: true, // TODO: Expand in results when found
+    VARIABLE_REFERENCE: true,
+    BACKTICK: true,
+    VALUE: true,
+    PARTIAL_VALUE: true,
+    SINGLE_QUOTE: true,
+    DOUBLE_QUOTE: true
+  };
+
+  const CLEAN_EXPECTED = {
+    BETWEEN_AND: 'AND',
+    OVERWRITE_DIRECTORY: 'OVERWRITE',
+    STORED_AS_DIRECTORIES: 'STORED',
+    LIKE_PARQUET: 'LIKE',
+    PARTITION_VALUE: 'PARTITION'
+  };
+
+  parser.parseSyntax = function(beforeCursor, afterCursor, debug) {
+    parser.yy.caseDetermined = false;
+    parser.yy.error = undefined;
+
+    parser.yy.latestTablePrimaries = [];
+    parser.yy.subQueries = [];
+    parser.yy.selectListAliases = [];
+    parser.yy.latestTablePrimaries = [];
+
+    parser.yy.activeDialect = 'generic';
+
+    // TODO: Find a way around throwing an exception when the parser finds a syntax error
+    try {
+      parser.yy.error = false;
+      parser.parse(beforeCursor + afterCursor);
+    } catch (err) {
+      if (debug) {
+        console.warn(err);
+        console.warn(err.stack);
+        console.warn(parser.yy.error);
+      }
+    }
+
+    if (
+      parser.yy.error &&
+      (parser.yy.error.loc.last_column < beforeCursor.length ||
+        !beforeCursor.endsWith(parser.yy.error.text))
+    ) {
+      const weightedExpected = [];
+
+      const addedExpected = {};
+
+      const isLowerCase =
+        (parser.yy.caseDetermined && parser.yy.lowerCase) ||
+        parser.yy.error.text.toLowerCase() === parser.yy.error.text;
+
+      if (
+        parser.yy.error.expected.length === 2 &&
+        parser.yy.error.expected.indexOf("';'") !== -1 &&
+        parser.yy.error.expected.indexOf("'EOF'") !== -1
+      ) {
+        parser.yy.error.expected = [];
+        parser.yy.error.expectedStatementEnd = true;
+        return parser.yy.error;
+      }
+      for (let i = 0; i < parser.yy.error.expected.length; i++) {
+        let expected = parser.yy.error.expected[i];
+        // Strip away the surrounding ' chars
+        expected = expected.substring(1, expected.length - 1);
+        // TODO: Only suggest alphanumeric?
+        if (expected === 'REGULAR_IDENTIFIER') {
+          parser.yy.error.expectedIdentifier = true;
+          if (/^<[a-z]+>/.test(parser.yy.error.token)) {
+            const text = '`' + parser.yy.error.text + '`';
+            weightedExpected.push({
+              text: text,
+              distance: stringDistance(parser.yy.error.text, text, true)
+            });
+            parser.yy.error.possibleReserved = true;
+          }
+        } else if (!IGNORED_EXPECTED[expected] && /[a-z_]+/i.test(expected)) {
+          if (/^<[a-z]+>/.test(expected)) {
+            continue;
+          }
+          expected = CLEAN_EXPECTED[expected] || expected;
+          if (expected === parser.yy.error.text.toUpperCase()) {
+            // Can happen when the lexer entry for a rule contains multiple words like 'stored' in 'stored as parquet'
+            return false;
+          }
+          const text = isLowerCase ? expected.toLowerCase() : expected;
+          if (text && !addedExpected[text]) {
+            addedExpected[text] = true;
+            weightedExpected.push({
+              text: text,
+              distance: stringDistance(parser.yy.error.text, text, true)
+            });
+          }
+        }
+      }
+      if (weightedExpected.length === 0) {
+        parser.yy.error.expected = [];
+        parser.yy.error.incompleteStatement = true;
+        return parser.yy.error;
+      }
+      weightedExpected.sort((a, b) => {
+        if (a.distance === b.distance) {
+          return a.text.localeCompare(b.text);
+        }
+        return a.distance - b.distance;
+      });
+      parser.yy.error.expected = weightedExpected;
+      parser.yy.error.incompleteStatement = true;
+      return parser.yy.error;
+    } else if (parser.yy.error) {
+      parser.yy.error.expected = [];
+      parser.yy.error.incompleteStatement = true;
+      return parser.yy.error;
+    }
+    return false;
+  };
+};
+
+export default {
+  initSqlParser: initSqlParser,
+  initSyntaxParser: initSyntaxParser
+};

+ 2 - 0
desktop/core/src/desktop/js/parse/sql/sqlParserRepository.js

@@ -20,6 +20,7 @@
  */
 const AUTOCOMPLETE_MODULES = {
   calcite: () => import(/* webpackChunkName: "calcite-parser" */ 'parse/sql/calcite/calciteAutocompleteParser'),
+  druid: () => import(/* webpackChunkName: "druid-parser" */ 'parse/sql/druid/druidAutocompleteParser'),
   generic: () => import(/* webpackChunkName: "generic-parser" */ 'parse/sql/generic/genericAutocompleteParser'),
   hive: () => import(/* webpackChunkName: "hive-parser" */ 'parse/sql/hive/hiveAutocompleteParser'),
   impala: () => import(/* webpackChunkName: "impala-parser" */ 'parse/sql/impala/impalaAutocompleteParser'),
@@ -27,6 +28,7 @@ const AUTOCOMPLETE_MODULES = {
 };
 const SYNTAX_MODULES = {
   calcite: () => import(/* webpackChunkName: "calcite-parser" */ 'parse/sql/calcite/calciteSyntaxParser'),
+  druid: () => import(/* webpackChunkName: "druid-parser" */ 'parse/sql/druid/druidSyntaxParser'),
   generic: () => import(/* webpackChunkName: "generic-parser" */ 'parse/sql/generic/genericSyntaxParser'),
   hive: () => import(/* webpackChunkName: "hive-parser" */ 'parse/sql/hive/hiveSyntaxParser'),
   impala: () => import(/* webpackChunkName: "impala-parser" */ 'parse/sql/impala/impalaSyntaxParser'),

Some files were not shown because too many files changed in this diff