diff --git a/README.md b/README.md index 85173fb45..27306c955 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,6 @@ We provide **JSON Schema** for `.sql-formatter.json` configuration file, enablin - [Using the schema in VSCode](https://code.visualstudio.com/docs/languages/json#_mapping-in-the-user-settings) - [Using the schema in Zed](https://zed.dev/docs/languages/json#schema-specification-via-settings) - ### Usage as ESLint plugin - Inside `eslint-plugin-sql` by using the rule [eslint-plugin-sql#format](https://github.com/gajus/eslint-plugin-sql#format). diff --git a/src/allDialects.ts b/src/allDialects.ts index 8b41f9735..e995aab54 100644 --- a/src/allDialects.ts +++ b/src/allDialects.ts @@ -1,4 +1,5 @@ export { bigquery } from './languages/bigquery/bigquery.formatter.js'; +export { clickhouse } from './languages/clickhouse/clickhouse.formatter.js'; export { db2 } from './languages/db2/db2.formatter.js'; export { db2i } from './languages/db2i/db2i.formatter.js'; export { duckdb } from './languages/duckdb/duckdb.formatter.js'; diff --git a/src/index.ts b/src/index.ts index b5677f680..0b40d6ad3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export { ConfigError } from './validateConfig.js'; // When adding a new dialect, be sure to add it to the list of exports below. export { bigquery } from './languages/bigquery/bigquery.formatter.js'; +export { clickhouse } from './languages/clickhouse/clickhouse.formatter.js'; export { db2 } from './languages/db2/db2.formatter.js'; export { db2i } from './languages/db2i/db2i.formatter.js'; export { duckdb } from './languages/duckdb/duckdb.formatter.js'; diff --git a/src/languages/clickhouse/clickhouse.formatter.ts b/src/languages/clickhouse/clickhouse.formatter.ts new file mode 100644 index 000000000..beed3cdf5 --- /dev/null +++ b/src/languages/clickhouse/clickhouse.formatter.ts @@ -0,0 +1,364 @@ +import { DialectOptions } from '../../dialect.js'; +import { expandPhrases } from '../../expandPhrases.js'; +import { EOF_TOKEN, Token, TokenType } from '../../lexer/token.js'; +import { functions } from './clickhouse.functions.js'; +import { dataTypes, keywords } from './clickhouse.keywords.js'; + +const reservedSelect = expandPhrases([ + 'SELECT [DISTINCT]', + // https://clickhouse.com/docs/sql-reference/statements/alter/view + 'MODIFY QUERY SELECT [DISTINCT]', +]); + +const reservedClauses = expandPhrases([ + 'SET', + // https://clickhouse.com/docs/sql-reference/statements/select + 'WITH', + 'FROM', + 'SAMPLE', + 'PREWHERE', + 'WHERE', + 'GROUP BY', + 'HAVING', + 'QUALIFY', + 'ORDER BY', + 'LIMIT', // Note: Clickhouse has no OFFSET clause + 'SETTINGS', + 'INTO OUTFILE', + 'FORMAT', + // https://clickhouse.com/docs/sql-reference/window-functions + 'WINDOW', + 'PARTITION BY', + // https://clickhouse.com/docs/sql-reference/statements/insert-into + 'INSERT INTO', + 'VALUES', + // https://clickhouse.com/docs/sql-reference/statements/create/view#refreshable-materialized-view + 'DEPENDS ON', + // https://clickhouse.com/docs/sql-reference/statements/move + 'MOVE {USER | ROLE | QUOTA | SETTINGS PROFILE | ROW POLICY}', + // https://clickhouse.com/docs/sql-reference/statements/grant + 'GRANT', + // https://clickhouse.com/docs/sql-reference/statements/revoke + 'REVOKE', + // https://clickhouse.com/docs/sql-reference/statements/check-grant + 'CHECK GRANT', + // https://clickhouse.com/docs/sql-reference/statements/set-role + 'SET [DEFAULT] ROLE [NONE | ALL | ALL EXCEPT]', + // https://clickhouse.com/docs/sql-reference/statements/optimize + 'DEDUPLICATE BY', + // https://clickhouse.com/docs/sql-reference/statements/alter/statistics + 'MODIFY STATISTICS', + // Used for ALTER INDEX ... TYPE and ALTER STATISTICS ... TYPE + 'TYPE', + // https://clickhouse.com/docs/sql-reference/statements/alter + 'ALTER USER [IF EXISTS]', + 'ALTER [ROW] POLICY [IF EXISTS]', + // https://clickhouse.com/docs/sql-reference/statements/drop + 'DROP {USER | ROLE | QUOTA | PROFILE | SETTINGS PROFILE | ROW POLICY | POLICY} [IF EXISTS]', +]); + +const standardOnelineClauses = expandPhrases([ + // https://clickhouse.com/docs/sql-reference/statements/create + 'CREATE [OR REPLACE] [TEMPORARY] TABLE [IF NOT EXISTS]', +]); +const tabularOnelineClauses = expandPhrases([ + 'ALL EXCEPT', + 'ON CLUSTER', + // https://clickhouse.com/docs/sql-reference/statements/update + 'UPDATE', + // https://clickhouse.com/docs/sql-reference/statements/system + 'SYSTEM RELOAD {DICTIONARIES | DICTIONARY | FUNCTIONS | FUNCTION | ASYNCHRONOUS METRICS}', + 'SYSTEM DROP {DNS CACHE | MARK CACHE | ICEBERG METADATA CACHE | TEXT INDEX DICTIONARY CACHE | TEXT INDEX HEADER CACHE | TEXT INDEX POSTINGS CACHE | REPLICA | DATABASE REPLICA | UNCOMPRESSED CACHE | COMPILED EXPRESSION CACHE | QUERY CONDITION CACHE | QUERY CACHE | FORMAT SCHEMA CACHE | FILESYSTEM CACHE}', + 'SYSTEM FLUSH LOGS', + 'SYSTEM RELOAD {CONFIG | USERS}', + 'SYSTEM SHUTDOWN', + 'SYSTEM KILL', + 'SYSTEM FLUSH DISTRIBUTED', + 'SYSTEM START DISTRIBUTED SENDS', + 'SYSTEM {STOP | START} {LISTEN | MERGES | TTL MERGES | MOVES | FETCHES | REPLICATED SENDS | REPLICATION QUEUES | PULLING REPLICATION LOG}', + 'SYSTEM {SYNC | RESTART | RESTORE} REPLICA', + 'SYSTEM {SYNC | RESTORE} DATABASE REPLICA', + 'SYSTEM RESTART REPLICAS', + 'SYSTEM UNFREEZE', + 'SYSTEM WAIT LOADING PARTS', + 'SYSTEM {LOAD | UNLOAD} PRIMARY KEY', + 'SYSTEM {STOP | START} [REPLICATED] VIEW', + 'SYSTEM {STOP | START} VIEWS', + 'SYSTEM {REFRESH | CANCEL | WAIT} VIEW', + 'WITH NAME', + // https://clickhouse.com/docs/sql-reference/statements/show + 'SHOW [CREATE] {TABLE | TEMPORARY TABLE | DICTIONARY | VIEW | DATABASE}', + 'SHOW DATABASES [[NOT] {LIKE | ILIKE}]', + 'SHOW [FULL] [TEMPORARY] TABLES [FROM | IN]', + 'SHOW [EXTENDED] [FULL] COLUMNS {FROM | IN}', + // https://clickhouse.com/docs/sql-reference/statements/attach + 'ATTACH {TABLE | DICTIONARY | DATABASE} [IF NOT EXISTS]', + // https://clickhouse.com/docs/sql-reference/statements/detach + 'DETACH {TABLE | DICTIONARY | DATABASE} [IF EXISTS]', + 'PERMANENTLY', + 'SYNC', + // https://clickhouse.com/docs/sql-reference/statements/drop + 'DROP {DICTIONARY | DATABASE | PROFILE | VIEW | FUNCTION | NAMED COLLECTION} [IF EXISTS]', + 'DROP [TEMPORARY] TABLE [IF EXISTS] [IF EMPTY]', + // https://clickhouse.com/docs/sql-reference/statements/alter/table#rename + 'RENAME TO', + // https://clickhouse.com/docs/sql-reference/statements/exists + 'EXISTS [TEMPORARY] {TABLE | DICTIONARY | DATABASE}', + // https://clickhouse.com/docs/sql-reference/statements/kill + 'KILL QUERY', + // https://clickhouse.com/docs/sql-reference/statements/optimize + 'OPTIMIZE TABLE', + // https://clickhouse.com/docs/sql-reference/statements/rename + 'RENAME {TABLE | DICTIONARY | DATABASE}', + // https://clickhouse.com/docs/sql-reference/statements/exchange + 'EXCHANGE {TABLES | DICTIONARIES}', + // https://clickhouse.com/docs/sql-reference/statements/truncate + 'TRUNCATE TABLE [IF EXISTS]', + // https://clickhouse.com/docs/sql-reference/statements/execute_as + 'EXECUTE AS', + // https://clickhouse.com/docs/sql-reference/statements/use + 'USE', + 'TO', + // https://clickhouse.com/docs/sql-reference/statements/undrop + 'UNDROP TABLE', + // https://clickhouse.com/docs/sql-reference/statements/create + 'CREATE {DATABASE | NAMED COLLECTION} [IF NOT EXISTS]', + 'CREATE [OR REPLACE] {VIEW | DICTIONARY} [IF NOT EXISTS]', + 'CREATE MATERIALIZED VIEW [IF NOT EXISTS]', + 'CREATE FUNCTION', + 'CREATE {USER | ROLE | QUOTA | SETTINGS PROFILE} [IF NOT EXISTS | OR REPLACE]', + 'CREATE [ROW] POLICY [IF NOT EXISTS | OR REPLACE]', + // https://clickhouse.com/docs/sql-reference/statements/create/table#replace-table + 'REPLACE [TEMPORARY] TABLE [IF NOT EXISTS]', + // https://clickhouse.com/docs/sql-reference/statements/alter + 'ALTER {ROLE | QUOTA | SETTINGS PROFILE} [IF EXISTS]', + 'ALTER [TEMPORARY] TABLE', + 'ALTER NAMED COLLECTION [IF EXISTS]', + // https://clickhouse.com/docs/sql-reference/statements/alter/user + 'GRANTEES', + 'NOT IDENTIFIED', + 'RESET AUTHENTICATION METHODS TO NEW', + '{IDENTIFIED | ADD IDENTIFIED} [WITH | BY]', + '[ADD | DROP] HOST {LOCAL | NAME | REGEXP | IP | LIKE}', + 'VALID UNTIL', + 'DROP [ALL] {PROFILES | SETTINGS}', + '{ADD | MODIFY} SETTINGS', + 'ADD PROFILES', + // https://clickhouse.com/docs/sql-reference/statements/alter/apply-deleted-mask + 'APPLY DELETED MASK', + 'IN PARTITION', + // https://clickhouse.com/docs/sql-reference/statements/alter/column + '{ADD | DROP | RENAME | CLEAR | COMMENT | MODIFY | ALTER | MATERIALIZE} COLUMN', + // https://clickhouse.com/docs/sql-reference/statements/alter/partition + '{DETACH | DROP | ATTACH | FETCH | MOVE} {PART | PARTITION}', + 'DROP DETACHED {PART | PARTITION}', + '{FORGET | REPLACE} PARTITION', + 'CLEAR COLUMN', + '{FREEZE | UNFREEZE} [PARTITION]', + 'CLEAR INDEX', + 'TO {DISK | VOLUME}', + '[DELETE | REWRITE PARTS] IN PARTITION', + // https://clickhouse.com/docs/sql-reference/statements/alter/setting + '{MODIFY | RESET} SETTING', + // https://clickhouse.com/docs/sql-reference/statements/alter/delete + 'DELETE WHERE', + // https://clickhouse.com/docs/sql-reference/statements/alter/order-by + 'MODIFY ORDER BY', + // https://clickhouse.com/docs/sql-reference/statements/alter/sample-by + '{MODIFY | REMOVE} SAMPLE BY', + // https://clickhouse.com/docs/sql-reference/statements/alter/skipping-index + '{ADD | MATERIALIZE | CLEAR} INDEX [IF NOT EXISTS]', + 'DROP INDEX [IF EXISTS]', + 'GRANULARITY', + 'AFTER', + 'FIRST', + + // https://clickhouse.com/docs/sql-reference/statements/alter/constraint + 'ADD CONSTRAINT [IF NOT EXISTS]', + 'DROP CONSTRAINT [IF EXISTS]', + // https://clickhouse.com/docs/sql-reference/statements/alter/ttl + 'MODIFY TTL', + 'REMOVE TTL', + // https://clickhouse.com/docs/sql-reference/statements/alter/statistics + 'ADD STATISTICS [IF NOT EXISTS]', + '{DROP | CLEAR} STATISTICS [IF EXISTS]', + 'MATERIALIZE STATISTICS [ALL | IF EXISTS]', + // https://clickhouse.com/docs/sql-reference/statements/alter/quota + 'KEYED BY', + 'NOT KEYED', + 'FOR [RANDOMIZED] INTERVAL', + // https://clickhouse.com/docs/sql-reference/statements/alter/row-policy + 'AS {PERMISSIVE | RESTRICTIVE}', + 'FOR SELECT', + // https://clickhouse.com/docs/sql-reference/statements/alter/projection + 'ADD PROJECTION [IF NOT EXISTS]', + '{DROP | MATERIALIZE | CLEAR} PROJECTION [IF EXISTS]', + // https://clickhouse.com/docs/sql-reference/statements/create/view#refreshable-materialized-view + 'REFRESH {EVERY | AFTER}', + 'RANDOMIZE FOR', + 'APPEND', + 'APPEND TO', + // https://clickhouse.com/docs/sql-reference/statements/delete + 'DELETE FROM', + // https://clickhouse.com/docs/sql-reference/statements/explain + 'EXPLAIN [AST | SYNTAX | QUERY TREE | PLAN | PIPELINE | ESTIMATE | TABLE OVERRIDE]', + // https://clickhouse.com/docs/sql-reference/statements/grant + 'GRANT ON CLUSTER', + 'GRANT CURRENT GRANTS', + 'WITH GRANT OPTION', + // https://clickhouse.com/docs/sql-reference/statements/revoke + 'REVOKE ON CLUSTER', + 'ADMIN OPTION FOR', + // https://clickhouse.com/docs/sql-reference/statements/check-table + 'CHECK TABLE', + 'PARTITION ID', + // https://clickhouse.com/docs/sql-reference/statements/describe-table + '{DESC | DESCRIBE} TABLE', +]); + +const reservedSetOperations = expandPhrases([ + // https://clickhouse.com/docs/sql-reference/statements/select/union + 'UNION [ALL | DISTINCT]', + // https://clickhouse.com/docs/sql-reference/statements/parallel_with + 'PARALLEL WITH', +]); + +const reservedJoins = expandPhrases([ + // https://clickhouse.com/docs/sql-reference/statements/select/join + '[GLOBAL] [INNER|LEFT|RIGHT|FULL|CROSS] [OUTER|SEMI|ANTI|ANY|ALL|ASOF] JOIN', +]); + +const reservedKeywordPhrases = expandPhrases([ + '{ROWS | RANGE} BETWEEN', + 'ALTER MATERIALIZE STATISTICS', +]); + +// https://clickhouse.com/docs/sql-reference/syntax +export const clickhouse: DialectOptions = { + name: 'clickhouse', + tokenizerOptions: { + reservedSelect, + reservedClauses: [...reservedClauses, ...standardOnelineClauses, ...tabularOnelineClauses], + reservedSetOperations, + reservedJoins, + reservedKeywordPhrases, + + reservedKeywords: keywords, + reservedDataTypes: dataTypes, + reservedFunctionNames: functions, + extraParens: ['[]'], + lineCommentTypes: ['#', '--'], + nestedBlockComments: false, + underscoresInNumbers: true, + stringTypes: ['$$', "''-qq-bs"], + identTypes: ['""-qq-bs', '``'], + paramTypes: { + // https://clickhouse.com/docs/sql-reference/syntax#defining-and-using-query-parameters + custom: [ + { + regex: String.raw`\{\s*[^:]+:[^}]+\}`, + key: v => { + const match = /\{([^:]+):/.exec(v); + return match ? match[1].trim() : v; + }, + }, + ], + }, + operators: [ + // Arithmetic + '%', // modulo + + // Ternary + '?', + ':', + + // Lambda creation + '->', + ], + postProcess, + }, + formatOptions: { + onelineClauses: [...standardOnelineClauses, ...tabularOnelineClauses], + tabularOnelineClauses, + }, +}; + +/** + * Converts IN and ANY from RESERVED_FUNCTION_NAME to RESERVED_KEYWORD + * when they are used as operators (not function calls). + * + * IN operator: foo IN (1, 2, 3) - IN comes after an identifier/expression + * IN function: IN(foo, 1, 2, 3) - IN comes at start or after operators/keywords + * + * ANY operator: foo = ANY (1, 2, 3) - ANY comes after an operator like = + * ANY function: ANY(foo, 1, 2, 3) - ANY comes at start or after operators/keywords + */ +function postProcess(tokens: Token[]): Token[] { + return tokens.map((token, i) => { + const nextToken = tokens[i + 1] || EOF_TOKEN; + const prevToken = tokens[i - 1] || EOF_TOKEN; + + // Only process IN and ANY that are currently RESERVED_FUNCTION_NAME + // Check text (uppercase canonical form) for matching, but preserve raw (original casing) + if ( + token.type === TokenType.RESERVED_FUNCTION_NAME && + (token.text === 'IN' || token.text === 'ANY') + ) { + // Must be followed by ( to be a function + if (nextToken.text !== '(') { + // Not followed by ( means it's an operator/keyword, convert to uppercase + return { ...token, type: TokenType.RESERVED_KEYWORD, raw: token.text }; + } + + // For IN: convert to keyword if previous token is an expression token + // For ANY: convert to keyword if previous token is an operator + if ( + (token.text === 'IN' && + (prevToken.type === TokenType.IDENTIFIER || + prevToken.type === TokenType.QUOTED_IDENTIFIER || + prevToken.type === TokenType.NUMBER || + prevToken.type === TokenType.STRING || + prevToken.type === TokenType.CLOSE_PAREN || + prevToken.type === TokenType.ASTERISK)) || + (token.text === 'ANY' && prevToken.type === TokenType.OPERATOR) + ) { + // Convert to keyword (operator) - use uppercase for display + return { ...token, type: TokenType.RESERVED_KEYWORD, raw: token.text }; + } + // Otherwise, keep as RESERVED_FUNCTION_NAME to preserve original casing via functionCase option + } + + // If we have queries like + // > GRANT SELECT, INSERT ON db.table TO john + // > GRANT SELECT(a, b), SELECT(c) ON db.table TO john + // we want to format them as + // > GRANT + // > SELECT, + // > INSERT ON db.table + // > TO john + // > GRANT + // > SELECT(a, b), + // > SELECT(c) ON db.table + // > TO john + // To do this we need to convert the SELECT keyword to a RESERVED_KEYWORD. + if ( + token.type === TokenType.RESERVED_SELECT && + (nextToken.type === TokenType.COMMA || + prevToken.type === TokenType.RESERVED_CLAUSE || + prevToken.type === TokenType.COMMA) + ) { + return { ...token, type: TokenType.RESERVED_KEYWORD }; + } + + // We should format `set(100)` as-is rather than `SET (100)` + if ( + token.type === TokenType.RESERVED_CLAUSE && + token.text === 'SET' && + nextToken.type === TokenType.OPEN_PAREN + ) { + return { ...token, type: TokenType.RESERVED_FUNCTION_NAME, text: token.raw }; + } + + return token; + }); +} diff --git a/src/languages/clickhouse/clickhouse.functions.ts b/src/languages/clickhouse/clickhouse.functions.ts new file mode 100644 index 000000000..281f0d45a --- /dev/null +++ b/src/languages/clickhouse/clickhouse.functions.ts @@ -0,0 +1,1751 @@ +export const functions: string[] = [ + // Derived from `select name from system.functions order by name;` on Clickhouse Cloud + // as of November 14, 2025. + 'BIT_AND', + 'BIT_OR', + 'BIT_XOR', + 'BLAKE3', + 'CAST', + 'CHARACTER_LENGTH', + 'CHAR_LENGTH', + 'COVAR_POP', + 'COVAR_SAMP', + 'CRC32', + 'CRC32IEEE', + 'CRC64', + 'DATABASE', + 'DATE', + 'DATE_DIFF', + 'DATE_FORMAT', + 'DATE_TRUNC', + 'DAY', + 'DAYOFMONTH', + 'DAYOFWEEK', + 'DAYOFYEAR', + 'FORMAT_BYTES', + 'FQDN', + 'FROM_BASE64', + 'FROM_DAYS', + 'FROM_UNIXTIME', + 'HOUR', + 'INET6_ATON', + 'INET6_NTOA', + 'INET_ATON', + 'INET_NTOA', + 'IPv4CIDRToRange', + 'IPv4NumToString', + 'IPv4NumToStringClassC', + 'IPv4StringToNum', + 'IPv4StringToNumOrDefault', + 'IPv4StringToNumOrNull', + 'IPv4ToIPv6', + 'IPv6CIDRToRange', + 'IPv6NumToString', + 'IPv6StringToNum', + 'IPv6StringToNumOrDefault', + 'IPv6StringToNumOrNull', + 'JSONAllPaths', + 'JSONAllPathsWithTypes', + 'JSONArrayLength', + 'JSONDynamicPaths', + 'JSONDynamicPathsWithTypes', + 'JSONExtract', + 'JSONExtractArrayRaw', + 'JSONExtractArrayRawCaseInsensitive', + 'JSONExtractBool', + 'JSONExtractBoolCaseInsensitive', + 'JSONExtractCaseInsensitive', + 'JSONExtractFloat', + 'JSONExtractFloatCaseInsensitive', + 'JSONExtractInt', + 'JSONExtractIntCaseInsensitive', + 'JSONExtractKeys', + 'JSONExtractKeysAndValues', + 'JSONExtractKeysAndValuesCaseInsensitive', + 'JSONExtractKeysAndValuesRaw', + 'JSONExtractKeysAndValuesRawCaseInsensitive', + 'JSONExtractKeysCaseInsensitive', + 'JSONExtractRaw', + 'JSONExtractRawCaseInsensitive', + 'JSONExtractString', + 'JSONExtractStringCaseInsensitive', + 'JSONExtractUInt', + 'JSONExtractUIntCaseInsensitive', + 'JSONHas', + 'JSONKey', + 'JSONLength', + 'JSONMergePatch', + 'JSONSharedDataPaths', + 'JSONSharedDataPathsWithTypes', + 'JSONType', + 'JSON_ARRAY_LENGTH', + 'JSON_EXISTS', + 'JSON_QUERY', + 'JSON_VALUE', + 'L1Distance', + 'L1Norm', + 'L1Normalize', + 'L2Distance', + 'L2Norm', + 'L2Normalize', + 'L2SquaredDistance', + 'L2SquaredNorm', + 'LAST_DAY', + 'LinfDistance', + 'LinfNorm', + 'LinfNormalize', + 'LpDistance', + 'LpNorm', + 'LpNormalize', + 'MACNumToString', + 'MACStringToNum', + 'MACStringToOUI', + 'MAP_FROM_ARRAYS', + 'MD4', + 'MD5', + 'MILLISECOND', + 'MINUTE', + 'MONTH', + 'OCTET_LENGTH', + 'QUARTER', + 'REGEXP_EXTRACT', + 'REGEXP_MATCHES', + 'REGEXP_REPLACE', + 'RIPEMD160', + 'SCHEMA', + 'SECOND', + 'SHA1', + 'SHA224', + 'SHA256', + 'SHA384', + 'SHA512', + 'SHA512_256', + 'STD', + 'STDDEV_POP', + 'STDDEV_SAMP', + 'ST_LineFromWKB', + 'ST_MLineFromWKB', + 'ST_MPolyFromWKB', + 'ST_PointFromWKB', + 'ST_PolyFromWKB', + 'SUBSTRING_INDEX', + 'SVG', + 'TIMESTAMP_DIFF', + 'TO_BASE64', + 'TO_DAYS', + 'TO_UNIXTIME', + 'ULIDStringToDateTime', + 'URLHash', + 'URLHierarchy', + 'URLPathHierarchy', + 'UTCTimestamp', + 'UTC_timestamp', + 'UUIDNumToString', + 'UUIDStringToNum', + 'UUIDToNum', + 'UUIDv7ToDateTime', + 'VAR_POP', + 'VAR_SAMP', + 'YEAR', + 'YYYYMMDDToDate', + 'YYYYMMDDToDate32', + 'YYYYMMDDhhmmssToDateTime', + 'YYYYMMDDhhmmssToDateTime64', + '_CAST', + '__actionName', + '__bitBoolMaskAnd', + '__bitBoolMaskOr', + '__bitSwapLastTwo', + '__bitWrapperFunc', + '__getScalar', + '__patchPartitionID', + '__scalarSubqueryResult', + 'abs', + 'accurateCast', + 'accurateCastOrDefault', + 'accurateCastOrNull', + 'acos', + 'acosh', + 'addDate', + 'addDays', + 'addHours', + 'addInterval', + 'addMicroseconds', + 'addMilliseconds', + 'addMinutes', + 'addMonths', + 'addNanoseconds', + 'addQuarters', + 'addSeconds', + 'addTupleOfIntervals', + 'addWeeks', + 'addYears', + 'addressToLine', + 'addressToLineWithInlines', + 'addressToSymbol', + 'aes_decrypt_mysql', + 'aes_encrypt_mysql', + 'age', + 'aggThrow', + 'alphaTokens', + 'analysisOfVariance', + 'and', + 'anova', + 'any', + 'anyHeavy', + 'anyLast', + 'anyLastRespectNulls', + 'anyLast_respect_nulls', + 'anyRespectNulls', + 'anyValueRespectNulls', + 'any_respect_nulls', + 'any_value', + 'any_value_respect_nulls', + 'appendTrailingCharIfAbsent', + 'approx_top_count', + 'approx_top_k', + 'approx_top_sum', + 'argMax', + 'argMin', + 'array', + 'arrayAUC', + 'arrayAUCPR', + 'arrayAll', + 'arrayAvg', + 'arrayCompact', + 'arrayConcat', + 'arrayCount', + 'arrayCumSum', + 'arrayCumSumNonNegative', + 'arrayDifference', + 'arrayDistinct', + 'arrayDotProduct', + 'arrayElement', + 'arrayElementOrNull', + 'arrayEnumerate', + 'arrayEnumerateDense', + 'arrayEnumerateDenseRanked', + 'arrayEnumerateUniq', + 'arrayEnumerateUniqRanked', + 'arrayExists', + 'arrayFill', + 'arrayFilter', + 'arrayFirst', + 'arrayFirstIndex', + 'arrayFirstOrNull', + 'arrayFlatten', + 'arrayFold', + 'arrayIntersect', + 'arrayJaccardIndex', + 'arrayJoin', + 'arrayLast', + 'arrayLastIndex', + 'arrayLastOrNull', + 'arrayLevenshteinDistance', + 'arrayLevenshteinDistanceWeighted', + 'arrayMap', + 'arrayMax', + 'arrayMin', + 'arrayNormalizedGini', + 'arrayPRAUC', + 'arrayPartialReverseSort', + 'arrayPartialShuffle', + 'arrayPartialSort', + 'arrayPopBack', + 'arrayPopFront', + 'arrayProduct', + 'arrayPushBack', + 'arrayPushFront', + 'arrayROCAUC', + 'arrayRandomSample', + 'arrayReduce', + 'arrayReduceInRanges', + 'arrayResize', + 'arrayReverse', + 'arrayReverseFill', + 'arrayReverseSort', + 'arrayReverseSplit', + 'arrayRotateLeft', + 'arrayRotateRight', + 'arrayShiftLeft', + 'arrayShiftRight', + 'arrayShingles', + 'arrayShuffle', + 'arraySimilarity', + 'arraySlice', + 'arraySort', + 'arraySplit', + 'arrayStringConcat', + 'arraySum', + 'arraySymmetricDifference', + 'arrayUnion', + 'arrayUniq', + 'arrayWithConstant', + 'arrayZip', + 'arrayZipUnaligned', + 'array_agg', + 'array_concat_agg', + 'ascii', + 'asin', + 'asinh', + 'assumeNotNull', + 'atan', + 'atan2', + 'atanh', + 'authenticatedUser', + 'avg', + 'avgWeighted', + 'bar', + 'base32Decode', + 'base32Encode', + 'base58Decode', + 'base58Encode', + 'base64Decode', + 'base64Encode', + 'base64URLDecode', + 'base64URLEncode', + 'basename', + 'bech32Decode', + 'bech32Encode', + 'bin', + 'bitAnd', + 'bitCount', + 'bitHammingDistance', + 'bitNot', + 'bitOr', + 'bitPositionsToArray', + 'bitRotateLeft', + 'bitRotateRight', + 'bitShiftLeft', + 'bitShiftRight', + 'bitSlice', + 'bitTest', + 'bitTestAll', + 'bitTestAny', + 'bitXor', + 'bitmapAnd', + 'bitmapAndCardinality', + 'bitmapAndnot', + 'bitmapAndnotCardinality', + 'bitmapBuild', + 'bitmapCardinality', + 'bitmapContains', + 'bitmapHasAll', + 'bitmapHasAny', + 'bitmapMax', + 'bitmapMin', + 'bitmapOr', + 'bitmapOrCardinality', + 'bitmapSubsetInRange', + 'bitmapSubsetLimit', + 'bitmapToArray', + 'bitmapTransform', + 'bitmapXor', + 'bitmapXorCardinality', + 'bitmaskToArray', + 'bitmaskToList', + 'blockNumber', + 'blockSerializedSize', + 'blockSize', + 'boundingRatio', + 'buildId', + 'byteHammingDistance', + 'byteSize', + 'byteSlice', + 'byteSwap', + 'caseWithExpr', + 'caseWithExpression', + 'caseWithoutExpr', + 'caseWithoutExpression', + 'catboostEvaluate', + 'categoricalInformationValue', + 'cbrt', + 'ceil', + 'ceiling', + 'changeDay', + 'changeHour', + 'changeMinute', + 'changeMonth', + 'changeSecond', + 'changeYear', + 'char', + 'cityHash64', + 'clamp', + 'coalesce', + 'colorOKLCHToSRGB', + 'colorSRGBToOKLCH', + 'compareSubstrings', + 'concat', + 'concatAssumeInjective', + 'concatWithSeparator', + 'concatWithSeparatorAssumeInjective', + 'concat_ws', + 'connectionId', + 'connection_id', + 'contingency', + 'convertCharset', + 'corr', + 'corrMatrix', + 'corrStable', + 'cos', + 'cosh', + 'cosineDistance', + 'count', + 'countDigits', + 'countEqual', + 'countMatches', + 'countMatchesCaseInsensitive', + 'countSubstrings', + 'countSubstringsCaseInsensitive', + 'countSubstringsCaseInsensitiveUTF8', + 'covarPop', + 'covarPopMatrix', + 'covarPopStable', + 'covarSamp', + 'covarSampMatrix', + 'covarSampStable', + 'cramersV', + 'cramersVBiasCorrected', + 'curdate', + 'currentDatabase', + 'currentProfiles', + 'currentQueryID', + 'currentRoles', + 'currentSchemas', + 'currentUser', + 'current_database', + 'current_date', + 'current_query_id', + 'current_schemas', + 'current_timestamp', + 'current_user', + 'cutFragment', + 'cutIPv6', + 'cutQueryString', + 'cutQueryStringAndFragment', + 'cutToFirstSignificantSubdomain', + 'cutToFirstSignificantSubdomainCustom', + 'cutToFirstSignificantSubdomainCustomRFC', + 'cutToFirstSignificantSubdomainCustomWithWWW', + 'cutToFirstSignificantSubdomainCustomWithWWWRFC', + 'cutToFirstSignificantSubdomainRFC', + 'cutToFirstSignificantSubdomainWithWWW', + 'cutToFirstSignificantSubdomainWithWWWRFC', + 'cutURLParameter', + 'cutWWW', + 'damerauLevenshteinDistance', + 'dateDiff', + 'dateName', + 'dateTime64ToSnowflake', + 'dateTime64ToSnowflakeID', + 'dateTimeToSnowflake', + 'dateTimeToSnowflakeID', + 'dateTimeToUUIDv7', + 'dateTrunc', + 'date_bin', + 'date_diff', + 'decodeHTMLComponent', + 'decodeURLComponent', + 'decodeURLFormComponent', + 'decodeXMLComponent', + 'decrypt', + 'defaultProfiles', + 'defaultRoles', + 'defaultValueOfArgumentType', + 'defaultValueOfTypeName', + 'degrees', + 'deltaSum', + 'deltaSumTimestamp', + 'demangle', + 'denseRank', + 'dense_rank', + 'detectCharset', + 'detectLanguage', + 'detectLanguageMixed', + 'detectLanguageUnknown', + 'detectProgrammingLanguage', + 'detectTonality', + 'dictGet', + 'dictGetAll', + 'dictGetChildren', + 'dictGetDate', + 'dictGetDateOrDefault', + 'dictGetDateTime', + 'dictGetDateTimeOrDefault', + 'dictGetDescendants', + 'dictGetFloat32', + 'dictGetFloat32OrDefault', + 'dictGetFloat64', + 'dictGetFloat64OrDefault', + 'dictGetHierarchy', + 'dictGetIPv4', + 'dictGetIPv4OrDefault', + 'dictGetIPv6', + 'dictGetIPv6OrDefault', + 'dictGetInt16', + 'dictGetInt16OrDefault', + 'dictGetInt32', + 'dictGetInt32OrDefault', + 'dictGetInt64', + 'dictGetInt64OrDefault', + 'dictGetInt8', + 'dictGetInt8OrDefault', + 'dictGetOrDefault', + 'dictGetOrNull', + 'dictGetString', + 'dictGetStringOrDefault', + 'dictGetUInt16', + 'dictGetUInt16OrDefault', + 'dictGetUInt32', + 'dictGetUInt32OrDefault', + 'dictGetUInt64', + 'dictGetUInt64OrDefault', + 'dictGetUInt8', + 'dictGetUInt8OrDefault', + 'dictGetUUID', + 'dictGetUUIDOrDefault', + 'dictHas', + 'dictIsIn', + 'displayName', + 'distanceL1', + 'distanceL2', + 'distanceL2Squared', + 'distanceLinf', + 'distanceLp', + 'distinctDynamicTypes', + 'distinctJSONPaths', + 'distinctJSONPathsAndTypes', + 'divide', + 'divideDecimal', + 'divideOrNull', + 'domain', + 'domainRFC', + 'domainWithoutWWW', + 'domainWithoutWWWRFC', + 'dotProduct', + 'dumpColumnStructure', + 'dynamicElement', + 'dynamicType', + 'e', + 'editDistance', + 'editDistanceUTF8', + 'empty', + 'emptyArrayDate', + 'emptyArrayDateTime', + 'emptyArrayFloat32', + 'emptyArrayFloat64', + 'emptyArrayInt16', + 'emptyArrayInt32', + 'emptyArrayInt64', + 'emptyArrayInt8', + 'emptyArrayString', + 'emptyArrayToSingle', + 'emptyArrayUInt16', + 'emptyArrayUInt32', + 'emptyArrayUInt64', + 'emptyArrayUInt8', + 'enabledProfiles', + 'enabledRoles', + 'encodeURLComponent', + 'encodeURLFormComponent', + 'encodeXMLComponent', + 'encrypt', + 'endsWith', + 'endsWithUTF8', + 'entropy', + 'equals', + 'erf', + 'erfc', + 'errorCodeToName', + 'estimateCompressionRatio', + 'evalMLMethod', + 'exp', + 'exp10', + 'exp2', + 'exponentialMovingAverage', + 'exponentialTimeDecayedAvg', + 'exponentialTimeDecayedCount', + 'exponentialTimeDecayedMax', + 'exponentialTimeDecayedSum', + 'extract', + 'extractAll', + 'extractAllGroups', + 'extractAllGroupsHorizontal', + 'extractAllGroupsVertical', + 'extractGroups', + 'extractKeyValuePairs', + 'extractKeyValuePairsWithEscaping', + 'extractTextFromHTML', + 'extractURLParameter', + 'extractURLParameterNames', + 'extractURLParameters', + 'factorial', + 'farmFingerprint64', + 'farmHash64', + 'file', + 'filesystemAvailable', + 'filesystemCapacity', + 'filesystemUnreserved', + 'finalizeAggregation', + 'financialInternalRateOfReturn', + 'financialInternalRateOfReturnExtended', + 'financialNetPresentValue', + 'financialNetPresentValueExtended', + 'firstLine', + 'firstSignificantSubdomain', + 'firstSignificantSubdomainCustom', + 'firstSignificantSubdomainCustomRFC', + 'firstSignificantSubdomainRFC', + 'firstValueRespectNulls', + 'first_value', + 'first_value_respect_nulls', + 'flameGraph', + 'flatten', + 'flattenTuple', + 'floor', + 'format', + 'formatDateTime', + 'formatDateTimeInJodaSyntax', + 'formatQuery', + 'formatQueryOrNull', + 'formatQuerySingleLine', + 'formatQuerySingleLineOrNull', + 'formatReadableDecimalSize', + 'formatReadableQuantity', + 'formatReadableSize', + 'formatReadableTimeDelta', + 'formatRow', + 'formatRowNoNewline', + 'fragment', + 'fromDaysSinceYearZero', + 'fromDaysSinceYearZero32', + 'fromModifiedJulianDay', + 'fromModifiedJulianDayOrNull', + 'fromUTCTimestamp', + 'fromUnixTimestamp', + 'fromUnixTimestamp64Micro', + 'fromUnixTimestamp64Milli', + 'fromUnixTimestamp64Nano', + 'fromUnixTimestamp64Second', + 'fromUnixTimestampInJodaSyntax', + 'from_utc_timestamp', + 'fullHostName', + 'fuzzBits', + 'gccMurmurHash', + 'gcd', + 'generateRandomStructure', + 'generateSerialID', + 'generateSnowflakeID', + 'generateULID', + 'generateUUIDv4', + 'generateUUIDv7', + 'geoDistance', + 'geoToH3', + 'geoToS2', + 'geohashDecode', + 'geohashEncode', + 'geohashesInBox', + 'getClientHTTPHeader', + 'getMacro', + 'getMaxTableNameLengthForDatabase', + 'getMergeTreeSetting', + 'getOSKernelVersion', + 'getServerPort', + 'getServerSetting', + 'getSetting', + 'getSettingOrDefault', + 'getSizeOfEnumType', + 'getSubcolumn', + 'getTypeSerializationStreams', + 'globalIn', + 'globalInIgnoreSet', + 'globalNotIn', + 'globalNotInIgnoreSet', + 'globalNotNullIn', + 'globalNotNullInIgnoreSet', + 'globalNullIn', + 'globalNullInIgnoreSet', + 'globalVariable', + 'greatCircleAngle', + 'greatCircleDistance', + 'greater', + 'greaterOrEquals', + 'greatest', + 'groupArray', + 'groupArrayInsertAt', + 'groupArrayIntersect', + 'groupArrayLast', + 'groupArrayMovingAvg', + 'groupArrayMovingSum', + 'groupArraySample', + 'groupArraySorted', + 'groupBitAnd', + 'groupBitOr', + 'groupBitXor', + 'groupBitmap', + 'groupBitmapAnd', + 'groupBitmapOr', + 'groupBitmapXor', + 'groupConcat', + 'groupNumericIndexedVector', + 'groupUniqArray', + 'group_concat', + 'h3CellAreaM2', + 'h3CellAreaRads2', + 'h3Distance', + 'h3EdgeAngle', + 'h3EdgeLengthKm', + 'h3EdgeLengthM', + 'h3ExactEdgeLengthKm', + 'h3ExactEdgeLengthM', + 'h3ExactEdgeLengthRads', + 'h3GetBaseCell', + 'h3GetDestinationIndexFromUnidirectionalEdge', + 'h3GetFaces', + 'h3GetIndexesFromUnidirectionalEdge', + 'h3GetOriginIndexFromUnidirectionalEdge', + 'h3GetPentagonIndexes', + 'h3GetRes0Indexes', + 'h3GetResolution', + 'h3GetUnidirectionalEdge', + 'h3GetUnidirectionalEdgeBoundary', + 'h3GetUnidirectionalEdgesFromHexagon', + 'h3HexAreaKm2', + 'h3HexAreaM2', + 'h3HexRing', + 'h3IndexesAreNeighbors', + 'h3IsPentagon', + 'h3IsResClassIII', + 'h3IsValid', + 'h3Line', + 'h3NumHexagons', + 'h3PointDistKm', + 'h3PointDistM', + 'h3PointDistRads', + 'h3ToCenterChild', + 'h3ToChildren', + 'h3ToGeo', + 'h3ToGeoBoundary', + 'h3ToParent', + 'h3ToString', + 'h3UnidirectionalEdgeIsValid', + 'h3kRing', + 'halfMD5', + 'has', + 'hasAll', + 'hasAny', + 'hasColumnInTable', + 'hasSubsequence', + 'hasSubsequenceCaseInsensitive', + 'hasSubsequenceCaseInsensitiveUTF8', + 'hasSubsequenceUTF8', + 'hasSubstr', + 'hasThreadFuzzer', + 'hasToken', + 'hasTokenCaseInsensitive', + 'hasTokenCaseInsensitiveOrNull', + 'hasTokenOrNull', + 'hex', + 'hilbertDecode', + 'hilbertEncode', + 'histogram', + 'hiveHash', + 'hop', + 'hopEnd', + 'hopStart', + 'hostName', + 'hostname', + 'hypot', + 'icebergBucket', + 'icebergHash', + 'icebergTruncate', + 'identity', + 'idnaDecode', + 'idnaEncode', + 'if', + 'ifNotFinite', + 'ifNull', + 'ignore', + 'ilike', + 'in', + 'inIgnoreSet', + 'indexHint', + 'indexOf', + 'indexOfAssumeSorted', + 'initcap', + 'initcapUTF8', + 'initialQueryID', + 'initialQueryStartTime', + 'initial_query_id', + 'initial_query_start_time', + 'initializeAggregation', + 'instr', + 'intDiv', + 'intDivOrNull', + 'intDivOrZero', + 'intExp10', + 'intExp2', + 'intHash32', + 'intHash64', + 'intervalLengthSum', + 'isConstant', + 'isDecimalOverflow', + 'isDynamicElementInSharedData', + 'isFinite', + 'isIPAddressInRange', + 'isIPv4String', + 'isIPv6String', + 'isInfinite', + 'isMergeTreePartCoveredBy', + 'isNaN', + 'isNotDistinctFrom', + 'isNotNull', + 'isNull', + 'isNullable', + 'isValidJSON', + 'isValidUTF8', + 'isZeroOrNull', + 'jaroSimilarity', + 'jaroWinklerSimilarity', + 'javaHash', + 'javaHashUTF16LE', + 'joinGet', + 'joinGetOrNull', + 'jsonMergePatch', + 'jumpConsistentHash', + 'kafkaMurmurHash', + 'keccak256', + 'kolmogorovSmirnovTest', + 'kostikConsistentHash', + 'kql_array_sort_asc', + 'kql_array_sort_desc', + 'kurtPop', + 'kurtSamp', + 'lag', + 'lagInFrame', + 'largestTriangleThreeBuckets', + 'lastValueRespectNulls', + 'last_value', + 'last_value_respect_nulls', + 'lcase', + 'lcm', + 'lead', + 'leadInFrame', + 'least', + 'left', + 'leftPad', + 'leftPadUTF8', + 'leftUTF8', + 'lemmatize', + 'length', + 'lengthUTF8', + 'less', + 'lessOrEquals', + 'levenshteinDistance', + 'levenshteinDistanceUTF8', + 'lgamma', + 'like', + 'ln', + 'locate', + 'log', + 'log10', + 'log1p', + 'log2', + 'logTrace', + 'lowCardinalityIndices', + 'lowCardinalityKeys', + 'lower', + 'lowerUTF8', + 'lpad', + 'ltrim', + 'lttb', + 'makeDate', + 'makeDate32', + 'makeDateTime', + 'makeDateTime64', + 'mannWhitneyUTest', + 'map', + 'mapAdd', + 'mapAll', + 'mapApply', + 'mapConcat', + 'mapContains', + 'mapContainsKey', + 'mapContainsKeyLike', + 'mapContainsValue', + 'mapContainsValueLike', + 'mapExists', + 'mapExtractKeyLike', + 'mapExtractValueLike', + 'mapFilter', + 'mapFromArrays', + 'mapFromString', + 'mapKeys', + 'mapPartialReverseSort', + 'mapPartialSort', + 'mapPopulateSeries', + 'mapReverseSort', + 'mapSort', + 'mapSubtract', + 'mapUpdate', + 'mapValues', + 'match', + 'materialize', + 'max', + 'max2', + 'maxIntersections', + 'maxIntersectionsPosition', + 'maxMappedArrays', + 'meanZTest', + 'median', + 'medianBFloat16', + 'medianBFloat16Weighted', + 'medianDD', + 'medianDeterministic', + 'medianExact', + 'medianExactHigh', + 'medianExactLow', + 'medianExactWeighted', + 'medianExactWeightedInterpolated', + 'medianGK', + 'medianInterpolatedWeighted', + 'medianTDigest', + 'medianTDigestWeighted', + 'medianTiming', + 'medianTimingWeighted', + 'mergeTreePartInfo', + 'metroHash64', + 'mid', + 'min', + 'min2', + 'minMappedArrays', + 'minSampleSizeContinous', + 'minSampleSizeContinuous', + 'minSampleSizeConversion', + 'minus', + 'mismatches', + 'mod', + 'modOrNull', + 'modulo', + 'moduloLegacy', + 'moduloOrNull', + 'moduloOrZero', + 'monthName', + 'mortonDecode', + 'mortonEncode', + 'multiFuzzyMatchAllIndices', + 'multiFuzzyMatchAny', + 'multiFuzzyMatchAnyIndex', + 'multiIf', + 'multiMatchAllIndices', + 'multiMatchAny', + 'multiMatchAnyIndex', + 'multiSearchAllPositions', + 'multiSearchAllPositionsCaseInsensitive', + 'multiSearchAllPositionsCaseInsensitiveUTF8', + 'multiSearchAllPositionsUTF8', + 'multiSearchAny', + 'multiSearchAnyCaseInsensitive', + 'multiSearchAnyCaseInsensitiveUTF8', + 'multiSearchAnyUTF8', + 'multiSearchFirstIndex', + 'multiSearchFirstIndexCaseInsensitive', + 'multiSearchFirstIndexCaseInsensitiveUTF8', + 'multiSearchFirstIndexUTF8', + 'multiSearchFirstPosition', + 'multiSearchFirstPositionCaseInsensitive', + 'multiSearchFirstPositionCaseInsensitiveUTF8', + 'multiSearchFirstPositionUTF8', + 'multiply', + 'multiplyDecimal', + 'murmurHash2_32', + 'murmurHash2_64', + 'murmurHash3_128', + 'murmurHash3_32', + 'murmurHash3_64', + 'negate', + 'neighbor', + 'nested', + 'netloc', + 'ngramDistance', + 'ngramDistanceCaseInsensitive', + 'ngramDistanceCaseInsensitiveUTF8', + 'ngramDistanceUTF8', + 'ngramMinHash', + 'ngramMinHashArg', + 'ngramMinHashArgCaseInsensitive', + 'ngramMinHashArgCaseInsensitiveUTF8', + 'ngramMinHashArgUTF8', + 'ngramMinHashCaseInsensitive', + 'ngramMinHashCaseInsensitiveUTF8', + 'ngramMinHashUTF8', + 'ngramSearch', + 'ngramSearchCaseInsensitive', + 'ngramSearchCaseInsensitiveUTF8', + 'ngramSearchUTF8', + 'ngramSimHash', + 'ngramSimHashCaseInsensitive', + 'ngramSimHashCaseInsensitiveUTF8', + 'ngramSimHashUTF8', + 'ngrams', + 'nonNegativeDerivative', + 'normL1', + 'normL2', + 'normL2Squared', + 'normLinf', + 'normLp', + 'normalizeL1', + 'normalizeL2', + 'normalizeLinf', + 'normalizeLp', + 'normalizeQuery', + 'normalizeQueryKeepNames', + 'normalizeUTF8NFC', + 'normalizeUTF8NFD', + 'normalizeUTF8NFKC', + 'normalizeUTF8NFKD', + 'normalizedQueryHash', + 'normalizedQueryHashKeepNames', + 'not', + 'notEmpty', + 'notEquals', + 'notILike', + 'notIn', + 'notInIgnoreSet', + 'notLike', + 'notNullIn', + 'notNullInIgnoreSet', + 'nothing', + 'nothingNull', + 'nothingUInt64', + 'now', + 'now64', + 'nowInBlock', + 'nowInBlock64', + 'nth_value', + 'ntile', + 'nullIf', + 'nullIn', + 'nullInIgnoreSet', + 'numbers', + 'numericIndexedVectorAllValueSum', + 'numericIndexedVectorBuild', + 'numericIndexedVectorCardinality', + 'numericIndexedVectorGetValue', + 'numericIndexedVectorPointwiseAdd', + 'numericIndexedVectorPointwiseDivide', + 'numericIndexedVectorPointwiseEqual', + 'numericIndexedVectorPointwiseGreater', + 'numericIndexedVectorPointwiseGreaterEqual', + 'numericIndexedVectorPointwiseLess', + 'numericIndexedVectorPointwiseLessEqual', + 'numericIndexedVectorPointwiseMultiply', + 'numericIndexedVectorPointwiseNotEqual', + 'numericIndexedVectorPointwiseSubtract', + 'numericIndexedVectorShortDebugString', + 'numericIndexedVectorToMap', + 'or', + 'overlay', + 'overlayUTF8', + 'parseDateTime', + 'parseDateTime32BestEffort', + 'parseDateTime32BestEffortOrNull', + 'parseDateTime32BestEffortOrZero', + 'parseDateTime64', + 'parseDateTime64BestEffort', + 'parseDateTime64BestEffortOrNull', + 'parseDateTime64BestEffortOrZero', + 'parseDateTime64BestEffortUS', + 'parseDateTime64BestEffortUSOrNull', + 'parseDateTime64BestEffortUSOrZero', + 'parseDateTime64InJodaSyntax', + 'parseDateTime64InJodaSyntaxOrNull', + 'parseDateTime64InJodaSyntaxOrZero', + 'parseDateTime64OrNull', + 'parseDateTime64OrZero', + 'parseDateTimeBestEffort', + 'parseDateTimeBestEffortOrNull', + 'parseDateTimeBestEffortOrZero', + 'parseDateTimeBestEffortUS', + 'parseDateTimeBestEffortUSOrNull', + 'parseDateTimeBestEffortUSOrZero', + 'parseDateTimeInJodaSyntax', + 'parseDateTimeInJodaSyntaxOrNull', + 'parseDateTimeInJodaSyntaxOrZero', + 'parseDateTimeOrNull', + 'parseDateTimeOrZero', + 'parseReadableSize', + 'parseReadableSizeOrNull', + 'parseReadableSizeOrZero', + 'parseTimeDelta', + 'partitionID', + 'partitionId', + 'path', + 'pathFull', + 'percentRank', + 'percent_rank', + 'pi', + 'plus', + 'pmod', + 'pmodOrNull', + 'pointInEllipses', + 'pointInPolygon', + 'polygonAreaCartesian', + 'polygonAreaSpherical', + 'polygonConvexHullCartesian', + 'polygonPerimeterCartesian', + 'polygonPerimeterSpherical', + 'polygonsDistanceCartesian', + 'polygonsDistanceSpherical', + 'polygonsEqualsCartesian', + 'polygonsIntersectCartesian', + 'polygonsIntersectSpherical', + 'polygonsIntersectionCartesian', + 'polygonsIntersectionSpherical', + 'polygonsSymDifferenceCartesian', + 'polygonsSymDifferenceSpherical', + 'polygonsUnionCartesian', + 'polygonsUnionSpherical', + 'polygonsWithinCartesian', + 'polygonsWithinSpherical', + 'port', + 'portRFC', + 'position', + 'positionCaseInsensitive', + 'positionCaseInsensitiveUTF8', + 'positionUTF8', + 'positiveModulo', + 'positiveModuloOrNull', + 'positive_modulo', + 'positive_modulo_or_null', + 'pow', + 'power', + 'printf', + 'proportionsZTest', + 'protocol', + 'punycodeDecode', + 'punycodeEncode', + 'quantile', + 'quantileBFloat16', + 'quantileBFloat16Weighted', + 'quantileDD', + 'quantileDeterministic', + 'quantileExact', + 'quantileExactExclusive', + 'quantileExactHigh', + 'quantileExactInclusive', + 'quantileExactLow', + 'quantileExactWeighted', + 'quantileExactWeightedInterpolated', + 'quantileGK', + 'quantileInterpolatedWeighted', + 'quantileTDigest', + 'quantileTDigestWeighted', + 'quantileTiming', + 'quantileTimingWeighted', + 'quantiles', + 'quantilesBFloat16', + 'quantilesBFloat16Weighted', + 'quantilesDD', + 'quantilesDeterministic', + 'quantilesExact', + 'quantilesExactExclusive', + 'quantilesExactHigh', + 'quantilesExactInclusive', + 'quantilesExactLow', + 'quantilesExactWeighted', + 'quantilesExactWeightedInterpolated', + 'quantilesGK', + 'quantilesInterpolatedWeighted', + 'quantilesTDigest', + 'quantilesTDigestWeighted', + 'quantilesTiming', + 'quantilesTimingWeighted', + 'queryID', + 'queryString', + 'queryStringAndFragment', + 'query_id', + 'radians', + 'rand', + 'rand32', + 'rand64', + 'randBernoulli', + 'randBinomial', + 'randCanonical', + 'randChiSquared', + 'randConstant', + 'randExponential', + 'randFisherF', + 'randLogNormal', + 'randNegativeBinomial', + 'randNormal', + 'randPoisson', + 'randStudentT', + 'randUniform', + 'randomFixedString', + 'randomPrintableASCII', + 'randomString', + 'randomStringUTF8', + 'range', + 'rank', + 'rankCorr', + 'readWKBLineString', + 'readWKBMultiLineString', + 'readWKBMultiPolygon', + 'readWKBPoint', + 'readWKBPolygon', + 'readWKTLineString', + 'readWKTMultiLineString', + 'readWKTMultiPolygon', + 'readWKTPoint', + 'readWKTPolygon', + 'readWKTRing', + 'regexpExtract', + 'regexpQuoteMeta', + 'regionHierarchy', + 'regionIn', + 'regionToArea', + 'regionToCity', + 'regionToContinent', + 'regionToCountry', + 'regionToDistrict', + 'regionToName', + 'regionToPopulation', + 'regionToTopContinent', + 'reinterpret', + 'reinterpretAsDate', + 'reinterpretAsDateTime', + 'reinterpretAsFixedString', + 'reinterpretAsFloat32', + 'reinterpretAsFloat64', + 'reinterpretAsInt128', + 'reinterpretAsInt16', + 'reinterpretAsInt256', + 'reinterpretAsInt32', + 'reinterpretAsInt64', + 'reinterpretAsInt8', + 'reinterpretAsString', + 'reinterpretAsUInt128', + 'reinterpretAsUInt16', + 'reinterpretAsUInt256', + 'reinterpretAsUInt32', + 'reinterpretAsUInt64', + 'reinterpretAsUInt8', + 'reinterpretAsUUID', + 'repeat', + 'replace', + 'replaceAll', + 'replaceOne', + 'replaceRegexpAll', + 'replaceRegexpOne', + 'replicate', + 'retention', + 'reverse', + 'reverseUTF8', + 'revision', + 'right', + 'rightPad', + 'rightPadUTF8', + 'rightUTF8', + 'round', + 'roundAge', + 'roundBankers', + 'roundDown', + 'roundDuration', + 'roundToExp2', + 'rowNumberInAllBlocks', + 'rowNumberInBlock', + 'row_number', + 'rpad', + 'rtrim', + 'runningAccumulate', + 'runningConcurrency', + 'runningDifference', + 'runningDifferenceStartingWithFirstValue', + 's2CapContains', + 's2CapUnion', + 's2CellsIntersect', + 's2GetNeighbors', + 's2RectAdd', + 's2RectContains', + 's2RectIntersection', + 's2RectUnion', + 's2ToGeo', + 'scalarProduct', + 'searchAll', + 'searchAny', + 'sequenceCount', + 'sequenceMatch', + 'sequenceMatchEvents', + 'sequenceNextNode', + 'seriesDecomposeSTL', + 'seriesOutliersDetectTukey', + 'seriesPeriodDetectFFT', + 'serverTimeZone', + 'serverTimezone', + 'serverUUID', + 'set', + 'shardCount', + 'shardNum', + 'showCertificate', + 'sigmoid', + 'sign', + 'simpleJSONExtractBool', + 'simpleJSONExtractFloat', + 'simpleJSONExtractInt', + 'simpleJSONExtractRaw', + 'simpleJSONExtractString', + 'simpleJSONExtractUInt', + 'simpleJSONHas', + 'simpleLinearRegression', + 'sin', + 'singleValueOrNull', + 'sinh', + 'sipHash128', + 'sipHash128Keyed', + 'sipHash128Reference', + 'sipHash128ReferenceKeyed', + 'sipHash64', + 'sipHash64Keyed', + 'skewPop', + 'skewSamp', + 'sleep', + 'sleepEachRow', + 'snowflakeIDToDateTime', + 'snowflakeIDToDateTime64', + 'snowflakeToDateTime', + 'snowflakeToDateTime64', + 'soundex', + 'space', + 'sparkBar', + 'sparkbar', + 'sparseGrams', + 'sparseGramsHashes', + 'sparseGramsHashesUTF8', + 'sparseGramsUTF8', + 'splitByAlpha', + 'splitByChar', + 'splitByNonAlpha', + 'splitByRegexp', + 'splitByString', + 'splitByWhitespace', + 'sqid', + 'sqidDecode', + 'sqidEncode', + 'sqrt', + 'startsWith', + 'startsWithUTF8', + 'stddevPop', + 'stddevPopStable', + 'stddevSamp', + 'stddevSampStable', + 'stem', + 'stochasticLinearRegression', + 'stochasticLogisticRegression', + 'str_to_date', + 'str_to_map', + 'stringBytesEntropy', + 'stringBytesUniq', + 'stringJaccardIndex', + 'stringJaccardIndexUTF8', + 'stringToH3', + 'structureToCapnProtoSchema', + 'structureToProtobufSchema', + 'studentTTest', + 'subBitmap', + 'subDate', + 'substr', + 'substring', + 'substringIndex', + 'substringIndexUTF8', + 'substringUTF8', + 'subtractDays', + 'subtractHours', + 'subtractInterval', + 'subtractMicroseconds', + 'subtractMilliseconds', + 'subtractMinutes', + 'subtractMonths', + 'subtractNanoseconds', + 'subtractQuarters', + 'subtractSeconds', + 'subtractTupleOfIntervals', + 'subtractWeeks', + 'subtractYears', + 'sum', + 'sumCount', + 'sumKahan', + 'sumMapFiltered', + 'sumMapFilteredWithOverflow', + 'sumMapWithOverflow', + 'sumMappedArrays', + 'sumWithOverflow', + 'svg', + 'synonyms', + 'tan', + 'tanh', + 'tcpPort', + 'tgamma', + 'theilsU', + 'throwIf', + 'tid', + 'timeDiff', + 'timeSeriesDeltaToGrid', + 'timeSeriesDerivToGrid', + 'timeSeriesFromGrid', + 'timeSeriesGroupArray', + 'timeSeriesIdToTags', + 'timeSeriesIdToTagsGroup', + 'timeSeriesIdeltaToGrid', + 'timeSeriesInstantDeltaToGrid', + 'timeSeriesInstantRateToGrid', + 'timeSeriesIrateToGrid', + 'timeSeriesLastToGrid', + 'timeSeriesLastTwoSamples', + 'timeSeriesPredictLinearToGrid', + 'timeSeriesRange', + 'timeSeriesRateToGrid', + 'timeSeriesResampleToGridWithStaleness', + 'timeSeriesStoreTags', + 'timeSeriesTagsGroupToTags', + 'timeSlot', + 'timeSlots', + 'timeZone', + 'timeZoneOf', + 'timeZoneOffset', + 'time_bucket', + 'timestamp', + 'timestampDiff', + 'timestamp_diff', + 'timezone', + 'timezoneOf', + 'timezoneOffset', + 'toBFloat16', + 'toBFloat16OrNull', + 'toBFloat16OrZero', + 'toBool', + 'toColumnTypeName', + 'toDate', + 'toDate32', + 'toDate32OrDefault', + 'toDate32OrNull', + 'toDate32OrZero', + 'toDateOrDefault', + 'toDateOrNull', + 'toDateOrZero', + 'toDateTime', + 'toDateTime32', + 'toDateTime64', + 'toDateTime64OrDefault', + 'toDateTime64OrNull', + 'toDateTime64OrZero', + 'toDateTimeOrDefault', + 'toDateTimeOrNull', + 'toDateTimeOrZero', + 'toDayOfMonth', + 'toDayOfWeek', + 'toDayOfYear', + 'toDaysSinceYearZero', + 'toDecimal128', + 'toDecimal128OrDefault', + 'toDecimal128OrNull', + 'toDecimal128OrZero', + 'toDecimal256', + 'toDecimal256OrDefault', + 'toDecimal256OrNull', + 'toDecimal256OrZero', + 'toDecimal32', + 'toDecimal32OrDefault', + 'toDecimal32OrNull', + 'toDecimal32OrZero', + 'toDecimal64', + 'toDecimal64OrDefault', + 'toDecimal64OrNull', + 'toDecimal64OrZero', + 'toDecimalString', + 'toFixedString', + 'toFloat32', + 'toFloat32OrDefault', + 'toFloat32OrNull', + 'toFloat32OrZero', + 'toFloat64', + 'toFloat64OrDefault', + 'toFloat64OrNull', + 'toFloat64OrZero', + 'toHour', + 'toIPv4', + 'toIPv4OrDefault', + 'toIPv4OrNull', + 'toIPv4OrZero', + 'toIPv6', + 'toIPv6OrDefault', + 'toIPv6OrNull', + 'toIPv6OrZero', + 'toISOWeek', + 'toISOYear', + 'toInt128', + 'toInt128OrDefault', + 'toInt128OrNull', + 'toInt128OrZero', + 'toInt16', + 'toInt16OrDefault', + 'toInt16OrNull', + 'toInt16OrZero', + 'toInt256', + 'toInt256OrDefault', + 'toInt256OrNull', + 'toInt256OrZero', + 'toInt32', + 'toInt32OrDefault', + 'toInt32OrNull', + 'toInt32OrZero', + 'toInt64', + 'toInt64OrDefault', + 'toInt64OrNull', + 'toInt64OrZero', + 'toInt8', + 'toInt8OrDefault', + 'toInt8OrNull', + 'toInt8OrZero', + 'toInterval', + 'toIntervalDay', + 'toIntervalHour', + 'toIntervalMicrosecond', + 'toIntervalMillisecond', + 'toIntervalMinute', + 'toIntervalMonth', + 'toIntervalNanosecond', + 'toIntervalQuarter', + 'toIntervalSecond', + 'toIntervalWeek', + 'toIntervalYear', + 'toJSONString', + 'toLastDayOfMonth', + 'toLastDayOfWeek', + 'toLowCardinality', + 'toMillisecond', + 'toMinute', + 'toModifiedJulianDay', + 'toModifiedJulianDayOrNull', + 'toMonday', + 'toMonth', + 'toMonthNumSinceEpoch', + 'toNullable', + 'toQuarter', + 'toRelativeDayNum', + 'toRelativeHourNum', + 'toRelativeMinuteNum', + 'toRelativeMonthNum', + 'toRelativeQuarterNum', + 'toRelativeSecondNum', + 'toRelativeWeekNum', + 'toRelativeYearNum', + 'toSecond', + 'toStartOfDay', + 'toStartOfFifteenMinutes', + 'toStartOfFiveMinute', + 'toStartOfFiveMinutes', + 'toStartOfHour', + 'toStartOfISOYear', + 'toStartOfInterval', + 'toStartOfMicrosecond', + 'toStartOfMillisecond', + 'toStartOfMinute', + 'toStartOfMonth', + 'toStartOfNanosecond', + 'toStartOfQuarter', + 'toStartOfSecond', + 'toStartOfTenMinutes', + 'toStartOfWeek', + 'toStartOfYear', + 'toString', + 'toStringCutToZero', + 'toTime', + 'toTime64', + 'toTime64OrNull', + 'toTime64OrZero', + 'toTimeOrNull', + 'toTimeOrZero', + 'toTimeWithFixedDate', + 'toTimeZone', + 'toTimezone', + 'toTypeName', + 'toUInt128', + 'toUInt128OrDefault', + 'toUInt128OrNull', + 'toUInt128OrZero', + 'toUInt16', + 'toUInt16OrDefault', + 'toUInt16OrNull', + 'toUInt16OrZero', + 'toUInt256', + 'toUInt256OrDefault', + 'toUInt256OrNull', + 'toUInt256OrZero', + 'toUInt32', + 'toUInt32OrDefault', + 'toUInt32OrNull', + 'toUInt32OrZero', + 'toUInt64', + 'toUInt64OrDefault', + 'toUInt64OrNull', + 'toUInt64OrZero', + 'toUInt8', + 'toUInt8OrDefault', + 'toUInt8OrNull', + 'toUInt8OrZero', + 'toUTCTimestamp', + 'toUUID', + 'toUUIDOrDefault', + 'toUUIDOrNull', + 'toUUIDOrZero', + 'toUnixTimestamp', + 'toUnixTimestamp64Micro', + 'toUnixTimestamp64Milli', + 'toUnixTimestamp64Nano', + 'toUnixTimestamp64Second', + 'toValidUTF8', + 'toWeek', + 'toYYYYMM', + 'toYYYYMMDD', + 'toYYYYMMDDhhmmss', + 'toYear', + 'toYearNumSinceEpoch', + 'toYearWeek', + 'to_utc_timestamp', + 'today', + 'tokens', + 'topK', + 'topKWeighted', + 'topLevelDomain', + 'topLevelDomainRFC', + 'transactionID', + 'transactionLatestSnapshot', + 'transactionOldestSnapshot', + 'transform', + 'translate', + 'translateUTF8', + 'trim', + 'trimBoth', + 'trimLeft', + 'trimRight', + 'trunc', + 'truncate', + 'tryBase32Decode', + 'tryBase58Decode', + 'tryBase64Decode', + 'tryBase64URLDecode', + 'tryDecrypt', + 'tryIdnaEncode', + 'tryPunycodeDecode', + 'tumble', + 'tumbleEnd', + 'tumbleStart', + 'tuple', + 'tupleConcat', + 'tupleDivide', + 'tupleDivideByNumber', + 'tupleElement', + 'tupleHammingDistance', + 'tupleIntDiv', + 'tupleIntDivByNumber', + 'tupleIntDivOrZero', + 'tupleIntDivOrZeroByNumber', + 'tupleMinus', + 'tupleModulo', + 'tupleModuloByNumber', + 'tupleMultiply', + 'tupleMultiplyByNumber', + 'tupleNames', + 'tupleNegate', + 'tuplePlus', + 'tupleToNameValuePairs', + 'ucase', + 'unbin', + 'unhex', + 'uniq', + 'uniqCombined', + 'uniqCombined64', + 'uniqExact', + 'uniqHLL12', + 'uniqTheta', + 'uniqThetaIntersect', + 'uniqThetaNot', + 'uniqThetaUnion', + 'uniqUpTo', + 'upper', + 'upperUTF8', + 'uptime', + 'user', + 'validateNestedArraySizes', + 'varPop', + 'varPopStable', + 'varSamp', + 'varSampStable', + 'variantElement', + 'variantType', + 'vectorDifference', + 'vectorSum', + 'version', + 'visibleWidth', + 'visitParamExtractBool', + 'visitParamExtractFloat', + 'visitParamExtractInt', + 'visitParamExtractRaw', + 'visitParamExtractString', + 'visitParamExtractUInt', + 'visitParamHas', + 'week', + 'welchTTest', + 'widthBucket', + 'width_bucket', + 'windowFunnel', + 'windowID', + 'wkb', + 'wkt', + 'wordShingleMinHash', + 'wordShingleMinHashArg', + 'wordShingleMinHashArgCaseInsensitive', + 'wordShingleMinHashArgCaseInsensitiveUTF8', + 'wordShingleMinHashArgUTF8', + 'wordShingleMinHashCaseInsensitive', + 'wordShingleMinHashCaseInsensitiveUTF8', + 'wordShingleMinHashUTF8', + 'wordShingleSimHash', + 'wordShingleSimHashCaseInsensitive', + 'wordShingleSimHashCaseInsensitiveUTF8', + 'wordShingleSimHashUTF8', + 'wyHash64', + 'xor', + 'xxHash32', + 'xxHash64', + 'xxh3', + 'yandexConsistentHash', + 'yearweek', + 'yesterday', + 'zookeeperSessionUptime', + + // Table Engines + // https://clickhouse.com/docs/engines/table-engines + 'MergeTree', + 'ReplacingMergeTree', + 'SummingMergeTree', + 'AggregatingMergeTree', + 'CollapsingMergeTree', + 'VersionedCollapsingMergeTree', + 'GraphiteMergeTree', + 'CoalescingMergeTree', + + // Database Engines + // https://clickhouse.com/docs/engines/database-engines + 'Atomic', + 'Shared', + 'Lazy', + 'Replicated', + 'PostgreSQL', + 'MySQL', + 'SQLite', + 'Backup', + 'MaterializedPostgreSQL', + 'DataLakeCatalog', +]; diff --git a/src/languages/clickhouse/clickhouse.keywords.ts b/src/languages/clickhouse/clickhouse.keywords.ts new file mode 100644 index 000000000..938b880a4 --- /dev/null +++ b/src/languages/clickhouse/clickhouse.keywords.ts @@ -0,0 +1,580 @@ +export const keywords: string[] = [ + // Derived from https://github.com/ClickHouse/ClickHouse/blob/827a7ef9f6d727ef511fea7785a1243541509efb/tests/fuzz/dictionaries/keywords.dict#L4 + // Clickhouse keywords can span multiple individual words (e.g., "ADD COLUMN"). See + // `keywordPhrases` below for all of these. + 'ACCESS', + 'ACTION', + 'ADD', + 'ADMIN', + 'AFTER', + 'ALGORITHM', + 'ALIAS', + 'ALL', + 'ALLOWED_LATENESS', + 'ALTER', + 'AND', + 'ANTI', + 'ANY', + 'APPEND', + 'APPLY', + 'ARRAY', + 'AS', + 'ASC', + 'ASCENDING', + 'ASOF', + 'ASSUME', + 'AST', + 'ASYNC', + 'ATTACH', + 'AUTO_INCREMENT', + 'AZURE', + 'BACKUP', + 'BAGEXPANSION', + 'BASE_BACKUP', + 'BCRYPT_HASH', + 'BCRYPT_PASSWORD', + 'BEGIN', + 'BETWEEN', + 'BIDIRECTIONAL', + 'BOTH', + 'BY', + 'CACHE', + 'CACHES', + 'CASCADE', + 'CASE', + 'CAST', + 'CHANGE', + 'CHANGEABLE_IN_READONLY', + 'CHANGED', + 'CHAR', + 'CHARACTER', + 'CHECK', + 'CLEANUP', + 'CLEAR', + 'CLUSTER', + 'CLUSTERS', + 'CLUSTER_HOST_IDS', + 'CN', + 'CODEC', + 'COLLATE', + 'COLLECTION', + 'COLUMN', + 'COLUMNS', + 'COMMENT', + 'COMMIT', + 'COMPRESSION', + 'CONST', + 'CONSTRAINT', + 'CREATE', + 'CROSS', + 'CUBE', + 'CURRENT', + 'CURRENTUSER', + 'CURRENT_USER', + 'D', + 'DATA', + 'DATABASE', + 'DATABASES', + 'DATE', + 'DAY', + 'DAYS', + 'DD', + 'DDL', + 'DEDUPLICATE', + 'DEFAULT', + 'DEFINER', + 'DELAY', + 'DELETE', + 'DELETED', + 'DEPENDS', + 'DESC', + 'DESCENDING', + 'DESCRIBE', + 'DETACH', + 'DETACHED', + 'DICTIONARIES', + 'DICTIONARY', + 'DISK', + 'DISTINCT', + 'DIV', + 'DOUBLE_SHA1_HASH', + 'DOUBLE_SHA1_PASSWORD', + 'DROP', + 'ELSE', + 'EMPTY', + 'ENABLED', + 'END', + 'ENFORCED', + 'ENGINE', + 'ENGINES', + 'EPHEMERAL', + 'ESTIMATE', + 'EVENT', + 'EVENTS', + 'EVERY', + 'EXCEPT', + 'EXCHANGE', + 'EXISTS', + 'EXPLAIN', + 'EXPRESSION', + 'EXTENDED', + 'EXTERNAL', + 'FAKE', + 'FALSE', + 'FETCH', + 'FIELDS', + 'FILE', + 'FILESYSTEM', + 'FILL', + 'FILTER', + 'FINAL', + 'FIRST', + 'FOLLOWING', + 'FOR', + 'FOREIGN', + 'FORMAT', + 'FREEZE', + 'FROM', + 'FULL', + 'FULLTEXT', + 'FUNCTION', + 'FUNCTIONS', + 'GLOBAL', + 'GRANT', + 'GRANTEES', + 'GRANTS', + 'GRANULARITY', + 'GROUP', + 'GROUPING', + 'GROUPS', + 'H', + 'HASH', + 'HAVING', + 'HDFS', + 'HH', + 'HIERARCHICAL', + 'HOST', + 'HOUR', + 'HOURS', + 'HTTP', + // Disabling this because it's a keyword, but formats far more than + // it should. + // 'ID', + 'IDENTIFIED', + 'IF', + 'IGNORE', + 'ILIKE', + 'IN', + 'INDEX', + 'INDEXES', + 'INDICES', + 'INFILE', + 'INHERIT', + 'INJECTIVE', + 'INNER', + 'INSERT', + 'INTERPOLATE', + 'INTERSECT', + 'INTERVAL', + 'INTO', + 'INVISIBLE', + 'INVOKER', + 'IP', + 'IS', + 'IS_OBJECT_ID', + 'JOIN', + 'JWT', + 'KERBEROS', + 'KEY', + 'KEYED', + 'KEYS', + 'KILL', + 'KIND', + 'LARGE', + 'LAST', + 'LAYOUT', + 'LDAP', + 'LEADING', + 'LEFT', + 'LESS', + 'LEVEL', + 'LIFETIME', + 'LIGHTWEIGHT', + 'LIKE', + 'LIMIT', + 'LIMITS', + 'LINEAR', + 'LIST', + 'LIVE', + 'LOCAL', + 'M', + 'MASK', + 'MATCH', + 'MATERIALIZE', + 'MATERIALIZED', + 'MAX', + 'MCS', + 'MEMORY', + 'MERGES', + 'METRICS', + 'MI', + 'MICROSECOND', + 'MICROSECONDS', + 'MILLISECOND', + 'MILLISECONDS', + 'MIN', + 'MINUTE', + 'MINUTES', + 'MM', + 'MOD', + 'MODIFY', + 'MONTH', + 'MONTHS', + 'MOVE', + 'MS', + 'MUTATION', + 'N', + 'NAME', + 'NAMED', + 'NANOSECOND', + 'NANOSECONDS', + 'NEXT', + 'NO', + 'NONE', + 'NOT', + 'NO_PASSWORD', + 'NS', + 'NULL', + 'NULLS', + 'OBJECT', + 'OFFSET', + 'ON', + 'ONLY', + 'OPTIMIZE', + 'OPTION', + 'OR', + 'ORDER', + 'OUTER', + 'OUTFILE', + 'OVER', + 'OVERRIDABLE', + 'OVERRIDE', + 'PART', + 'PARTIAL', + 'PARTITION', + 'PARTITIONS', + 'PART_MOVE_TO_SHARD', + 'PASTE', + 'PERIODIC', + 'PERMANENTLY', + 'PERMISSIVE', + 'PERSISTENT', + 'PIPELINE', + 'PLAINTEXT_PASSWORD', + 'PLAN', + 'POLICY', + 'POPULATE', + 'PRECEDING', + 'PRECISION', + 'PREWHERE', + 'PRIMARY', + 'PRIVILEGES', + 'PROCESSLIST', + 'PROFILE', + 'PROJECTION', + 'PROTOBUF', + 'PULL', + 'Q', + 'QQ', + 'QUALIFY', + 'QUARTER', + 'QUARTERS', + 'QUERY', + 'QUOTA', + 'RANDOMIZE', + 'RANDOMIZED', + 'RANGE', + 'READONLY', + 'REALM', + 'RECOMPRESS', + 'RECURSIVE', + 'REFERENCES', + 'REFRESH', + 'REGEXP', + 'REMOVE', + 'RENAME', + 'REPLACE', + 'RESET', + 'RESPECT', + 'RESTORE', + 'RESTRICT', + 'RESTRICTIVE', + 'RESUME', + 'REVOKE', + 'RIGHT', + 'ROLE', + 'ROLES', + 'ROLLBACK', + 'ROLLUP', + 'ROW', + 'ROWS', + 'S', + 'S3', + 'SALT', + 'SAMPLE', + 'SAN', + 'SCHEME', + 'SECOND', + 'SECONDS', + 'SECURITY', + 'SELECT', + 'SEMI', + 'SEQUENTIAL', + 'SERVER', + 'SET', + 'SETS', + 'SETTING', + 'SETTINGS', + 'SHA256_HASH', + 'SHA256_PASSWORD', + 'SHARD', + 'SHOW', + 'SIGNED', + 'SIMPLE', + 'SNAPSHOT', + 'SOURCE', + 'SPATIAL', + 'SQL', + 'SQL_TSI_DAY', + 'SQL_TSI_HOUR', + 'SQL_TSI_MICROSECOND', + 'SQL_TSI_MILLISECOND', + 'SQL_TSI_MINUTE', + 'SQL_TSI_MONTH', + 'SQL_TSI_NANOSECOND', + 'SQL_TSI_QUARTER', + 'SQL_TSI_SECOND', + 'SQL_TSI_WEEK', + 'SQL_TSI_YEAR', + 'SS', + 'SSH_KEY', + 'SSL_CERTIFICATE', + 'STALENESS', + 'START', + 'STATISTICS', + 'STDOUT', + 'STEP', + 'STORAGE', + 'STRICT', + 'STRICTLY_ASCENDING', + 'SUBPARTITION', + 'SUBPARTITIONS', + 'SUSPEND', + 'SYNC', + 'SYNTAX', + 'SYSTEM', + 'TABLE', + 'TABLES', + 'TAGS', + 'TEMPORARY', + 'TEST', + 'THAN', + 'THEN', + 'TIES', + 'TIME', + 'TIMESTAMP', + 'TO', + 'TOP', + 'TOTALS', + 'TRACKING', + 'TRAILING', + 'TRANSACTION', + 'TREE', + 'TRIGGER', + 'TRUE', + 'TRUNCATE', + 'TTL', + 'TYPE', + 'TYPEOF', + 'UNBOUNDED', + 'UNDROP', + 'UNFREEZE', + 'UNION', + 'UNIQUE', + 'UNSET', + 'UNSIGNED', + 'UNTIL', + 'UPDATE', + 'URL', + 'USE', + 'USER', + 'USING', + 'UUID', + 'VALID', + 'VALUES', + 'VARYING', + 'VIEW', + 'VISIBLE', + 'VOLUME', + 'WATCH', + 'WATERMARK', + 'WEEK', + 'WEEKS', + 'WHEN', + 'WHERE', + 'WINDOW', + 'WITH', + 'WITH_ITEMINDEX', + 'WK', + 'WRITABLE', + 'WW', + 'YEAR', + 'YEARS', + 'YY', + 'YYYY', + 'ZKPATH', +]; + +export const dataTypes: string[] = [ + // Derived from `SELECT name FROM system.data_type_families ORDER BY name` on Clickhouse Cloud + // as of November 14, 2025. + 'AGGREGATEFUNCTION', + 'ARRAY', + 'BFLOAT16', + 'BIGINT', + 'BIGINT SIGNED', + 'BIGINT UNSIGNED', + 'BINARY', + 'BINARY LARGE OBJECT', + 'BINARY VARYING', + 'BIT', + 'BLOB', + 'BYTE', + 'BYTEA', + 'BOOL', + 'CHAR', + 'CHAR LARGE OBJECT', + 'CHAR VARYING', + 'CHARACTER', + 'CHARACTER LARGE OBJECT', + 'CHARACTER VARYING', + 'CLOB', + 'DEC', + 'DOUBLE', + 'DOUBLE PRECISION', + 'DATE', + 'DATE32', + 'DATETIME', + 'DATETIME32', + 'DATETIME64', + 'DECIMAL', + 'DECIMAL128', + 'DECIMAL256', + 'DECIMAL32', + 'DECIMAL64', + 'DYNAMIC', + 'ENUM', + 'ENUM', + 'ENUM16', + 'ENUM8', + 'FIXED', + 'FLOAT', + 'FIXEDSTRING', + 'FLOAT32', + 'FLOAT64', + 'GEOMETRY', + 'INET4', + 'INET6', + 'INT', + 'INT SIGNED', + 'INT UNSIGNED', + 'INT1', + 'INT1 SIGNED', + 'INT1 UNSIGNED', + 'INTEGER', + 'INTEGER SIGNED', + 'INTEGER UNSIGNED', + 'IPV4', + 'IPV6', + 'INT128', + 'INT16', + 'INT256', + 'INT32', + 'INT64', + 'INT8', + 'INTERVALDAY', + 'INTERVALHOUR', + 'INTERVALMICROSECOND', + 'INTERVALMILLISECOND', + 'INTERVALMINUTE', + 'INTERVALMONTH', + 'INTERVALNANOSECOND', + 'INTERVALQUARTER', + 'INTERVALSECOND', + 'INTERVALWEEK', + 'INTERVALYEAR', + 'JSON', + 'LONGBLOB', + 'LONGTEXT', + 'LINESTRING', + 'LOWCARDINALITY', + 'MEDIUMBLOB', + 'MEDIUMINT', + 'MEDIUMINT SIGNED', + 'MEDIUMINT UNSIGNED', + 'MEDIUMTEXT', + 'MAP', + 'MULTILINESTRING', + 'MULTIPOLYGON', + 'NATIONAL CHAR', + 'NATIONAL CHAR VARYING', + 'NATIONAL CHARACTER', + 'NATIONAL CHARACTER LARGE OBJECT', + 'NATIONAL CHARACTER VARYING', + 'NCHAR', + 'NCHAR LARGE OBJECT', + 'NCHAR VARYING', + 'NUMERIC', + 'NVARCHAR', + 'NESTED', + 'NOTHING', + 'NULLABLE', + 'OBJECT', + 'POINT', + 'POLYGON', + 'REAL', + 'RING', + 'SET', + 'SIGNED', + 'SINGLE', + 'SMALLINT', + 'SMALLINT SIGNED', + 'SMALLINT UNSIGNED', + 'SIMPLEAGGREGATEFUNCTION', + 'STRING', + 'TEXT', + 'TIMESTAMP', + 'TINYBLOB', + 'TINYINT', + 'TINYINT SIGNED', + 'TINYINT UNSIGNED', + 'TINYTEXT', + 'TIME', + 'TIME64', + 'TUPLE', + 'UINT128', + 'UINT16', + 'UINT256', + 'UINT32', + 'UINT64', + 'UINT8', + 'UNSIGNED', + 'UUID', + 'VARBINARY', + 'VARCHAR', + 'VARCHAR2', + 'VARIANT', + 'YEAR', + 'BOOL', + 'BOOLEAN', +]; diff --git a/src/sqlFormatter.ts b/src/sqlFormatter.ts index f89573c80..d65159b6f 100644 --- a/src/sqlFormatter.ts +++ b/src/sqlFormatter.ts @@ -7,6 +7,7 @@ import { ConfigError, validateConfig } from './validateConfig.js'; const dialectNameMap: Record = { bigquery: 'bigquery', + clickhouse: 'clickhouse', db2: 'db2', db2i: 'db2i', duckdb: 'duckdb', diff --git a/test/clickhouse.test.ts b/test/clickhouse.test.ts new file mode 100644 index 000000000..3c49a7d72 --- /dev/null +++ b/test/clickhouse.test.ts @@ -0,0 +1,1787 @@ +import dedent from 'dedent-js'; + +import { format as originalFormat, FormatFn } from '../src/sqlFormatter.js'; +import behavesLikeSqlFormatter from './behavesLikeSqlFormatter.js'; + +import supportsCreateTable from './features/createTable.js'; +import supportsDropTable from './features/dropTable.js'; +import supportsStrings from './features/strings.js'; +import supportsArrayLiterals from './features/arrayLiterals.js'; +import supportsArrayAndMapAccessors from './features/arrayAndMapAccessors.js'; +import supportsBetween from './features/between.js'; +import supportsJoin from './features/join.js'; +import supportsOperators from './features/operators.js'; +import supportsDeleteFrom from './features/deleteFrom.js'; +import supportsComments from './features/comments.js'; +import supportsIdentifiers from './features/identifiers.js'; +import supportsWindow from './features/window.js'; +import supportsSetOperations from './features/setOperations.js'; +import supportsLimiting from './features/limiting.js'; +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 supportsAlterTable from './features/alterTable.js'; +import supportsDataTypeCase from './options/dataTypeCase.js'; +import supportsNumbers from './features/numbers.js'; + +describe('ClickhouseFormatter', () => { + const language = 'clickhouse'; + const format: FormatFn = (query, cfg = {}) => originalFormat(query, { ...cfg, language }); + + behavesLikeSqlFormatter(format); + supportsNumbers(format); + supportsComments(format, { hashComments: true }); + supportsCreateView(format, { orReplace: true, materialized: true, ifNotExists: true }); + supportsCreateTable(format, { + orReplace: true, + ifNotExists: true, + columnComment: true, + tableComment: true, + }); + supportsDropTable(format, { ifExists: true }); + + supportsAlterTable(format, { + addColumn: true, + dropColumn: true, + renameTo: true, + renameColumn: false, + }); + // We disable `renameColumn` above because we handle TO + // differently than the default. + it('formats ALTER TABLE ... RENAME COLUMN statement', () => { + const result = format('ALTER TABLE supplier RENAME COLUMN supplier_id TO id;'); + expect(result).toBe(dedent` + ALTER TABLE supplier + RENAME COLUMN supplier_id + TO id; + `); + }); + + supportsDeleteFrom(format); + supportsInsertInto(format); + supportsUpdate(format); + supportsTruncateTable(format); + supportsStrings(format, ["''-qq-bs"]); + supportsIdentifiers(format, ['""-qq-bs', '``']); + supportsArrayLiterals(format, { withoutArrayPrefix: true }); + supportsBetween(format); + supportsJoin(format, { + without: ['NATURAL'], + additionally: [ + 'GLOBAL LEFT OUTER JOIN', + 'GLOBAL RIGHT OUTER JOIN', + 'GLOBAL FULL OUTER JOIN', + 'GLOBAL CROSS OUTER JOIN', + + 'GLOBAL INNER SEMI JOIN', + 'GLOBAL LEFT SEMI JOIN', + 'GLOBAL RIGHT SEMI JOIN', + 'GLOBAL FULL SEMI JOIN', + 'GLOBAL CROSS SEMI JOIN', + + 'GLOBAL INNER ANTI JOIN', + 'GLOBAL LEFT ANTI JOIN', + 'GLOBAL RIGHT ANTI JOIN', + 'GLOBAL FULL ANTI JOIN', + 'GLOBAL CROSS ANTI JOIN', + + 'GLOBAL INNER ANY JOIN', + 'GLOBAL LEFT ANY JOIN', + 'GLOBAL RIGHT ANY JOIN', + 'GLOBAL FULL ANY JOIN', + 'GLOBAL CROSS ANY JOIN', + + 'GLOBAL INNER ALL JOIN', + 'GLOBAL LEFT ALL JOIN', + 'GLOBAL RIGHT ALL JOIN', + 'GLOBAL FULL ALL JOIN', + 'GLOBAL CROSS ALL JOIN', + + 'GLOBAL INNER ASOF JOIN', + 'GLOBAL LEFT ASOF JOIN', + 'GLOBAL RIGHT ASOF JOIN', + 'GLOBAL FULL ASOF JOIN', + 'GLOBAL CROSS ASOF JOIN', + + 'GLOBAL INNER JOIN', + 'GLOBAL LEFT JOIN', + 'GLOBAL RIGHT JOIN', + 'GLOBAL FULL JOIN', + 'GLOBAL CROSS JOIN', + + 'CROSS OUTER JOIN', + + 'INNER SEMI JOIN', + 'LEFT SEMI JOIN', + 'RIGHT SEMI JOIN', + 'FULL SEMI JOIN', + 'CROSS SEMI JOIN', + + 'INNER ANTI JOIN', + 'LEFT ANTI JOIN', + 'RIGHT ANTI JOIN', + 'FULL ANTI JOIN', + 'CROSS ANTI JOIN', + + 'INNER ANY JOIN', + 'LEFT ANY JOIN', + 'RIGHT ANY JOIN', + 'FULL ANY JOIN', + 'CROSS ANY JOIN', + + 'INNER ALL JOIN', + 'LEFT ALL JOIN', + 'RIGHT ALL JOIN', + 'FULL ALL JOIN', + 'CROSS ALL JOIN', + + 'INNER ASOF JOIN', + 'LEFT ASOF JOIN', + 'RIGHT ASOF JOIN', + 'FULL ASOF JOIN', + 'CROSS ASOF JOIN', + + 'GLOBAL OUTER JOIN', + 'GLOBAL SEMI JOIN', + 'GLOBAL ANTI JOIN', + 'GLOBAL ANY JOIN', + 'GLOBAL ALL JOIN', + 'GLOBAL ASOF JOIN', + + 'GLOBAL JOIN', + + 'OUTER JOIN', + 'SEMI JOIN', + 'ANTI JOIN', + 'ANY JOIN', + 'ALL JOIN', + 'ASOF JOIN', + ], + }); + supportsSetOperations(format, ['UNION', 'UNION ALL', 'UNION DISTINCT', 'PARALLEL WITH']); + supportsOperators(format, ['%'], { any: true }); + supportsWindow(format); + supportsLimiting(format, { limit: true, offset: false }); + supportsArrayAndMapAccessors(format); + supportsDataTypeCase(format); + + // Should support the ternary operator + it('supports the ternary operator', () => { + // NOTE: Ternary operators have a missing space because + // ExpressionFormatter's `formatOperator` method special-cases `:`. + expect(format('SELECT foo?bar: baz;')).toBe('SELECT\n foo ? bar: baz;'); + }); + + // Should support the lambda creation operator + it('supports the lambda creation operator', () => { + expect(format('SELECT arrayMap(x->2*x, [1,2,3,4]) AS result;')).toBe( + 'SELECT\n arrayMap(x -> 2 * x, [1, 2, 3, 4]) AS result;' + ); + }); + + describe('in/any set operators', () => { + it('should respect the IN operator as a keyword when used as an operator', () => { + expect(format('SELECT 1 in foo;')).toBe(dedent` + SELECT + 1 IN foo; + `); + + expect(format('SELECT foo in (1,2,3);')).toBe(dedent` + SELECT + foo IN (1, 2, 3); + `); + expect(format('SELECT "foo" in (1,2,3);')).toBe(dedent` + SELECT + "foo" IN (1, 2, 3); + `); + expect(format('SELECT 1 in (1,2,3);')).toBe(dedent` + SELECT + 1 IN (1, 2, 3); + `); + }); + it('should respect the ANY operator as a keyword when used as an operator', () => { + expect(format('SELECT 1 = any foo;')).toBe(dedent` + SELECT + 1 = ANY foo; + `); + + expect(format('SELECT foo = any (1,2,3);')).toBe(dedent` + SELECT + foo = ANY (1, 2, 3); + `); + expect(format('SELECT "foo" = any (1,2,3);')).toBe(dedent` + SELECT + "foo" = ANY (1, 2, 3); + `); + expect(format('SELECT 1 = any (1,2,3);')).toBe(dedent` + SELECT + 1 = ANY (1, 2, 3); + `); + }); + + it('should respect the IN operator as a keyword when used as a function', () => { + expect(format('SELECT in(foo, [1,2,3]);')).toBe(dedent` + SELECT + in(foo, [1, 2, 3]); + `); + expect(format('SELECT in("foo", "bar");')).toBe(dedent` + SELECT + in("foo", "bar"); + `); + }); + it('should respect the ANY operator as a keyword when used as a function', () => { + expect(format('SELECT any(foo);')).toBe(dedent` + SELECT + any(foo); + `); + expect(format('SELECT any("foo");')).toBe(dedent` + SELECT + any("foo"); + `); + }); + }); + + it('should support parameters', () => { + expect(format('SELECT {foo:Uint64};', { params: { foo: "'123'" } })).toBe("SELECT\n '123';"); + expect(format('SELECT {foo:Map(String, String)};', { params: { foo: "{'bar': 'baz'}" } })).toBe( + dedent` + SELECT + {'bar': 'baz'}; + ` + ); + }); + + // https://clickhouse.com/docs/sql-reference/statements/select + describe('SELECT statements', () => { + it('formats SELECT with COLUMNS expression', () => { + expect(format("SELECT COLUMNS('a') FROM col_names")).toBe(dedent` + SELECT + COLUMNS ('a') + FROM + col_names + `); + }); + + it('formats SELECT with multiple COLUMNS and functions', () => { + expect( + format("SELECT COLUMNS('a'), COLUMNS('c'), toTypeName(COLUMNS('c')) FROM col_names") + ).toBe( + dedent` + SELECT + COLUMNS ('a'), + COLUMNS ('c'), + toTypeName(COLUMNS ('c')) + FROM + col_names + ` + ); + }); + + it('formats SELECT with COLUMNS arithmetic', () => { + expect(format("SELECT COLUMNS('a') + COLUMNS('c') FROM col_names")).toBe(dedent` + SELECT + COLUMNS ('a') + COLUMNS ('c') + FROM + col_names + `); + }); + + it('formats SELECT with COLUMNS and APPLY modifier', () => { + expect( + format( + "SELECT COLUMNS('[jk]') APPLY(toString) APPLY(length) APPLY(max) FROM columns_transformers;" + ) + ).toBe(dedent` + SELECT + COLUMNS ('[jk]') APPLY (toString) APPLY (length) APPLY (max) + FROM + columns_transformers; + `); + }); + + it('formats SELECT with REPLACE, EXCEPT, and APPLY modifiers', () => { + expect( + format('SELECT * REPLACE(i + 1 AS i) EXCEPT (j) APPLY(sum) from columns_transformers;') + ).toBe( + dedent` + SELECT + * REPLACE(i + 1 AS i) EXCEPT (j) APPLY (sum) + from + columns_transformers; + ` + ); + }); + + it('formats SELECT with SETTINGS clause', () => { + expect( + format('SELECT * FROM some_table SETTINGS optimize_read_in_order=1, cast_keep_nullable=1;') + ).toBe( + dedent` + SELECT + * + FROM + some_table + SETTINGS + optimize_read_in_order = 1, + cast_keep_nullable = 1; + ` + ); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/insert-into + describe('INSERT INTO statements', () => { + it('formats INSERT INTO with asterisk', () => { + expect(format("INSERT INTO insert_select_testtable (*) VALUES (1, 'a', 1) ;")).toBe(dedent` + INSERT INTO + insert_select_testtable (*) + VALUES + (1, 'a', 1); + `); + }); + + it('formats INSERT INTO with EXCEPT modifier', () => { + expect(format('INSERT INTO insert_select_testtable (* EXCEPT(b)) VALUES (2, 2);')) + .toBe(dedent` + INSERT INTO + insert_select_testtable (* EXCEPT (b)) + VALUES + (2, 2); + `); + }); + + it('formats INSERT INTO with DEFAULT', () => { + expect(format('INSERT INTO insert_select_testtable VALUES (1, DEFAULT, 1) ;')).toBe(dedent` + INSERT INTO + insert_select_testtable + VALUES + (1, DEFAULT, 1); + `); + }); + + it('formats INSERT INTO with WITH clause after INSERT', () => { + expect(format('INSERT INTO x WITH y AS (SELECT * FROM numbers(10)) SELECT * FROM y;')) + .toBe(dedent` + INSERT INTO + x + WITH + y AS ( + SELECT + * + FROM + numbers(10) + ) + SELECT + * + FROM + y; + `); + }); + + it('formats WITH clause before INSERT INTO', () => { + expect(format('WITH y AS (SELECT * FROM numbers(10)) INSERT INTO x SELECT * FROM y;')) + .toBe(dedent` + WITH + y AS ( + SELECT + * + FROM + numbers(10) + ) + INSERT INTO + x + SELECT + * + FROM + y; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/update + describe('UPDATE statements', () => { + it('formats UPDATE with WHERE clause', () => { + expect(format("UPDATE hits SET Title = 'Updated Title' WHERE EventDate = today();")) + .toBe(dedent` + UPDATE hits + SET + Title = 'Updated Title' + WHERE + EventDate = today(); + `); + }); + + it('formats UPDATE with multiple SET assignments', () => { + expect( + format("UPDATE wikistat SET hits = hits + 1, time = now() WHERE path = 'ClickHouse';") + ).toBe( + dedent` + UPDATE wikistat + SET + hits = hits + 1, + time = now() + WHERE + path = 'ClickHouse'; + ` + ); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/delete + describe('DELETE statements', () => { + it('formats DELETE FROM with ON CLUSTER and IN PARTITION', () => { + expect( + format("DELETE FROM db.table ON CLUSTER foo IN PARTITION '2025-01-01' WHERE x = 1;") + ).toBe( + dedent` + DELETE FROM db.table + ON CLUSTER foo + IN PARTITION '2025-01-01' + WHERE + x = 1; + ` + ); + }); + }); + + // https://clickhouse.com/docs/sql-reference/window-functions + describe('Window functions', () => { + it('formats SELECT with window function', () => { + expect( + format( + 'SELECT part_key, value, order, groupArray(value) OVER (PARTITION BY part_key) AS frame_values FROM wf_partition ORDER BY part_key ASC, value ASC;' + ) + ).toBe(dedent` + SELECT + part_key, + value, + order, + groupArray(value) OVER ( + PARTITION BY + part_key + ) AS frame_values + FROM + wf_partition + ORDER BY + part_key ASC, + value ASC; + `); + }); + + it('formats SELECT with window function and ROWS BETWEEN', () => { + // NOTE: This is a little ugly, but we have `{ROWS | RANGE} BETWEEN` + // as a reserved keyword phrase instead of a reserved clause so + // that we satisfy the window function feature tests. + expect( + format( + 'SELECT part_key, value, order, groupArray(value) OVER (PARTITION BY part_key ORDER BY order ASC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS frame_values FROM wf_frame ORDER BY part_key ASC, value ASC;' + ) + ).toBe(dedent` + SELECT + part_key, + value, + order, + groupArray(value) OVER ( + PARTITION BY + part_key + ORDER BY + order ASC ROWS BETWEEN UNBOUNDED PRECEDING + AND UNBOUNDED FOLLOWING + ) AS frame_values + FROM + wf_frame + ORDER BY + part_key ASC, + value ASC; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/create + describe('CREATE statements', () => { + it('formats CREATE TABLE with PROJECTION', () => { + expect( + format( + 'CREATE TABLE visits (user_id UInt64, user_name String, pages_visited Nullable(Float64), user_agent String, PROJECTION projection_visits_by_user (SELECT user_agent, sum(pages_visited) GROUP BY user_id, user_agent)) ENGINE = MergeTree() ORDER BY user_agent;' + ) + ).toBe(dedent` + CREATE TABLE visits ( + user_id UInt64, + user_name String, + pages_visited Nullable(Float64), + user_agent String, + PROJECTION projection_visits_by_user ( + SELECT + user_agent, + sum(pages_visited) + GROUP BY + user_id, + user_agent + ) + ) ENGINE = MergeTree() + ORDER BY + user_agent; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/user + describe('ALTER USER statements', () => { + it('formats ALTER USER IF EXISTS user1 RENAME TO user1_new, user2 RENAME TO user2_new DROP ALL SETTINGS', () => { + expect( + format( + 'ALTER USER IF EXISTS user1 RENAME TO user1_new, user2 RENAME TO user2_new DROP ALL SETTINGS;' + ) + ).toBe(dedent` + ALTER USER IF EXISTS + user1 + RENAME TO user1_new, + user2 + RENAME TO user2_new + DROP ALL SETTINGS; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/column + describe('ALTER COLUMN statements', () => { + it('formats ALTER TABLE DROP COLUMN', () => { + expect(format('ALTER TABLE visits DROP COLUMN browser;')).toBe(dedent` + ALTER TABLE visits + DROP COLUMN browser; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/partition + describe('ALTER PARTITION statements', () => { + it('formats ALTER TABLE DROP PARTITION', () => { + expect(format("ALTER TABLE posts DROP PARTITION '2008';")).toBe(dedent` + ALTER TABLE posts + DROP PARTITION '2008'; + `); + }); + + it('formats ALTER TABLE DROP PART', () => { + expect(format("ALTER TABLE mt DROP PART 'all_4_4_0';")).toBe(dedent` + ALTER TABLE mt + DROP PART 'all_4_4_0'; + `); + }); + + it('formats ALTER TABLE DROP DETACHED PARTITION', () => { + expect(format("ALTER TABLE mt DROP DETACHED PARTITION '2020-01-01';")).toBe(dedent` + ALTER TABLE mt + DROP DETACHED PARTITION '2020-01-01'; + `); + }); + + it('formats ALTER TABLE DROP DETACHED PARTITION ALL', () => { + expect(format('ALTER TABLE mt DROP DETACHED PARTITION ALL;')).toBe(dedent` + ALTER TABLE mt + DROP DETACHED PARTITION ALL; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/setting + describe('ALTER SETTING statements', () => { + it('formats ALTER TABLE MODIFY SETTING', () => { + expect( + format( + 'ALTER TABLE example_table MODIFY SETTING max_part_loading_threads=8, max_parts_in_total=50000;' + ) + ).toBe(dedent` + ALTER TABLE example_table + MODIFY SETTING max_part_loading_threads = 8, + max_parts_in_total = 50000; + `); + }); + + it('formats ALTER TABLE RESET SETTING', () => { + expect(format('ALTER TABLE example_table RESET SETTING max_part_loading_threads;')).toBe( + dedent` + ALTER TABLE example_table + RESET SETTING max_part_loading_threads; + ` + ); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/delete + describe('ALTER DELETE statements', () => { + it('formats ALTER TABLE DELETE WHERE', () => { + expect( + format( + 'ALTER TABLE db.events ON CLUSTER prod DELETE WHERE timestamp < now() - INTERVAL 30 DAY;' + ) + ).toBe(dedent` + ALTER TABLE db.events + ON CLUSTER prod + DELETE WHERE timestamp < now() - INTERVAL 30 DAY; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/order-by + describe('ALTER ORDER BY statements', () => { + it('formats ALTER TABLE MODIFY ORDER BY', () => { + expect( + format('ALTER TABLE db.events ON CLUSTER prod MODIFY ORDER BY (user_id, timestamp);') + ).toBe(dedent` + ALTER TABLE db.events + ON CLUSTER prod + MODIFY ORDER BY (user_id, timestamp); + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/sample-by + describe('ALTER SAMPLE BY statements', () => { + it('formats ALTER TABLE MODIFY SAMPLE BY', () => { + expect(format('ALTER TABLE db.events ON CLUSTER prod MODIFY SAMPLE BY user_id;')).toBe(dedent` + ALTER TABLE db.events + ON CLUSTER prod + MODIFY SAMPLE BY user_id; + `); + }); + + it('formats ALTER TABLE REMOVE SAMPLE BY', () => { + expect(format('ALTER TABLE db.events ON CLUSTER prod REMOVE SAMPLE BY;')).toBe(dedent` + ALTER TABLE db.events + ON CLUSTER prod + REMOVE SAMPLE BY; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/skipping-index + describe('ALTER INDEX statements', () => { + it('formats ALTER TABLE ADD INDEX', () => { + expect( + format( + "ALTER TABLE db.table_name ON CLUSTER 'my_cluster' ADD INDEX IF NOT EXISTS my_index (column1 + column2) TYPE set(100) GRANULARITY 2 AFTER another_column;" + ) + ).toBe(dedent` + ALTER TABLE db.table_name + ON CLUSTER 'my_cluster' + ADD INDEX IF NOT EXISTS my_index (column1 + column2) + TYPE + set(100) + GRANULARITY 2 + AFTER another_column; + `); + expect( + format( + 'ALTER TABLE db.table_name ADD INDEX my_index column1 TYPE minmax GRANULARITY 1 FIRST;' + ) + ).toBe(dedent` + ALTER TABLE db.table_name + ADD INDEX my_index column1 + TYPE + minmax + GRANULARITY 1 + FIRST; + `); + }); + it('formats ALTER TABLE DROP INDEX', () => { + expect( + format("ALTER TABLE db.table_name ON CLUSTER 'my_cluster' DROP INDEX IF EXISTS my_index;") + ).toBe(dedent` + ALTER TABLE db.table_name + ON CLUSTER 'my_cluster' + DROP INDEX IF EXISTS my_index; + `); + }); + + it('formats ALTER TABLE MATERIALIZE INDEX', () => { + expect( + format( + "ALTER TABLE db.table_name ON CLUSTER 'my_cluster' MATERIALIZE INDEX IF EXISTS my_index IN PARTITION '202301';" + ) + ).toBe(dedent` + ALTER TABLE db.table_name + ON CLUSTER 'my_cluster' + MATERIALIZE INDEX IF EXISTS my_index + IN PARTITION '202301'; + `); + }); + + it('formats ALTER TABLE CLEAR INDEX', () => { + expect( + format( + "ALTER TABLE db.table_name ON CLUSTER 'my_cluster' CLEAR INDEX IF EXISTS my_index IN PARTITION '202301';" + ) + ).toBe(dedent` + ALTER TABLE db.table_name + ON CLUSTER 'my_cluster' + CLEAR INDEX IF EXISTS my_index + IN PARTITION '202301'; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/constraint + describe('ALTER CONSTRAINT statements', () => { + it('formats ALTER TABLE ADD CONSTRAINT', () => { + expect(format('ALTER TABLE t1 ADD CONSTRAINT IF NOT EXISTS c1 CHECK (a > 0);')).toBe(dedent` + ALTER TABLE t1 + ADD CONSTRAINT IF NOT EXISTS c1 CHECK (a > 0); + `); + }); + + it('formats ALTER TABLE DROP CONSTRAINT', () => { + expect(format('ALTER TABLE t1 DROP CONSTRAINT IF EXISTS c1;')).toBe(dedent` + ALTER TABLE t1 + DROP CONSTRAINT IF EXISTS c1; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/ttl + describe('ALTER TTL statements', () => { + it('formats ALTER TABLE REMOVE TTL', () => { + expect(format('ALTER TABLE t1 REMOVE TTL;')).toBe(dedent` + ALTER TABLE t1 + REMOVE TTL; + `); + }); + + it('formats ALTER TABLE MODIFY TTL', () => { + expect(format('ALTER TABLE t1 MODIFY TTL 1 year;')).toBe(dedent` + ALTER TABLE t1 + MODIFY TTL 1 year; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/statistics + describe('ALTER STATISTICS statements', () => { + it('formats ALTER TABLE MODIFY STATISTICS', () => { + expect(format('ALTER TABLE t1 MODIFY STATISTICS c, d TYPE TDigest, Uniq;')).toBe(dedent` + ALTER TABLE t1 + MODIFY STATISTICS + c, + d + TYPE + TDigest, + Uniq; + `); + }); + + it('formats ALTER TABLE ADD STATISTICS', () => { + expect(format('ALTER TABLE t1 ADD STATISTICS (c, d) TYPE TDigest, Uniq;')).toBe(dedent` + ALTER TABLE t1 + ADD STATISTICS (c, d) + TYPE + TDigest, + Uniq; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/quota + describe('ALTER QUOTA statements', () => { + it('formats ALTER QUOTA IF EXISTS qA FOR INTERVAL 15 month MAX queries = 123 TO CURRENT_USER;', () => { + expect( + format('ALTER QUOTA IF EXISTS qA FOR INTERVAL 15 month MAX queries = 123 TO CURRENT_USER;') + ).toBe(dedent` + ALTER QUOTA IF EXISTS qA + FOR INTERVAL 15 month MAX queries = 123 + TO CURRENT_USER; + `); + }); + + it('formats ALTER QUOTA IF EXISTS qB FOR INTERVAL 30 minute MAX execution_time = 0.5, FOR INTERVAL 5 quarter MAX queries = 321, errors = 10 TO default;', () => { + expect( + format( + 'ALTER QUOTA IF EXISTS qB RENAME TO qC NOT KEYED FOR INTERVAL 30 minute MAX execution_time = 0.5 FOR INTERVAL 5 quarter MAX queries = 321, errors = 10 TO default;' + ) + ).toBe(dedent` + ALTER QUOTA IF EXISTS qB + RENAME TO qC + NOT KEYED + FOR INTERVAL 30 minute MAX execution_time = 0.5 + FOR INTERVAL 5 quarter MAX queries = 321, + errors = 10 + TO default; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/row-policy + describe('ALTER ROW POLICY statements', () => { + it('formats ALTER ROW POLICY', () => { + expect( + format( + 'ALTER ROW POLICY IF EXISTS policy1 ON CLUSTER cluster_name1 ON database1.table1 RENAME TO new_name1;' + ) + ).toBe(dedent` + ALTER ROW POLICY IF EXISTS + policy1 + ON CLUSTER cluster_name1 ON database1.table1 + RENAME TO new_name1; + `); + }); + + it('formats ALTER ROW POLICY with multiple policies', () => { + expect( + format( + 'ALTER ROW POLICY IF EXISTS policy1 ON CLUSTER cluster_name1 ON database1.table1 RENAME TO new_name1, policy2 ON CLUSTER cluster_name2 ON database2.table2 RENAME TO new_name2;' + ) + ).toBe(dedent` + ALTER ROW POLICY IF EXISTS + policy1 + ON CLUSTER cluster_name1 ON database1.table1 + RENAME TO new_name1, + policy2 + ON CLUSTER cluster_name2 ON database2.table2 + RENAME TO new_name2; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/projection + describe('ALTER PROJECTION statements', () => { + it('formats ALTER TABLE ADD PROJECTION', () => { + expect( + format( + 'ALTER TABLE visits_order ADD PROJECTION user_name_projection (SELECT * ORDER BY user_name);' + ) + ).toBe(dedent` + ALTER TABLE visits_order + ADD PROJECTION user_name_projection ( + SELECT + * + ORDER BY + user_name + ); + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/view + describe('ALTER VIEW statements', () => { + it('formats ALTER TABLE MODIFY QUERY', () => { + expect(format('ALTER TABLE mv MODIFY QUERY SELECT a * 2 as a FROM src_table;')).toBe(dedent` + ALTER TABLE mv + MODIFY QUERY SELECT + a * 2 as a + FROM + src_table; + `); + }); + + it('formats ALTER TABLE MODIFY QUERY with GROUP BY', () => { + expect( + format( + 'ALTER TABLE mv MODIFY QUERY SELECT toStartOfDay(ts) ts, event_type, browser, count() events_cnt, sum(cost) cost FROM events GROUP BY ts, event_type, browser;' + ) + ).toBe(dedent` + ALTER TABLE mv + MODIFY QUERY SELECT + toStartOfDay(ts) ts, + event_type, + browser, + count() events_cnt, + sum(cost) cost + FROM + events + GROUP BY + ts, + event_type, + browser; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/apply-deleted-mask + describe('ALTER APPLY DELETED MASK statements', () => { + it('formats ALTER TABLE APPLY DELETED MASK with ON CLUSTER and IN PARTITION', () => { + expect( + format("ALTER TABLE visits ON CLUSTER prod APPLY DELETED MASK IN PARTITION '2025-01-01';") + ).toBe(dedent` + ALTER TABLE visits + ON CLUSTER prod + APPLY DELETED MASK + IN PARTITION '2025-01-01'; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/drop + describe('DROP statements', () => { + it('formats DROP DATABASE', () => { + expect(format('DROP DATABASE db;')).toBe(dedent` + DROP DATABASE db; + `); + }); + + it('formats DROP DATABASE IF EXISTS with ON CLUSTER and SYNC', () => { + expect(format('DROP DATABASE IF EXISTS db ON CLUSTER my_cluster SYNC;')).toBe(dedent` + DROP DATABASE IF EXISTS db + ON CLUSTER my_cluster + SYNC; + `); + }); + + it('formats DROP TEMPORARY TABLE', () => { + expect(format('DROP TEMPORARY TABLE temp_table;')).toBe(dedent` + DROP TEMPORARY TABLE temp_table; + `); + }); + + it('formats DROP TABLE IF EMPTY', () => { + expect(format('DROP TABLE IF EMPTY mydb.my_table;')).toBe(dedent` + DROP TABLE IF EMPTY mydb.my_table; + `); + }); + + it('formats DROP multiple tables', () => { + expect(format('DROP TABLE mydb.tab1, mydb.tab2;')).toBe(dedent` + DROP TABLE mydb.tab1, + mydb.tab2; + `); + }); + + it('formats DROP DICTIONARY with various options', () => { + expect(format('DROP DICTIONARY IF EXISTS mydb.my_dict SYNC;')).toBe(dedent` + DROP DICTIONARY IF EXISTS mydb.my_dict + SYNC; + `); + }); + + it('formats DROP USER single and multiple', () => { + expect(format('DROP USER IF EXISTS user1, user2 ON CLUSTER my_cluster;')).toBe(dedent` + DROP USER IF EXISTS + user1, + user2 + ON CLUSTER my_cluster; + `); + }); + + it('formats DROP ROLE', () => { + expect(format('DROP ROLE IF EXISTS role1, role2 ON CLUSTER my_cluster;')).toBe(dedent` + DROP ROLE IF EXISTS + role1, + role2 + ON CLUSTER my_cluster; + `); + }); + + it('formats DROP ROW POLICY', () => { + expect(format('DROP ROW POLICY IF EXISTS policy1, policy2 ON db1.table1;')).toBe(dedent` + DROP ROW POLICY IF EXISTS + policy1, + policy2 ON db1.table1; + `); + }); + + it('formats DROP POLICY short form', () => { + expect(format('DROP POLICY IF EXISTS policy1, policy2 ON db1.table1;')).toBe(dedent` + DROP POLICY IF EXISTS + policy1, + policy2 ON db1.table1; + `); + }); + + it('formats DROP QUOTA', () => { + expect(format('DROP QUOTA IF EXISTS quota1, quota2 ON CLUSTER my_cluster;')).toBe(dedent` + DROP QUOTA IF EXISTS + quota1, + quota2 + ON CLUSTER my_cluster; + `); + }); + + it('formats DROP SETTINGS PROFILE', () => { + expect(format('DROP SETTINGS PROFILE IF EXISTS profile1, profile2 ON CLUSTER my_cluster;')).toBe(dedent` + DROP SETTINGS PROFILE IF EXISTS + profile1, + profile2 + ON CLUSTER my_cluster; + `); + }); + + it('formats DROP PROFILE short form', () => { + expect(format('DROP PROFILE IF EXISTS profile1;')).toBe(dedent` + DROP PROFILE IF EXISTS profile1; + `); + }); + + it('formats DROP VIEW with SYNC', () => { + expect(format('DROP VIEW IF EXISTS mydb.my_view ON CLUSTER my_cluster SYNC;')).toBe(dedent` + DROP VIEW IF EXISTS mydb.my_view + ON CLUSTER my_cluster + SYNC; + `); + }); + + it('formats DROP FUNCTION', () => { + expect(format('DROP FUNCTION IF EXISTS my_function ON CLUSTER my_cluster;')).toBe(dedent` + DROP FUNCTION IF EXISTS my_function + ON CLUSTER my_cluster; + `); + }); + + it('formats DROP NAMED COLLECTION', () => { + expect(format('DROP NAMED COLLECTION IF EXISTS my_collection ON CLUSTER my_cluster;')) + .toBe(dedent` + DROP NAMED COLLECTION IF EXISTS my_collection + ON CLUSTER my_cluster; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/truncate + describe('TRUNCATE statements', () => { + it('formats TRUNCATE TABLE IF EXISTS with ON CLUSTER and SYNC', () => { + expect(format('TRUNCATE TABLE IF EXISTS db.table ON CLUSTER prod SYNC;')).toBe(dedent` + TRUNCATE TABLE IF EXISTS db.table + ON CLUSTER prod + SYNC; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/system + describe('SYSTEM statements', () => { + it('formats SYSTEM STOP MERGES on cluster', () => { + expect(format('SYSTEM STOP MERGES ON CLUSTER prod;')).toBe(dedent` + SYSTEM STOP MERGES + ON CLUSTER prod; + `); + }); + + it('formats SYSTEM START TTL MERGES on table', () => { + expect(format('SYSTEM START TTL MERGES db.my_table;')).toBe(dedent` + SYSTEM START TTL MERGES db.my_table; + `); + }); + + it('formats SYSTEM UNFREEZE with backup name', () => { + expect(format('SYSTEM UNFREEZE WITH NAME backup_20250101;')).toBe(dedent` + SYSTEM UNFREEZE + WITH NAME backup_20250101; + `); + }); + + it('formats SYSTEM WAIT LOADING PARTS', () => { + expect(format('SYSTEM WAIT LOADING PARTS db.events;')).toBe(dedent` + SYSTEM WAIT LOADING PARTS db.events; + `); + }); + + it('formats SYSTEM STOP FETCHES on replicated table', () => { + expect(format('SYSTEM STOP FETCHES ON CLUSTER prod db.replicated_table;')).toBe(dedent` + SYSTEM STOP FETCHES + ON CLUSTER prod db.replicated_table; + `); + }); + + it('formats SYSTEM START REPLICATION QUEUES', () => { + expect(format('SYSTEM START REPLICATION QUEUES db.replicated_table;')).toBe(dedent` + SYSTEM START REPLICATION QUEUES db.replicated_table; + `); + }); + + it('formats SYSTEM FLUSH DISTRIBUTED on cluster', () => { + expect(format('SYSTEM FLUSH DISTRIBUTED db.dist_table ON CLUSTER prod;')).toBe(dedent` + SYSTEM FLUSH DISTRIBUTED db.dist_table + ON CLUSTER prod; + `); + }); + + it('formats SYSTEM STOP LISTEN with protocol', () => { + expect(format('SYSTEM STOP LISTEN ON CLUSTER prod TCP SECURE;')).toBe(dedent` + SYSTEM STOP LISTEN + ON CLUSTER prod TCP SECURE; + `); + }); + + it('formats SYSTEM REFRESH VIEW', () => { + expect(format('SYSTEM REFRESH VIEW db.mv_hourly;')).toBe(dedent` + SYSTEM REFRESH VIEW db.mv_hourly; + `); + }); + + it('formats SYSTEM STOP VIEWS', () => { + expect(format('SYSTEM STOP VIEWS;')).toBe(dedent` + SYSTEM STOP VIEWS; + `); + }); + + it('formats SYSTEM DROP REPLICA from table', () => { + expect(format("SYSTEM DROP REPLICA 'replica1' FROM TABLE mydb.my_replicated_table;")) + .toBe(dedent` + SYSTEM DROP REPLICA 'replica1' + FROM + TABLE mydb.my_replicated_table; + `); + }); + + it('formats SYSTEM DROP REPLICA from database', () => { + expect(format("SYSTEM DROP REPLICA 'replica1' FROM DATABASE mydb;")).toBe(dedent` + SYSTEM DROP REPLICA 'replica1' + FROM + DATABASE mydb; + `); + }); + + it('formats SYSTEM DROP REPLICA on local server', () => { + expect(format("SYSTEM DROP REPLICA 'replica1';")).toBe(dedent` + SYSTEM DROP REPLICA 'replica1'; + `); + }); + + it('formats SYSTEM DROP REPLICA from ZooKeeper path', () => { + expect( + format( + "SYSTEM DROP REPLICA 'replica1' FROM ZKPATH '/clickhouse/tables/01/mydb/my_replicated_table';" + ) + ).toBe(dedent` + SYSTEM DROP REPLICA 'replica1' + FROM + ZKPATH '/clickhouse/tables/01/mydb/my_replicated_table'; + `); + }); + + it('formats SYSTEM DROP DATABASE REPLICA', () => { + expect(format("SYSTEM DROP DATABASE REPLICA 'replica1' FROM DATABASE mydb;")).toBe(dedent` + SYSTEM DROP DATABASE REPLICA 'replica1' + FROM + DATABASE mydb; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/show + describe('SHOW statements', () => { + it('formats SHOW CREATE TABLE with INTO OUTFILE and FORMAT', () => { + expect(format("SHOW CREATE TABLE db.table INTO OUTFILE 'file.txt' FORMAT CSV;")).toBe(dedent` + SHOW CREATE TABLE db.table + INTO OUTFILE + 'file.txt' + FORMAT + CSV; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/explain + describe('EXPLAIN statements', () => { + it('formats EXPLAIN SELECT with UNION ALL and ORDER BY', () => { + expect( + format( + 'EXPLAIN AST SELECT sum(number) FROM numbers(10) UNION ALL SELECT sum(number) FROM numbers(10) ORDER BY sum(number) ASC FORMAT TSV;' + ) + ).toBe(dedent` + EXPLAIN AST SELECT sum(number) + FROM + numbers(10) + UNION ALL + SELECT + sum(number) + FROM + numbers(10) + ORDER BY + sum(number) ASC + FORMAT + TSV; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/attach + describe('ATTACH statements', () => { + it('formats ATTACH DATABASE with ON CLUSTER and SYNC', () => { + expect(format('ATTACH DATABASE IF NOT EXISTS test_db ON CLUSTER prod;')).toBe(dedent` + ATTACH DATABASE IF NOT EXISTS test_db + ON CLUSTER prod; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/detach + describe('DETACH statements', () => { + it('formats DETACH DATABASE with ON CLUSTER and SYNC', () => { + expect(format('DETACH DATABASE test_db ON CLUSTER prod PERMANENTLY SYNC;')).toBe(dedent` + DETACH DATABASE test_db + ON CLUSTER prod + PERMANENTLY + SYNC; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/exists + describe('EXISTS statements', () => { + it('formats EXISTS TEMPORARY TABLE', () => { + expect(format('EXISTS TEMPORARY TABLE temp_data;')).toBe(dedent` + EXISTS TEMPORARY TABLE temp_data; + `); + }); + + it('formats EXISTS with FORMAT', () => { + expect(format('EXISTS TABLE events FORMAT TabSeparated;')).toBe(dedent` + EXISTS TABLE events + FORMAT + TabSeparated; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/kill + describe('KILL statements', () => { + it('formats KILL QUERY with SYNC', () => { + expect(format("KILL QUERY WHERE user = 'john' SYNC;")).toBe(dedent` + KILL QUERY + WHERE + user = 'john' + SYNC; + `); + }); + + it('formats KILL QUERY with ON CLUSTER and FORMAT', () => { + expect(format('KILL QUERY ON CLUSTER prod WHERE elapsed > 300 FORMAT JSON;')).toBe(dedent` + KILL QUERY + ON CLUSTER prod + WHERE + elapsed > 300 + FORMAT + JSON; + `); + }); + + it('formats KILL QUERY with TEST', () => { + expect(format('KILL QUERY WHERE query_duration_ms > 60000 TEST;')).toBe(dedent` + KILL QUERY + WHERE + query_duration_ms > 60000 TEST; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/optimize + describe('OPTIMIZE statements', () => { + it('formats OPTIMIZE TABLE with FINAL', () => { + expect(format('OPTIMIZE TABLE my_table FINAL;')).toBe(dedent` + OPTIMIZE TABLE my_table FINAL; + `); + }); + + it('formats OPTIMIZE TABLE with PARTITION and DEDUPLICATE', () => { + expect(format('OPTIMIZE TABLE events PARTITION 202501 DEDUPLICATE;')).toBe(dedent` + OPTIMIZE TABLE events PARTITION 202501 DEDUPLICATE; + `); + }); + + it('formats OPTIMIZE TABLE with ON CLUSTER and DEDUPLICATE BY', () => { + expect(format('OPTIMIZE TABLE logs ON CLUSTER prod DEDUPLICATE BY user_id, timestamp;')) + .toBe(dedent` + OPTIMIZE TABLE logs + ON CLUSTER prod + DEDUPLICATE BY + user_id, + timestamp; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/rename + describe('RENAME statements', () => { + it('formats RENAME TABLE', () => { + expect(format('RENAME DATABASE atomic_database1 TO atomic_database2 ON CLUSTER production;')) + .toBe(dedent` + RENAME DATABASE atomic_database1 + TO atomic_database2 + ON CLUSTER production; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/exchange + describe('EXCHANGE statements', () => { + it('formats EXCHANGE TABLES', () => { + expect(format('EXCHANGE TABLES table1 AND table2;')).toBe(dedent` + EXCHANGE TABLES table1 + AND table2; + `); + }); + + it('formats EXCHANGE DICTIONARIES', () => { + expect(format('EXCHANGE DICTIONARIES dict1 AND dict2;')).toBe(dedent` + EXCHANGE DICTIONARIES dict1 + AND dict2; + `); + }); + + it('formats EXCHANGE DICTIONARIES with databases and cluster', () => { + expect(format('EXCHANGE DICTIONARIES db1.dict_A AND db2.dict_B ON CLUSTER prod;')) + .toBe(dedent` + EXCHANGE DICTIONARIES db1.dict_A + AND db2.dict_B + ON CLUSTER prod; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/set-role + describe('SET ROLE statements', () => { + it('formats SET ROLE with multiple roles', () => { + expect(format('SET ROLE admin, developer, analyst;')).toBe(dedent` + SET ROLE + admin, + developer, + analyst; + `); + }); + + it('formats SET ROLE ALL', () => { + expect(format('SET ROLE ALL;')).toBe(dedent` + SET ROLE ALL; + `); + }); + + it('formats SET ROLE ALL EXCEPT', () => { + expect(format('SET ROLE ALL EXCEPT guest, readonly;')).toBe(dedent` + SET ROLE ALL EXCEPT + guest, + readonly; + `); + }); + + it('formats SET DEFAULT ROLE NONE', () => { + expect(format('SET DEFAULT ROLE NONE TO john;')).toBe(dedent` + SET DEFAULT ROLE NONE + TO john; + `); + }); + + it('formats SET DEFAULT ROLE with single role to multiple users', () => { + expect(format('SET DEFAULT ROLE admin TO john, alice;')).toBe(dedent` + SET DEFAULT ROLE + admin + TO john, + alice; + `); + }); + + it('formats SET DEFAULT ROLE with multiple roles', () => { + expect(format('SET DEFAULT ROLE admin, developer TO john;')).toBe(dedent` + SET DEFAULT ROLE + admin, + developer + TO john; + `); + }); + + it('formats SET DEFAULT ROLE ALL to CURRENT_USER', () => { + expect(format('SET DEFAULT ROLE ALL TO CURRENT_USER;')).toBe(dedent` + SET DEFAULT ROLE ALL + TO CURRENT_USER; + `); + }); + + it('formats SET DEFAULT ROLE ALL EXCEPT', () => { + expect(format('SET DEFAULT ROLE ALL EXCEPT guest TO john, alice;')).toBe(dedent` + SET DEFAULT ROLE ALL EXCEPT + guest + TO john, + alice; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/execute_as + describe('EXECUTE AS statements', () => { + it('formats EXECUTE AS with SELECT', () => { + expect(format('EXECUTE AS james SELECT currentUser(), authenticatedUser();')).toBe(dedent` + EXECUTE AS james + SELECT + currentUser(), + authenticatedUser(); + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/move + describe('MOVE statements', () => { + it('formats MOVE USER', () => { + expect(format('MOVE USER john, alice TO disk_storage;')).toBe(dedent` + MOVE USER + john, + alice + TO disk_storage; + `); + }); + + it('formats MOVE ROLE', () => { + expect(format('MOVE ROLE admin, developer TO local_directory;')).toBe(dedent` + MOVE ROLE + admin, + developer + TO local_directory; + `); + }); + + it('formats MOVE QUOTA', () => { + expect(format('MOVE QUOTA user_quota TO replicated_storage;')).toBe(dedent` + MOVE QUOTA + user_quota + TO replicated_storage; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/check-grant + describe('CHECK GRANT statements', () => { + it('formats CHECK GRANT with simple privilege', () => { + expect(format('CHECK GRANT SELECT ON db.table')).toBe(dedent` + CHECK GRANT + SELECT ON db.table + `); + }); + + it('formats CHECK GRANT with column list', () => { + expect(format('CHECK GRANT SELECT(id, name) ON db.table')).toBe(dedent` + CHECK GRANT + SELECT (id, name) ON db.table + `); + }); + + // This one is unfortunately ugly because SELECT is a + // tabular one-line clause, and we can't make it not one. + it('formats CHECK GRANT with multiple privileges', () => { + expect(format('CHECK GRANT SELECT, INSERT ON db.table')).toBe(dedent` + CHECK GRANT + SELECT, + INSERT ON db.table + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/undrop + describe('UNDROP statements', () => { + it('formats simple UNDROP TABLE', () => { + expect(format('UNDROP TABLE my_table;')).toBe(dedent` + UNDROP TABLE my_table; + `); + }); + + it('formats UNDROP TABLE with UUID', () => { + expect(format("UNDROP TABLE my_table UUID '550e8400-e29b-41d4-a716-446655440000';")) + .toBe(dedent` + UNDROP TABLE my_table UUID '550e8400-e29b-41d4-a716-446655440000'; + `); + }); + + it('formats UNDROP TABLE with database and ON CLUSTER', () => { + expect(format('UNDROP TABLE db.my_table ON CLUSTER production;')).toBe(dedent` + UNDROP TABLE db.my_table + ON CLUSTER production; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/create/table#replace-table + describe('REPLACE TABLE statements', () => { + it('formats REPLACE TABLE with ENGINE, ORDER BY, and SELECT', () => { + expect( + format( + 'REPLACE TABLE myOldTable ENGINE = MergeTree() ORDER BY CounterID AS SELECT * FROM myOldTable WHERE CounterID <12345;' + ) + ).toBe(dedent` + REPLACE TABLE myOldTable ENGINE = MergeTree() + ORDER BY + CounterID AS + SELECT + * + FROM + myOldTable + WHERE + CounterID < 12345; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/create/view#refreshable-materialized-view + describe('Refreshable materialized view statements', () => { + it('formats CREATE MATERIALIZED VIEW with REFRESH EVERY', () => { + expect( + format('CREATE MATERIALIZED VIEW mv1 REFRESH EVERY 1 HOUR AS SELECT * FROM source_table;') + ).toBe( + dedent` + CREATE MATERIALIZED VIEW mv1 + REFRESH EVERY 1 HOUR AS + SELECT + * + FROM + source_table; + ` + ); + }); + + it('formats CREATE MATERIALIZED VIEW with REFRESH AFTER and OFFSET', () => { + expect( + format( + 'CREATE MATERIALIZED VIEW mv2 REFRESH AFTER 30 MINUTE OFFSET 5 MINUTE AS SELECT count() FROM events;' + ) + ).toBe(dedent` + CREATE MATERIALIZED VIEW mv2 + REFRESH AFTER 30 MINUTE OFFSET 5 MINUTE AS + SELECT + count() + FROM + events; + `); + }); + + it('formats CREATE MATERIALIZED VIEW with RANDOMIZE FOR', () => { + expect( + format( + 'CREATE MATERIALIZED VIEW mv3 REFRESH EVERY 1 DAY RANDOMIZE FOR 2 HOUR AS SELECT * FROM logs;' + ) + ).toBe(dedent` + CREATE MATERIALIZED VIEW mv3 + REFRESH EVERY 1 DAY + RANDOMIZE FOR 2 HOUR AS + SELECT + * + FROM + logs; + `); + }); + + it('formats CREATE MATERIALIZED VIEW with DEPENDS ON', () => { + expect( + format( + 'CREATE MATERIALIZED VIEW mv4 REFRESH EVERY 1 HOUR DEPENDS ON table1, table2 AS SELECT * FROM combined;' + ) + ).toBe(dedent` + CREATE MATERIALIZED VIEW mv4 + REFRESH EVERY 1 HOUR + DEPENDS ON + table1, + table2 AS + SELECT + * + FROM + combined; + `); + }); + + it('formats CREATE MATERIALIZED VIEW with APPEND TO', () => { + expect( + format( + 'CREATE MATERIALIZED VIEW mv5 REFRESH EVERY 1 HOUR APPEND TO target_table AS SELECT * FROM source;' + ) + ).toBe(dedent` + CREATE MATERIALIZED VIEW mv5 + REFRESH EVERY 1 HOUR + APPEND TO target_table AS + SELECT + * + FROM + source; + `); + }); + + it('formats complex refreshable materialized view with multiple clauses', () => { + expect( + format( + "CREATE MATERIALIZED VIEW IF NOT EXISTS mv6 ON CLUSTER prod REFRESH EVERY 1 HOUR RANDOMIZE FOR 30 MINUTE DEPENDS ON table1 APPEND SETTINGS max_threads = 4 AS SELECT date, count() as cnt FROM events GROUP BY date COMMENT 'Hourly aggregation';" + ) + ).toBe(dedent` + CREATE MATERIALIZED VIEW IF NOT EXISTS mv6 + ON CLUSTER prod + REFRESH EVERY 1 HOUR + RANDOMIZE FOR 30 MINUTE + DEPENDS ON + table1 + APPEND + SETTINGS + max_threads = 4 AS + SELECT + date, + count() as cnt + FROM + events + GROUP BY + date COMMENT 'Hourly aggregation'; + `); + }); + }); + + describe('CREATE FUNCTION statements', () => { + it('formats CREATE FUNCTION with simple function', () => { + expect(format('CREATE FUNCTION my_function AS (x) -> x + 1;')).toBe(dedent` + CREATE FUNCTION my_function AS (x) -> x + 1; + `); + }); + + it('formats CREATE FUNCTION with extra syntax', () => { + expect(format('CREATE FUNCTION linear_equation AS (x, k, b) -> k*x + b;')).toBe(dedent` + CREATE FUNCTION linear_equation AS (x, k, b) -> k * x + b; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/grant + describe('GRANT statements', () => { + it('formats GRANT SELECT with column list and WITH GRANT OPTION', () => { + expect(format('GRANT SELECT(x,y) ON db.table TO john WITH GRANT OPTION')).toBe(dedent` + GRANT + SELECT (x, y) ON db.table + TO john + WITH GRANT OPTION + `); + }); + + it('formats GRANT ALTER MATERIALIZE STATISTICS', () => { + expect(format('GRANT ALTER MATERIALIZE STATISTICS on db.table TO john WITH GRANT OPTION')) + .toBe(dedent` + GRANT + ALTER MATERIALIZE STATISTICS on db.table + TO john + WITH GRANT OPTION + `); + }); + + it('formats GRANT SELECT with column list', () => { + expect(format('GRANT SELECT(x,y) ON db.table TO john')).toBe(dedent` + GRANT + SELECT (x, y) ON db.table + TO john + `); + }); + + it('formats GRANT READ ON S3 with complex regex pattern', () => { + expect(format("GRANT READ ON S3('s3://mybucket/data/2024/.*\\.parquet') TO analyst")) + .toBe(dedent` + GRANT + READ ON S3 ('s3://mybucket/data/2024/.*\\.parquet') + TO analyst + `); + }); + + it('formats GRANT CURRENT GRANTS', () => { + expect(format('GRANT CURRENT GRANTS(READ ON S3) TO alice')).toBe(dedent` + GRANT CURRENT GRANTS (READ ON S3) + TO alice + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/revoke + describe('REVOKE statements', () => { + // These are a little ugly, because `ON` should be treated as a + // tabular one-line clause in this statement. But if we make it + // one, it'll make JOINs ugly (along with a bunch of other things). + + it('formats REVOKE SELECT with wildcard', () => { + expect(format('REVOKE SELECT ON accounts.* FROM john;')).toBe(dedent` + REVOKE + SELECT ON accounts.* + FROM + john; + `); + }); + + it('formats REVOKE SELECT with column list', () => { + expect(format('REVOKE SELECT(wage), SELECT(id) ON accounts.staff FROM mira;')).toBe(dedent` + REVOKE + SELECT (wage), + SELECT (id) ON accounts.staff + FROM + mira; + `); + }); + + it('formats REVOKE with ON CLUSTER and ADMIN OPTION', () => { + expect(format('REVOKE ON CLUSTER foo role FROM john;')).toBe(dedent` + REVOKE ON CLUSTER foo role + FROM + john; + `); + expect(format('REVOKE ON CLUSTER foo ADMIN OPTION FOR role FROM john;')).toBe(dedent` + REVOKE ON CLUSTER foo + ADMIN OPTION FOR role + FROM + john; + `); + }); + + it('formats REVOKE with ALL EXCEPT', () => { + expect(format('REVOKE ON CLUSTER foo ADMIN OPTION FOR role FROM john, matt ALL EXCEPT foo;')) + .toBe(dedent` + REVOKE ON CLUSTER foo + ADMIN OPTION FOR role + FROM + john, + matt + ALL EXCEPT foo; + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/check-table + describe('CHECK TABLE statements', () => { + it('formats simple CHECK TABLE', () => { + expect(format('CHECK TABLE test_table;')).toBe(dedent` + CHECK TABLE test_table; + `); + }); + + it('formats CHECK TABLE with PARTITION, FORMAT, and SETTINGS', () => { + expect( + format( + "CHECK TABLE t0 PARTITION ID '201003' FORMAT PrettyCompactMonoBlock SETTINGS check_query_single_value_result = 0" + ) + ).toBe(dedent` + CHECK TABLE t0 + PARTITION ID '201003' + FORMAT + PrettyCompactMonoBlock + SETTINGS + check_query_single_value_result = 0 + `); + }); + + it('formats CHECK TABLE with PART', () => { + expect(format("CHECK TABLE t0 PART '201003_111_222_0'")).toBe(dedent` + CHECK TABLE t0 PART '201003_111_222_0' + `); + }); + }); + + // https://clickhouse.com/docs/sql-reference/statements/describe-table + describe('DESCRIBE TABLE statements', () => { + expect(format('DESCRIBE TABLE table1;')).toBe(dedent` + DESCRIBE TABLE table1; + `); + expect(format('DESC TABLE table1;')).toBe(dedent` + DESC TABLE table1; + `); + }); + + // https://clickhouse.com/docs/sql-reference/statements/parallel_with + describe('PARALLEL WITH statements', () => { + expect( + format(` + CREATE TABLE table1(x Int32) ENGINE = MergeTree ORDER BY tuple() + PARALLEL WITH + CREATE TABLE table2(y String) ENGINE = MergeTree ORDER BY tuple(); + `) + ).toBe(dedent` + CREATE TABLE table1 (x Int32) ENGINE = MergeTree + ORDER BY + tuple() + PARALLEL WITH + CREATE TABLE table2 (y String) ENGINE = MergeTree + ORDER BY + tuple(); + `); + }); +}); diff --git a/test/features/identifiers.ts b/test/features/identifiers.ts index 942ef96de..7c4a2a63b 100644 --- a/test/features/identifiers.ts +++ b/test/features/identifiers.ts @@ -4,12 +4,14 @@ import { FormatFn } from '../../src/sqlFormatter.js'; type IdentType = | '""-qq' // with repeated-quote escaping + | '""-bs' // with backslash escaping + | '""-qq-bs' // with repeated-quote and backslash escaping | '``' // with repeated-quote escaping | '[]' // with ]] escaping | 'U&""'; // with repeated-quote escaping export default function supportsIdentifiers(format: FormatFn, identifierTypes: IdentType[]) { - if (identifierTypes.includes('""-qq')) { + if (identifierTypes.includes('""-qq') || identifierTypes.includes('""-bs')) { it('supports double-quoted identifiers', () => { expect(format('"foo JOIN bar"')).toBe('"foo JOIN bar"'); expect(format('SELECT "where" FROM "update"')).toBe(dedent` @@ -27,13 +29,35 @@ export default function supportsIdentifiers(format: FormatFn, identifierTypes: I "my table"."col name"; `); }); + } + if (identifierTypes.includes('""-qq')) { it('supports escaping double-quote by doubling it', () => { expect(format('"foo""bar"')).toBe('"foo""bar"'); }); - it('does not support escaping double-quote with a backslash', () => { - expect(() => format('"foo \\" JOIN bar"')).toThrowError('Parse error: Unexpected "'); + if (!identifierTypes.includes('""-bs')) { + it('does not support escaping double-quote with a backslash', () => { + expect(() => format('"foo \\" JOIN bar"')).toThrowError('Parse error: Unexpected "'); + }); + } + } + + if (identifierTypes.includes('""-bs')) { + it('supports escaping double-quote by escaping it with a backslash', () => { + expect(format('"foo\\"bar"')).toBe('"foo\\"bar"'); + }); + + if (!identifierTypes.includes('""-qq')) { + it('does not support escaping double-quote by doubling it', () => { + expect(format('"foo "" JOIN bar"')).toBe('"foo " " JOIN bar"'); + }); + } + } + + if (identifierTypes.includes('""-qq-bs')) { + it('supports escaping double-quote with a backslash and a repeated quote', () => { + expect(format('"foo \\" JOIN ""bar"')).toBe('"foo \\" JOIN ""bar"'); }); } diff --git a/test/features/strings.ts b/test/features/strings.ts index 4f3ae4a2a..0fd8a3962 100644 --- a/test/features/strings.ts +++ b/test/features/strings.ts @@ -10,6 +10,7 @@ type StringType = // Note: ''-qq and ''-bs can be combined to allow for both types of escaping | "''-qq" // with repeated-quote escaping | "''-bs" // with backslash escaping + | "''-qq-bs" // with repeated-quote and backslash escaping | "U&''" // with repeated-quote escaping | "N''" // with escaping style depending on whether also ''-qq or ''-bs was specified | "X''" // no escaping @@ -94,6 +95,12 @@ export default function supportsStrings(format: FormatFn, stringTypes: StringTyp } } + if (stringTypes.includes("''-qq-bs")) { + it('supports escaping single-quote with a backslash and a repeated quote', () => { + expect(format("'foo \\' JOIN ''bar'")).toBe("'foo \\' JOIN ''bar'"); + }); + } + if (stringTypes.includes("U&''")) { it('supports unicode single-quoted strings', () => { expect(format("U&'foo JOIN bar'")).toBe("U&'foo JOIN bar'");