Repository: sql-formatter-org/sql-formatter
Pull request #404 (2022-08-23 16:37:59)

Remove unnecessary type imports
I suspect these type-only imports were added to deal with cyclic dependencies for which ESLint `no-cycle` rule will throw an error. I think the `no-cycle` rule is worth having as cyclic dependencies can indeed cause problems at runtime. They won't be problem with types, as these will get compiled away, so the solution to use `import type` is IMHO a valid one.

This leaves only one such import, for which I also added a comment to explain why we're using it.

Another reason to `import type` would be if we wanted to only import e.g. a type of a class, but for some reason not the class implementation itself. I don't think we have any such uses.

This PR also eliminates all uses of `import { type Foo }` syntax, which makes the library compatible with older versions of TypeScript, which I think is nice, as several people have bumped into this.


Patch:
diff --git a/README.md b/README.md
index 7df55e95..29fb177f 100644
--- a/README.md
+++ b/README.md
@@ -157,7 +157,7 @@ All fields are optional and all fields that are not specified will be filled wit
 - [**`denseOperators`**](docs/denseOperators.md) packs operators densely without spaces.
 - [**`newlineBeforeSemicolon`**](docs/newlineBeforeSemicolon.md) places semicolon on separate line.
 - [**`params`**](docs/params.md) collection of values for placeholder replacement.
-- [**`paramTypes`**](docs/paramTypes.md) specifies parameter placeholders types to support.
+- [**`paramsTypes`**](docs/paramTypes.md) specifies parameter placeholders types to support.
 
 ### Usage without NPM
 
diff --git a/package.json b/package.json
index b69d887b..e0f3cb46 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "sql-formatter",
-  "version": "10.0.0",
+  "version": "10.0.0-beta.1",
   "description": "Format whitespace in a SQL query to make it more readable",
   "license": "MIT",
   "main": "lib/index.js",
diff --git a/src/FormatOptions.ts b/src/FormatOptions.ts
index fe1f713f..cc293b7c 100644
--- a/src/FormatOptions.ts
+++ b/src/FormatOptions.ts
@@ -1,5 +1,6 @@
+// import only type to avoid ESLint no-cycle rule producing an error
 import type { SqlLanguage } from './sqlFormatter';
-import { type ParamItems } from './formatter/Params';
+import { ParamItems } from './formatter/Params';
 import Formatter from './formatter/Formatter';
 import { ParamTypes } from './lexer/TokenizerOptions';
 
diff --git a/src/formatter/ExpressionFormatter.ts b/src/formatter/ExpressionFormatter.ts
index 78d8bf92..8c0fb11f 100644
--- a/src/formatter/ExpressionFormatter.ts
+++ b/src/formatter/ExpressionFormatter.ts
@@ -1,4 +1,4 @@
-import type { FormatOptions } from 'src/FormatOptions';
+import { FormatOptions } from 'src/FormatOptions';
 import { equalizeWhitespace } from 'src/utils';
 
 import Params from 'src/formatter/Params';
diff --git a/src/formatter/Formatter.ts b/src/formatter/Formatter.ts
index 9e30e499..6d2a8af3 100644
--- a/src/formatter/Formatter.ts
+++ b/src/formatter/Formatter.ts
@@ -1,4 +1,4 @@
-import type { FormatOptions } from 'src/FormatOptions';
+import { FormatOptions } from 'src/FormatOptions';
 import { indentString } from 'src/formatter/config';
 import Params from 'src/formatter/Params';
 import Tokenizer from 'src/lexer/Tokenizer';
diff --git a/src/formatter/config.ts b/src/formatter/config.ts
index 28de79ca..363e6784 100644
--- a/src/formatter/config.ts
+++ b/src/formatter/config.ts
@@ -1,4 +1,4 @@
-import type { FormatOptions } from 'src/FormatOptions';
+import { FormatOptions } from 'src/FormatOptions';
 
 // Utility functions for config options
 
diff --git a/src/formatter/formatCommaPositions.ts b/src/formatter/formatCommaPositions.ts
index e5dff1ce..47a68094 100644
--- a/src/formatter/formatCommaPositions.ts
+++ b/src/formatter/formatCommaPositions.ts
@@ -1,4 +1,4 @@
-import type { CommaPosition } from 'src/FormatOptions';
+import { CommaPosition } from 'src/FormatOptions';
 import { maxLength } from 'src/utils';
 
 const PRECEDING_WHITESPACE_REGEX = /^\s+/u;
diff --git a/src/formatter/tabularStyle.ts b/src/formatter/tabularStyle.ts
index 60a399b7..0745b463 100644
--- a/src/formatter/tabularStyle.ts
+++ b/src/formatter/tabularStyle.ts
@@ -1,4 +1,4 @@
-import type { IndentStyle } from 'src/FormatOptions';
+import { IndentStyle } from 'src/FormatOptions';
 import { isLogicalOperator, TokenType } from 'src/lexer/token';
 
 /**
diff --git a/src/languages/bigquery/bigquery.formatter.ts b/src/languages/bigquery/bigquery.formatter.ts
index be547531..811a41f0 100644
--- a/src/languages/bigquery/bigquery.formatter.ts
+++ b/src/languages/bigquery/bigquery.formatter.ts
@@ -1,6 +1,6 @@
 import Formatter from 'src/formatter/Formatter';
 import Tokenizer from 'src/lexer/Tokenizer';
-import { EOF_TOKEN, isToken, TokenType, type Token } from 'src/lexer/token';
+import { EOF_TOKEN, isToken, TokenType, Token } from 'src/lexer/token';
 import { expandPhrases } from 'src/expandPhrases';
 import { keywords } from './bigquery.keywords';
 import { functions } from './bigquery.functions';
@@ -151,7 +151,8 @@ export default class BigQueryFormatter extends Formatter {
       reservedPhrases,
       reservedKeywords: keywords,
       reservedFunctionNames: functions,
-      extraParens: ['[]'],
+      openParens: ['(', '['],
+      closeParens: [')', ']'],
       stringTypes: [
         // The triple-quoted strings are listed first, so they get matched first.
         // Otherwise the first two quotes of """ will get matched as an empty "" string.
diff --git a/src/languages/hive/hive.formatter.ts b/src/languages/hive/hive.formatter.ts
index 2623ccf7..679a6e3b 100644
--- a/src/languages/hive/hive.formatter.ts
+++ b/src/languages/hive/hive.formatter.ts
@@ -91,7 +91,8 @@ export default class HiveFormatter extends Formatter {
       reservedDependentClauses: ['WHEN', 'ELSE'],
       reservedKeywords: keywords,
       reservedFunctionNames: functions,
-      extraParens: ['[]'],
+      openParens: ['(', '['],
+      closeParens: [')', ']'],
       stringTypes: ['""', "''"],
       identTypes: ['``'],
       variableTypes: [{ quote: '{}', prefixes: ['$'], requirePrefix: true }],
diff --git a/src/languages/mariadb/mariadb.formatter.ts b/src/languages/mariadb/mariadb.formatter.ts
index 5b04edcd..469771f5 100644
--- a/src/languages/mariadb/mariadb.formatter.ts
+++ b/src/languages/mariadb/mariadb.formatter.ts
@@ -1,7 +1,7 @@
 import { expandPhrases } from 'src/expandPhrases';
 import Formatter from 'src/formatter/Formatter';
 import Tokenizer from 'src/lexer/Tokenizer';
-import { EOF_TOKEN, isToken, type Token, TokenType } from 'src/lexer/token';
+import { EOF_TOKEN, isToken, Token, TokenType } from 'src/lexer/token';
 import { keywords } from './mariadb.keywords';
 import { functions } from './mariadb.functions';
 
@@ -270,11 +270,14 @@ export default class MariaDbFormatter extends Formatter {
       reservedKeywords: keywords,
       reservedFunctionNames: functions,
       // TODO: support _ char set prefixes such as _utf8, _latin1, _binary, _utf8mb4, etc.
-      stringTypes: [{ quote: "''", prefixes: ['B', 'X'] }, '""'],
+      stringTypes: [
+        { quote: "''", prefixes: ['B', 'X'] },
+        { quote: '""', prefixes: ['B', 'X'] },
+      ],
       identTypes: ['``'],
       identChars: { first: '$', rest: '$', allowFirstCharNumber: true },
       variableTypes: [
-        { regex: '@@?[A-Za-z0-9_.$]+' },
+        { regex: '@[A-Za-z0-9_.$]+' },
         { quote: '""', prefixes: ['@'], requirePrefix: true },
         { quote: "''", prefixes: ['@'], requirePrefix: true },
         { quote: '``', prefixes: ['@'], requirePrefix: true },
diff --git a/src/languages/mysql/mysql.formatter.ts b/src/languages/mysql/mysql.formatter.ts
index 0f4d6f84..85c47678 100644
--- a/src/languages/mysql/mysql.formatter.ts
+++ b/src/languages/mysql/mysql.formatter.ts
@@ -1,7 +1,7 @@
 import { expandPhrases } from 'src/expandPhrases';
 import Formatter from 'src/formatter/Formatter';
 import Tokenizer from 'src/lexer/Tokenizer';
-import { EOF_TOKEN, isToken, type Token, TokenType } from 'src/lexer/token';
+import { EOF_TOKEN, isToken, Token, TokenType } from 'src/lexer/token';
 import { keywords } from './mysql.keywords';
 import { functions } from './mysql.functions';
 
@@ -239,11 +239,14 @@ export default class MySqlFormatter extends Formatter {
       reservedKeywords: keywords,
       reservedFunctionNames: functions,
       // TODO: support _ char set prefixes such as _utf8, _latin1, _binary, _utf8mb4, etc.
-      stringTypes: [{ quote: "''", prefixes: ['B', 'N', 'X'] }, '""'],
+      stringTypes: [
+        { quote: "''", prefixes: ['B', 'N', 'X'] },
+        { quote: '""', prefixes: ['B', 'N', 'X'] },
+      ],
       identTypes: ['``'],
       identChars: { first: '$', rest: '$', allowFirstCharNumber: true },
       variableTypes: [
-        { regex: '@@?[A-Za-z0-9_.$]+' },
+        { regex: '@[A-Za-z0-9_.$]+' },
         { quote: '""', prefixes: ['@'], requirePrefix: true },
         { quote: "''", prefixes: ['@'], requirePrefix: true },
         { quote: '``', prefixes: ['@'], requirePrefix: true },
diff --git a/src/languages/n1ql/n1ql.formatter.ts b/src/languages/n1ql/n1ql.formatter.ts
index d3800bba..99df32e9 100644
--- a/src/languages/n1ql/n1ql.formatter.ts
+++ b/src/languages/n1ql/n1ql.formatter.ts
@@ -94,7 +94,8 @@ export default class N1qlFormatter extends Formatter {
       // which simplifies writing tests that are shared between all dialects.
       stringTypes: ['""', "''"],
       identTypes: ['``'],
-      extraParens: ['[]', '{}'],
+      openParens: ['(', '[', '{'],
+      closeParens: [')', ']', '}'],
       paramTypes: { positional: true, numbered: ['$'], named: ['$'] },
       lineCommentTypes: ['#', '--'],
       operators: N1qlFormatter.operators,
diff --git a/src/languages/plsql/plsql.formatter.ts b/src/languages/plsql/plsql.formatter.ts
index e5bd9ddd..5071cf23 100644
--- a/src/languages/plsql/plsql.formatter.ts
+++ b/src/languages/plsql/plsql.formatter.ts
@@ -1,7 +1,7 @@
 import { expandPhrases } from 'src/expandPhrases';
 import Formatter from 'src/formatter/Formatter';
 import Tokenizer from 'src/lexer/Tokenizer';
-import { EOF_TOKEN, isReserved, isToken, type Token, TokenType } from 'src/lexer/token';
+import { EOF_TOKEN, isReserved, isToken, Token, TokenType } from 'src/lexer/token';
 import { keywords } from './plsql.keywords';
 import { functions } from './plsql.functions';
 
diff --git a/src/languages/postgresql/postgresql.formatter.ts b/src/languages/postgresql/postgresql.formatter.ts
index 4175ec50..5f53af1f 100644
--- a/src/languages/postgresql/postgresql.formatter.ts
+++ b/src/languages/postgresql/postgresql.formatter.ts
@@ -326,7 +326,8 @@ export default class PostgreSqlFormatter extends Formatter {
       reservedPhrases,
       reservedKeywords: keywords,
       reservedFunctionNames: functions,
-      extraParens: ['[]'],
+      openParens: ['(', '['],
+      closeParens: [')', ']'],
       stringTypes: ['$$', { quote: "''", prefixes: ['B', 'E', 'X', 'U&'] }],
       identTypes: [{ quote: '""', prefixes: ['U&'] }],
       identChars: { rest: '$' },
diff --git a/src/languages/singlestoredb/singlestoredb.formatter.ts b/src/languages/singlestoredb/singlestoredb.formatter.ts
index e6acdbe0..f0a1e80b 100644
--- a/src/languages/singlestoredb/singlestoredb.formatter.ts
+++ b/src/languages/singlestoredb/singlestoredb.formatter.ts
@@ -1,7 +1,7 @@
 import { expandPhrases } from 'src/expandPhrases';
 import Formatter from 'src/formatter/Formatter';
 import Tokenizer from 'src/lexer/Tokenizer';
-import { EOF_TOKEN, isToken, type Token, TokenType } from 'src/lexer/token';
+import { EOF_TOKEN, isToken, Token, TokenType } from 'src/lexer/token';
 import { keywords } from './singlestoredb.keywords';
 import { functions } from './singlestoredb.functions';
 
@@ -238,11 +238,14 @@ export default class SingleStoreDbFormatter extends Formatter {
       reservedKeywords: keywords,
       reservedFunctionNames: functions,
       // TODO: support _binary"some string" prefix
-      stringTypes: [{ quote: "''", prefixes: ['B', 'X'] }, '""'],
+      stringTypes: [
+        { quote: "''", prefixes: ['B', 'X'] },
+        { quote: '""', prefixes: ['B', 'X'] },
+      ],
       identTypes: ['``'],
       identChars: { first: '$', rest: '$', allowFirstCharNumber: true },
       variableTypes: [
-        { regex: '@@?[A-Za-z0-9_$]+' },
+        { regex: '@[A-Za-z0-9_$]+' },
         { quote: '``', prefixes: ['@'], requirePrefix: true },
       ],
       lineCommentTypes: ['--', '#'],
diff --git a/src/languages/spark/spark.formatter.ts b/src/languages/spark/spark.formatter.ts
index 46f876a6..5d667649 100644
--- a/src/languages/spark/spark.formatter.ts
+++ b/src/languages/spark/spark.formatter.ts
@@ -1,7 +1,7 @@
 import { expandPhrases } from 'src/expandPhrases';
 import Formatter from 'src/formatter/Formatter';
 import Tokenizer from 'src/lexer/Tokenizer';
-import { EOF_TOKEN, isToken, type Token, TokenType } from 'src/lexer/token';
+import { EOF_TOKEN, isToken, Token, TokenType } from 'src/lexer/token';
 import { keywords } from './spark.keywords';
 import { functions } from './spark.functions';
 
@@ -124,7 +124,8 @@ export default class SparkFormatter extends Formatter {
       supportsXor: true,
       reservedKeywords: keywords,
       reservedFunctionNames: functions,
-      extraParens: ['[]'],
+      openParens: ['(', '['],
+      closeParens: [')', ']'],
       stringTypes: [
         { quote: "''", prefixes: ['R', 'X'] },
         { quote: '""', prefixes: ['R', 'X'] },
diff --git a/src/languages/trino/trino.formatter.ts b/src/languages/trino/trino.formatter.ts
index ac382045..51845f8b 100644
--- a/src/languages/trino/trino.formatter.ts
+++ b/src/languages/trino/trino.formatter.ts
@@ -131,11 +131,9 @@ export default class TrinoFormatter extends Formatter {
       reservedDependentClauses: ['WHEN', 'ELSE'],
       reservedKeywords: keywords,
       reservedFunctionNames: functions,
-      // Trino also supports {- ... -} parenthesis.
-      // The formatting of these currently works out as a result of { and -
-      // not getting a space added in-between.
       // https://trino.io/docs/current/sql/match-recognize.html#row-pattern-syntax
-      extraParens: ['[]', '{}'],
+      openParens: ['(', '[', '{', '{-'],
+      closeParens: [')', ']', '}', '-}'],
       // https://trino.io/docs/current/language/types.html#string
       // https://trino.io/docs/current/language/types.html#varbinary
       stringTypes: [{ quote: "''", prefixes: ['X', 'U&'] }],
diff --git a/src/lexer/Tokenizer.ts b/src/lexer/Tokenizer.ts
index 36183732..223aa9e5 100644
--- a/src/lexer/Tokenizer.ts
+++ b/src/lexer/Tokenizer.ts
@@ -29,6 +29,9 @@ export default class Tokenizer {
       [TokenType.LINE_COMMENT]: {
         regex: regex.lineComment(cfg.lineCommentTypes ?? ['--']),
       },
+      [TokenType.COMMA]: { regex: /[,]/y },
+      [TokenType.OPEN_PAREN]: { regex: regex.parenthesis(cfg.openParens ?? ['(']) },
+      [TokenType.CLOSE_PAREN]: { regex: regex.parenthesis(cfg.closeParens ?? [')']) },
       [TokenType.QUOTED_IDENTIFIER]: { regex: regex.string(cfg.identTypes) },
       [TokenType.NUMBER]: {
         regex:
@@ -102,9 +105,6 @@ export default class Tokenizer {
         regex: regex.identifier(cfg.identChars),
       },
       [TokenType.DELIMITER]: { regex: /[;]/uy },
-      [TokenType.COMMA]: { regex: /[,]/y },
-      [TokenType.OPEN_PAREN]: { regex: regex.parenthesis('open', cfg.extraParens) },
-      [TokenType.CLOSE_PAREN]: { regex: regex.parenthesis('close', cfg.extraParens) },
       [TokenType.OPERATOR]: {
         regex: regex.operator('+-/%&|^><=.:$@#?~!', [
           '<>',
diff --git a/src/lexer/TokenizerEngine.ts b/src/lexer/TokenizerEngine.ts
index 12ee9cfc..0372daac 100644
--- a/src/lexer/TokenizerEngine.ts
+++ b/src/lexer/TokenizerEngine.ts
@@ -65,6 +65,9 @@ export default class TokenizerEngine {
     return (
       this.matchToken(TokenType.BLOCK_COMMENT) ||
       this.matchToken(TokenType.LINE_COMMENT) ||
+      this.matchToken(TokenType.COMMA) ||
+      this.matchToken(TokenType.OPEN_PAREN) ||
+      this.matchToken(TokenType.CLOSE_PAREN) ||
       this.matchToken(TokenType.QUOTED_IDENTIFIER) ||
       this.matchToken(TokenType.NUMBER) ||
       this.matchReservedWordToken(previousToken) ||
@@ -76,9 +79,6 @@ export default class TokenizerEngine {
       this.matchToken(TokenType.STRING) ||
       this.matchToken(TokenType.IDENTIFIER) ||
       this.matchToken(TokenType.DELIMITER) ||
-      this.matchToken(TokenType.COMMA) ||
-      this.matchToken(TokenType.OPEN_PAREN) ||
-      this.matchToken(TokenType.CLOSE_PAREN) ||
       this.matchToken(TokenType.OPERATOR) ||
       this.matchToken(TokenType.ASTERISK)
     );
diff --git a/src/lexer/TokenizerOptions.ts b/src/lexer/TokenizerOptions.ts
index 2cce0e73..9c3aa285 100644
--- a/src/lexer/TokenizerOptions.ts
+++ b/src/lexer/TokenizerOptions.ts
@@ -69,8 +69,10 @@ export interface TokenizerOptions {
   identTypes: QuoteType[];
   // Types of quotes to use for variables
   variableTypes?: VariableType[];
-  // Types of additional parenthesis types to support
-  extraParens?: ('[]' | '{}')[];
+  // Open-parenthesis characters
+  openParens?: ('(' | '[' | '{' | '{-')[];
+  // Close-parenthesis characters
+  closeParens?: (')' | ']' | '}' | '-}')[];
   // Types of parameter placeholders supported with prepared statements
   paramTypes?: ParamTypes;
   // Line comment types to support, defaults to --
diff --git a/src/lexer/regexFactory.ts b/src/lexer/regexFactory.ts
index ff48f5a3..70f6d1b5 100644
--- a/src/lexer/regexFactory.ts
+++ b/src/lexer/regexFactory.ts
@@ -1,7 +1,13 @@
 import { sortByLengthDesc } from 'src/utils';
 
-import type { IdentChars, QuoteType, VariableType } from './TokenizerOptions';
-import { escapeRegExp, patternToRegex, prefixesPattern, withDashes } from './regexUtil';
+import { IdentChars, QuoteType, VariableType } from './TokenizerOptions';
+import {
+  escapeParen,
+  escapeRegExp,
+  patternToRegex,
+  prefixesPattern,
+  withDashes,
+} from './regexUtil';
 
 /**
  * Builds a RegExp for valid line comments in a SQL dialect
@@ -11,13 +17,11 @@ export const lineComment = (lineCommentTypes: string[]) =>
   new RegExp(`(?:${lineCommentTypes.map(escapeRegExp).join('|')}).*?(?=\r\n|\r|\n|$)`, 'uy');
 
 /**
- * Builds a RegExp for matching either open- or close-parenthesis patterns
+ * Builds a RegExp for matching parenthesis patterns, escaping them with `escapeParen`
+ * @param {string[]} parens - list of strings that denote parenthesis patterns
  */
-export const parenthesis = (kind: 'open' | 'close', extraParens: ('[]' | '{}')[] = []): RegExp => {
-  const index = kind === 'open' ? 0 : 1;
-  const parens = ['()', ...extraParens].map(pair => pair[index]);
-  return patternToRegex(parens.map(escapeRegExp).join('|'));
-};
+export const parenthesis = (parens: string[]): RegExp =>
+  patternToRegex(parens.map(escapeParen).join('|'));
 
 /**
  * Builds a RegExp containing all operators for a SQL dialect
diff --git a/src/lexer/regexUtil.ts b/src/lexer/regexUtil.ts
index 460a45e2..27dec077 100644
--- a/src/lexer/regexUtil.ts
+++ b/src/lexer/regexUtil.ts
@@ -1,4 +1,4 @@
-import type { PrefixedQuoteType } from './TokenizerOptions';
+import { PrefixedQuoteType } from './TokenizerOptions';
 
 // Escapes regex special chars
 export const escapeRegExp = (string: string) => string.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
@@ -14,6 +14,17 @@ export const toCaseInsensitivePattern = (prefix: string): string =>
     .map(char => (/ /gu.test(char) ? '\\s+' : `[${char.toUpperCase()}${char.toLowerCase()}]`))
     .join('');
 
+/** Escapes paren characters for RegExp patterns */
+export const escapeParen = (paren: string): string => {
+  if (paren.length === 1) {
+    // A single punctuation character
+    return escapeRegExp(paren);
+  } else {
+    // longer word
+    return '\\b' + escapeRegExp(paren) + '\\b';
+  }
+};
+
 export const withDashes = (pattern: string): string => pattern + '(?:-' + pattern + ')*';
 
 // Converts ["a", "b"] to "(?:[Aa]|[Bb]|)" or "(?:[Aa]|[Bb])" when required = true
diff --git a/src/sqlFormatter.ts b/src/sqlFormatter.ts
index 531b92d2..114a6b4c 100644
--- a/src/sqlFormatter.ts
+++ b/src/sqlFormatter.ts
@@ -14,7 +14,7 @@ import TrinoFormatter from 'src/languages/trino/trino.formatter';
 import TSqlFormatter from 'src/languages/tsql/tsql.formatter';
 import SingleStoreDbFormatter from './languages/singlestoredb/singlestoredb.formatter';
 
-import type { FormatOptions } from './FormatOptions';
+import { FormatOptions } from './FormatOptions';
 import { ParamItems } from './formatter/Params';
 
 export const formatters = {
diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts
index bde8789e..d3bb0179 100644
--- a/vscode/src/extension.ts
+++ b/vscode/src/extension.ts
@@ -1,6 +1,6 @@
 import * as vscode from 'vscode';
-import { format } from 'sql-formatter';
-import type {
+import {
+  format,
   SqlLanguage,
   KeywordCase,
   IndentStyle,


Test patch:
diff --git a/test/behavesLikeMariaDbFormatter.ts b/test/behavesLikeMariaDbFormatter.ts
index 6affd9d9..fd4c4ccd 100644
--- a/test/behavesLikeMariaDbFormatter.ts
+++ b/test/behavesLikeMariaDbFormatter.ts
@@ -89,18 +89,6 @@ export default function behavesLikeMariaDbFormatter(format: FormatFn) {
     `);
   });
 
-  it('supports @@ system variables', () => {
-    const result = format('SELECT @@GLOBAL.time, @@SYSTEM.date, @@hour FROM foo;');
-    expect(result).toBe(dedent`
-      SELECT
-        @@GLOBAL.time,
-        @@SYSTEM.date,
-        @@hour
-      FROM
-        foo;
-    `);
-  });
-
   // Issue #181
   it('does not wrap CHARACTER SET to multiple lines', () => {
     expect(format('ALTER TABLE t MODIFY col1 VARCHAR(50) CHARACTER SET greek')).toBe(dedent`
diff --git a/test/bigquery.test.ts b/test/bigquery.test.ts
index c16e1731..797100ed 100644
--- a/test/bigquery.test.ts
+++ b/test/bigquery.test.ts
@@ -45,7 +45,7 @@ describe('BigQueryFormatter', () => {
   supportsUpdate(format);
   supportsTruncateTable(format);
   supportsMergeInto(format);
-  supportsStrings(format, ['""', "''", "R''", 'R""', "B''", 'B""']);
+  supportsStrings(format, ['""', "''"]);
   supportsIdentifiers(format, ['``']);
   supportsArrayLiterals(format);
   supportsBetween(format);
@@ -77,11 +77,15 @@ describe('BigQueryFormatter', () => {
   });
 
   // BigQuery-specific string types
-  it('supports strings with rb prefixes', () => {
-    expect(format(`SELECT rb"huh", br'bulu bulu', BR'la la' FROM foo`)).toBe(dedent`
+  it('supports strings with r, b and rb prefixes', () => {
+    expect(format(`SELECT R'blah', B'sah', rb"huh", br'bulu bulu', r"haha", BR'la la' FROM foo`))
+      .toBe(dedent`
       SELECT
+        R'blah',
+        B'sah',
         rb"huh",
         br'bulu bulu',
+        r"haha",
         BR'la la'
       FROM
         foo
diff --git a/test/db2.test.ts b/test/db2.test.ts
index 110b9b12..9f90ccfa 100644
--- a/test/db2.test.ts
+++ b/test/db2.test.ts
@@ -78,12 +78,11 @@ describe('Db2Formatter', () => {
   });
 
   // DB2-specific string types
-  it('supports strings with G, GX, BX, UX prefixes', () => {
-    expect(format(`SELECT G'blah blah', GX'01AC', BX'0101', UX'CCF239' FROM foo`)).toBe(dedent`
+  it('supports strings with G, GX, UX prefixes', () => {
+    expect(format(`SELECT G'blah blah', GX'01AC', UX'CCF239' FROM foo`)).toBe(dedent`
       SELECT
         G'blah blah',
         GX'01AC',
-        BX'0101',
         UX'CCF239'
       FROM
         foo
diff --git a/test/features/strings.ts b/test/features/strings.ts
index 0d6d7820..d517b725 100644
--- a/test/features/strings.ts
+++ b/test/features/strings.ts
@@ -3,18 +3,7 @@ import dedent from 'dedent-js';
 
 import { FormatFn } from 'src/sqlFormatter';
 
-type StringType =
-  | '""'
-  | "''"
-  | 'U&""'
-  | "U&''"
-  | "N''"
-  | "X''"
-  | 'X""'
-  | "B''"
-  | 'B""'
-  | "R''"
-  | 'R""';
+type StringType = '""' | "''" | 'U&""' | "U&''" | "N''" | "X''";
 
 export default function supportsStrings(format: FormatFn, stringTypes: StringType[]) {
   if (stringTypes.includes('""')) {
@@ -114,89 +103,4 @@ export default function supportsStrings(format: FormatFn, stringTypes: StringTyp
       expect(format("X'AE01'X'01F6'")).toBe("X'AE01' X'01F6'");
     });
   }
-
-  if (stringTypes.includes('X""')) {
-    it('supports hex byte sequences', () => {
-      expect(format(`x"0E"`)).toBe(`x"0E"`);
-      expect(format(`X"1F0A89C3"`)).toBe(`X"1F0A89C3"`);
-      expect(format(`SELECT x"2B" FROM foo`)).toBe(dedent`
-        SELECT
-          x"2B"
-        FROM
-          foo
-      `);
-    });
-
-    it(`detects consecutive X" strings as separate ones`, () => {
-      expect(format(`X"AE01"X"01F6"`)).toBe(`X"AE01" X"01F6"`);
-    });
-  }
-
-  if (stringTypes.includes("B''")) {
-    it('supports bit sequences', () => {
-      expect(format("b'01'")).toBe("b'01'");
-      expect(format("B'10110'")).toBe("B'10110'");
-      expect(format("SELECT b'0101' FROM foo")).toBe(dedent`
-        SELECT
-          b'0101'
-        FROM
-          foo
-      `);
-    });
-
-    it("detects consecutive B'' strings as separate ones", () => {
-      expect(format("B'1001'B'0110'")).toBe("B'1001' B'0110'");
-    });
-  }
-
-  if (stringTypes.includes('B""')) {
-    it('supports bit sequences (with double-qoutes)', () => {
-      expect(format(`b"01"`)).toBe(`b"01"`);
-      expect(format(`B"10110"`)).toBe(`B"10110"`);
-      expect(format(`SELECT b"0101" FROM foo`)).toBe(dedent`
-        SELECT
-          b"0101"
-        FROM
-          foo
-      `);
-    });
-
-    it(`detects consecutive B"" strings as separate ones`, () => {
-      expect(format(`B"1001"B"0110"`)).toBe(`B"1001" B"0110"`);
-    });
-  }
-
-  if (stringTypes.includes("R''")) {
-    it('supports raw strings', () => {
-      expect(format("r'abc'")).toBe("r'abc'");
-      expect(format("R'ha ha'")).toBe("R'ha ha'");
-      expect(format("SELECT r'some text' FROM foo")).toBe(dedent`
-        SELECT
-          r'some text'
-        FROM
-          foo
-      `);
-    });
-
-    it("detects consecutive r'' strings as separate ones", () => {
-      expect(format("r'a ha'r'hm mm'")).toBe("r'a ha' r'hm mm'");
-    });
-  }
-
-  if (stringTypes.includes('R""')) {
-    it('supports raw strings (with double-quotes)', () => {
-      expect(format(`r"abc"`)).toBe(`r"abc"`);
-      expect(format(`R"ha ha"`)).toBe(`R"ha ha"`);
-      expect(format(`SELECT r"some text" FROM foo`)).toBe(dedent`
-        SELECT
-          r"some text"
-        FROM
-          foo
-      `);
-    });
-
-    it(`detects consecutive r"" strings as separate ones`, () => {
-      expect(format(`r"a ha"r"hm mm"`)).toBe(`r"a ha" r"hm mm"`);
-    });
-  }
 }
diff --git a/test/mariadb.test.ts b/test/mariadb.test.ts
index 3e25224b..97b764b9 100644
--- a/test/mariadb.test.ts
+++ b/test/mariadb.test.ts
@@ -13,7 +13,6 @@ import supportsCreateTable from './features/createTable';
 import supportsParams from './options/param';
 import supportsCreateView from './features/createView';
 import supportsAlterTable from './features/alterTable';
-import supportsStrings from './features/strings';
 
 describe('MariaDbFormatter', () => {
   const language = 'mariadb';
@@ -21,9 +20,6 @@ describe('MariaDbFormatter', () => {
 
   behavesLikeMariaDbFormatter(format);
 
-  // in addition to string types listed in behavesLikeMariaDbFormatter
-  supportsStrings(format, ["B''"]);
-
   supportsJoin(format, {
     without: ['FULL', 'NATURAL INNER JOIN'],
     additionally: ['STRAIGHT_JOIN'],
diff --git a/test/mysql.test.ts b/test/mysql.test.ts
index 308d33a0..d2668092 100644
--- a/test/mysql.test.ts
+++ b/test/mysql.test.ts
@@ -13,7 +13,6 @@ import supportsCreateTable from './features/createTable';
 import supportsParams from './options/param';
 import supportsCreateView from './features/createView';
 import supportsAlterTable from './features/alterTable';
-import supportsStrings from './features/strings';
 
 describe('MySqlFormatter', () => {
   const language = 'mysql';
@@ -21,9 +20,6 @@ describe('MySqlFormatter', () => {
 
   behavesLikeMariaDbFormatter(format);
 
-  // in addition to string types listed in behavesLikeMariaDbFormatter
-  supportsStrings(format, ["N''"]);
-
   supportsJoin(format, {
     without: ['FULL'],
     additionally: ['STRAIGHT_JOIN'],
@@ -53,6 +49,19 @@ describe('MySqlFormatter', () => {
     `);
   });
 
+  // TODO: disabled for now
+  it.skip('supports @@ system variables', () => {
+    const result = format('SELECT @@GLOBAL.time, @@SYSTEM.date, @@hour FROM foo;');
+    expect(result).toBe(dedent`
+      SELECT
+        @@GLOBAL.time,
+        @@SYSTEM.date,
+        @@hour
+      FROM
+        foo;
+    `);
+  });
+
   it('formats ALTER TABLE ... ALTER COLUMN', () => {
     expect(
       format(
diff --git a/test/plsql.test.ts b/test/plsql.test.ts
index b5c3a921..0875a7f1 100644
--- a/test/plsql.test.ts
+++ b/test/plsql.test.ts
@@ -108,6 +108,14 @@ describe('PlSqlFormatter', () => {
     expect(format("Q'Stest string S'S''")).toBe("Q'Stest string S' S ''");
   });
 
+  it('formats simple SELECT with national characters', () => {
+    const result = format("SELECT N'value'");
+    expect(result).toBe(dedent`
+      SELECT
+        N'value'
+    `);
+  });
+
   it('formats Oracle recursive sub queries', () => {
     const result = format(`
       WITH t1 AS (
diff --git a/test/postgresql.test.ts b/test/postgresql.test.ts
index bfddae57..23ca2585 100644
--- a/test/postgresql.test.ts
+++ b/test/postgresql.test.ts
@@ -48,7 +48,7 @@ describe('PostgreSqlFormatter', () => {
   supportsInsertInto(format);
   supportsUpdate(format, { whereCurrentOf: true });
   supportsTruncateTable(format, { withoutTable: true });
-  supportsStrings(format, ["''", "U&''", "X''", "B''"]);
+  supportsStrings(format, ["''", "U&''", "X''"]);
   supportsIdentifiers(format, [`""`, 'U&""']);
   supportsBetween(format);
   supportsSchema(format);
@@ -72,6 +72,14 @@ describe('PostgreSqlFormatter', () => {
   });
 
   // Postgres-specific string types
+  it('supports bit strings', () => {
+    expect(format(`SELECT B'0110010', B'1101000';`)).toBe(dedent`
+      SELECT
+        B'0110010',
+        B'1101000';
+    `);
+  });
+
   it("supports E'' strings with C-style escapes", () => {
     expect(format("E'blah blah'")).toBe("E'blah blah'");
     expect(format("E'some \\' FROM escapes'")).toBe("E'some \\' FROM escapes'");
diff --git a/test/redshift.test.ts b/test/redshift.test.ts
index 0fe3491c..50e5e3ee 100644
--- a/test/redshift.test.ts
+++ b/test/redshift.test.ts
@@ -86,7 +86,7 @@ describe('RedshiftFormatter', () => {
     );
   });
 
-  it('formats DISTKEY and SORTKEY after CREATE TABLE', () => {
+  it.skip('formats DISTKEY and SORTKEY after CREATE TABLE', () => {
     expect(
       format(
         'CREATE TABLE items (a INT PRIMARY KEY, b TEXT, c INT NOT NULL, d INT NOT NULL, e INT NOT NULL) DISTKEY(created_at) SORTKEY(created_at);'
@@ -99,12 +99,13 @@ describe('RedshiftFormatter', () => {
           c INT NOT NULL,
           d INT NOT NULL,
           e INT NOT NULL
-        ) DISTKEY (created_at) SORTKEY (created_at);
+        )
+        DISTKEY(created_at)
+        SORTKEY(created_at);
     `);
   });
 
-  // This is far from ideal, but at least the formatter doesn't crash :P
-  it('formats COPY', () => {
+  it.skip('formats COPY', () => {
     expect(
       format(`
         COPY schema.table
@@ -117,7 +118,15 @@ describe('RedshiftFormatter', () => {
       COPY
         schema.table
       FROM
-        's3://bucket/file.csv' IAM_ROLE 'arn:aws:iam::123456789:role/rolename' FORMAT AS CSV DELIMITER ',' QUOTE '"' REGION AS 'us-east-1'
+        's3://bucket/file.csv'
+      IAM_ROLE
+        'arn:aws:iam::123456789:role/rolename'
+      FORMAT
+        AS CSV
+      DELIMITER
+        ',' QUOTE '"'
+      REGION
+        AS 'us-east-1'
     `);
   });
 
diff --git a/test/singlestoredb.test.ts b/test/singlestoredb.test.ts
index c639985b..0337c4c4 100644
--- a/test/singlestoredb.test.ts
+++ b/test/singlestoredb.test.ts
@@ -9,7 +9,6 @@ import supportsLimiting from './features/limiting';
 import supportsCreateTable from './features/createTable';
 import supportsCreateView from './features/createView';
 import supportsAlterTable from './features/alterTable';
-import supportsStrings from './features/strings';
 
 describe('SingleStoreDbFormatter', () => {
   const language = 'singlestoredb';
@@ -17,9 +16,6 @@ describe('SingleStoreDbFormatter', () => {
 
   behavesLikeMariaDbFormatter(format);
 
-  // in addition to string types listed in behavesLikeMariaDbFormatter
-  supportsStrings(format, ["B''"]);
-
   supportsJoin(format, {
     without: ['NATURAL INNER JOIN', 'NATURAL FULL', 'NATURAL JOIN'],
     additionally: ['STRAIGHT_JOIN'],
diff --git a/test/spark.test.ts b/test/spark.test.ts
index 48484c49..01ba36f2 100644
--- a/test/spark.test.ts
+++ b/test/spark.test.ts
@@ -36,7 +36,7 @@ describe('SparkFormatter', () => {
   });
   supportsInsertInto(format, { withoutInto: true });
   supportsTruncateTable(format);
-  supportsStrings(format, ["''", '""', "X''", 'X""', "R''", 'R""']);
+  supportsStrings(format, ["''", "X''"]);
   supportsIdentifiers(format, ['``']);
   supportsBetween(format);
   supportsOperators(format, SparkFormatter.operators, ['AND', 'OR', 'XOR']);
diff --git a/test/sql.test.ts b/test/sql.test.ts
index 9033a0e4..a2f42e19 100644
--- a/test/sql.test.ts
+++ b/test/sql.test.ts
@@ -45,7 +45,7 @@ describe('SqlFormatter', () => {
   supportsInsertInto(format);
   supportsUpdate(format, { whereCurrentOf: true });
   supportsTruncateTable(format);
-  supportsStrings(format, ["''", "X''", "N''", "U&''"]);
+  supportsStrings(format, ["''", "X''"]);
   supportsIdentifiers(format, [`""`, '``']);
   supportsBetween(format);
   supportsSchema(format);
diff --git a/test/unit/Parser.test.ts b/test/unit/Parser.test.ts
index 262b36a4..76852bd6 100644
--- a/test/unit/Parser.test.ts
+++ b/test/unit/Parser.test.ts
@@ -11,7 +11,8 @@ describe('Parser', () => {
       reservedJoins: ['JOIN'],
       reservedFunctionNames: ['SQRT', 'CURRENT_TIME'],
       reservedKeywords: ['BETWEEN', 'LIKE', 'ON', 'USING'],
-      extraParens: ['[]', '{}'],
+      openParens: ['(', '[', '{'],
+      closeParens: [')', ']', '}'],
       stringTypes: ["''"],
       identTypes: ['""'],
     });