diff --git a/README.md b/README.md index b41da467ba..a6899b2002 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,8 @@ All fields are optional and all fields that are not specified will be filled wit - [**`useTabs`**](docs/useTabs.md) to use tabs for indentation. - [**`keywordCase`**](docs/keywordCase.md) uppercases or lowercases keywords. - [**`identifierCase`**](docs/identifierCase.md) uppercases or lowercases identifiers. (**experimental!**) +- [**`dataTypeCase`**](docs/dataTypeCase.md) uppercases or lowercases data types. (**experimental!**) +- [**`functionCase`**](docs/functionCase.md) uppercases or lowercases function names. (**experimental!**) - [**`indentStyle`**](docs/indentStyle.md) defines overall indentation style. - [**`logicalOperatorNewline`**](docs/logicalOperatorNewline.md) newline before or after boolean operator (AND, OR, XOR). - [**`expressionWidth`**](docs/expressionWidth.md) maximum number of characters in parenthesized expressions to be kept on single line. diff --git a/docs/dataTypeCase.md b/docs/dataTypeCase.md new file mode 100644 index 0000000000..97b4deb970 --- /dev/null +++ b/docs/dataTypeCase.md @@ -0,0 +1,52 @@ +# dataTypeCase (experimental) + +Converts data types to upper- or lowercase. + +Caveat: Only supported by languages which export `dataTypes` from their `.keywords.ts` file (eg. `bigquery`, `postgresql` and others) + +Note: Casing of function names like `VARCHAR(30)` are not modified - instead rely on the `functionCase` option for this. + +## Options + +- `"preserve"` (default) preserves the original case. +- `"upper"` converts to uppercase. +- `"lower"` converts to lowercase. + +### preserve + +```sql +CREATE TABLE + users ( + id InTeGeR PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + first_name VarChaR(30) NOT NULL, + bio teXT, + is_email_verified BooL NOT NULL DEFAULT FALSE, + created_timestamp timestamPtz NOT NULL DEFAULT NOW() + ) +``` + +### upper + +```sql +CREATE TABLE + users ( + id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + first_name VarChaR(30) NOT NULL, + bio TEXT, + is_email_verified BOOL NOT NULL DEFAULT FALSE, + created_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW() + ) +``` + +### lower + +```sql +CREATE TABLE + users ( + id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + first_name VarChaR(30) NOT NULL, + bio text, + is_email_verified bool NOT NULL DEFAULT FALSE, + created_timestamp timestamptz NOT NULL DEFAULT NOW() + ) +``` diff --git a/docs/functionCase.md b/docs/functionCase.md new file mode 100644 index 0000000000..cb740d62fb --- /dev/null +++ b/docs/functionCase.md @@ -0,0 +1,48 @@ +# functionCase (experimental) + +Converts function names to upper- or lowercase. + +## Options + +- `"preserve"` (default) preserves the original case. +- `"upper"` converts to uppercase. +- `"lower"` converts to lowercase. + +### preserve + +```sql +CREATE TABLE + users ( + id iNtegeR PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + first_name VarChaR(30) NOT NULL, + bio TEXT, + is_email_verified BOOL NOT NULL DEFAULT FALSE, + created_timestamp TIMESTAMPTZ NOT NULL DEFAULT NoW() + ) +``` + +### upper + +```sql +CREATE TABLE + users ( + id iNtegeR PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + first_name VARCHAR(30) NOT NULL, + bio TEXT, + is_email_verified BOOL NOT NULL DEFAULT FALSE, + created_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW() + ) +``` + +### lower + +```sql +CREATE TABLE + users ( + id iNtegeR PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + first_name varchar(30) NOT NULL, + bio TEXT, + is_email_verified BOOL NOT NULL DEFAULT FALSE, + created_timestamp TIMESTAMPTZ NOT NULL DEFAULT now() + ) +``` diff --git a/src/FormatOptions.ts b/src/FormatOptions.ts index 7cf102759a..937a4ac858 100644 --- a/src/FormatOptions.ts +++ b/src/FormatOptions.ts @@ -8,6 +8,10 @@ export type KeywordCase = 'preserve' | 'upper' | 'lower'; export type IdentifierCase = 'preserve' | 'upper' | 'lower'; +export type DataTypeCase = 'preserve' | 'upper' | 'lower'; + +export type FunctionCase = 'preserve' | 'upper' | 'lower'; + export type LogicalOperatorNewline = 'before' | 'after'; export interface FormatOptions { @@ -15,6 +19,8 @@ export interface FormatOptions { useTabs: boolean; keywordCase: KeywordCase; identifierCase: IdentifierCase; + dataTypeCase: DataTypeCase; + functionCase: FunctionCase; indentStyle: IndentStyle; logicalOperatorNewline: LogicalOperatorNewline; expressionWidth: number; diff --git a/src/formatter/ExpressionFormatter.ts b/src/formatter/ExpressionFormatter.ts index 88a6f2694f..489889da4a 100644 --- a/src/formatter/ExpressionFormatter.ts +++ b/src/formatter/ExpressionFormatter.ts @@ -28,6 +28,7 @@ import { CaseExpressionNode, CaseWhenNode, CaseElseNode, + DataTypeNode, } from '../parser/ast.js'; import Layout, { WS } from './Layout.js'; @@ -130,6 +131,8 @@ export default class ExpressionFormatter { return this.formatLineComment(node); case NodeType.block_comment: return this.formatBlockComment(node); + case NodeType.data_type: + return this.formatDataType(node); case NodeType.keyword: return this.formatKeywordNode(node); } @@ -137,19 +140,30 @@ export default class ExpressionFormatter { private formatFunctionCall(node: FunctionCallNode) { this.withComments(node.nameKw, () => { - this.layout.add(this.showKw(node.nameKw)); + this.layout.add(this.showFunctionKw(node.nameKw)); }); this.formatNode(node.parenthesis); } private formatArraySubscript(node: ArraySubscriptNode) { + let formattedArray: string; + + switch (node.array.type) { + case NodeType.data_type: + formattedArray = this.showDataType(node.array); + break; + case NodeType.keyword: + formattedArray = this.showKw(node.array); + break; + default: + formattedArray = this.showIdentifier(node.array); + break; + } + this.withComments(node.array, () => { - this.layout.add( - node.array.type === NodeType.keyword - ? this.showKw(node.array) - : this.showIdentifier(node.array) - ); + this.layout.add(formattedArray); }); + this.formatNode(node.parenthesis); } @@ -489,6 +503,10 @@ export default class ExpressionFormatter { } } + private formatDataType(node: DataTypeNode) { + this.layout.add(this.showDataType(node), WS.SPACE); + } + private showKw(node: KeywordNode): string { if (isTabularToken(node.tokenType)) { return toTabularFormat(this.showNonTabularKw(node), this.cfg.indentStyle); @@ -509,6 +527,26 @@ export default class ExpressionFormatter { } } + private showFunctionKw(node: KeywordNode): string { + if (isTabularToken(node.tokenType)) { + return toTabularFormat(this.showNonTabularFunctionKw(node), this.cfg.indentStyle); + } else { + return this.showNonTabularFunctionKw(node); + } + } + + // Like showFunctionKw(), but skips tabular formatting + private showNonTabularFunctionKw(node: KeywordNode): string { + switch (this.cfg.functionCase) { + case 'preserve': + return equalizeWhitespace(node.raw); + case 'upper': + return node.text; + case 'lower': + return node.text.toLowerCase(); + } + } + private showIdentifier(node: IdentifierNode): string { if (node.quoted) { return node.text; @@ -523,4 +561,15 @@ export default class ExpressionFormatter { } } } + + private showDataType(node: DataTypeNode): string { + switch (this.cfg.dataTypeCase) { + case 'preserve': + return equalizeWhitespace(node.raw); + case 'upper': + return node.text; + case 'lower': + return node.text.toLowerCase(); + } + } } diff --git a/src/languages/bigquery/bigquery.formatter.ts b/src/languages/bigquery/bigquery.formatter.ts index d162cfbf60..06ef621b1a 100644 --- a/src/languages/bigquery/bigquery.formatter.ts +++ b/src/languages/bigquery/bigquery.formatter.ts @@ -163,9 +163,8 @@ export const bigquery: DialectOptions = { reservedSetOperations, reservedJoins, reservedPhrases, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, extraParens: ['[]'], stringTypes: [ diff --git a/src/languages/db2/db2.formatter.ts b/src/languages/db2/db2.formatter.ts index a9021831ad..c0cc4ed8c8 100644 --- a/src/languages/db2/db2.formatter.ts +++ b/src/languages/db2/db2.formatter.ts @@ -265,9 +265,8 @@ export const db2: DialectOptions = { reservedSetOperations, reservedJoins, reservedPhrases, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, extraParens: ['[]'], stringTypes: [ diff --git a/src/languages/db2/db2.keywords.ts b/src/languages/db2/db2.keywords.ts index d012308080..6196cdbc2a 100644 --- a/src/languages/db2/db2.keywords.ts +++ b/src/languages/db2/db2.keywords.ts @@ -403,12 +403,17 @@ export const keywords: string[] = [ export const dataTypes: string[] = [ // https://www.ibm.com/docs/en/db2-for-zos/12?topic=columns-data-types + 'ARRAY', + 'BIGINT', 'CCSID', 'CHAR', 'CHARACTER', 'DATE', 'DOUBLE', + 'INT', + 'INTEGER', 'LONG', + 'SMALLINT', 'TIME', 'TIMESTAMP', ]; diff --git a/src/languages/db2i/db2i.formatter.ts b/src/languages/db2i/db2i.formatter.ts index 1bdf258f7a..fc94a422a8 100644 --- a/src/languages/db2i/db2i.formatter.ts +++ b/src/languages/db2i/db2i.formatter.ts @@ -161,9 +161,8 @@ export const db2i: DialectOptions = { reservedSetOperations, reservedJoins, reservedPhrases, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, nestedBlockComments: true, extraParens: ['[]'], diff --git a/src/languages/db2i/db2i.keywords.ts b/src/languages/db2i/db2i.keywords.ts index 9e54732fca..d2664ea4a5 100644 --- a/src/languages/db2i/db2i.keywords.ts +++ b/src/languages/db2i/db2i.keywords.ts @@ -498,6 +498,7 @@ export const keywords: string[] = [ export const dataTypes: string[] = [ // https://www.ibm.com/docs/en/i/7.2?topic=iaodsd-odbc-data-types-how-they-correspond-db2-i-database-types + 'ARRAY', 'BIGINT', 'BINARY', 'BIT', @@ -516,6 +517,7 @@ export const dataTypes: string[] = [ 'DOUBLE', 'FLOAT', 'GRAPHIC', + 'INT', 'INTEGER', 'LONG', 'NUMERIC', diff --git a/src/languages/hive/hive.formatter.ts b/src/languages/hive/hive.formatter.ts index 33386d5e7e..eabe87a90e 100644 --- a/src/languages/hive/hive.formatter.ts +++ b/src/languages/hive/hive.formatter.ts @@ -90,9 +90,8 @@ export const hive: DialectOptions = { reservedSetOperations, reservedJoins, reservedPhrases, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, extraParens: ['[]'], stringTypes: ['""-bs', "''-bs"], diff --git a/src/languages/mariadb/mariadb.formatter.ts b/src/languages/mariadb/mariadb.formatter.ts index 7f2ee9ad44..4e0a412cfa 100644 --- a/src/languages/mariadb/mariadb.formatter.ts +++ b/src/languages/mariadb/mariadb.formatter.ts @@ -273,9 +273,8 @@ export const mariadb: DialectOptions = { reservedJoins, reservedPhrases, supportsXor: true, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, // TODO: support _ char set prefixes such as _utf8, _latin1, _binary, _utf8mb4, etc. stringTypes: [ diff --git a/src/languages/mysql/mysql.formatter.ts b/src/languages/mysql/mysql.formatter.ts index 4bce901a96..3b993c08ba 100644 --- a/src/languages/mysql/mysql.formatter.ts +++ b/src/languages/mysql/mysql.formatter.ts @@ -240,9 +240,8 @@ export const mysql: DialectOptions = { reservedJoins, reservedPhrases, supportsXor: true, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, // TODO: support _ char set prefixes such as _utf8, _latin1, _binary, _utf8mb4, etc. stringTypes: [ diff --git a/src/languages/mysql/mysql.keywords.ts b/src/languages/mysql/mysql.keywords.ts index bbbf5be7f9..6769724642 100644 --- a/src/languages/mysql/mysql.keywords.ts +++ b/src/languages/mysql/mysql.keywords.ts @@ -235,8 +235,12 @@ export const dataTypes: string[] = [ 'BIGINT', // (R) 'BINARY', // (R) 'BLOB', // (R) + 'BOOL', // (R) + 'BOOLEAN', // (R) 'CHAR', // (R) 'CHARACTER', // (R) + 'DATE', // (R) + 'DATETIME', // (R) 'DEC', // (R) 'DECIMAL', // (R) 'DOUBLE', // (R) @@ -260,6 +264,7 @@ export const dataTypes: string[] = [ 'PRECISION', // (R) 'REAL', // (R) 'SMALLINT', // (R) + 'TIMESTAMP', // (R) 'TINYBLOB', // (R) 'TINYINT', // (R) 'TINYTEXT', // (R) diff --git a/src/languages/n1ql/n1ql.formatter.ts b/src/languages/n1ql/n1ql.formatter.ts index 63a579c9cb..1a101f71ec 100644 --- a/src/languages/n1ql/n1ql.formatter.ts +++ b/src/languages/n1ql/n1ql.formatter.ts @@ -92,9 +92,8 @@ export const n1ql: DialectOptions = { reservedJoins, reservedPhrases, supportsXor: true, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, // NOTE: single quotes are actually not supported in N1QL, // but we support them anyway as all other SQL dialects do, diff --git a/src/languages/plsql/plsql.formatter.ts b/src/languages/plsql/plsql.formatter.ts index f73c33d2de..7b9ebd65bb 100644 --- a/src/languages/plsql/plsql.formatter.ts +++ b/src/languages/plsql/plsql.formatter.ts @@ -90,9 +90,8 @@ export const plsql: DialectOptions = { reservedJoins, reservedPhrases, supportsXor: true, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, stringTypes: [ { quote: "''-qq", prefixes: ['N'] }, diff --git a/src/languages/postgresql/postgresql.formatter.ts b/src/languages/postgresql/postgresql.formatter.ts index 6c6315d752..cb64c5a4e8 100644 --- a/src/languages/postgresql/postgresql.formatter.ts +++ b/src/languages/postgresql/postgresql.formatter.ts @@ -260,9 +260,8 @@ export const postgresql: DialectOptions = { reservedSetOperations, reservedJoins, reservedPhrases, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, nestedBlockComments: true, extraParens: ['[]'], diff --git a/src/languages/redshift/redshift.formatter.ts b/src/languages/redshift/redshift.formatter.ts index 8c9dfb96fd..164c9b33d3 100644 --- a/src/languages/redshift/redshift.formatter.ts +++ b/src/languages/redshift/redshift.formatter.ts @@ -147,9 +147,8 @@ export const redshift: DialectOptions = { reservedSetOperations, reservedJoins, reservedPhrases, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, stringTypes: ["''-qq"], identTypes: [`""-qq`], diff --git a/src/languages/redshift/redshift.keywords.ts b/src/languages/redshift/redshift.keywords.ts index dbf1afe492..6b548e6422 100644 --- a/src/languages/redshift/redshift.keywords.ts +++ b/src/languages/redshift/redshift.keywords.ts @@ -201,6 +201,13 @@ export const keywords: string[] = [ export const dataTypes: string[] = [ // https://docs.aws.amazon.com/redshift/latest/dg/r_Character_types.html#r_Character_types-text-and-bpchar-types 'ARRAY', + 'BIGINT', 'BPCHAR', + 'INT', + 'INT2', + 'INT4', + 'INT8', + 'INTEGER', + 'SMALLINT', 'TEXT', ]; diff --git a/src/languages/singlestoredb/singlestoredb.formatter.ts b/src/languages/singlestoredb/singlestoredb.formatter.ts index a1f80a9fd6..25274b10ae 100644 --- a/src/languages/singlestoredb/singlestoredb.formatter.ts +++ b/src/languages/singlestoredb/singlestoredb.formatter.ts @@ -240,9 +240,8 @@ export const singlestoredb: DialectOptions = { reservedSetOperations, reservedJoins, reservedPhrases, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, // TODO: support _binary"some string" prefix stringTypes: [ diff --git a/src/languages/snowflake/snowflake.formatter.ts b/src/languages/snowflake/snowflake.formatter.ts index c583a0b5ad..df1e2e715a 100644 --- a/src/languages/snowflake/snowflake.formatter.ts +++ b/src/languages/snowflake/snowflake.formatter.ts @@ -303,9 +303,8 @@ export const snowflake: DialectOptions = { reservedSetOperations, reservedJoins, reservedPhrases, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, stringTypes: ['$$', `''-qq-bs`], identTypes: ['""-qq'], diff --git a/src/languages/spark/spark.formatter.ts b/src/languages/spark/spark.formatter.ts index fadbbfe878..fc393f8563 100644 --- a/src/languages/spark/spark.formatter.ts +++ b/src/languages/spark/spark.formatter.ts @@ -127,9 +127,8 @@ export const spark: DialectOptions = { reservedJoins, reservedPhrases, supportsXor: true, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, extraParens: ['[]'], stringTypes: [ diff --git a/src/languages/sql/sql.formatter.ts b/src/languages/sql/sql.formatter.ts index 95697e44a6..6be3b6a9cc 100644 --- a/src/languages/sql/sql.formatter.ts +++ b/src/languages/sql/sql.formatter.ts @@ -81,9 +81,8 @@ export const sql: DialectOptions = { reservedSetOperations, reservedJoins, reservedPhrases, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, stringTypes: [ { quote: "''-qq-bs", prefixes: ['N', 'U&'] }, diff --git a/src/languages/sqlite/sqlite.formatter.ts b/src/languages/sqlite/sqlite.formatter.ts index b3c1c166e8..df9c1d9412 100644 --- a/src/languages/sqlite/sqlite.formatter.ts +++ b/src/languages/sqlite/sqlite.formatter.ts @@ -72,9 +72,8 @@ export const sqlite: DialectOptions = { reservedSetOperations, reservedJoins, reservedPhrases, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, stringTypes: [ "''-qq", diff --git a/src/languages/transactsql/transactsql.formatter.ts b/src/languages/transactsql/transactsql.formatter.ts index 8f4091c8b0..b092fb8072 100644 --- a/src/languages/transactsql/transactsql.formatter.ts +++ b/src/languages/transactsql/transactsql.formatter.ts @@ -229,9 +229,8 @@ export const transactsql: DialectOptions = { reservedSetOperations, reservedJoins, reservedPhrases, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, nestedBlockComments: true, stringTypes: [{ quote: "''-qq", prefixes: ['N'] }], diff --git a/src/languages/trino/trino.formatter.ts b/src/languages/trino/trino.formatter.ts index d13c3e7030..d56fdea8c1 100644 --- a/src/languages/trino/trino.formatter.ts +++ b/src/languages/trino/trino.formatter.ts @@ -132,9 +132,8 @@ export const trino: DialectOptions = { reservedSetOperations, reservedJoins, reservedPhrases, - reservedKeywords: - // Temporary, will be replaced by reservedDataTypes - [...new Set(keywords.concat(dataTypes))], + reservedKeywords: keywords, + reservedDataTypes: dataTypes, reservedFunctionNames: functions, // Trino also supports {- ... -} parenthesis. // The formatting of these currently works out as a result of { and - diff --git a/src/lexer/Tokenizer.ts b/src/lexer/Tokenizer.ts index 2df5a6335a..438455e627 100644 --- a/src/lexer/Tokenizer.ts +++ b/src/lexer/Tokenizer.ts @@ -130,6 +130,11 @@ export default class Tokenizer { regex: regex.reservedWord(cfg.reservedFunctionNames, cfg.identChars), text: toCanonical, }, + { + type: TokenType.RESERVED_DATA_TYPE, + regex: regex.reservedWord(cfg.reservedDataTypes ?? [], cfg.identChars), + text: toCanonical, + }, { type: TokenType.RESERVED_KEYWORD, regex: regex.reservedWord(cfg.reservedKeywords, cfg.identChars), diff --git a/src/lexer/TokenizerOptions.ts b/src/lexer/TokenizerOptions.ts index beca4575e5..24fb8ff66e 100644 --- a/src/lexer/TokenizerOptions.ts +++ b/src/lexer/TokenizerOptions.ts @@ -69,6 +69,8 @@ export interface TokenizerOptions { reservedPhrases?: string[]; // built in function names reservedFunctionNames: string[]; + // data types + reservedDataTypes?: string[]; // all other reserved words (not included to any of the above lists) reservedKeywords: string[]; // Types of quotes to use for strings diff --git a/src/lexer/disambiguateTokens.ts b/src/lexer/disambiguateTokens.ts index 4527f6192f..ad32cff675 100644 --- a/src/lexer/disambiguateTokens.ts +++ b/src/lexer/disambiguateTokens.ts @@ -35,7 +35,40 @@ const funcNameToKeyword = (token: Token, i: number, tokens: Token[]): Token => { if (token.type === TokenType.RESERVED_FUNCTION_NAME) { const nextToken = nextNonCommentToken(tokens, i); if (!nextToken || !isOpenParen(nextToken)) { - return { ...token, type: TokenType.RESERVED_KEYWORD }; + return { + ...token, + type: + // Function names which are also data types + [ + 'BIGINT', + 'BINARY', + 'BIT', + 'BLOB', + 'BOOLEAN', + 'CHAR', + 'DATE', + 'DECIMAL', + 'DOUBLE', + 'FLOAT', + 'INT', + 'INTEGER', + 'JSON', + 'NCHAR', + 'NUMBER', + 'NUMERIC', + 'NVARCHAR', + 'REAL', + 'SMALLINT', + 'TEXT', + 'TIME', + 'TIMESTAMP', + 'TINYINT', + 'VARCHAR', + 'XML', + ].includes(token.text) + ? TokenType.RESERVED_DATA_TYPE + : TokenType.RESERVED_KEYWORD, + }; } } return token; @@ -57,6 +90,11 @@ const keywordToArrayKeyword = (token: Token, i: number, tokens: Token[]): Token if (nextToken && isOpenBracket(nextToken)) { return { ...token, type: TokenType.ARRAY_KEYWORD }; } + } else if (token.type === TokenType.RESERVED_DATA_TYPE) { + const nextToken = nextNonCommentToken(tokens, i); + if (nextToken && isOpenBracket(nextToken)) { + return { ...token, type: TokenType.ARRAY_DATA_TYPE }; + } } return token; }; diff --git a/src/lexer/token.ts b/src/lexer/token.ts index 4df232bbd8..9bee7a90c1 100644 --- a/src/lexer/token.ts +++ b/src/lexer/token.ts @@ -4,6 +4,7 @@ export enum TokenType { IDENTIFIER = 'IDENTIFIER', STRING = 'STRING', VARIABLE = 'VARIABLE', + RESERVED_DATA_TYPE = 'RESERVED_DATA_TYPE', RESERVED_KEYWORD = 'RESERVED_KEYWORD', RESERVED_FUNCTION_NAME = 'RESERVED_FUNCTION_NAME', RESERVED_PHRASE = 'RESERVED_PHRASE', @@ -12,6 +13,7 @@ export enum TokenType { RESERVED_SELECT = 'RESERVED_SELECT', RESERVED_JOIN = 'RESERVED_JOIN', ARRAY_IDENTIFIER = 'ARRAY_IDENTIFIER', // IDENTIFIER token in front of [ + ARRAY_DATA_TYPE = 'ARRAY_DATA_TYPE', // RESERVED_DATA_TYPE token in front of [ ARRAY_KEYWORD = 'ARRAY_KEYWORD', // RESERVED_KEYWORD token in front of [ CASE = 'CASE', END = 'END', @@ -73,16 +75,17 @@ export const testToken = /** Util object that allows for easy checking of Reserved Keywords */ export const isToken = { - ARRAY: testToken({ text: 'ARRAY', type: TokenType.RESERVED_KEYWORD }), + ARRAY: testToken({ text: 'ARRAY', type: TokenType.RESERVED_DATA_TYPE }), BY: testToken({ text: 'BY', type: TokenType.RESERVED_KEYWORD }), SET: testToken({ text: 'SET', type: TokenType.RESERVED_CLAUSE }), - STRUCT: testToken({ text: 'STRUCT', type: TokenType.RESERVED_KEYWORD }), + STRUCT: testToken({ text: 'STRUCT', type: TokenType.RESERVED_DATA_TYPE }), WINDOW: testToken({ text: 'WINDOW', type: TokenType.RESERVED_CLAUSE }), VALUES: testToken({ text: 'VALUES', type: TokenType.RESERVED_CLAUSE }), }; /** Checks if token is any Reserved Keyword or Clause */ export const isReserved = (type: TokenType): boolean => + type === TokenType.RESERVED_DATA_TYPE || type === TokenType.RESERVED_KEYWORD || type === TokenType.RESERVED_FUNCTION_NAME || type === TokenType.RESERVED_PHRASE || @@ -90,6 +93,7 @@ export const isReserved = (type: TokenType): boolean => type === TokenType.RESERVED_SELECT || type === TokenType.RESERVED_SET_OPERATION || type === TokenType.RESERVED_JOIN || + type === TokenType.ARRAY_DATA_TYPE || type === TokenType.ARRAY_KEYWORD || type === TokenType.CASE || type === TokenType.END || diff --git a/src/parser/ast.ts b/src/parser/ast.ts index 2a5a7e8424..81e72776fc 100644 --- a/src/parser/ast.ts +++ b/src/parser/ast.ts @@ -17,6 +17,7 @@ export enum NodeType { literal = 'literal', identifier = 'identifier', keyword = 'keyword', + data_type = 'data_type', parameter = 'parameter', operator = 'operator', comma = 'comma', @@ -56,7 +57,7 @@ export interface FunctionCallNode extends BaseNode { // [] export interface ArraySubscriptNode extends BaseNode { type: NodeType.array_subscript; - array: IdentifierNode | KeywordNode; + array: IdentifierNode | KeywordNode | DataTypeNode; parenthesis: ParenthesisNode; } @@ -129,6 +130,12 @@ export interface IdentifierNode extends BaseNode { text: string; } +export interface DataTypeNode extends BaseNode { + type: NodeType.data_type; + text: string; + raw: string; +} + export interface KeywordNode extends BaseNode { type: NodeType.keyword; tokenType: TokenType; @@ -180,6 +187,7 @@ export type AstNode = | AllColumnsAsteriskNode | LiteralNode | IdentifierNode + | DataTypeNode | KeywordNode | ParameterNode | OperatorNode diff --git a/src/parser/grammar.ne b/src/parser/grammar.ne index f380c6bff7..901c17bd1b 100644 --- a/src/parser/grammar.ne +++ b/src/parser/grammar.ne @@ -1,7 +1,7 @@ @preprocessor typescript @{% import LexerAdapter from './LexerAdapter.js'; -import { NodeType, AstNode, CommentNode, KeywordNode, IdentifierNode } from './ast.js'; +import { NodeType, AstNode, CommentNode, KeywordNode, IdentifierNode, DataTypeNode } from './ast.js'; import { Token, TokenType } from '../lexer/token.js'; // The lexer here is only to provide the has() method, @@ -23,6 +23,12 @@ const toKeywordNode = (token: Token): KeywordNode => ({ raw: token.raw, }); +const toDataTypeNode = (token: Token): DataTypeNode => ({ + type: NodeType.data_type, + text: token.text, + raw: token.raw, +}); + interface CommentAttachments { leading?: CommentNode[]; trailing?: CommentNode[]; @@ -197,6 +203,7 @@ atomic_expression -> | identifier | parameter | literal + | data_type | keyword ) {% unwrap %} array_subscript -> %ARRAY_IDENTIFIER _ square_brackets {% @@ -206,6 +213,13 @@ array_subscript -> %ARRAY_IDENTIFIER _ square_brackets {% parenthesis: brackets, }) %} +array_subscript -> %ARRAY_DATA_TYPE _ square_brackets {% + ([arrayToken, _, brackets]) => ({ + type: NodeType.array_subscript, + array: addComments(toDataTypeNode(arrayToken), { trailing: _ }), + parenthesis: brackets, + }) +%} array_subscript -> %ARRAY_KEYWORD _ square_brackets {% ([arrayToken, _, brackets]) => ({ type: NodeType.array_subscript, @@ -329,6 +343,11 @@ keyword -> ([[token]]) => toKeywordNode(token) %} +data_type -> + ( %RESERVED_DATA_TYPE ) {% + ([[token]]) => toDataTypeNode(token) +%} + logic_operator -> ( %AND | %OR diff --git a/src/sqlFormatter.ts b/src/sqlFormatter.ts index 620038ce2f..50cb2cbed9 100644 --- a/src/sqlFormatter.ts +++ b/src/sqlFormatter.ts @@ -42,6 +42,8 @@ const defaultOptions: FormatOptions = { useTabs: false, keywordCase: 'preserve', identifierCase: 'preserve', + dataTypeCase: 'preserve', + functionCase: 'preserve', indentStyle: 'standard', logicalOperatorNewline: 'before', expressionWidth: 50, diff --git a/test/behavesLikeMariaDbFormatter.ts b/test/behavesLikeMariaDbFormatter.ts index 9fe47ddd0e..aa32f58c23 100644 --- a/test/behavesLikeMariaDbFormatter.ts +++ b/test/behavesLikeMariaDbFormatter.ts @@ -163,7 +163,10 @@ export default function behavesLikeMariaDbFormatter(format: FormatFn) { `create table account (id int comment 'the most important column'); select * from mysql.user; insert into user (id, name) values (1, 'Blah');`, - { keywordCase: 'upper' } + { + keywordCase: 'upper', + dataTypeCase: 'upper', + } ) ).toBe(dedent` CREATE TABLE diff --git a/test/behavesLikeSqlFormatter.ts b/test/behavesLikeSqlFormatter.ts index 25f990c6ea..bcd8da6537 100644 --- a/test/behavesLikeSqlFormatter.ts +++ b/test/behavesLikeSqlFormatter.ts @@ -16,6 +16,7 @@ import supportsNewlineBeforeSemicolon from './options/newlineBeforeSemicolon.js' import supportsLogicalOperatorNewline from './options/logicalOperatorNewline.js'; import supportsParamTypes from './options/paramTypes.js'; import supportsWindowFunctions from './features/windowFunctions.js'; +import supportsFunctionCase from './options/functionCase.js'; /** * Core tests for all SQL formatters @@ -29,6 +30,7 @@ export default function behavesLikeSqlFormatter(format: FormatFn) { supportsUseTabs(format); supportsKeywordCase(format); supportsIdentifierCase(format); + supportsFunctionCase(format); supportsIndentStyle(format); supportsLinesBetweenQueries(format); supportsExpressionWidth(format); diff --git a/test/bigquery.test.ts b/test/bigquery.test.ts index 2456953bcf..11a1a9f9b9 100644 --- a/test/bigquery.test.ts +++ b/test/bigquery.test.ts @@ -24,6 +24,7 @@ import supportsMergeInto from './features/mergeInto.js'; import supportsCreateView from './features/createView.js'; import supportsAlterTable from './features/alterTable.js'; import supportsIsDistinctFrom from './features/isDistinctFrom.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; describe('BigQueryFormatter', () => { const language = 'bigquery'; @@ -32,7 +33,11 @@ describe('BigQueryFormatter', () => { behavesLikeSqlFormatter(format); supportsComments(format, { hashComments: true }); supportsCreateView(format, { orReplace: true, materialized: true, ifNotExists: true }); - supportsCreateTable(format, { orReplace: true, ifNotExists: true }); + supportsCreateTable(format, { + orReplace: true, + ifNotExists: true, + dialectDoesntHaveVarchar: true, + }); supportsDropTable(format, { ifExists: true }); supportsAlterTable(format, { addColumn: true, @@ -60,6 +65,7 @@ describe('BigQueryFormatter', () => { supportsParams(format, { positional: true, named: ['@'], quoted: ['@``'] }); supportsWindow(format); supportsLimiting(format, { limit: true, offset: true }); + supportsDataTypeCase(format); // Note: BigQuery supports single dashes inside identifiers, so my-ident would be // detected as identifier, while other SQL dialects would detect it as diff --git a/test/db2.test.ts b/test/db2.test.ts index 8dc642a3fc..9cde32e856 100644 --- a/test/db2.test.ts +++ b/test/db2.test.ts @@ -11,6 +11,7 @@ import supportsStrings from './features/strings.js'; import supportsComments from './features/comments.js'; import supportsOperators from './features/operators.js'; import supportsLimiting from './features/limiting.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; describe('Db2Formatter', () => { const language = 'db2'; @@ -49,6 +50,7 @@ describe('Db2Formatter', () => { ]); // Additional U& string type in addition to others shared by all DB2 implementations supportsStrings(format, ["U&''"]); + supportsDataTypeCase(format); it('supports non-standard FOR clause', () => { expect(format('SELECT * FROM tbl FOR UPDATE OF other_tbl FOR RS USE AND KEEP EXCLUSIVE LOCKS')) diff --git a/test/db2i.test.ts b/test/db2i.test.ts index 36b3c23885..bc56529e1d 100644 --- a/test/db2i.test.ts +++ b/test/db2i.test.ts @@ -8,6 +8,7 @@ import supportsDropTable from './features/dropTable.js'; import supportsJoin from './features/join.js'; import supportsOperators from './features/operators.js'; import supportsLimiting from './features/limiting.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; describe('Db2iFormatter', () => { const language = 'db2i'; @@ -28,4 +29,5 @@ describe('Db2iFormatter', () => { additionally: ['EXCEPTION JOIN', 'LEFT EXCEPTION JOIN', 'RIGHT EXCEPTION JOIN'], }); supportsOperators(format, ['**', '¬=', '¬>', '¬<', '!>', '!<', '||', '=>']); + supportsDataTypeCase(format); }); diff --git a/test/features/arrayLiterals.ts b/test/features/arrayLiterals.ts index beaa24e659..f7761b4bb3 100644 --- a/test/features/arrayLiterals.ts +++ b/test/features/arrayLiterals.ts @@ -12,45 +12,126 @@ export default function supportsArrayLiterals(format: FormatFn, cfg: ArrayLitera it('supports ARRAY[] literals', () => { expect( format( - `SELECT ARRAY[1, 2, 3] FROM ARRAY['cammon', 'seriously', 'this', 'is', 'one', 'hello-of-a', 'damn', 'long', 'array'];` + `SELECT ARRAY[1, 2, 3] FROM ARRAY['come-on', 'seriously', 'this', 'is', 'a', 'very', 'very', 'long', 'array'];` ) ).toBe(dedent` SELECT ARRAY[1, 2, 3] FROM ARRAY[ - 'cammon', + 'come-on', 'seriously', 'this', 'is', - 'one', - 'hello-of-a', - 'damn', + 'a', + 'very', + 'very', 'long', 'array' ]; `); }); + + it('supports preserving ARRAY[] literals keywords casing', () => { + expect( + format( + `SELECT ArrAy[1, 2] FROM aRRAY['aaa', 'bbb', 'ccc', 'ddd', 'eee', 'fff', 'ggg', 'hhh', 'iii', 'jjj'];`, + { + dataTypeCase: 'preserve', + } + ) + ).toBe(dedent` + SELECT + ArrAy[1, 2] + FROM + aRRAY[ + 'aaa', + 'bbb', + 'ccc', + 'ddd', + 'eee', + 'fff', + 'ggg', + 'hhh', + 'iii', + 'jjj' + ]; + `); + }); + + it('supports converting ARRAY[] literals keywords to uppercase', () => { + expect( + format( + `SELECT ArrAy[1, 2] FROM aRRAY['aaa', 'bbb', 'ccc', 'ddd', 'eee', 'fff', 'ggg', 'hhh', 'iii', 'jjj'];`, + { + dataTypeCase: 'upper', + } + ) + ).toBe(dedent` + SELECT + ARRAY[1, 2] + FROM + ARRAY[ + 'aaa', + 'bbb', + 'ccc', + 'ddd', + 'eee', + 'fff', + 'ggg', + 'hhh', + 'iii', + 'jjj' + ]; + `); + }); + + it('supports converting ARRAY[] literals keywords to lowercase', () => { + expect( + format( + `SELECT ArrAy[1, 2] FROM aRRAY['aaa', 'bbb', 'ccc', 'ddd', 'eee', 'fff', 'ggg', 'hhh', 'iii', 'jjj'];`, + { + dataTypeCase: 'lower', + } + ) + ).toBe(dedent` + SELECT + array[1, 2] + FROM + array[ + 'aaa', + 'bbb', + 'ccc', + 'ddd', + 'eee', + 'fff', + 'ggg', + 'hhh', + 'iii', + 'jjj' + ]; + `); + }); } if (cfg.withoutArrayPrefix) { it('supports array literals', () => { expect( format( - `SELECT [1, 2, 3] FROM ['cammon', 'seriously', 'this', 'is', 'one', 'hello-of-a', 'damn', 'long', 'array'];` + `SELECT [1, 2, 3] FROM ['come-on', 'seriously', 'this', 'is', 'a', 'very', 'very', 'long', 'array'];` ) ).toBe(dedent` SELECT [1, 2, 3] FROM [ - 'cammon', + 'come-on', 'seriously', 'this', 'is', - 'one', - 'hello-of-a', - 'damn', + 'a', + 'very', + 'very', 'long', 'array' ]; diff --git a/test/features/case.ts b/test/features/case.ts index defb1b4bcf..b44febc035 100644 --- a/test/features/case.ts +++ b/test/features/case.ts @@ -79,7 +79,10 @@ export default function supportsCase(format: FormatFn) { it('properly converts to uppercase in case statements', () => { const result = format( "case trim(sqrt(my_field)) when 'one' then 1 when 'two' then 2 when 'three' then 3 else 4 end;", - { keywordCase: 'upper' } + { + keywordCase: 'upper', + functionCase: 'upper', + } ); expect(result).toBe(dedent` CASE TRIM(SQRT(my_field)) diff --git a/test/features/createTable.ts b/test/features/createTable.ts index 508d9c1a86..223a8c981d 100644 --- a/test/features/createTable.ts +++ b/test/features/createTable.ts @@ -7,12 +7,10 @@ interface CreateTableConfig { ifNotExists?: boolean; columnComment?: boolean; tableComment?: boolean; + dialectDoesntHaveVarchar?: boolean; } -export default function supportsCreateTable( - format: FormatFn, - { orReplace, ifNotExists, columnComment, tableComment }: CreateTableConfig = {} -) { +export default function supportsCreateTable(format: FormatFn, cfg: CreateTableConfig = {}) { it('formats short CREATE TABLE', () => { expect(format('CREATE TABLE tbl (a INT PRIMARY KEY, b TEXT);')).toBe(dedent` CREATE TABLE @@ -20,6 +18,19 @@ export default function supportsCreateTable( `); }); + if (!cfg.dialectDoesntHaveVarchar) { + it('formats short CREATE TABLE with lowercase data types', () => { + expect( + format('CREATE TABLE tbl (a INT PRIMARY KEY, b VARCHAR);', { + dataTypeCase: 'lower', + }) + ).toBe(dedent` + CREATE TABLE + tbl (a int PRIMARY KEY, b varchar); + `); + }); + } + // The decision to place it to multiple lines is made based on the length of text inside braces // ignoring the whitespace. (Which is not quite right :P) it('formats long CREATE TABLE', () => { @@ -36,7 +47,7 @@ export default function supportsCreateTable( `); }); - if (orReplace) { + if (cfg.orReplace) { it('formats short CREATE OR REPLACE TABLE', () => { expect(format('CREATE OR REPLACE TABLE tbl (a INT PRIMARY KEY, b TEXT);')).toBe(dedent` CREATE OR REPLACE TABLE @@ -45,7 +56,7 @@ export default function supportsCreateTable( }); } - if (ifNotExists) { + if (cfg.ifNotExists) { it('formats short CREATE TABLE IF NOT EXISTS', () => { expect(format('CREATE TABLE IF NOT EXISTS tbl (a INT PRIMARY KEY, b TEXT);')).toBe(dedent` CREATE TABLE IF NOT EXISTS @@ -54,7 +65,7 @@ export default function supportsCreateTable( }); } - if (columnComment) { + if (cfg.columnComment) { it('formats short CREATE TABLE with column comments', () => { expect( format(`CREATE TABLE tbl (a INT COMMENT 'Hello world!', b TEXT COMMENT 'Here we are!');`) @@ -68,7 +79,7 @@ export default function supportsCreateTable( }); } - if (tableComment) { + if (cfg.tableComment) { it('formats short CREATE TABLE with comment', () => { expect(format(`CREATE TABLE tbl (a INT, b TEXT) COMMENT = 'Hello, world!';`)).toBe(dedent` CREATE TABLE diff --git a/test/hive.test.ts b/test/hive.test.ts index 8327fe7321..eb4554fb36 100644 --- a/test/hive.test.ts +++ b/test/hive.test.ts @@ -22,6 +22,7 @@ import supportsDeleteFrom from './features/deleteFrom.js'; import supportsTruncateTable from './features/truncateTable.js'; import supportsMergeInto from './features/mergeInto.js'; import supportsCreateView from './features/createView.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; describe('HiveFormatter', () => { const language = 'hive'; @@ -50,6 +51,7 @@ describe('HiveFormatter', () => { supportsArrayAndMapAccessors(format); supportsWindow(format); supportsLimiting(format, { limit: true }); + supportsDataTypeCase(format); // eslint-disable-next-line no-template-curly-in-string it('recognizes ${hivevar:name} substitution variables', () => { diff --git a/test/mariadb.test.ts b/test/mariadb.test.ts index d76dfdbcf2..cfb081273c 100644 --- a/test/mariadb.test.ts +++ b/test/mariadb.test.ts @@ -14,6 +14,7 @@ import supportsCreateView from './features/createView.js'; import supportsAlterTable from './features/alterTable.js'; import supportsStrings from './features/strings.js'; import supportsConstraints from './features/constraints.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; describe('MariaDbFormatter', () => { const language = 'mariadb'; @@ -52,6 +53,7 @@ describe('MariaDbFormatter', () => { renameTo: true, renameColumn: true, }); + supportsDataTypeCase(format); it(`supports @"name" variables`, () => { expect(format(`SELECT @"foo fo", @"foo\\"x", @"foo""y" FROM tbl;`)).toBe(dedent` diff --git a/test/mysql.test.ts b/test/mysql.test.ts index 405476f83c..3bad166e4c 100644 --- a/test/mysql.test.ts +++ b/test/mysql.test.ts @@ -14,6 +14,7 @@ import supportsCreateView from './features/createView.js'; import supportsAlterTable from './features/alterTable.js'; import supportsStrings from './features/strings.js'; import supportsConstraints from './features/constraints.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; describe('MySqlFormatter', () => { const language = 'mysql'; @@ -54,6 +55,7 @@ describe('MySqlFormatter', () => { renameTo: true, renameColumn: true, }); + supportsDataTypeCase(format); it(`supports @"name" variables`, () => { expect(format(`SELECT @"foo fo", @"foo\\"x", @"foo""y" FROM tbl;`)).toBe(dedent` diff --git a/test/options/dataTypeCase.ts b/test/options/dataTypeCase.ts new file mode 100644 index 0000000000..1e3e799938 --- /dev/null +++ b/test/options/dataTypeCase.ts @@ -0,0 +1,67 @@ +import dedent from 'dedent-js'; + +import { FormatFn } from '../../src/sqlFormatter.js'; + +export default function supportsDataTypeCase(format: FormatFn) { + it('preserves data type keyword case by default', () => { + const result = format('CREATE TABLE users ( id iNt PRIMARY KEY )'); + expect(result).toBe(dedent` + CREATE TABLE + users (id iNt PRIMARY KEY) + `); + }); + + it('converts data type keyword case to uppercase', () => { + const result = format('CREATE TABLE users ( id iNt PRIMARY KEY )', { + dataTypeCase: 'upper', + }); + expect(result).toBe(dedent` + CREATE TABLE + users (id INT PRIMARY KEY) + `); + }); + + it('converts data type keyword case to lowercase', () => { + const result = format('CREATE TABLE users ( id iNt PRIMARY KEY )', { + dataTypeCase: 'lower', + }); + expect(result).toBe(dedent` + CREATE TABLE + users (id int PRIMARY KEY) + `); + }); + + it('preserves data type keyword case in cast by default', () => { + const result = format('SELECT CAST(quantity AS InT) FROM orders'); + expect(result).toBe(dedent` + SELECT + CAST(quantity AS InT) + FROM + orders + `); + }); + + it('converts data type keyword case in cast to uppercase', () => { + const result = format('SELECT CAST(quantity AS InT) FROM orders', { + dataTypeCase: 'upper', + }); + expect(result).toBe(dedent` + SELECT + CAST(quantity AS INT) + FROM + orders + `); + }); + + it('converts data type keyword case in cast to lowercase', () => { + const result = format('SELECT CAST(quantity AS InT) FROM orders', { + dataTypeCase: 'lower', + }); + expect(result).toBe(dedent` + SELECT + CAST(quantity AS int) + FROM + orders + `); + }); +} diff --git a/test/options/functionCase.ts b/test/options/functionCase.ts new file mode 100644 index 0000000000..437951fc5b --- /dev/null +++ b/test/options/functionCase.ts @@ -0,0 +1,39 @@ +import dedent from 'dedent-js'; + +import { FormatFn } from '../../src/sqlFormatter.js'; + +export default function supportsFunctionCase(format: FormatFn) { + it('preserves function name case by default', () => { + const result = format('SELECT MiN(price) AS min_price FROM products'); + expect(result).toBe(dedent` + SELECT + MiN(price) AS min_price + FROM + products + `); + }); + + it('converts function names to uppercase', () => { + const result = format('SELECT MiN(price) AS min_price FROM products', { + functionCase: 'upper', + }); + expect(result).toBe(dedent` + SELECT + MIN(price) AS min_price + FROM + products + `); + }); + + it('converts function names to lowercase', () => { + const result = format('SELECT MiN(price) AS min_price FROM products', { + functionCase: 'lower', + }); + expect(result).toBe(dedent` + SELECT + min(price) AS min_price + FROM + products + `); + }); +} diff --git a/test/plsql.test.ts b/test/plsql.test.ts index d1fb8924f9..2d233c9575 100644 --- a/test/plsql.test.ts +++ b/test/plsql.test.ts @@ -25,6 +25,7 @@ import supportsUpdate from './features/update.js'; import supportsTruncateTable from './features/truncateTable.js'; import supportsMergeInto from './features/mergeInto.js'; import supportsCreateView from './features/createView.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; describe('PlSqlFormatter', () => { const language = 'plsql'; @@ -64,6 +65,7 @@ describe('PlSqlFormatter', () => { supportsReturning(format); supportsParams(format, { numbered: [':'], named: [':'] }); supportsLimiting(format, { offset: true, fetchFirst: true, fetchNext: true }); + supportsDataTypeCase(format); it('recognizes _, $, # as part of identifiers', () => { const result = format('SELECT my_col$1#, col.a$, type#, procedure$, user# FROM tbl;'); diff --git a/test/postgresql.test.ts b/test/postgresql.test.ts index 607f376a97..24f54a72d7 100644 --- a/test/postgresql.test.ts +++ b/test/postgresql.test.ts @@ -29,6 +29,7 @@ import supportsCreateView from './features/createView.js'; import supportsOnConflict from './features/onConflict.js'; import supportsIsDistinctFrom from './features/isDistinctFrom.js'; import supportsArrayLiterals from './features/arrayLiterals.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; describe('PostgreSqlFormatter', () => { const language = 'postgresql'; @@ -150,6 +151,7 @@ describe('PostgreSqlFormatter', () => { supportsParams(format, { numbered: ['$'] }); supportsWindow(format); supportsLimiting(format, { limit: true, offset: true, fetchFirst: true, fetchNext: true }); + supportsDataTypeCase(format); it('allows $ character as part of identifiers', () => { expect(format('SELECT foo$, some$$ident')).toBe(dedent` diff --git a/test/redshift.test.ts b/test/redshift.test.ts index c44a1d0859..d7b6e6766e 100644 --- a/test/redshift.test.ts +++ b/test/redshift.test.ts @@ -20,6 +20,7 @@ import supportsInsertInto from './features/insertInto.js'; import supportsUpdate from './features/update.js'; import supportsTruncateTable from './features/truncateTable.js'; import supportsCreateView from './features/createView.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; describe('RedshiftFormatter', () => { const language = 'redshift'; @@ -49,6 +50,7 @@ describe('RedshiftFormatter', () => { supportsSetOperations(format, ['UNION', 'UNION ALL', 'EXCEPT', 'INTERSECT', 'MINUS']); supportsParams(format, { numbered: ['$'] }); supportsLimiting(format, { limit: true, offset: true }); + supportsDataTypeCase(format); it('formats type-cast operator without spaces', () => { expect(format('SELECT 2 :: numeric AS foo;')).toBe(dedent` diff --git a/test/singlestoredb.test.ts b/test/singlestoredb.test.ts index deac08e45a..8ae5a75d73 100644 --- a/test/singlestoredb.test.ts +++ b/test/singlestoredb.test.ts @@ -10,6 +10,7 @@ import supportsCreateTable from './features/createTable.js'; import supportsCreateView from './features/createView.js'; import supportsAlterTable from './features/alterTable.js'; import supportsStrings from './features/strings.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; describe('SingleStoreDbFormatter', () => { const language = 'singlestoredb'; @@ -46,6 +47,7 @@ describe('SingleStoreDbFormatter', () => { modify: true, renameTo: true, }); + supportsDataTypeCase(format); describe(`formats traversal of semi structured data`, () => { it(`formats '::' path-operator without spaces`, () => { diff --git a/test/snowflake.test.ts b/test/snowflake.test.ts index 561c91b406..23cbc10dd8 100644 --- a/test/snowflake.test.ts +++ b/test/snowflake.test.ts @@ -21,6 +21,7 @@ import supportsUpdate from './features/update.js'; import supportsTruncateTable from './features/truncateTable.js'; import supportsCreateView from './features/createView.js'; import supportsConstraints from './features/constraints.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; describe('SnowflakeFormatter', () => { const language = 'snowflake'; @@ -57,6 +58,7 @@ describe('SnowflakeFormatter', () => { supportsJoin(format, { without: ['NATURAL INNER JOIN'] }); supportsSetOperations(format, ['UNION', 'UNION ALL', 'MINUS', 'EXCEPT', 'INTERSECT']); supportsLimiting(format, { limit: true, offset: true, fetchFirst: true, fetchNext: true }); + supportsDataTypeCase(format); it('allows $ character as part of unquoted identifiers', () => { expect(format('SELECT foo$')).toBe(dedent` @@ -167,12 +169,12 @@ describe('SnowflakeFormatter', () => { `); }); - it('detects data types as keywords', () => { + it('detects data types', () => { expect( format( `CREATE TABLE tbl (first_column double Precision, second_column numBer (38, 0), third String);`, { - keywordCase: 'upper', + dataTypeCase: 'upper', } ) ).toBe(dedent` diff --git a/test/spark.test.ts b/test/spark.test.ts index 7dd1cffaa4..f406e1ce2c 100644 --- a/test/spark.test.ts +++ b/test/spark.test.ts @@ -18,6 +18,7 @@ import supportsLimiting from './features/limiting.js'; import supportsInsertInto from './features/insertInto.js'; import supportsTruncateTable from './features/truncateTable.js'; import supportsCreateView from './features/createView.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; describe('SparkFormatter', () => { const language = 'spark'; @@ -60,6 +61,7 @@ describe('SparkFormatter', () => { }); supportsSetOperations(format); supportsLimiting(format, { limit: true }); + supportsDataTypeCase(format); it('formats basic WINDOW clause', () => { const result = format(`SELECT * FROM tbl WINDOW win1, WINDOW win2, WINDOW win3;`); diff --git a/test/sql.test.ts b/test/sql.test.ts index 9a8bf58093..02405852c2 100644 --- a/test/sql.test.ts +++ b/test/sql.test.ts @@ -23,6 +23,7 @@ import supportsInsertInto from './features/insertInto.js'; import supportsUpdate from './features/update.js'; import supportsTruncateTable from './features/truncateTable.js'; import supportsCreateView from './features/createView.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; describe('SqlFormatter', () => { const language = 'sql'; @@ -54,6 +55,7 @@ describe('SqlFormatter', () => { supportsParams(format, { positional: true }); supportsWindow(format); supportsLimiting(format, { limit: true, offset: true, fetchFirst: true, fetchNext: true }); + supportsDataTypeCase(format); it('throws error when encountering characters or operators it does not recognize', () => { expect(() => format('SELECT @name, :bar FROM foo;')).toThrowError( diff --git a/test/sqlite.test.ts b/test/sqlite.test.ts index 78ec8e7438..7d12dea399 100644 --- a/test/sqlite.test.ts +++ b/test/sqlite.test.ts @@ -22,6 +22,7 @@ import supportsInsertInto from './features/insertInto.js'; import supportsUpdate from './features/update.js'; import supportsCreateView from './features/createView.js'; import supportsOnConflict from './features/onConflict.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; describe('SqliteFormatter', () => { const language = 'sqlite'; @@ -53,6 +54,7 @@ describe('SqliteFormatter', () => { supportsParams(format, { positional: true, numbered: ['?'], named: [':', '$', '@'] }); supportsWindow(format); supportsLimiting(format, { limit: true, offset: true }); + supportsDataTypeCase(format); it('supports REPLACE INTO syntax', () => { expect(format(`REPLACE INTO tbl VALUES (1,'Leopard'),(2,'Dog');`)).toBe(dedent` diff --git a/test/transactsql.test.ts b/test/transactsql.test.ts index 0a9f0fefbb..96704ac58b 100644 --- a/test/transactsql.test.ts +++ b/test/transactsql.test.ts @@ -23,6 +23,7 @@ import supportsUpdate from './features/update.js'; import supportsTruncateTable from './features/truncateTable.js'; import supportsMergeInto from './features/mergeInto.js'; import supportsCreateView from './features/createView.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; describe('TransactSqlFormatter', () => { const language = 'transactsql'; @@ -68,6 +69,7 @@ describe('TransactSqlFormatter', () => { supportsParams(format, { named: ['@'], quoted: ['@""', '@[]'] }); supportsWindow(format); supportsLimiting(format, { offset: true, fetchFirst: true, fetchNext: true }); + supportsDataTypeCase(format); it('supports language:tsql alias', () => { const result = originalFormat('SELECT [my column] FROM [my table];', { language: 'tsql' }); diff --git a/test/trino.test.ts b/test/trino.test.ts index a300ee4e92..216ed70708 100644 --- a/test/trino.test.ts +++ b/test/trino.test.ts @@ -25,6 +25,7 @@ import supportsTruncateTable from './features/truncateTable.js'; import supportsCreateView from './features/createView.js'; import supportsAlterTable from './features/alterTable.js'; import supportsIsDistinctFrom from './features/isDistinctFrom.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; describe('TrinoFormatter', () => { const language = 'trino'; @@ -59,6 +60,7 @@ describe('TrinoFormatter', () => { supportsParams(format, { positional: true }); supportsWindow(format); supportsLimiting(format, { limit: true, offset: true, fetchFirst: true, fetchNext: true }); + supportsDataTypeCase(format); it('formats SET SESSION', () => { const result = format('SET SESSION foo = 444;');