Repository: sql-formatter-org/sql-formatter (ts)

Issue:
Regression with the token PRECEDING with version 10 for bigquery formatting
**Describe the bug**
There seems to be a regression in what is supported by sql-formatter for the bigquery SQL language.

**Expected behavior**
I expected the version 10 to still support the token PRECEDING.

**Actual behavior**

I got the following error : 
<img width="1448" alt="187225626-99e10276-3d9e-49a1-914e-4015bfc19f6d" src="https://user-images.githubusercontent.com/1980396/187443697-c61a7e49-a384-467d-9992-06cd61eec8e3.png">

**Usage**

- How are you calling / using the library?
```
format(sql, {
    language: 'bigquery',
});
```
- What SQL language(s) does this apply to?
 bigquery
- Which SQL Formatter version are you using?
10

Pull request:
Fix crash caused by ROWS BETWEEN
A quick fix for BigQuery. There's most likely the same problem with many other dialects. Will investigate further and apply the fix to other dialects as well. 

Fixes #415

Patch:
diff --git a/src/languages/bigquery/bigquery.formatter.ts b/src/languages/bigquery/bigquery.formatter.ts
index 4383c69d..a1054329 100644
--- a/src/languages/bigquery/bigquery.formatter.ts
+++ b/src/languages/bigquery/bigquery.formatter.ts
@@ -127,14 +127,16 @@ const reservedJoins = expandPhrases([
   '{INNER | CROSS} JOIN',
 ]);
 
-const reservedPhrases = [
+const reservedPhrases = expandPhrases([
   // https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#tablesample_operator
   'TABLESAMPLE SYSTEM',
   // From DDL: https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language
   'ANY TYPE',
   'ALL COLUMNS',
   'NOT DETERMINISTIC',
-];
+  // inside window definitions
+  '{ROWS | RANGE} BETWEEN',
+]);
 
 // https://cloud.google.com/bigquery/docs/reference/#standard-sql-reference
 export default class BigQueryFormatter extends Formatter {
diff --git a/src/languages/db2/db2.formatter.ts b/src/languages/db2/db2.formatter.ts
index 93c41f08..524020bb 100644
--- a/src/languages/db2/db2.formatter.ts
+++ b/src/languages/db2/db2.formatter.ts
@@ -13,6 +13,7 @@ const reservedCommands = expandPhrases([
   'WHERE',
   'GROUP BY',
   'HAVING',
+  'PARTITION BY',
   'ORDER BY [INPUT SEQUENCE]',
   'FETCH FIRST',
   // Data modification
@@ -165,7 +166,7 @@ const reservedJoins = expandPhrases([
   '{INNER | CROSS} JOIN',
 ]);
 
-const reservedPhrases = ['ON DELETE', 'ON UPDATE'];
+const reservedPhrases = expandPhrases(['ON DELETE', 'ON UPDATE', '{ROWS | RANGE} BETWEEN']);
 
 // https://www.ibm.com/support/knowledgecenter/en/ssw_ibm_i_72/db2/rbafzintro.htm
 export default class Db2Formatter extends Formatter {
diff --git a/src/languages/db2/db2.functions.ts b/src/languages/db2/db2.functions.ts
index 92a4a9c0..d3c8faef 100644
--- a/src/languages/db2/db2.functions.ts
+++ b/src/languages/db2/db2.functions.ts
@@ -302,7 +302,20 @@ export const functions = flatKeywordList({
   // https://www.ibm.com/docs/en/db2-for-zos/11?topic=functions-row
   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'],
+  olap: [
+    'CUME_DIST',
+    'PERCENT_RANK',
+    'RANK',
+    'DENSE_RANK',
+    'NTILE',
+    'LAG',
+    'LEAD',
+    'ROW_NUMBER',
+    'FIRST_VALUE',
+    'LAST_VALUE',
+    'NTH_VALUE',
+    'RATIO_TO_REPORT',
+  ],
   // Type casting
   cast: ['CAST'],
 });
diff --git a/src/languages/hive/hive.formatter.ts b/src/languages/hive/hive.formatter.ts
index 2623ccf7..2f61bb45 100644
--- a/src/languages/hive/hive.formatter.ts
+++ b/src/languages/hive/hive.formatter.ts
@@ -78,6 +78,8 @@ const reservedJoins = expandPhrases([
   'LEFT SEMI JOIN',
 ]);
 
+const reservedPhrases = expandPhrases(['{ROWS | RANGE} BETWEEN']);
+
 // https://cwiki.apache.org/confluence/display/Hive/LanguageManual
 export default class HiveFormatter extends Formatter {
   static operators = ['<=>', '==', '||'];
@@ -89,6 +91,7 @@ export default class HiveFormatter extends Formatter {
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE'],
+      reservedPhrases,
       reservedKeywords: keywords,
       reservedFunctionNames: functions,
       extraParens: ['[]'],
diff --git a/src/languages/mariadb/mariadb.formatter.ts b/src/languages/mariadb/mariadb.formatter.ts
index 5d2b66a6..78e283f3 100644
--- a/src/languages/mariadb/mariadb.formatter.ts
+++ b/src/languages/mariadb/mariadb.formatter.ts
@@ -14,6 +14,7 @@ const reservedCommands = expandPhrases([
   'WHERE',
   'GROUP BY',
   'HAVING',
+  'PARTITION BY',
   'ORDER BY',
   'LIMIT',
   'OFFSET',
@@ -252,7 +253,12 @@ const reservedJoins = expandPhrases([
   'STRAIGHT_JOIN',
 ]);
 
-const reservedPhrases = ['ON DELETE', 'ON UPDATE', 'CHARACTER SET'];
+const reservedPhrases = expandPhrases([
+  'ON DELETE',
+  'ON UPDATE',
+  'CHARACTER SET',
+  '{ROWS | RANGE} BETWEEN',
+]);
 
 // For reference: https://mariadb.com/kb/en/sql-statements-structure/
 export default class MariaDbFormatter extends Formatter {
diff --git a/src/languages/mysql/mysql.formatter.ts b/src/languages/mysql/mysql.formatter.ts
index b524c0ba..a7364c78 100644
--- a/src/languages/mysql/mysql.formatter.ts
+++ b/src/languages/mysql/mysql.formatter.ts
@@ -221,7 +221,12 @@ const reservedJoins = expandPhrases([
   'STRAIGHT_JOIN',
 ]);
 
-const reservedPhrases = ['ON DELETE', 'ON UPDATE', 'CHARACTER SET'];
+const reservedPhrases = expandPhrases([
+  'ON DELETE',
+  'ON UPDATE',
+  'CHARACTER SET',
+  '{ROWS | RANGE} BETWEEN',
+]);
 
 // https://dev.mysql.com/doc/refman/8.0/en/
 export default class MySqlFormatter extends Formatter {
diff --git a/src/languages/n1ql/n1ql.formatter.ts b/src/languages/n1ql/n1ql.formatter.ts
index d3800bba..e6c0e0da 100644
--- a/src/languages/n1ql/n1ql.formatter.ts
+++ b/src/languages/n1ql/n1ql.formatter.ts
@@ -75,6 +75,8 @@ const reservedSetOperations = expandPhrases(['UNION [ALL]', 'EXCEPT [ALL]', 'INT
 
 const reservedJoins = expandPhrases(['JOIN', '{LEFT | RIGHT} [OUTER] JOIN', 'INNER JOIN']);
 
+const reservedPhrases = expandPhrases(['{ROWS | RANGE | GROUPS} BETWEEN']);
+
 // For reference: http://docs.couchbase.com.s3-website-us-west-1.amazonaws.com/server/6.0/n1ql/n1ql-language-reference/index.html
 export default class N1qlFormatter extends Formatter {
   static operators = ['==', '||'];
@@ -86,6 +88,7 @@ export default class N1qlFormatter extends Formatter {
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE'],
+      reservedPhrases,
       supportsXor: true,
       reservedKeywords: keywords,
       reservedFunctionNames: functions,
diff --git a/src/languages/plsql/plsql.formatter.ts b/src/languages/plsql/plsql.formatter.ts
index 5071cf23..32a8552b 100644
--- a/src/languages/plsql/plsql.formatter.ts
+++ b/src/languages/plsql/plsql.formatter.ts
@@ -14,6 +14,7 @@ const reservedCommands = expandPhrases([
   'WHERE',
   'GROUP BY',
   'HAVING',
+  'PARTITION BY',
   'ORDER [SIBLINGS] BY',
   'OFFSET',
   'FETCH {FIRST | NEXT}',
@@ -69,7 +70,12 @@ const reservedJoins = expandPhrases([
   '{CROSS | OUTER} APPLY',
 ]);
 
-const reservedPhrases = ['ON DELETE', 'ON UPDATE', 'ON COMMIT'];
+const reservedPhrases = expandPhrases([
+  'ON DELETE',
+  'ON UPDATE',
+  'ON COMMIT',
+  '{ROWS | RANGE} BETWEEN',
+]);
 
 export default class PlSqlFormatter extends Formatter {
   static operators = [
diff --git a/src/languages/postgresql/postgresql.formatter.ts b/src/languages/postgresql/postgresql.formatter.ts
index 4175ec50..b77583eb 100644
--- a/src/languages/postgresql/postgresql.formatter.ts
+++ b/src/languages/postgresql/postgresql.formatter.ts
@@ -237,7 +237,11 @@ const reservedJoins = expandPhrases([
   'NATURAL {LEFT | RIGHT | FULL} [OUTER] JOIN',
 ]);
 
-const reservedPhrases = ['ON DELETE', 'ON UPDATE'];
+const reservedPhrases = expandPhrases([
+  'ON DELETE',
+  'ON UPDATE',
+  '{ROWS | RANGE | GROUPS} BETWEEN',
+]);
 
 const binaryOperators = [
   // Math Operators
diff --git a/src/languages/redshift/redshift.formatter.ts b/src/languages/redshift/redshift.formatter.ts
index f58a12c5..120f7aa8 100644
--- a/src/languages/redshift/redshift.formatter.ts
+++ b/src/languages/redshift/redshift.formatter.ts
@@ -13,6 +13,7 @@ const reservedCommands = expandPhrases([
   'WHERE',
   'GROUP BY',
   'HAVING',
+  'PARTITION BY',
   'ORDER BY',
   'LIMIT',
   'OFFSET',
@@ -127,13 +128,15 @@ const reservedJoins = expandPhrases([
   'NATURAL {LEFT | RIGHT | FULL} [OUTER] JOIN',
 ]);
 
-const reservedPhrases = [
+const reservedPhrases = expandPhrases([
   // https://docs.aws.amazon.com/redshift/latest/dg/copy-parameters-data-conversion.html
   'NULL AS',
   // https://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_EXTERNAL_SCHEMA.html
   'DATA CATALOG',
   'HIVE METASTORE',
-];
+  // in window specifications
+  '{ROWS | RANGE} BETWEEN',
+]);
 
 // https://docs.aws.amazon.com/redshift/latest/dg/cm_chap_SQLCommandRef.html
 export default class RedshiftFormatter extends Formatter {
diff --git a/src/languages/singlestoredb/singlestoredb.formatter.ts b/src/languages/singlestoredb/singlestoredb.formatter.ts
index 4822e10f..20a6479b 100644
--- a/src/languages/singlestoredb/singlestoredb.formatter.ts
+++ b/src/languages/singlestoredb/singlestoredb.formatter.ts
@@ -14,6 +14,7 @@ const reservedCommands = expandPhrases([
   'WHERE',
   'GROUP BY',
   'HAVING',
+  'PARTITION BY',
   'ORDER BY',
   'LIMIT',
   'OFFSET',
@@ -222,7 +223,12 @@ const reservedJoins = expandPhrases([
   'STRAIGHT_JOIN',
 ]);
 
-const reservedPhrases = ['ON DELETE', 'ON UPDATE', 'CHARACTER SET'];
+const reservedPhrases = expandPhrases([
+  'ON DELETE',
+  'ON UPDATE',
+  'CHARACTER SET',
+  '{ROWS | RANGE} BETWEEN',
+]);
 
 export default class SingleStoreDbFormatter extends Formatter {
   static operators = ['~', ':=', '<=>', '<<', '>>', '&&', '||'];
diff --git a/src/languages/spark/spark.formatter.ts b/src/languages/spark/spark.formatter.ts
index 6f23c309..ae815dd1 100644
--- a/src/languages/spark/spark.formatter.ts
+++ b/src/languages/spark/spark.formatter.ts
@@ -16,6 +16,7 @@ const reservedCommands = expandPhrases([
   'GROUP BY',
   'HAVING',
   'WINDOW',
+  'PARTITION BY',
   'ORDER BY',
   'SORT BY',
   'CLUSTER BY',
@@ -107,7 +108,12 @@ const reservedJoins = expandPhrases([
   'NATURAL [LEFT] {ANTI | SEMI} JOIN',
 ]);
 
-const reservedPhrases = ['ON DELETE', 'ON UPDATE', 'CURRENT ROW'];
+const reservedPhrases = expandPhrases([
+  'ON DELETE',
+  'ON UPDATE',
+  'CURRENT ROW',
+  '{ROWS | RANGE} BETWEEN',
+]);
 
 // http://spark.apache.org/docs/latest/sql-programming-guide.html
 export default class SparkFormatter extends Formatter {
diff --git a/src/languages/sql/sql.formatter.ts b/src/languages/sql/sql.formatter.ts
index bc6e633c..c7bc9b83 100644
--- a/src/languages/sql/sql.formatter.ts
+++ b/src/languages/sql/sql.formatter.ts
@@ -65,7 +65,7 @@ const reservedJoins = expandPhrases([
   'NATURAL {LEFT | RIGHT | FULL} [OUTER] JOIN',
 ]);
 
-const reservedPhrases = ['ON DELETE', 'ON UPDATE'];
+const reservedPhrases = expandPhrases(['ON DELETE', 'ON UPDATE', '{ROWS | RANGE} BETWEEN']);
 
 export default class SqlFormatter extends Formatter {
   static operators = [];
diff --git a/src/languages/sqlite/sqlite.formatter.ts b/src/languages/sqlite/sqlite.formatter.ts
index 92f78406..d06d4d90 100644
--- a/src/languages/sqlite/sqlite.formatter.ts
+++ b/src/languages/sqlite/sqlite.formatter.ts
@@ -54,7 +54,11 @@ const reservedJoins = expandPhrases([
   'NATURAL {LEFT | RIGHT | FULL} [OUTER] JOIN',
 ]);
 
-const reservedPhrases = ['ON DELETE', 'ON UPDATE'];
+const reservedPhrases = expandPhrases([
+  'ON DELETE',
+  'ON UPDATE',
+  '{ROWS | RANGE | GROUPS} BETWEEN',
+]);
 
 export default class SqliteFormatter extends Formatter {
   // https://www.sqlite.org/lang_expr.html
diff --git a/src/languages/trino/trino.formatter.ts b/src/languages/trino/trino.formatter.ts
index ac382045..2b6c2691 100644
--- a/src/languages/trino/trino.formatter.ts
+++ b/src/languages/trino/trino.formatter.ts
@@ -117,6 +117,8 @@ const reservedJoins = expandPhrases([
   'NATURAL {LEFT | RIGHT | FULL} [OUTER] JOIN',
 ]);
 
+const reservedPhrases = expandPhrases(['{ROWS | RANGE | GROUPS} BETWEEN']);
+
 export default class TrinoFormatter extends Formatter {
   // https://trino.io/docs/current/functions/list.html#id1
   // https://trino.io/docs/current/sql/match-recognize.html#row-pattern-syntax
@@ -129,6 +131,7 @@ export default class TrinoFormatter extends Formatter {
       reservedSetOperations,
       reservedJoins,
       reservedDependentClauses: ['WHEN', 'ELSE'],
+      reservedPhrases,
       reservedKeywords: keywords,
       reservedFunctionNames: functions,
       // Trino also supports {- ... -} parenthesis.
diff --git a/src/languages/tsql/tsql.formatter.ts b/src/languages/tsql/tsql.formatter.ts
index 595b0f2d..224ae6de 100644
--- a/src/languages/tsql/tsql.formatter.ts
+++ b/src/languages/tsql/tsql.formatter.ts
@@ -210,7 +210,7 @@ const reservedJoins = expandPhrases([
   '{CROSS | OUTER} APPLY',
 ]);
 
-const reservedPhrases = ['ON DELETE', 'ON UPDATE'];
+const reservedPhrases = expandPhrases(['ON DELETE', 'ON UPDATE', '{ROWS | RANGE} BETWEEN']);
 
 // https://docs.microsoft.com/en-us/sql/t-sql/language-reference?view=sql-server-ver15
 export default class TSqlFormatter extends Formatter {


Test patch:
diff --git a/test/behavesLikeSqlFormatter.ts b/test/behavesLikeSqlFormatter.ts
index d667c527..e299801a 100644
--- a/test/behavesLikeSqlFormatter.ts
+++ b/test/behavesLikeSqlFormatter.ts
@@ -16,6 +16,7 @@ import supportsNewlineBeforeSemicolon from './options/newlineBeforeSemicolon';
 import supportsLogicalOperatorNewline from './options/logicalOperatorNewline';
 import supportsTabulateAlias from './options/tabulateAlias';
 import supportsParamTypes from './options/paramTypes';
+import supportsWindowFunctions from './features/windowFunctions';
 
 /**
  * Core tests for all SQL formatters
@@ -36,6 +37,7 @@ export default function behavesLikeSqlFormatter(format: FormatFn) {
   supportsCommaPosition(format);
   supportsLogicalOperatorNewline(format);
   supportsParamTypes(format);
+  supportsWindowFunctions(format);
 
   it('formats simple SELECT query', () => {
     const result = format('SELECT count(*),Column1 FROM Table1;');
diff --git a/test/features/windowFunctions.ts b/test/features/windowFunctions.ts
new file mode 100644
index 00000000..14205232
--- /dev/null
+++ b/test/features/windowFunctions.ts
@@ -0,0 +1,30 @@
+import dedent from 'dedent-js';
+
+import { FormatFn } from 'src/sqlFormatter';
+
+export default function supportsWindowFunctions(format: FormatFn) {
+  it('supports ROWS BETWEEN in window functions', () => {
+    expect(
+      format(`
+        SELECT
+          RANK() OVER (
+            PARTITION BY explosion
+            ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
+          ) AS amount
+        FROM
+          tbl
+      `)
+    ).toBe(dedent`
+      SELECT
+        RANK() OVER (
+          PARTITION BY
+            explosion
+          ORDER BY
+            day ROWS BETWEEN 6 PRECEDING
+            AND CURRENT ROW
+        ) AS amount
+      FROM
+        tbl
+    `);
+  });
+}