Repository: sql-formatter-org/sql-formatter
Pull request #398 (2022-08-17 11:20:23)

Add RESERVED_SELECT and ASTERISK tokens
These are needed to finish the final bit of functionality (distinguishing `*` in `SELECT *` from other uses of `*` operator) that Nearley parser currently lacks compared to our current parser.

It also eliminates the hack for detecting `SELECT` token using a regex.

Using the name `ASTERISK` as that's what [SQL standard](https://jakewheat.github.io/sql-overview/sql-2008-foundation-grammar.html#asterisk) uses.


Patch:
diff --git a/src/formatter/ExpressionFormatter.ts b/src/formatter/ExpressionFormatter.ts
index 5ddcc61b..f9476eeb 100644
--- a/src/formatter/ExpressionFormatter.ts
+++ b/src/formatter/ExpressionFormatter.ts
@@ -209,6 +209,7 @@ export default class ExpressionFormatter {
       case TokenType.COMMA:
         return this.formatComma(token);
       case TokenType.OPERATOR:
+      case TokenType.ASTERISK:
         return this.formatOperator(token);
       case TokenType.IDENTIFIER:
       case TokenType.QUOTED_IDENTIFIER:
diff --git a/src/formatter/tabularStyle.ts b/src/formatter/tabularStyle.ts
index 7ad623d2..cd0eb386 100644
--- a/src/formatter/tabularStyle.ts
+++ b/src/formatter/tabularStyle.ts
@@ -33,6 +33,7 @@ export function isTabularToken(token: Token): boolean {
     isLogicalOperator(token) ||
     token.type === TokenType.RESERVED_DEPENDENT_CLAUSE ||
     token.type === TokenType.RESERVED_COMMAND ||
+    token.type === TokenType.RESERVED_SELECT ||
     token.type === TokenType.RESERVED_SET_OPERATION ||
     token.type === TokenType.RESERVED_JOIN
   );
diff --git a/src/languages/bigquery/bigquery.formatter.ts b/src/languages/bigquery/bigquery.formatter.ts
index 53cf7cc5..e8da2c78 100644
--- a/src/languages/bigquery/bigquery.formatter.ts
+++ b/src/languages/bigquery/bigquery.formatter.ts
@@ -5,10 +5,11 @@ import { expandPhrases } from 'src/expandPhrases';
 import { keywords } from './bigquery.keywords';
 import { functions } from './bigquery.functions';
 
+const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT] [AS STRUCT | AS VALUE]']);
+
 const reservedCommands = expandPhrases([
   // Queries: https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax
   'WITH [RECURSIVE]',
-  'SELECT [ALL | DISTINCT] [AS STRUCT | AS VALUE]',
   'FROM',
   'WHERE',
   'GROUP BY',
@@ -143,6 +144,7 @@ export default class BigQueryFormatter extends Formatter {
   tokenizer() {
     return new Tokenizer({
       reservedCommands,
+      reservedSelect,
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE'],
diff --git a/src/languages/db2/db2.formatter.ts b/src/languages/db2/db2.formatter.ts
index 2d311300..93c41f08 100644
--- a/src/languages/db2/db2.formatter.ts
+++ b/src/languages/db2/db2.formatter.ts
@@ -4,10 +4,11 @@ import Tokenizer from 'src/lexer/Tokenizer';
 import { functions } from './db2.functions';
 import { keywords } from './db2.keywords';
 
+const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT]']);
+
 const reservedCommands = expandPhrases([
   // queries
   'WITH',
-  'SELECT [ALL | DISTINCT]',
   'FROM',
   'WHERE',
   'GROUP BY',
@@ -173,6 +174,7 @@ export default class Db2Formatter extends Formatter {
   tokenizer() {
     return new Tokenizer({
       reservedCommands,
+      reservedSelect,
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE', 'ELSEIF'],
diff --git a/src/languages/hive/hive.formatter.ts b/src/languages/hive/hive.formatter.ts
index de634093..679a6e3b 100644
--- a/src/languages/hive/hive.formatter.ts
+++ b/src/languages/hive/hive.formatter.ts
@@ -4,10 +4,11 @@ import Tokenizer from 'src/lexer/Tokenizer';
 import { functions } from './hive.functions';
 import { keywords } from './hive.keywords';
 
+const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT]']);
+
 const reservedCommands = expandPhrases([
   // queries
   'WITH',
-  'SELECT [ALL | DISTINCT]',
   'FROM',
   'WHERE',
   'GROUP BY',
@@ -84,6 +85,7 @@ export default class HiveFormatter extends Formatter {
   tokenizer() {
     return new Tokenizer({
       reservedCommands,
+      reservedSelect,
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE'],
diff --git a/src/languages/mariadb/mariadb.formatter.ts b/src/languages/mariadb/mariadb.formatter.ts
index 7ee5be41..e692b1b2 100644
--- a/src/languages/mariadb/mariadb.formatter.ts
+++ b/src/languages/mariadb/mariadb.formatter.ts
@@ -5,10 +5,11 @@ import { EOF_TOKEN, isToken, type Token, TokenType } from 'src/lexer/token';
 import { keywords } from './mariadb.keywords';
 import { functions } from './mariadb.functions';
 
+const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT | DISTINCTROW]']);
+
 const reservedCommands = expandPhrases([
   // queries
   'WITH [RECURSIVE]',
-  'SELECT [ALL | DISTINCT | DISTINCTROW]',
   'FROM',
   'WHERE',
   'GROUP BY',
@@ -131,7 +132,6 @@ const reservedCommands = expandPhrases([
   'REVOKE',
   'ROLLBACK',
   'SAVEPOINT',
-  'SELECT',
   'SET CHARACTER SET',
   'SET DEFAULT ROLE',
   'SET GLOBAL TRANSACTION',
@@ -261,6 +261,7 @@ export default class MariaDbFormatter extends Formatter {
   tokenizer() {
     return new Tokenizer({
       reservedCommands,
+      reservedSelect,
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE', 'ELSEIF', 'ELSIF'],
diff --git a/src/languages/mysql/mysql.formatter.ts b/src/languages/mysql/mysql.formatter.ts
index 4b1af0ee..d410ded8 100644
--- a/src/languages/mysql/mysql.formatter.ts
+++ b/src/languages/mysql/mysql.formatter.ts
@@ -5,10 +5,11 @@ import { EOF_TOKEN, isToken, type Token, TokenType } from 'src/lexer/token';
 import { keywords } from './mysql.keywords';
 import { functions } from './mysql.functions';
 
+const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT | DISTINCTROW]']);
+
 const reservedCommands = expandPhrases([
   // queries
   'WITH [RECURSIVE]',
-  'SELECT [ALL | DISTINCT | DISTINCTROW]',
   'FROM',
   'WHERE',
   'GROUP BY',
@@ -229,6 +230,7 @@ export default class MySqlFormatter extends Formatter {
   tokenizer() {
     return new Tokenizer({
       reservedCommands,
+      reservedSelect,
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE', 'ELSEIF'],
diff --git a/src/languages/n1ql/n1ql.formatter.ts b/src/languages/n1ql/n1ql.formatter.ts
index ad6c2c62..99df32e9 100644
--- a/src/languages/n1ql/n1ql.formatter.ts
+++ b/src/languages/n1ql/n1ql.formatter.ts
@@ -4,10 +4,11 @@ import Tokenizer from 'src/lexer/Tokenizer';
 import { functions } from './n1ql.functions';
 import { keywords } from './n1ql.keywords';
 
+const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT]']);
+
 const reservedCommands = expandPhrases([
   // queries
   'WITH',
-  'SELECT [ALL | DISTINCT]',
   'FROM',
   'WHERE',
   'GROUP BY',
@@ -81,6 +82,7 @@ export default class N1qlFormatter extends Formatter {
   tokenizer() {
     return new Tokenizer({
       reservedCommands,
+      reservedSelect,
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE'],
diff --git a/src/languages/plsql/plsql.formatter.ts b/src/languages/plsql/plsql.formatter.ts
index 5626097a..df0fd098 100644
--- a/src/languages/plsql/plsql.formatter.ts
+++ b/src/languages/plsql/plsql.formatter.ts
@@ -5,10 +5,11 @@ import { EOF_TOKEN, isReserved, isToken, type Token, TokenType } from 'src/lexer
 import { keywords } from './plsql.keywords';
 import { functions } from './plsql.functions';
 
+const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT | UNIQUE]']);
+
 const reservedCommands = expandPhrases([
   // queries
   'WITH',
-  'SELECT [ALL | DISTINCT | UNIQUE]',
   'FROM',
   'WHERE',
   'GROUP BY',
@@ -86,6 +87,7 @@ export default class PlSqlFormatter extends Formatter {
   tokenizer() {
     return new Tokenizer({
       reservedCommands,
+      reservedSelect,
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE'],
diff --git a/src/languages/postgresql/postgresql.formatter.ts b/src/languages/postgresql/postgresql.formatter.ts
index c5852bdc..5f53af1f 100644
--- a/src/languages/postgresql/postgresql.formatter.ts
+++ b/src/languages/postgresql/postgresql.formatter.ts
@@ -4,10 +4,11 @@ import Tokenizer from 'src/lexer/Tokenizer';
 import { functions } from './postgresql.functions';
 import { keywords } from './postgresql.keywords';
 
+const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT]']);
+
 const reservedCommands = expandPhrases([
   // queries
   'WITH [RECURSIVE]',
-  'SELECT [ALL | DISTINCT]',
   'FROM',
   'WHERE',
   'GROUP BY [ALL | DISTINCT]',
@@ -318,6 +319,7 @@ export default class PostgreSqlFormatter extends Formatter {
   tokenizer() {
     return new Tokenizer({
       reservedCommands,
+      reservedSelect,
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE'],
diff --git a/src/languages/redshift/redshift.formatter.ts b/src/languages/redshift/redshift.formatter.ts
index 4ca2a18d..f58a12c5 100644
--- a/src/languages/redshift/redshift.formatter.ts
+++ b/src/languages/redshift/redshift.formatter.ts
@@ -4,10 +4,11 @@ import Tokenizer from 'src/lexer/Tokenizer';
 import { functions } from './redshift.functions';
 import { keywords } from './redshift.keywords';
 
+const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT]']);
+
 const reservedCommands = expandPhrases([
   // queries
   'WITH [RECURSIVE]',
-  'SELECT [ALL | DISTINCT]',
   'FROM',
   'WHERE',
   'GROUP BY',
@@ -141,6 +142,7 @@ export default class RedshiftFormatter extends Formatter {
   tokenizer() {
     return new Tokenizer({
       reservedCommands,
+      reservedSelect,
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE'],
diff --git a/src/languages/singlestoredb/singlestoredb.formatter.ts b/src/languages/singlestoredb/singlestoredb.formatter.ts
index a22002db..44d2fefd 100644
--- a/src/languages/singlestoredb/singlestoredb.formatter.ts
+++ b/src/languages/singlestoredb/singlestoredb.formatter.ts
@@ -5,10 +5,11 @@ import { EOF_TOKEN, isToken, type Token, TokenType } from 'src/lexer/token';
 import { keywords } from './singlestoredb.keywords';
 import { functions } from './singlestoredb.functions';
 
+const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT | DISTINCTROW]']);
+
 const reservedCommands = expandPhrases([
   // queries
   'WITH',
-  'SELECT [ALL | DISTINCT | DISTINCTROW]',
   'FROM',
   'WHERE',
   'GROUP BY',
@@ -229,6 +230,7 @@ export default class SingleStoreDbFormatter extends Formatter {
   tokenizer() {
     return new Tokenizer({
       reservedCommands,
+      reservedSelect,
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE', 'ELSEIF'],
diff --git a/src/languages/spark/spark.formatter.ts b/src/languages/spark/spark.formatter.ts
index 28cd941e..0518dece 100644
--- a/src/languages/spark/spark.formatter.ts
+++ b/src/languages/spark/spark.formatter.ts
@@ -6,10 +6,11 @@ import { keywords } from './spark.keywords';
 import { functions } from './spark.functions';
 
 // http://spark.apache.org/docs/latest/sql-ref-syntax.html
+const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT]']);
+
 const reservedCommands = expandPhrases([
   // queries
   'WITH',
-  'SELECT [ALL | DISTINCT]',
   'FROM',
   'WHERE',
   'GROUP BY',
@@ -115,6 +116,7 @@ export default class SparkFormatter extends Formatter {
   tokenizer() {
     return new Tokenizer({
       reservedCommands,
+      reservedSelect,
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE'],
diff --git a/src/languages/sql/sql.formatter.ts b/src/languages/sql/sql.formatter.ts
index 0974163f..bc6e633c 100644
--- a/src/languages/sql/sql.formatter.ts
+++ b/src/languages/sql/sql.formatter.ts
@@ -4,10 +4,11 @@ import Tokenizer from 'src/lexer/Tokenizer';
 import { functions } from './sql.functions';
 import { keywords } from './sql.keywords';
 
+const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT]']);
+
 const reservedCommands = expandPhrases([
   // queries
   'WITH [RECURSIVE]',
-  'SELECT [ALL | DISTINCT]',
   'FROM',
   'WHERE',
   'GROUP BY [ALL | DISTINCT]',
@@ -72,6 +73,7 @@ export default class SqlFormatter extends Formatter {
   tokenizer() {
     return new Tokenizer({
       reservedCommands,
+      reservedSelect,
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE'],
diff --git a/src/languages/sqlite/sqlite.formatter.ts b/src/languages/sqlite/sqlite.formatter.ts
index 1a7e518b..92f78406 100644
--- a/src/languages/sqlite/sqlite.formatter.ts
+++ b/src/languages/sqlite/sqlite.formatter.ts
@@ -4,10 +4,11 @@ import Tokenizer from 'src/lexer/Tokenizer';
 import { functions } from './sqlite.functions';
 import { keywords } from './sqlite.keywords';
 
+const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT]']);
+
 const reservedCommands = expandPhrases([
   // queries
   'WITH [RECURSIVE]',
-  'SELECT [ALL | DISTINCT]',
   'FROM',
   'WHERE',
   'GROUP BY',
@@ -62,6 +63,7 @@ export default class SqliteFormatter extends Formatter {
   tokenizer() {
     return new Tokenizer({
       reservedCommands,
+      reservedSelect,
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE'],
diff --git a/src/languages/trino/trino.formatter.ts b/src/languages/trino/trino.formatter.ts
index 87fbb335..51845f8b 100644
--- a/src/languages/trino/trino.formatter.ts
+++ b/src/languages/trino/trino.formatter.ts
@@ -4,11 +4,12 @@ import Tokenizer from 'src/lexer/Tokenizer';
 import { functions } from './trino.functions';
 import { keywords } from './trino.keywords';
 
+const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT]']);
+
 // https://github.com/trinodb/trino/blob/432d2897bdef99388c1a47188743a061c4ac1f34/core/trino-parser/src/main/antlr4/io/trino/sql/parser/SqlBase.g4#L41
 const reservedCommands = expandPhrases([
   // queries
   'WITH [RECURSIVE]',
-  'SELECT [ALL | DISTINCT]',
   'FROM',
   'WHERE',
   'GROUP BY [ALL | DISTINCT]',
@@ -124,6 +125,7 @@ export default class TrinoFormatter extends Formatter {
   tokenizer() {
     return new Tokenizer({
       reservedCommands,
+      reservedSelect,
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE'],
diff --git a/src/languages/tsql/tsql.formatter.ts b/src/languages/tsql/tsql.formatter.ts
index 17184a5d..595b0f2d 100644
--- a/src/languages/tsql/tsql.formatter.ts
+++ b/src/languages/tsql/tsql.formatter.ts
@@ -4,10 +4,11 @@ import Tokenizer from 'src/lexer/Tokenizer';
 import { functions } from './tsql.functions';
 import { keywords } from './tsql.keywords';
 
+const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT]']);
+
 const reservedCommands = expandPhrases([
   // queries
   'WITH',
-  'SELECT [ALL | DISTINCT]',
   'FROM',
   'WHERE',
   'GROUP BY',
@@ -218,6 +219,7 @@ export default class TSqlFormatter extends Formatter {
   tokenizer() {
     return new Tokenizer({
       reservedCommands,
+      reservedSelect,
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE'],
diff --git a/src/lexer/Tokenizer.ts b/src/lexer/Tokenizer.ts
index b123a3c9..90f8a9e0 100644
--- a/src/lexer/Tokenizer.ts
+++ b/src/lexer/Tokenizer.ts
@@ -57,6 +57,10 @@ export default class Tokenizer {
         regex: regex.reservedWord(cfg.reservedCommands, cfg.identChars),
         value: toCanonical,
       },
+      [TokenType.RESERVED_SELECT]: {
+        regex: regex.reservedWord(cfg.reservedSelect, cfg.identChars),
+        value: toCanonical,
+      },
       [TokenType.RESERVED_SET_OPERATION]: {
         regex: regex.reservedWord(cfg.reservedSetOperations, cfg.identChars),
         value: toCanonical,
@@ -102,7 +106,7 @@ export default class Tokenizer {
       },
       [TokenType.DELIMITER]: { regex: /[;]/uy },
       [TokenType.OPERATOR]: {
-        regex: regex.operator('+-/*%&|^><=.:$@#?~![]{}', [
+        regex: regex.operator('+-/%&|^><=.:$@#?~![]{}', [
           '<>',
           '<=',
           '>=',
@@ -110,6 +114,7 @@ export default class Tokenizer {
           ...(cfg.operators ?? []),
         ]),
       },
+      [TokenType.ASTERISK]: { regex: /[*]/uy },
     });
   }
 
diff --git a/src/lexer/TokenizerEngine.ts b/src/lexer/TokenizerEngine.ts
index cad781e7..0372daac 100644
--- a/src/lexer/TokenizerEngine.ts
+++ b/src/lexer/TokenizerEngine.ts
@@ -79,7 +79,8 @@ export default class TokenizerEngine {
       this.matchToken(TokenType.STRING) ||
       this.matchToken(TokenType.IDENTIFIER) ||
       this.matchToken(TokenType.DELIMITER) ||
-      this.matchToken(TokenType.OPERATOR)
+      this.matchToken(TokenType.OPERATOR) ||
+      this.matchToken(TokenType.ASTERISK)
     );
   }
 
@@ -111,6 +112,7 @@ export default class TokenizerEngine {
       this.matchToken(TokenType.BETWEEN) ||
       this.matchToken(TokenType.LIMIT) ||
       this.matchToken(TokenType.RESERVED_COMMAND) ||
+      this.matchToken(TokenType.RESERVED_SELECT) ||
       this.matchToken(TokenType.RESERVED_SET_OPERATION) ||
       this.matchToken(TokenType.RESERVED_DEPENDENT_CLAUSE) ||
       this.matchToken(TokenType.RESERVED_JOIN) ||
diff --git a/src/lexer/TokenizerOptions.ts b/src/lexer/TokenizerOptions.ts
index bb48e6ff..9c3aa285 100644
--- a/src/lexer/TokenizerOptions.ts
+++ b/src/lexer/TokenizerOptions.ts
@@ -43,8 +43,10 @@ export interface ParamTypes {
 }
 
 export interface TokenizerOptions {
-  // Main clauses that start new block, like: SELECT, FROM, WHERE, ORDER BY
+  // Main clauses that start new block, like: WITH, FROM, WHERE, ORDER BY
   reservedCommands: string[];
+  // SELECT clause and its variations
+  reservedSelect: string[];
   // True to support XOR in addition to AND and OR
   supportsXor?: boolean;
   // Keywords in CASE expressions that begin new line, like: WHEN, ELSE
diff --git a/src/lexer/token.ts b/src/lexer/token.ts
index 88dc82a8..d6a2e895 100644
--- a/src/lexer/token.ts
+++ b/src/lexer/token.ts
@@ -10,6 +10,7 @@ export enum TokenType {
   RESERVED_DEPENDENT_CLAUSE = 'RESERVED_DEPENDENT_CLAUSE',
   RESERVED_SET_OPERATION = 'RESERVED_SET_OPERATION',
   RESERVED_COMMAND = 'RESERVED_COMMAND',
+  RESERVED_SELECT = 'RESERVED_SELECT',
   RESERVED_JOIN = 'RESERVED_JOIN',
   CASE = 'CASE',
   END = 'END',
@@ -20,6 +21,7 @@ export enum TokenType {
   XOR = 'XOR',
   OPERATOR = 'OPERATOR',
   COMMA = 'COMMA',
+  ASTERISK = 'ASTERISK', // *
   OPEN_PAREN = 'OPEN_PAREN',
   CLOSE_PAREN = 'CLOSE_PAREN',
   LINE_COMMENT = 'LINE_COMMENT',
@@ -66,8 +68,6 @@ export const testToken =
 export const isToken = {
   ARRAY: testToken({ text: 'ARRAY', type: TokenType.RESERVED_KEYWORD }),
   BY: testToken({ text: 'BY', type: TokenType.RESERVED_KEYWORD }),
-  SELECT: (token: Token) =>
-    /^SELECT\b/.test(token.text) && token.type === TokenType.RESERVED_COMMAND,
   SET: testToken({ text: 'SET', type: TokenType.RESERVED_COMMAND }),
   STRUCT: testToken({ text: 'STRUCT', type: TokenType.RESERVED_KEYWORD }),
   WINDOW: testToken({ text: 'WINDOW', type: TokenType.RESERVED_COMMAND }),
@@ -80,6 +80,7 @@ export const isReserved = (token: Token): boolean =>
   token.type === TokenType.RESERVED_PHRASE ||
   token.type === TokenType.RESERVED_DEPENDENT_CLAUSE ||
   token.type === TokenType.RESERVED_COMMAND ||
+  token.type === TokenType.RESERVED_SELECT ||
   token.type === TokenType.RESERVED_SET_OPERATION ||
   token.type === TokenType.RESERVED_JOIN ||
   token.type === TokenType.CASE ||
diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts
index dbafbc27..05c668aa 100644
--- a/src/parser/Parser.ts
+++ b/src/parser/Parser.ts
@@ -1,5 +1,5 @@
 /* eslint-disable no-cond-assign */
-import { EOF_TOKEN, type Token, TokenType, isToken } from 'src/lexer/token';
+import { EOF_TOKEN, type Token, TokenType } from 'src/lexer/token';
 import {
   AllColumnsAsterisk,
   ArraySubscript,
@@ -65,7 +65,10 @@ export default class Parser {
   }
 
   private clause(): Clause | undefined {
-    if (this.look().type === TokenType.RESERVED_COMMAND) {
+    if (
+      this.look().type === TokenType.RESERVED_COMMAND ||
+      this.look().type === TokenType.RESERVED_SELECT
+    ) {
       const name = this.next();
       const children = this.expressionsUntilClauseEnd();
       return { type: NodeType.clause, nameToken: name, children };
@@ -163,7 +166,10 @@ export default class Parser {
   }
 
   private allColumnsAsterisk(): AllColumnsAsterisk | undefined {
-    if (this.look().text === '*' && isToken.SELECT(this.look(-1))) {
+    if (
+      this.look().type === TokenType.ASTERISK &&
+      this.look(-1).type === TokenType.RESERVED_SELECT
+    ) {
       this.next();
       return { type: NodeType.all_columns_asterisk };
     }
@@ -176,6 +182,7 @@ export default class Parser {
     const children: AstNode[] = [];
     while (
       this.look().type !== TokenType.RESERVED_COMMAND &&
+      this.look().type !== TokenType.RESERVED_SELECT &&
       this.look().type !== TokenType.LIMIT &&
       this.look().type !== TokenType.RESERVED_SET_OPERATION &&
       this.look().type !== TokenType.EOF &&


Test patch:
diff --git a/test/unit/Parser.test.ts b/test/unit/Parser.test.ts
index 1318144b..bd51137b 100644
--- a/test/unit/Parser.test.ts
+++ b/test/unit/Parser.test.ts
@@ -4,7 +4,8 @@ import Tokenizer from 'src/lexer/Tokenizer';
 describe('Parser', () => {
   const parse = (sql: string) => {
     const tokens = new Tokenizer({
-      reservedCommands: ['SELECT', 'SELECT DISTINCT', 'FROM', 'WHERE', 'LIMIT', 'CREATE TABLE'],
+      reservedCommands: ['FROM', 'WHERE', 'LIMIT', 'CREATE TABLE'],
+      reservedSelect: ['SELECT', 'SELECT DISTINCT'],
       reservedDependentClauses: ['WHEN', 'ELSE'],
       reservedSetOperations: ['UNION'],
       reservedJoins: ['JOIN'],
@@ -106,7 +107,7 @@ describe('Parser', () => {
                 "raw": "SELECT",
                 "start": 0,
                 "text": "SELECT",
-                "type": "RESERVED_COMMAND",
+                "type": "RESERVED_SELECT",
               },
               "type": "clause",
             },
@@ -179,7 +180,7 @@ describe('Parser', () => {
                 "raw": "SELECT",
                 "start": 0,
                 "text": "SELECT",
-                "type": "RESERVED_COMMAND",
+                "type": "RESERVED_SELECT",
               },
               "type": "clause",
             },
@@ -274,7 +275,7 @@ describe('Parser', () => {
                 "raw": "SELECT",
                 "start": 0,
                 "text": "SELECT",
-                "type": "RESERVED_COMMAND",
+                "type": "RESERVED_SELECT",
               },
               "type": "clause",
             },
@@ -462,7 +463,7 @@ describe('Parser', () => {
                 "raw": "SELECT",
                 "start": 0,
                 "text": "SELECT",
-                "type": "RESERVED_COMMAND",
+                "type": "RESERVED_SELECT",
               },
               "type": "clause",
             },
@@ -491,7 +492,7 @@ describe('Parser', () => {
                 "raw": "SELECT DISTINCT",
                 "start": 0,
                 "text": "SELECT DISTINCT",
-                "type": "RESERVED_COMMAND",
+                "type": "RESERVED_SELECT",
               },
               "type": "clause",
             },