Repository: sql-formatter-org/sql-formatter
Pull request #307 (2022-07-12 19:37:21)

New approach to function call matching
Previously the approach to detecting function calls was as follows:

- Inside Tokenizer: save preceding whitespace of each token to `whitespaceBefore` field
- Inside Parser: check if `RESERVED_KEYWORD` or `IDENTIFIER` is followed by `(` and there's no whitespace between them.

This has been a great hack to format function calls without really having a knowledge of which words are function names. But this approach has the fundamental downside of it relying on the whitespace of original code to make formatting decisions, which leads to surprising behavior (see #140 and #243) where re-formatting an already formatted SQL produces a different result.

The new approach introduced in this PR goes as follows:

- Inside Tokenizer: label all function names as `type: RESERVED_FUNCTION_NAME` tokens.
- Inside Parser: check if `RESERVED_FUNCTION_NAME` is followed by `(`.

This is mostly how one would expect this to work. However there are various caveats:

- A common convention is to format data types similarly to function calls e.g. `DECIMAL(5, 2)`. For now this is solved by also including data types (that can take parameters) also to the functions list. In the future should completely separate data types from functions.
- `CAST` is used in various forms, but as its main use is in `CAST(expr AS type)`, it's for now listed alongside functions.
- `ANY` and `SOME` can be used as aggregate functions, in which case they should be formatted as `HAVING ANY(condition)`. But a way more common usage is as an operator: `WHERE x = ANY (1, 2, 3)`. So I've opted to remove these from list of functions.
- `NULLIF` and `COALESCE` have sometimes been listed as keywords, at other times as function names. Changed them to be function names in every dialect. That really hints of a general problem that these keyword/function-name lists need a proper review.

The major downside of this new approach is, that:

- calls of a custom function like `foo(1, 2)` are now formatted as `foo (1, 2)`.

But there's also a major benefit from parsing side:

- this change should greatly simplify switching to a parser-generator, as the parser won't have to look at whitespace (e.g. Nearley or Jison can't really distinguish between a token with and without preceding whitespace. Unless we go and introduce separate whitespace tokens, it's a major blocker for adopting a parser generator).

So, in the end it's about tradeoffs. IMHO the sacrifices are worth the gains.


Patch:
diff --git a/src/formatter/AliasAs.ts b/src/formatter/AliasAs.ts
index 5aa6487b..4620c7c3 100644
--- a/src/formatter/AliasAs.ts
+++ b/src/formatter/AliasAs.ts
@@ -99,7 +99,8 @@ export default class AliasAs {
       isToken.CAST(this.getPreviousReservedToken()) &&
       isToken.AS(this.lookAhead()) &&
       (this.lookAhead(2).type === TokenType.IDENTIFIER ||
-        this.lookAhead(2).type === TokenType.RESERVED_KEYWORD) &&
+        this.lookAhead(2).type === TokenType.RESERVED_KEYWORD ||
+        this.lookAhead(2).type === TokenType.RESERVED_FUNCTION_NAME) &&
       this.lookAhead(3).value === ')'
     );
   }
diff --git a/src/formatter/ExpressionFormatter.ts b/src/formatter/ExpressionFormatter.ts
index 86978a95..ba2cf110 100644
--- a/src/formatter/ExpressionFormatter.ts
+++ b/src/formatter/ExpressionFormatter.ts
@@ -200,6 +200,7 @@ export default class ExpressionFormatter {
       case TokenType.RESERVED_LOGICAL_OPERATOR:
         return this.formatLogicalOperator(token);
       case TokenType.RESERVED_KEYWORD:
+      case TokenType.RESERVED_FUNCTION_NAME:
         return this.formatKeyword(token);
       case TokenType.RESERVED_CASE_START:
         return this.formatCaseStart(token);
diff --git a/src/languages/bigquery.formatter.ts b/src/languages/bigquery.formatter.ts
index 19304339..d19da5ee 100644
--- a/src/languages/bigquery.formatter.ts
+++ b/src/languages/bigquery.formatter.ts
@@ -69,7 +69,7 @@ const reservedFunctions = {
   ],
   // https://cloud.google.com/bigquery/docs/reference/standard-sql/array_functions
   array: [
-    'ARRAY',
+    // 'ARRAY',
     'ARRAY_CONCAT',
     'ARRAY_LENGTH',
     'ARRAY_TO_STRING',
@@ -545,6 +545,9 @@ const reservedFunctions = {
   ],
   other: ['BQ.JOBS.CANCEL', 'BQ.REFRESH_MATERIALIZED_VIEW'],
   pivot: ['PIVOT', 'UNPIVOT'],
+  // Data types with parameters like VARCHAR(100)
+  // https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#parameterized_data_types
+  dataTypes: ['BYTES', 'NUMERIC', 'DECIMAL', 'BIGNUMERIC', 'BIGDECIMAL', 'STRING'],
 };
 
 /**
@@ -837,10 +840,8 @@ export default class BigQueryFormatter extends Formatter {
       reservedBinaryCommands,
       reservedJoins,
       reservedDependentClauses,
-      reservedKeywords: dedupe([
-        ...Object.values(reservedFunctions).flat(),
-        ...Object.values(reservedKeywords).flat(),
-      ]),
+      reservedKeywords: dedupe(Object.values(reservedKeywords).flat()),
+      reservedFunctionNames: dedupe(Object.values(reservedFunctions).flat()),
       openParens: ['(', '['],
       closeParens: [')', ']'],
       stringTypes: [
@@ -867,14 +868,14 @@ function postProcess(tokens: Token[]): Token[] {
   return detectArraySubscripts(combineParameterizedTypes(tokens));
 }
 
-// Converts OFFSET token inside array from RESERVED_COMMAND to RESERVED_KEYWORD
+// Converts OFFSET token inside array from RESERVED_COMMAND to RESERVED_FUNCTION_NAME
 // See: https://cloud.google.com/bigquery/docs/reference/standard-sql/functions-and-operators#array_subscript_operator
 function detectArraySubscripts(tokens: Token[]) {
   let prevToken = EOF_TOKEN;
   return tokens.map(token => {
     if (token.value === 'OFFSET' && prevToken.value === '[') {
       prevToken = token;
-      return { ...token, type: TokenType.RESERVED_KEYWORD };
+      return { ...token, type: TokenType.RESERVED_FUNCTION_NAME };
     } else {
       prevToken = token;
       return token;
diff --git a/src/languages/db2.formatter.ts b/src/languages/db2.formatter.ts
index e9dd6079..53019268 100644
--- a/src/languages/db2.formatter.ts
+++ b/src/languages/db2.formatter.ts
@@ -310,6 +310,8 @@ const reservedFunctions = {
   row: ['UNPACK'],
   // https://www.ibm.com/docs/en/db2-for-zos/12?topic=expressions-olap-specification
   olap: ['FIRST_VALUE', 'LAG', 'LAST_VALUE', 'LEAD', 'NTH_VALUE', 'NTILE', 'RATIO_TO_REPORT'],
+  // Type casting
+  cast: ['CAST'],
 };
 
 /**
@@ -342,7 +344,7 @@ const reservedKeywords = {
     'BY',
     'CAPTURE',
     'CASCADED',
-    'CAST',
+    // 'CAST',
     'CCSID',
     'CHARACTER',
     'CHECK',
@@ -862,10 +864,8 @@ export default class Db2Formatter extends Formatter {
       reservedBinaryCommands,
       reservedJoins,
       reservedDependentClauses,
-      reservedKeywords: dedupe([
-        ...Object.values(reservedFunctions).flat(),
-        ...Object.values(reservedKeywords).flat(),
-      ]),
+      reservedKeywords: dedupe(Object.values(reservedKeywords).flat()),
+      reservedFunctionNames: dedupe(Object.values(reservedFunctions).flat()),
       stringTypes: [{ quote: "''", prefixes: ['X', 'G', 'N', 'GX', 'UX', 'U&'] }],
       identTypes: [`""`],
       positionalParams: true,
diff --git a/src/languages/hive.formatter.ts b/src/languages/hive.formatter.ts
index 7d0e4473..7a8a5374 100644
--- a/src/languages/hive.formatter.ts
+++ b/src/languages/hive.formatter.ts
@@ -190,6 +190,24 @@ const reservedFunctions = {
     'VARIANCE',
   ],
   table: ['EXPLODE', 'INLINE', 'JSON_TUPLE', 'PARSE_URL_TUPLE', 'POSEXPLODE', 'STACK'],
+  // https://cwiki.apache.org/confluence/display/Hive/LanguageManual+WindowingAndAnalytics
+  window: [
+    'LEAD',
+    'LAG',
+    'FIRST_VALUE',
+    'LAST_VALUE',
+    'RANK',
+    'ROW_NUMBER',
+    'DENSE_RANK',
+    'CUME_DIST',
+    'PERCENT_RANK',
+    'NTILE',
+  ],
+  // Parameterized data types
+  // https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=82706456
+  // Though in reality Hive only supports parameters for DECIMAL(),
+  // it doesn't hurt to allow others in here as well.
+  dataTypes: ['DECIMAL', 'NUMERIC', 'VARCHAR', 'CHAR'],
 };
 
 /**
@@ -619,10 +637,8 @@ export default class HiveFormatter extends Formatter {
       reservedBinaryCommands,
       reservedJoins,
       reservedDependentClauses,
-      reservedKeywords: dedupe([
-        ...Object.values(reservedFunctions).flat(),
-        ...Object.values(reservedKeywords).flat(),
-      ]),
+      reservedKeywords: dedupe(Object.values(reservedKeywords).flat()),
+      reservedFunctionNames: dedupe(Object.values(reservedFunctions).flat()),
       openParens: ['(', '['],
       closeParens: [')', ']'],
       stringTypes: ['""', "''"],
diff --git a/src/languages/mariadb.formatter.ts b/src/languages/mariadb.formatter.ts
index b1dc28d2..3d477925 100644
--- a/src/languages/mariadb.formatter.ts
+++ b/src/languages/mariadb.formatter.ts
@@ -180,7 +180,6 @@ const reservedFunctions = [
   'NAME_CONST',
   'NVL',
   'NVL2',
-  'NULLIF',
   'OCT',
   'OCTET_LENGTH',
   'ORD',
@@ -244,6 +243,41 @@ const reservedFunctions = [
   'WSREP_LAST_SEEN_GTID',
   'WSREP_SYNC_WAIT_UPTO_GTID',
   'YEARWEEK',
+  // CASE expression shorthands
+  'COALESCE',
+  'NULLIF',
+  // Data types with parameters
+  // https://mariadb.com/kb/en/data-types/
+  'TINYINT',
+  'SMALLINT',
+  'MEDIUMINT',
+  'INT',
+  'INTEGER',
+  'BIGINT',
+  'DECIMAL',
+  'DEC',
+  'NUMERIC',
+  'FIXED',
+  // 'NUMBER', // ?? In oracle mode only
+  'FLOAT',
+  'DOUBLE',
+  'DOUBLE PRECISION',
+  'REAL',
+  'BIT',
+  'BINARY',
+  'BLOB',
+  'CHAR',
+  'NATIONAL CHAR',
+  'CHAR BYTE',
+  'ENUM',
+  'VARBINARY',
+  'VARCHAR',
+  'NATIONAL VARCHAR',
+  // 'SET' // handled as special-case in postProcess
+  'TIME',
+  'DATETIME',
+  'TIMESTAMP',
+  'YEAR',
 ];
 
 /**
@@ -1160,7 +1194,8 @@ export default class MariaDbFormatter extends Formatter {
       reservedJoins,
       reservedDependentClauses,
       reservedLogicalOperators: ['AND', 'OR', 'XOR'],
-      reservedKeywords: dedupe([...reservedKeywords, ...reservedFunctions]),
+      reservedKeywords: dedupe(reservedKeywords),
+      reservedFunctionNames: dedupe(reservedFunctions),
       stringTypes: ['""', { quote: "''", prefixes: ['X'] }],
       identTypes: ['``'],
       identChars: { first: '$', rest: '$' },
@@ -1183,7 +1218,7 @@ function postProcess(tokens: Token[]) {
     const nextToken = tokens[i + 1] || EOF_TOKEN;
     if (isToken.SET(token) && nextToken.value === '(') {
       // This is SET datatype, not SET statement
-      return { ...token, type: TokenType.RESERVED_KEYWORD };
+      return { ...token, type: TokenType.RESERVED_FUNCTION_NAME };
     }
     return token;
   });
diff --git a/src/languages/mysql.formatter.ts b/src/languages/mysql.formatter.ts
index a23bfdf1..08fcebbb 100644
--- a/src/languages/mysql.formatter.ts
+++ b/src/languages/mysql.formatter.ts
@@ -115,7 +115,7 @@ const reservedFunctions = [
   'ICU_VERSION',
   'IF',
   'IFNULL',
-  'IN',
+  // 'IN',
   'INET_ATON',
   'INET_NTOA',
   'INET6_ATON',
@@ -432,6 +432,37 @@ const reservedFunctions = [
   // 'XOR',
   'YEAR',
   'YEARWEEK',
+  // Data types with parameters
+  // https://dev.mysql.com/doc/refman/8.0/en/data-types.html
+  'BIT',
+  'TINYINT',
+  'SMALLINT',
+  'MEDIUMINT',
+  'INT',
+  'INTEGER',
+  'BIGINT',
+  'DECIMAL',
+  'DEC',
+  'NUMERIC',
+  'FIXED',
+  'FLOAT',
+  'DOUBLE',
+  'DOUBLE PRECISION',
+  'REAL',
+  'DATETIME',
+  'TIMESTAMP',
+  'TIME',
+  'YEAR',
+  'CHAR',
+  'NATIONAL CHAR',
+  'VARCHAR',
+  'NATIONAL VARCHAR',
+  'BINARY',
+  'VARBINARY',
+  'BLOB',
+  'TEXT',
+  'ENUM',
+  // 'SET' // handled as special-case in postProcess
 ];
 
 /**
@@ -656,6 +687,7 @@ const reservedKeywords = [
   'INSERT_METHOD',
   'INSTALL',
   'INSTANCE',
+  'IN', // <-- moved over from functions
   'INT',
   'INT1',
   'INT2',
@@ -1325,7 +1357,8 @@ export default class MySqlFormatter extends Formatter {
       reservedJoins,
       reservedDependentClauses,
       reservedLogicalOperators: ['AND', 'OR', 'XOR'],
-      reservedKeywords: dedupe([...reservedKeywords, ...reservedFunctions]),
+      reservedKeywords: dedupe(reservedKeywords),
+      reservedFunctionNames: dedupe(reservedFunctions),
       stringTypes: ['""', { quote: "''", prefixes: ['X'] }],
       identTypes: ['``'],
       identChars: { first: '$', rest: '$' },
@@ -1348,7 +1381,7 @@ function postProcess(tokens: Token[]) {
     const nextToken = tokens[i + 1] || EOF_TOKEN;
     if (isToken.SET(token) && nextToken.value === '(') {
       // This is SET datatype, not SET statement
-      return { ...token, type: TokenType.RESERVED_KEYWORD };
+      return { ...token, type: TokenType.RESERVED_FUNCTION_NAME };
     }
     return token;
   });
diff --git a/src/languages/n1ql.formatter.ts b/src/languages/n1ql.formatter.ts
index 58ee0bc9..cb52f702 100644
--- a/src/languages/n1ql.formatter.ts
+++ b/src/languages/n1ql.formatter.ts
@@ -234,6 +234,10 @@ const reservedFunctions = [
   'VAR_SAMP',
   'WEEKDAY_MILLIS',
   'WEEKDAY_STR',
+  // type casting
+  // not implemented in N1QL, but added here now for the sake of tests
+  // https://docs.couchbase.com/server/current/analytics/3_query.html#Vs_SQL-92
+  'CAST',
 ];
 
 /**
@@ -518,7 +522,8 @@ export default class N1qlFormatter extends Formatter {
       reservedJoins,
       reservedDependentClauses,
       reservedLogicalOperators: ['AND', 'OR', 'XOR'],
-      reservedKeywords: dedupe([...reservedKeywords, ...reservedFunctions]),
+      reservedKeywords: dedupe(reservedKeywords),
+      reservedFunctionNames: dedupe(reservedFunctions),
       // NOTE: single quotes are actually not supported in N1QL,
       // but we support them anyway as all other SQL dialects do,
       // which simplifies writing tests that are shared between all dialects.
diff --git a/src/languages/plsql.formatter.ts b/src/languages/plsql.formatter.ts
index f0f4c18f..2312269c 100644
--- a/src/languages/plsql.formatter.ts
+++ b/src/languages/plsql.formatter.ts
@@ -234,6 +234,36 @@ const reservedFunctions = {
   window: ['FIRST_VALUE', 'LAG', 'LAST_VALUE', 'LEAD', 'NTILE', 'RATIO_TO_REPORT', 'ROW_NUMBER'],
   objectReference: ['DEREF', 'MAKE_REF', 'REF', 'REFTOHEX', 'VALUE'],
   model: ['CV', 'ITERATION_NUMBER', 'PRESENTNNV', 'PRESENTV', 'PREVIOUS'],
+  // Parameterized data types
+  // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Data-Types.html
+  dataTypes: [
+    // Oracle builtin data types
+    'VARCHAR2',
+    'NVARCHAR2',
+    'NUMBER',
+    'FLOAT',
+    'TIMESTAMP',
+    'INTERVAL YEAR',
+    'INTERVAL DAY',
+    'RAW',
+    'UROWID',
+    'NCHAR',
+    // ANSI Data Types
+    'CHARACTER',
+    'CHAR',
+    'CHARACTER VARYING',
+    'CHAR VARYING',
+    'NATIONAL CHARACTER',
+    'NATIONAL CHAR',
+    'NATIONAL CHARACTER VARYING',
+    'NATIONAL CHAR VARYING',
+    'NCHAR VARYING',
+    'NUMERIC',
+    'DECIMAL',
+    'FLOAT',
+    // SQL/DS and DB2 Data Types
+    'VARCHAR',
+  ],
 };
 
 /**
@@ -691,7 +721,8 @@ export default class PlSqlFormatter extends Formatter {
       reservedJoins,
       reservedDependentClauses,
       reservedLogicalOperators: ['AND', 'OR', 'XOR'],
-      reservedKeywords: dedupe([...reservedKeywords, ...Object.values(reservedFunctions).flat()]),
+      reservedKeywords: dedupe(reservedKeywords),
+      reservedFunctionNames: dedupe(Object.values(reservedFunctions).flat()),
       // TODO: support custom-delimited strings: Q'{..}' q'<..>' etc
       stringTypes: [{ quote: "''", prefixes: ['N'] }],
       identTypes: [`""`],
diff --git a/src/languages/postgresql.formatter.ts b/src/languages/postgresql.formatter.ts
index 1dd49b34..1b9ed3a8 100644
--- a/src/languages/postgresql.formatter.ts
+++ b/src/languages/postgresql.formatter.ts
@@ -417,7 +417,7 @@ const reservedFunctions = {
   ],
   // https://www.postgresql.org/docs/14/functions-aggregate.html
   aggregate: [
-    'ANY',
+    // 'ANY',
     'ARRAY_AGG',
     'AVG',
     'BIT_AND',
@@ -456,7 +456,7 @@ const reservedFunctions = {
     'REGR_SXX',
     'REGR_SXY',
     'REGR_SYY',
-    'SOME',
+    // 'SOME',
     'STDDEV',
     'STDDEV_POP',
     'STDDEV_SAMP',
@@ -719,6 +719,22 @@ const reservedFunctions = {
   ],
   // https://www.postgresql.org/docs/14/functions-statistics.html
   stats: ['PG_MCV_LIST_ITEMS'],
+  cast: ['CAST'],
+  // Parameterized data types
+  // https://www.postgresql.org/docs/current/datatype.html
+  dataTypes: [
+    'BIT',
+    'BIT VARYING',
+    'CHARACTER',
+    'CHARACTER VARYING',
+    'VARCHAR',
+    'CHAR',
+    'DECIMAL',
+    'NUMERIC',
+    'TIME',
+    'TIMESTAMP',
+    'ENUM',
+  ],
 };
 
 /**
@@ -742,6 +758,7 @@ const reservedKeywords = [
   'ALTER',
   'ALWAYS',
   'ANALYSE',
+  'ANY', // <-- moved over from functions
   'ARE',
   'ARRAY',
   'ARRAY_MAX_CARDINALITY',
@@ -1241,6 +1258,7 @@ const reservedKeywords = [
   'SKIP',
   'SMALLINT',
   'SNAPSHOT',
+  'SOME', // <-- moved over from functions
   'SOURCE',
   'SPACE',
   'SPECIFIC',
@@ -1702,7 +1720,8 @@ export default class PostgreSqlFormatter extends Formatter {
       reservedBinaryCommands,
       reservedJoins,
       reservedDependentClauses,
-      reservedKeywords: dedupe([...reservedKeywords, ...Object.values(reservedFunctions).flat()]),
+      reservedKeywords: dedupe(reservedKeywords),
+      reservedFunctionNames: dedupe(Object.values(reservedFunctions).flat()),
       openParens: ['(', '['],
       closeParens: [')', ']'],
       stringTypes: [{ quote: "''", prefixes: ['U&', 'E', 'X', 'B'] }, '$$'],
diff --git a/src/languages/redshift.formatter.ts b/src/languages/redshift.formatter.ts
index 59512d48..704cee6d 100644
--- a/src/languages/redshift.formatter.ts
+++ b/src/languages/redshift.formatter.ts
@@ -359,6 +359,17 @@ const reservedFunctions = {
     'USER',
     'VERSION',
   ],
+  dataTypes: [
+    'DECIMAL',
+    'NUMERIC',
+    'CHAR',
+    'CHARACTER',
+    'VARCHAR',
+    'CHARACTER VARYING',
+    'NCHAR',
+    'NVARCHAR',
+    'VARBYTE',
+  ],
 };
 
 /**
@@ -719,10 +730,8 @@ export default class RedshiftFormatter extends Formatter {
       reservedBinaryCommands,
       reservedJoins,
       reservedDependentClauses,
-      reservedKeywords: dedupe([
-        ...Object.values(reservedKeywords).flat(),
-        ...Object.values(reservedFunctions).flat(),
-      ]),
+      reservedKeywords: dedupe(Object.values(reservedKeywords).flat()),
+      reservedFunctionNames: dedupe(Object.values(reservedFunctions).flat()),
       stringTypes: ["''"],
       identTypes: [`""`],
       numberedParamTypes: ['$'],
diff --git a/src/languages/spark.formatter.ts b/src/languages/spark.formatter.ts
index b82c9b57..09fc7dbc 100644
--- a/src/languages/spark.formatter.ts
+++ b/src/languages/spark.formatter.ts
@@ -12,7 +12,7 @@ import { dedupe } from 'src/utils';
 const reservedFunctions = {
   // http://spark.apache.org/docs/latest/sql-ref-functions-builtin.html#aggregate-functions
   aggregate: [
-    'ANY',
+    // 'ANY',
     'APPROX_COUNT_DISTINCT',
     'APPROX_PERCENTILE',
     'AVG',
@@ -48,7 +48,7 @@ const reservedFunctions = {
     'PERCENTILE',
     'PERCENTILE_APPROX',
     'SKEWNESS',
-    'SOME',
+    // 'SOME',
     'STD',
     'STDDEV',
     'STDDEV_POP',
@@ -350,6 +350,18 @@ const reservedFunctions = {
     'XXHASH64',
     'ZIP_WITH',
   ],
+  cast: ['CAST'],
+  // Shorthand functions to use in place of CASE expression
+  caseAbbrev: ['COALESCE', 'NULLIF'],
+  // Parameterized data types
+  // https://spark.apache.org/docs/latest/sql-ref-datatypes.html
+  dataTypes: [
+    'DECIMAL',
+    'DEC',
+    'NUMERIC',
+    // No varchar type in Spark, only STRING. Added for the sake of tests
+    'VARCHAR',
+  ],
 };
 
 /**
@@ -780,7 +792,8 @@ export default class SparkFormatter extends Formatter {
       reservedJoins,
       reservedDependentClauses,
       reservedLogicalOperators: ['AND', 'OR', 'XOR'],
-      reservedKeywords: dedupe([...reservedKeywords, ...Object.values(reservedFunctions).flat()]),
+      reservedKeywords: dedupe(reservedKeywords),
+      reservedFunctionNames: dedupe(Object.values(reservedFunctions).flat()),
       openParens: ['(', '['],
       closeParens: [')', ']'],
       stringTypes: [{ quote: "''", prefixes: ['X'] }],
@@ -799,8 +812,8 @@ function postProcess(tokens: Token[]) {
 
     // [WINDOW](...)
     if (isToken.WINDOW(token) && nextToken.type === TokenType.OPEN_PAREN) {
-      // This is a function call, treat it as a reserved word
-      return { ...token, type: TokenType.RESERVED_KEYWORD };
+      // This is a function call, treat it as a reserved function name
+      return { ...token, type: TokenType.RESERVED_FUNCTION_NAME };
     }
 
     // TODO: deprecate this once ITEMS is merged with COLLECTION
diff --git a/src/languages/sql.formatter.ts b/src/languages/sql.formatter.ts
index c7ea0c12..0fd708a8 100644
--- a/src/languages/sql.formatter.ts
+++ b/src/languages/sql.formatter.ts
@@ -54,9 +54,9 @@ const reservedFunctions = {
     'MAX',
     'MIN',
     'SUM',
-    'EVERY',
-    'ANY',
-    'SOME',
+    // 'EVERY',
+    // 'ANY',
+    // 'SOME',
     'STDDEV_POP',
     'STDDEV_SAMP',
     'VAR_SAMP',
@@ -84,7 +84,42 @@ const reservedFunctions = {
   // - CAST(...) WITH ...
   // - CAST FROM int
   // - CREATE CAST(mycol AS int) WITH ...
-  // cast: ['CAST'], (disabled for now)
+  cast: ['CAST'],
+  // Shorthand functions to use in place of CASE expression
+  caseAbbrev: ['COALESCE', 'NULLIF'],
+  // Non-standard functions that have widespread support
+  nonStandard: ['ROUND', 'SIN', 'COS', 'TAN', 'ASIN', 'ACOS', 'ATAN'],
+  // Data types with parameters like VARCHAR(100)
+  // https://jakewheat.github.io/sql-overview/sql-2008-foundation-grammar.html#predefined-type
+  dataTypes: [
+    'CHARACTER',
+    'CHAR',
+    'CHARACTER VARYING',
+    'CHAR VARYING',
+    'VARCHAR',
+    'CHARACTER LARGE OBJECT',
+    'CHAR LARGE OBJECT',
+    'CLOB',
+    'NATIONAL CHARACTER',
+    'NATIONAL CHAR',
+    'NCHAR',
+    'NATIONAL CHARACTER VARYING',
+    'NATIONAL CHAR VARYING',
+    'NCHAR VARYING',
+    'NATIONAL CHARACTER LARGE OBJECT',
+    'NCHAR LARGE OBJECT',
+    'NCLOB',
+    'BINARY',
+    'BINARY VARYING',
+    'VARBINARY',
+    'BINARY LARGE OBJECT',
+    'BLOB',
+    'NUMERIC',
+    'DECIMAL',
+    'DEC',
+    'TIME',
+    'TIMESTAMP',
+  ],
 };
 
 /**
@@ -98,6 +133,7 @@ const reservedKeywords = [
   'ALL',
   'ALLOCATE',
   'ALTER',
+  'ANY', // <- moved over from functions
   'ARE',
   'ARRAY',
   'AS',
@@ -117,13 +153,13 @@ const reservedKeywords = [
   'CALL',
   'CALLED',
   'CASCADED',
-  'CAST',
+  // 'CAST',
   'CHAR',
   'CHARACTER',
   'CHECK',
   'CLOB',
   'CLOSE',
-  'COALESCE',
+  // 'COALESCE',
   'COLLATE',
   'COLUMN',
   'COMMIT',
@@ -164,6 +200,7 @@ const reservedKeywords = [
   'ELEMENT',
   'END-EXEC',
   'ESCAPE',
+  'EVERY', // <- moved over from functions
   'EXCEPT',
   'EXEC',
   'EXECUTE',
@@ -225,7 +262,7 @@ const reservedKeywords = [
   'NONE',
   'NOT',
   'NULL',
-  'NULLIF',
+  // 'NULLIF',
   'NUMERIC',
   'OF',
   'OLD',
@@ -272,6 +309,7 @@ const reservedKeywords = [
   'SET',
   'SIMILAR',
   'SMALLINT',
+  'SOME', // <- moved over from functions
   'SPECIFIC',
   'SQL',
   'SQLEXCEPTION',
@@ -392,7 +430,8 @@ export default class SqlFormatter extends Formatter {
       reservedBinaryCommands,
       reservedJoins,
       reservedDependentClauses,
-      reservedKeywords: dedupe([...reservedKeywords, ...Object.values(reservedFunctions).flat()]),
+      reservedKeywords: dedupe(reservedKeywords),
+      reservedFunctionNames: dedupe(Object.values(reservedFunctions).flat()),
       stringTypes: [{ quote: "''", prefixes: ['X'] }],
       identTypes: [`""`, '``'],
       positionalParams: true,
diff --git a/src/languages/sqlite.formatter.ts b/src/languages/sqlite.formatter.ts
index 545fe387..85b946bd 100644
--- a/src/languages/sqlite.formatter.ts
+++ b/src/languages/sqlite.formatter.ts
@@ -52,6 +52,89 @@ const reservedFunctions = {
   aggregate: ['AVG', 'COUNT', 'GROUP_CONCAT', 'MAX', 'MIN', 'SUM', 'TOTAL'],
   // https://www.sqlite.org/lang_datefunc.html
   datetime: ['DATE', 'TIME', 'DATETIME', 'JULIANDAY', 'UNIXEPOCH', 'STRFTIME'],
+  // https://www.sqlite.org/windowfunctions.html#biwinfunc
+  window: [
+    'row_number',
+    'rank',
+    'dense_rank',
+    'percent_rank',
+    'cume_dist',
+    'ntile',
+    'lag',
+    'lead',
+    'first_value',
+    'last_value',
+    'nth_value',
+  ],
+  // https://www.sqlite.org/lang_mathfunc.html
+  math: [
+    'ACOS',
+    'ACOSH',
+    'ASIN',
+    'ASINH',
+    'ATAN',
+    'ATAN2',
+    'ATANH',
+    'CEIL',
+    'CEILING',
+    'COS',
+    'COSH',
+    'DEGREES',
+    'EXP',
+    'FLOOR',
+    'LN',
+    'LOG',
+    'LOG',
+    'LOG10',
+    'LOG2',
+    'MOD',
+    'PI',
+    'POW',
+    'POWER',
+    'RADIANS',
+    'SIN',
+    'SINH',
+    'SQRT',
+    'TAN',
+    'TANH',
+    'TRUNC',
+  ],
+  // https://www.sqlite.org/json1.html
+  json: [
+    'JSON',
+    'JSON_ARRAY',
+    'JSON_ARRAY_LENGTH',
+    'JSON_ARRAY_LENGTH',
+    'JSON_EXTRACT',
+    'JSON_INSERT',
+    'JSON_OBJECT',
+    'JSON_PATCH',
+    'JSON_REMOVE',
+    'JSON_REPLACE',
+    'JSON_SET',
+    'JSON_TYPE',
+    'JSON_TYPE',
+    'JSON_VALID',
+    'JSON_QUOTE',
+    'JSON_GROUP_ARRAY',
+    'JSON_GROUP_OBJECT',
+    'JSON_EACH',
+    'JSON_TREE',
+  ],
+  cast: ['CAST'],
+  // SQLite allows parameters for all data types
+  // Well, in fact it allows any word as a data type, e.g. CREATE TABLE foo (col1 madeupname(123));
+  // https://www.sqlite.org/datatype3.html
+  dataTypes: [
+    'CHARACTER',
+    'VARCHAR',
+    'VARYING CHARACTER',
+    'NCHAR',
+    'NATIVE CHARACTER',
+    'NVARCHAR',
+    'NUMERIC',
+    'DECIMAL',
+  ],
 };
 
 // https://www.sqlite.org/lang_keywords.html
@@ -281,7 +364,8 @@ export default class SqliteFormatter extends Formatter {
       reservedBinaryCommands,
       reservedJoins,
       reservedDependentClauses,
-      reservedKeywords: dedupe([...reservedKeywords, ...Object.values(reservedFunctions).flat()]),
+      reservedKeywords: dedupe(reservedKeywords),
+      reservedFunctionNames: dedupe(Object.values(reservedFunctions).flat()),
       stringTypes: [{ quote: "''", prefixes: ['X'] }],
       identTypes: [`""`, '``', '[]'],
       // https://www.sqlite.org/lang_expr.html#parameters
diff --git a/src/languages/tsql.formatter.ts b/src/languages/tsql.formatter.ts
index 08230a8c..7ee22697 100644
--- a/src/languages/tsql.formatter.ts
+++ b/src/languages/tsql.formatter.ts
@@ -305,6 +305,25 @@ const reservedFunctions = {
     'TEXTVALID',
   ],
   trigger: ['COLUMNS_UPDATED', 'EVENTDATA', 'TRIGGER_NESTLEVEL', 'UPDATE'],
+  // Shorthand functions to use in place of CASE expression
+  caseAbbrev: ['COALESCE', 'NULLIF'],
+  // Parameterized types
+  // https://docs.microsoft.com/en-us/sql/t-sql/data-types/data-types-transact-sql?view=sql-server-ver15
+  dataTypes: [
+    'DECIMAL',
+    'NUMERIC',
+    'FLOAT',
+    'REAL',
+    'DATETIME2',
+    'DATETIMEOFFSET',
+    'TIME',
+    'CHAR',
+    'VARCHAR',
+    'NCHAR',
+    'NVARCHAR',
+    'BINARY',
+    'VARBINARY',
+  ],
 };
 
 // TODO: dedupe these reserved word lists
@@ -1237,10 +1256,8 @@ export default class TSqlFormatter extends Formatter {
       reservedBinaryCommands,
       reservedJoins,
       reservedDependentClauses,
-      reservedKeywords: dedupe([
-        ...Object.values(reservedKeywords).flat(),
-        ...Object.values(reservedFunctions).flat(),
-      ]),
+      reservedKeywords: dedupe(Object.values(reservedKeywords).flat()),
+      reservedFunctionNames: dedupe(Object.values(reservedFunctions).flat()),
       stringTypes: [{ quote: "''", prefixes: ['N'] }],
       identTypes: [`""`, '[]'],
       identChars: { first: '#@', rest: '#@$' },
diff --git a/src/lexer/Tokenizer.ts b/src/lexer/Tokenizer.ts
index f4e5ebc9..44802343 100644
--- a/src/lexer/Tokenizer.ts
+++ b/src/lexer/Tokenizer.ts
@@ -19,6 +19,8 @@ interface TokenizerOptions {
   reservedJoins: string[];
   // keywords used for JOIN conditions, defaults to: [ON, USING]
   reservedJoinConditions?: string[];
+  // built in function names
+  reservedFunctionNames: string[];
   // all other reserved words (not included to any of the above lists)
   reservedKeywords: string[];
   // Types of quotes to use for strings
@@ -99,6 +101,10 @@ export default class Tokenizer {
         regex: regex.reservedWord(cfg.reservedKeywords, cfg.identChars),
         value: v => v.toUpperCase(),
       },
+      [TokenType.RESERVED_FUNCTION_NAME]: {
+        regex: regex.reservedWord(cfg.reservedFunctionNames, cfg.identChars),
+        value: v => v.toUpperCase(),
+      },
       [TokenType.RESERVED_LOGICAL_OPERATOR]: {
         regex: regex.reservedWord(cfg.reservedLogicalOperators ?? ['AND', 'OR'], cfg.identChars),
         value: v => v.toUpperCase(),
diff --git a/src/lexer/TokenizerEngine.ts b/src/lexer/TokenizerEngine.ts
index 4664fab2..eccf4a10 100644
--- a/src/lexer/TokenizerEngine.ts
+++ b/src/lexer/TokenizerEngine.ts
@@ -34,8 +34,8 @@ export default class TokenizerEngine {
 
     // Keep processing the string until end is reached
     while (this.index < this.input.length) {
-      // grab any preceding whitespace
-      const whitespaceBefore = this.getWhitespace();
+      // skip any preceding whitespace
+      this.skipWhitespace();
 
       if (this.index < this.input.length) {
         // Get the next token and the token type
@@ -44,21 +44,18 @@ export default class TokenizerEngine {
           throw new Error(`Parse error: Unexpected "${input.slice(this.index, 100)}"`);
         }
 
-        tokens.push({ ...token, whitespaceBefore });
+        tokens.push(token);
       }
     }
     return tokens;
   }
 
-  private getWhitespace(): string {
+  private skipWhitespace(): void {
     WHITESPACE_REGEX.lastIndex = this.index;
     const matches = WHITESPACE_REGEX.exec(this.input);
     if (matches) {
       // Advance current position by matched whitespace length
       this.index += matches[0].length;
-      return matches[0];
-    } else {
-      return '';
     }
   }
 
@@ -113,6 +110,7 @@ export default class TokenizerEngine {
       this.matchToken(TokenType.RESERVED_BINARY_COMMAND) ||
       this.matchToken(TokenType.RESERVED_DEPENDENT_CLAUSE) ||
       this.matchToken(TokenType.RESERVED_JOIN) ||
+      this.matchToken(TokenType.RESERVED_FUNCTION_NAME) ||
       this.matchToken(TokenType.RESERVED_KEYWORD) ||
       this.matchToken(TokenType.RESERVED_LOGICAL_OPERATOR) ||
       this.matchToken(TokenType.RESERVED_JOIN_CONDITION)
diff --git a/src/lexer/regexUtil.ts b/src/lexer/regexUtil.ts
index 485b614a..39b9eccf 100644
--- a/src/lexer/regexUtil.ts
+++ b/src/lexer/regexUtil.ts
@@ -5,7 +5,7 @@ export const escapeRegExp = (string: string) => string.replace(/[.*+?^${}()|[\]\
 
 export const NULL_REGEX = /^(?!)/; // zero-width negative lookahead, matches nothing
 
-export const WHITESPACE_REGEX = /(\s+)/uy;
+export const WHITESPACE_REGEX = /\s+/uy;
 
 export const patternToRegex = (pattern: string): RegExp => new RegExp(`(?:${pattern})`, 'uy');
 
diff --git a/src/lexer/token.ts b/src/lexer/token.ts
index d7385d5a..ad372625 100644
--- a/src/lexer/token.ts
+++ b/src/lexer/token.ts
@@ -5,6 +5,7 @@ export enum TokenType {
   STRING = 'STRING',
   VARIABLE = 'VARIABLE',
   RESERVED_KEYWORD = 'RESERVED_KEYWORD',
+  RESERVED_FUNCTION_NAME = 'RESERVED_FUNCTION_NAME',
   RESERVED_LOGICAL_OPERATOR = 'RESERVED_LOGICAL_OPERATOR',
   RESERVED_DEPENDENT_CLAUSE = 'RESERVED_DEPENDENT_CLAUSE',
   RESERVED_BINARY_COMMAND = 'RESERVED_BINARY_COMMAND',
@@ -34,7 +35,6 @@ export interface Token {
   text: string; // The raw original text that was matched
   value: string; // Cleaned up `text` e.g. keyword converted to uppercase and extra spaces removed
   key?: string;
-  whitespaceBefore?: string;
 }
 
 /**
@@ -56,7 +56,7 @@ export const isToken = {
   ARRAY: testToken({ value: 'ARRAY', type: TokenType.RESERVED_KEYWORD }),
   BETWEEN: testToken({ value: 'BETWEEN', type: TokenType.RESERVED_KEYWORD }),
   CASE: testToken({ value: 'CASE', type: TokenType.RESERVED_CASE_START }),
-  CAST: testToken({ value: 'CAST', type: TokenType.RESERVED_KEYWORD }),
+  CAST: testToken({ value: 'CAST', type: TokenType.RESERVED_FUNCTION_NAME }),
   BY: testToken({ value: 'BY', type: TokenType.RESERVED_KEYWORD }),
   END: testToken({ value: 'END', type: TokenType.RESERVED_CASE_END }),
   FROM: testToken({ value: 'FROM', type: TokenType.RESERVED_COMMAND }),
@@ -76,6 +76,7 @@ export const isCommand = (token: Token): boolean =>
 /** Checks if token is any Reserved Keyword or Command */
 export const isReserved = (token: Token): boolean =>
   token.type === TokenType.RESERVED_KEYWORD ||
+  token.type === TokenType.RESERVED_FUNCTION_NAME ||
   token.type === TokenType.RESERVED_LOGICAL_OPERATOR ||
   token.type === TokenType.RESERVED_DEPENDENT_CLAUSE ||
   token.type === TokenType.RESERVED_JOIN_CONDITION ||
diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts
index df228f78..a73b9aed 100644
--- a/src/parser/Parser.ts
+++ b/src/parser/Parser.ts
@@ -83,12 +83,7 @@ export default class Parser {
   }
 
   private functionCall(): FunctionCall | undefined {
-    if (
-      (this.look().type === TokenType.RESERVED_KEYWORD ||
-        this.look().type === TokenType.IDENTIFIER) &&
-      this.look(1).value === '(' &&
-      !this.look(1).whitespaceBefore
-    ) {
+    if (this.look().type === TokenType.RESERVED_FUNCTION_NAME && this.look(1).value === '(') {
       return {
         type: NodeType.function_call,
         nameToken: this.next(),


Test patch:
diff --git a/test/behavesLikeSqlFormatter.ts b/test/behavesLikeSqlFormatter.ts
index 68e379b2..a9a45f2a 100644
--- a/test/behavesLikeSqlFormatter.ts
+++ b/test/behavesLikeSqlFormatter.ts
@@ -66,7 +66,7 @@ export default function behavesLikeSqlFormatter(format: FormatFn) {
   it('formats SELECT with complex WHERE', () => {
     const result = format(`
       SELECT * FROM foo WHERE Column1 = 'testing'
-      AND ( (Column2 = Column3 OR Column4 >= NOW()) );
+      AND ( (Column2 = Column3 OR Column4 >= ABS(5)) );
     `);
     expect(result).toBe(dedent`
       SELECT
@@ -78,7 +78,7 @@ export default function behavesLikeSqlFormatter(format: FormatFn) {
         AND (
           (
             Column2 = Column3
-            OR Column4 >= NOW()
+            OR Column4 >= ABS(5)
           )
         );
     `);
@@ -195,18 +195,18 @@ export default function behavesLikeSqlFormatter(format: FormatFn) {
   });
 
   it('keeps short parenthesized list with nested parenthesis on single line', () => {
-    const result = format('SELECT (a + b * (c - NOW()));');
+    const result = format('SELECT (a + b * (c - SIN(1)));');
     expect(result).toBe(dedent`
       SELECT
-        (a + b * (c - NOW()));
+        (a + b * (c - SIN(1)));
     `);
   });
 
   it('breaks long parenthesized lists to multiple lines', () => {
     const result = format(`
       INSERT INTO some_table (id_product, id_shop, id_currency, id_country, id_registration) (
-      SELECT IF(dq.id_discounter_shopping = 2, dq.value, dq.value / 100),
-      IF (dq.id_discounter_shopping = 2, 'amount', 'percentage') FROM foo);
+      SELECT COALESCE(dq.id_discounter_shopping = 2, dq.value, dq.value / 100),
+      COALESCE (dq.id_discounter_shopping = 2, 'amount', 'percentage') FROM foo);
     `);
     expect(result).toBe(dedent`
       INSERT INTO
@@ -218,12 +218,12 @@ export default function behavesLikeSqlFormatter(format: FormatFn) {
           id_registration
         ) (
           SELECT
-            IF(
+            COALESCE(
               dq.id_discounter_shopping = 2,
               dq.value,
               dq.value / 100
             ),
-            IF (
+            COALESCE(
               dq.id_discounter_shopping = 2,
               'amount',
               'percentage'
diff --git a/test/bigquery.test.ts b/test/bigquery.test.ts
index e8e75c28..f7e9a715 100644
--- a/test/bigquery.test.ts
+++ b/test/bigquery.test.ts
@@ -103,7 +103,7 @@ describe('BigQueryFormatter', () => {
     );
     expect(result).toBe(dedent`
       SELECT
-        STRUCT("Alpha" as name, [23.4, 26.3, 26.4, 26.1] as splits)
+        STRUCT ("Alpha" as name, [23.4, 26.3, 26.4, 26.1] as splits)
       FROM
         beta
     `);
@@ -116,10 +116,11 @@ describe('BigQueryFormatter', () => {
     `);
   });
 
+  // TODO: Possibly incorrect formatting of STRUCT<>() and ARRAY<>()
   it('supports parametric STRUCT', () => {
     expect(format('SELECT STRUCT<ARRAY<INT64>>([])')).toBe(dedent`
       SELECT
-        STRUCT<ARRAY<INT64>>([])
+        STRUCT<ARRAY<INT64>> ([])
     `);
   });
 
@@ -128,8 +129,8 @@ describe('BigQueryFormatter', () => {
     expect(format('SELECT STRUCT<y INT64, z STRING>(1,"foo"), STRUCT<arr ARRAY<INT64>>([1,2,3]);'))
       .toBe(dedent`
       SELECT
-        STRUCT<y INT64, z STRING>(1, "foo"),
-        STRUCT<arr ARRAY<INT64>>([1, 2, 3]);
+        STRUCT<y INT64, z STRING> (1, "foo"),
+        STRUCT<arr ARRAY<INT64>> ([1, 2, 3]);
     `);
   });
 
@@ -137,7 +138,7 @@ describe('BigQueryFormatter', () => {
     expect(format('select struct<Nr int64, myName string>(1,"foo");', { keywordCase: 'upper' }))
       .toBe(dedent`
       SELECT
-        STRUCT<Nr INT64, myName STRING>(1, "foo");
+        STRUCT<Nr INT64, myName STRING> (1, "foo");
     `);
   });
 
@@ -148,7 +149,7 @@ describe('BigQueryFormatter', () => {
     expect(format('SELECT STRUCT<Nr INT64, myName STRING>(1,"foo");', { keywordCase: 'lower' }))
       .toBe(dedent`
       select
-        STRUCT<Nr INT64, myName STRING>(1, "foo");
+        STRUCT<Nr INT64, myName STRING> (1, "foo");
     `);
   });
 
diff --git a/test/features/case.ts b/test/features/case.ts
index a8e07e63..ee28f8e3 100644
--- a/test/features/case.ts
+++ b/test/features/case.ts
@@ -20,12 +20,12 @@ export default function supportsCase(format: FormatFn) {
 
   it('formats CASE ... WHEN with an expression', () => {
     const result = format(
-      "CASE toString(getNumber()) WHEN 'one' THEN 1 WHEN 'two' THEN 2 WHEN 'three' THEN 3 ELSE 4 END;"
+      "CASE trim(sqrt(2)) WHEN 'one' THEN 1 WHEN 'two' THEN 2 WHEN 'three' THEN 3 ELSE 4 END;"
     );
 
     expect(result).toBe(dedent`
       CASE
-        toString(getNumber())
+        trim(sqrt(2))
         WHEN 'one' THEN 1
         WHEN 'two' THEN 2
         WHEN 'three' THEN 3
@@ -80,12 +80,12 @@ export default function supportsCase(format: FormatFn) {
 
   it('properly converts to uppercase in case statements', () => {
     const result = format(
-      "case stringify(getNumber()) when 'one' then 1 when 'two' then 2 when 'three' then 3 else 4 end;",
+      "case trim(sqrt(my_field)) when 'one' then 1 when 'two' then 2 when 'three' then 3 else 4 end;",
       { keywordCase: 'upper' }
     );
     expect(result).toBe(dedent`
       CASE
-        stringify(getNumber())
+        TRIM(SQRT(my_field))
         WHEN 'one' THEN 1
         WHEN 'two' THEN 2
         WHEN 'three' THEN 3
diff --git a/test/features/window.ts b/test/features/window.ts
index 020d993d..e1c34dc9 100644
--- a/test/features/window.ts
+++ b/test/features/window.ts
@@ -5,12 +5,12 @@ import { FormatFn } from 'src/sqlFormatter';
 export default function supportsWindow(format: FormatFn) {
   it('formats WINDOW clause at top level', () => {
     const result = format(
-      'SELECT *, LAG(value) OVER wnd AS next_value FROM tbl WINDOW wnd AS (PARTITION BY id ORDER BY time);'
+      'SELECT *, ROW_NUMBER() OVER wnd AS next_value FROM tbl WINDOW wnd AS (PARTITION BY id ORDER BY time);'
     );
     expect(result).toBe(dedent`
       SELECT
         *,
-        LAG(value) OVER wnd AS next_value
+        ROW_NUMBER() OVER wnd AS next_value
       FROM
         tbl
       WINDOW
diff --git a/test/plsql.test.ts b/test/plsql.test.ts
index fe797810..23af0942 100644
--- a/test/plsql.test.ts
+++ b/test/plsql.test.ts
@@ -103,6 +103,7 @@ describe('PlSqlFormatter', () => {
     `);
   });
 
+  // TODO: improve formatting of custom functions
   it('formats SELECT query with CROSS APPLY', () => {
     const result = format('SELECT a, b FROM t CROSS APPLY fn(t.id)');
     expect(result).toBe(dedent`
@@ -112,7 +113,7 @@ describe('PlSqlFormatter', () => {
       FROM
         t
       CROSS APPLY
-      fn(t.id)
+      fn (t.id)
     `);
   });
 
@@ -133,7 +134,7 @@ describe('PlSqlFormatter', () => {
       FROM
         t
       OUTER APPLY
-      fn(t.id)
+      fn (t.id)
     `);
   });
 
@@ -164,7 +165,7 @@ describe('PlSqlFormatter', () => {
     `);
     expect(result).toBe(dedent`
       WITH
-        t1(id, parent_id) AS (
+        t1 (id, parent_id) AS (
           -- Anchor member.
           SELECT
             id,
@@ -227,7 +228,7 @@ describe('PlSqlFormatter', () => {
     `);
     expect(result).toBe(dedent/* sql */ `
       WITH
-        t1(id, parent_id) AS (
+        t1 (id, parent_id) AS (
           -- Anchor member.
           SELECT
             id,
diff --git a/test/unit/Parser.test.ts b/test/unit/Parser.test.ts
index b1d50a8c..eaec6910 100644
--- a/test/unit/Parser.test.ts
+++ b/test/unit/Parser.test.ts
@@ -9,7 +9,8 @@ describe('Parser', () => {
       reservedBinaryCommands: ['UNION'],
       reservedJoins: ['JOIN'],
       reservedJoinConditions: ['ON', 'USING'],
-      reservedKeywords: ['BETWEEN', 'LIKE', 'SQRT'],
+      reservedKeywords: ['BETWEEN', 'LIKE'],
+      reservedFunctionNames: ['SQRT', 'OFFSET'],
       openParens: ['(', '['],
       closeParens: [')', ']'],
       stringTypes: ["''"],
@@ -32,7 +33,6 @@ describe('Parser', () => {
                 "text": "foo",
                 "type": "IDENTIFIER",
                 "value": "foo",
-                "whitespaceBefore": "",
               },
               "type": "token",
             },
@@ -47,7 +47,6 @@ describe('Parser', () => {
                 "text": "bar",
                 "type": "IDENTIFIER",
                 "value": "bar",
-                "whitespaceBefore": " ",
               },
               "type": "token",
             },
@@ -69,9 +68,8 @@ describe('Parser', () => {
                 Object {
                   "nameToken": Object {
                     "text": "SQRT",
-                    "type": "RESERVED_KEYWORD",
+                    "type": "RESERVED_FUNCTION_NAME",
                     "value": "SQRT",
-                    "whitespaceBefore": " ",
                   },
                   "parenthesis": Object {
                     "children": Array [
@@ -80,7 +78,6 @@ describe('Parser', () => {
                           "text": "2",
                           "type": "NUMBER",
                           "value": "2",
-                          "whitespaceBefore": "",
                         },
                         "type": "token",
                       },
@@ -96,7 +93,6 @@ describe('Parser', () => {
                 "text": "SELECT",
                 "type": "RESERVED_COMMAND",
                 "value": "SELECT",
-                "whitespaceBefore": "",
               },
               "type": "clause",
             },
@@ -120,16 +116,14 @@ describe('Parser', () => {
                     "text": "my_array",
                     "type": "IDENTIFIER",
                     "value": "my_array",
-                    "whitespaceBefore": " ",
                   },
                   "parenthesis": Object {
                     "children": Array [
                       Object {
                         "nameToken": Object {
                           "text": "OFFSET",
-                          "type": "IDENTIFIER",
+                          "type": "RESERVED_FUNCTION_NAME",
                           "value": "OFFSET",
-                          "whitespaceBefore": "",
                         },
                         "parenthesis": Object {
                           "children": Array [
@@ -138,7 +132,6 @@ describe('Parser', () => {
                                 "text": "5",
                                 "type": "NUMBER",
                                 "value": "5",
-                                "whitespaceBefore": "",
                               },
                               "type": "token",
                             },
@@ -161,7 +154,6 @@ describe('Parser', () => {
                 "text": "SELECT",
                 "type": "RESERVED_COMMAND",
                 "value": "SELECT",
-                "whitespaceBefore": "",
               },
               "type": "clause",
             },
@@ -187,7 +179,6 @@ describe('Parser', () => {
                         "text": "birth_year",
                         "type": "IDENTIFIER",
                         "value": "birth_year",
-                        "whitespaceBefore": "",
                       },
                       "type": "token",
                     },
@@ -196,7 +187,6 @@ describe('Parser', () => {
                         "text": "-",
                         "type": "OPERATOR",
                         "value": "-",
-                        "whitespaceBefore": " ",
                       },
                       "type": "token",
                     },
@@ -207,7 +197,6 @@ describe('Parser', () => {
                             "text": "CURRENT_DATE",
                             "type": "IDENTIFIER",
                             "value": "CURRENT_DATE",
-                            "whitespaceBefore": "",
                           },
                           "type": "token",
                         },
@@ -216,7 +205,6 @@ describe('Parser', () => {
                             "text": "+",
                             "type": "OPERATOR",
                             "value": "+",
-                            "whitespaceBefore": " ",
                           },
                           "type": "token",
                         },
@@ -225,7 +213,6 @@ describe('Parser', () => {
                             "text": "1",
                             "type": "NUMBER",
                             "value": "1",
-                            "whitespaceBefore": " ",
                           },
                           "type": "token",
                         },
@@ -244,7 +231,6 @@ describe('Parser', () => {
                 "text": "SELECT",
                 "type": "RESERVED_COMMAND",
                 "value": "SELECT",
-                "whitespaceBefore": "",
               },
               "type": "clause",
             },
@@ -268,7 +254,6 @@ describe('Parser', () => {
                     "text": "age",
                     "type": "IDENTIFIER",
                     "value": "age",
-                    "whitespaceBefore": " ",
                   },
                   "type": "token",
                 },
@@ -277,25 +262,21 @@ describe('Parser', () => {
                     "text": "and",
                     "type": "RESERVED_LOGICAL_OPERATOR",
                     "value": "AND",
-                    "whitespaceBefore": " ",
                   },
                   "betweenToken": Object {
                     "text": "BETWEEN",
                     "type": "RESERVED_KEYWORD",
                     "value": "BETWEEN",
-                    "whitespaceBefore": " ",
                   },
                   "expr1": Object {
                     "text": "10",
                     "type": "NUMBER",
                     "value": "10",
-                    "whitespaceBefore": " ",
                   },
                   "expr2": Object {
                     "text": "15",
                     "type": "NUMBER",
                     "value": "15",
-                    "whitespaceBefore": " ",
                   },
                   "type": "between_predicate",
                 },
@@ -304,7 +285,6 @@ describe('Parser', () => {
                 "text": "WHERE",
                 "type": "RESERVED_COMMAND",
                 "value": "WHERE",
-                "whitespaceBefore": "",
               },
               "type": "clause",
             },
@@ -328,7 +308,6 @@ describe('Parser', () => {
                     "text": "10",
                     "type": "NUMBER",
                     "value": "10",
-                    "whitespaceBefore": " ",
                   },
                   "type": "token",
                 },
@@ -337,7 +316,6 @@ describe('Parser', () => {
                 "text": "LIMIT",
                 "type": "RESERVED_COMMAND",
                 "value": "LIMIT",
-                "whitespaceBefore": "",
               },
               "type": "limit_clause",
             },
@@ -361,7 +339,6 @@ describe('Parser', () => {
                     "text": "10",
                     "type": "NUMBER",
                     "value": "10",
-                    "whitespaceBefore": " ",
                   },
                   "type": "token",
                 },
@@ -370,7 +347,6 @@ describe('Parser', () => {
                 "text": "LIMIT",
                 "type": "RESERVED_COMMAND",
                 "value": "LIMIT",
-                "whitespaceBefore": "",
               },
               "offset": Array [
                 Object {
@@ -378,7 +354,6 @@ describe('Parser', () => {
                     "text": "200",
                     "type": "NUMBER",
                     "value": "200",
-                    "whitespaceBefore": " ",
                   },
                   "type": "token",
                 },
@@ -408,7 +383,6 @@ describe('Parser', () => {
                 "text": "SELECT",
                 "type": "RESERVED_COMMAND",
                 "value": "SELECT",
-                "whitespaceBefore": "",
               },
               "type": "clause",
             },