From 80f6e1ee8924f50a5efe24ef0cfe76022adb532b Mon Sep 17 00:00:00 2001 From: Matt Basta Date: Fri, 14 Nov 2025 15:58:59 -0500 Subject: [PATCH 1/6] Add preliminary Clickhouse formatter --- README.md | 1 - src/allDialects.ts | 1 + src/index.ts | 1 + .../clickhouse/clickhouse.formatter.ts | 161 ++ .../clickhouse/clickhouse.functions.ts | 1724 +++++++++++++++++ .../clickhouse/clickhouse.keywords.ts | 782 ++++++++ src/sqlFormatter.ts | 1 + 7 files changed, 2670 insertions(+), 1 deletion(-) create mode 100644 src/languages/clickhouse/clickhouse.formatter.ts create mode 100644 src/languages/clickhouse/clickhouse.functions.ts create mode 100644 src/languages/clickhouse/clickhouse.keywords.ts diff --git a/README.md b/README.md index 85173fb45a..27306c9555 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 8b41f97358..e995aab544 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 b5677f6801..0b40d6ad32 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 0000000000..cadba79869 --- /dev/null +++ b/src/languages/clickhouse/clickhouse.formatter.ts @@ -0,0 +1,161 @@ +import { DialectOptions } from '../../dialect.js'; +import { expandPhrases } from '../../expandPhrases.js'; +import { functions } from './clickhouse.functions.js'; +import { dataTypes, keywords, keywordPhrases } from './clickhouse.keywords.js'; + +const reservedSelect = expandPhrases(['SELECT [DISTINCT]']); + +const reservedClauses = expandPhrases([ + // https://clickhouse.com/docs/sql-reference/statements/explain + 'EXPLAIN [AST | SYNTAX | QUERY TREE | PLAN | PIPELINE | ESTIMATE | TABLE OVERRIDE]', +]); + +const standardOnelineClauses = expandPhrases([ + // https://clickhouse.com/docs/sql-reference/statements/create + 'CREATE [OR REPLACE] [TEMPORARY] TABLE [IF NOT EXISTS]', + // 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} [ON CLUSTER]', + '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 | DROP 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', + // 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]', + // https://clickhouse.com/docs/sql-reference/statements/drop + 'DROP {DICTIONARY | DATABASE | USER | ROLE | QUOTA | PROFILE | SETTINGS PROFILE | VIEW | FUNCTION | NAMED COLLECTION | ROW POLICY | POLICY} [IF EXISTS]', + 'DROP [TEMPORARY] TABLE [IF EXISTS] [IF EMPTY]', + // https://clickhouse.com/docs/sql-reference/statements/exists + 'EXISTS [TEMPORARY] {TABLE | DICTIONARY | DATABASE}', + // https://clickhouse.com/docs/sql-reference/statements/kill + 'KILL QUERY [ON CLUSTER]', + // 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/set + 'SET', + // https://clickhouse.com/docs/sql-reference/statements/set-role + 'SET ROLE [DEFAULT | NONE | ALL | ALL EXCEPT]', + 'SET DEFAULT ROLE [NONE]', + // 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', + // https://clickhouse.com/docs/sql-reference/statements/move + 'MOVE {USER | ROLE | QUOTA | SETTINGS PROFILE | ROW POLICY}', + // https://clickhouse.com/docs/sql-reference/statements/check-grant + 'CHECK GRANT', + // https://clickhouse.com/docs/sql-reference/statements/undrop + 'UNDROP TABLE', +]); +const tabularOnelineClauses = expandPhrases([ + // 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 [TEMPORARY] TABLE', + 'ALTER {USER | ROLE | QUOTA | SETTINGS PROFILE} [IF EXISTS]', + 'ALTER [ROW] POLICY [IF EXISTS]', + 'ALTER NAMED COLLECTION [IF EXISTS]', + // https://clickhouse.com/docs/sql-reference/statements/delete + 'DELETE FROM', + // https://clickhouse.com/docs/sql-reference/statements/grant + 'GRANT [ON CLUSTER]', + // https://clickhouse.com/docs/sql-reference/statements/revoke + 'REVOKE [ON CLUSTER]', + // https://clickhouse.com/docs/sql-reference/statements/check-table + 'CHECK TABLE', + // https://clickhouse.com/docs/sql-reference/statements/describe-table + '{DESC | DESCRIBE} TABLE', +]); + +const reservedSetOperations = expandPhrases([ + // https://clickhouse.com/docs/sql-reference/statements/select/set-operations + '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', +]); + +// https://clickhouse.com/docs/sql-reference/syntax +export const clickhouse: DialectOptions = { + name: 'clickhouse', + tokenizerOptions: { + reservedSelect, + reservedClauses: [...reservedClauses, ...standardOnelineClauses, ...tabularOnelineClauses], + reservedSetOperations, + reservedJoins, + reservedKeywordPhrases: keywordPhrases, + + reservedKeywords: keywords, + reservedDataTypes: dataTypes, + reservedFunctionNames: functions, + nestedBlockComments: false, + underscoresInNumbers: true, + stringTypes: ['$$', "''-qq", "''-qq-bs"], + identTypes: ['""-qq', '``'], + paramTypes: { + // https://clickhouse.com/docs/sql-reference/syntax#defining-and-using-query-parameters + custom: [ + { + regex: String.raw`\{\s*[a-zA-Z0-9_]+\s*:\s*[a-zA-Z0-9_]+\s*\}`, + key: v => { + const [key] = v.split(':'); + return key.trim(); + }, + }, + ], + }, + operators: [ + // Arithmetic + '%', // modulo + + // Ternary + '?', + ':', + + // Lambda creation + '->', + ], + }, + formatOptions: { + onelineClauses: standardOnelineClauses, + tabularOnelineClauses, + }, +}; diff --git a/src/languages/clickhouse/clickhouse.functions.ts b/src/languages/clickhouse/clickhouse.functions.ts new file mode 100644 index 0000000000..d2f2c296e7 --- /dev/null +++ b/src/languages/clickhouse/clickhouse.functions.ts @@ -0,0 +1,1724 @@ +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', + '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', + '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', + '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', +]; diff --git a/src/languages/clickhouse/clickhouse.keywords.ts b/src/languages/clickhouse/clickhouse.keywords.ts new file mode 100644 index 0000000000..4f6eda91dd --- /dev/null +++ b/src/languages/clickhouse/clickhouse.keywords.ts @@ -0,0 +1,782 @@ +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', + '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 keywordPhrases: string[] = [ + // See documentation of `keywords` above + 'ADD COLUMN', + 'ADD CONSTRAINT', + 'ADD INDEX', + 'ADD PROJECTION', + 'ADD STATISTICS', + 'ADMIN OPTION FOR', + 'ALTER COLUMN', + 'ALTER DATABASE', + 'ALTER POLICY', + 'ALTER PROFILE', + 'ALTER QUOTA', + 'ALTER ROLE', + 'ALTER ROW POLICY', + 'ALTER SETTINGS PROFILE', + 'ALTER TABLE', + 'ALTER TEMPORARY TABLE', + 'ALTER USER', + 'AND STDOUT', + 'APPLY DELETED MASK', + 'ARRAY JOIN', + 'ATTACH PART', + 'ATTACH PARTITION', + 'ATTACH POLICY', + 'ATTACH PROFILE', + 'ATTACH QUOTA', + 'ATTACH ROLE', + 'ATTACH ROW POLICY', + 'ATTACH SETTINGS PROFILE', + 'ATTACH USER', + 'BEGIN TRANSACTION', + 'CHAR VARYING', + 'CHARACTER LARGE OBJECT', + 'CHARACTER VARYING', + 'CHECK ALL TABLES', + 'CHECK TABLE', + 'CLEAR COLUMN', + 'CLEAR INDEX', + 'CLEAR PROJECTION', + 'CLEAR STATISTICS', + 'COMMENT COLUMN', + 'CREATE POLICY', + 'CREATE PROFILE', + 'CREATE QUOTA', + 'CREATE ROLE', + 'CREATE ROW POLICY', + 'CREATE SETTINGS PROFILE', + 'CREATE TABLE', + 'CREATE TEMPORARY TABLE', + 'CREATE USER', + 'CURRENT GRANTS', + 'CURRENT QUOTA', + 'CURRENT ROLES', + 'CURRENT ROW', + 'CURRENT TRANSACTION', + 'DATA INNER UUID', + 'DEFAULT DATABASE', + 'DEFAULT ROLE', + 'DEPENDS ON', + 'DETACH PART', + 'DETACH PARTITION', + 'DISTINCT ON', + 'DROP COLUMN', + 'DROP CONSTRAINT', + 'DROP DEFAULT', + 'DROP DETACHED PART', + 'DROP DETACHED PARTITION', + 'DROP INDEX', + 'DROP PART', + 'DROP PARTITION', + 'DROP PROJECTION', + 'DROP STATISTICS', + 'DROP TABLE', + 'DROP TEMPORARY TABLE', + 'EMPTY AS', + 'ENABLED ROLES', + 'EPHEMERAL SEQUENTIAL', + 'EXCEPT DATABASE', + 'EXCEPT DATABASES', + 'EXCEPT TABLE', + 'EXCEPT TABLES', + 'EXCHANGE DICTIONARIES', + 'EXCHANGE TABLES', + 'EXTERNAL DDL FROM', + 'FETCH PART', + 'FETCH PARTITION', + 'FILESYSTEM CACHE', + 'FILESYSTEM CACHES', + 'FOREIGN KEY', + 'FORGET PARTITION', + 'FROM INFILE', + 'FROM SHARD', + 'GLOBAL IN', + 'GLOBAL NOT IN', + 'GRANT OPTION FOR', + 'GROUP BY', + 'GROUPING SETS', + 'IF EMPTY', + 'IF EXISTS', + 'IF NOT EXISTS', + 'IGNORE NULLS', + 'IN PARTITION', + 'INSERT INTO', + 'INTO OUTFILE', + 'IS NOT DISTINCT FROM', + 'IS NOT NULL', + 'IS NULL', + 'KEY BY', + 'KEYED BY', + 'LARGE OBJECT', + 'LEFT ARRAY JOIN', + 'LESS THAN', + 'MATERIALIZE COLUMN', + 'MATERIALIZE INDEX', + 'MATERIALIZE PROJECTION', + 'MATERIALIZE STATISTICS', + 'MATERIALIZE TTL', + 'METRICS INNER UUID', + 'MODIFY COLUMN', + 'MODIFY COMMENT', + 'MODIFY DEFINER', + 'MODIFY ORDER BY', + 'MODIFY QUERY', + 'MODIFY REFRESH', + 'MODIFY SAMPLE BY', + 'MODIFY SETTING', + 'MODIFY SQL SECURITY', + 'MODIFY STATISTICS', + 'MODIFY TTL', + 'MOVE PART', + 'MOVE PARTITION', + 'NAMED COLLECTION', + 'NO ACTION', + 'NO DELAY', + 'NO LIMITS', + 'NOT BETWEEN', + 'NOT IDENTIFIED', + 'NOT ILIKE', + 'NOT IN', + 'NOT KEYED', + 'NOT LIKE', + 'NOT OVERRIDABLE', + 'ON DELETE', + 'ON UPDATE', + 'ON VOLUME', + 'OPTIMIZE TABLE', + 'OR REPLACE', + 'ORDER BY', + 'PARTITION BY', + 'PERIODIC REFRESH', + 'PERSISTENT SEQUENTIAL', + 'PRIMARY KEY', + 'QUERY TREE', + 'RANDOMIZE FOR', + 'REMOVE SAMPLE BY', + 'REMOVE TTL', + 'RENAME COLUMN', + 'RENAME DATABASE', + 'RENAME DICTIONARY', + 'RENAME TABLE', + 'RENAME TO', + 'REPLACE PARTITION', + 'RESET SETTING', + 'RESPECT NULLS', + 'SAMPLE BY', + 'SET DEFAULT', + 'SET DEFAULT ROLE', + 'SET FAKE TIME', + 'SET NULL', + 'SET ROLE', + 'SET ROLE DEFAULT', + 'SET TRANSACTION SNAPSHOT', + 'SHOW ACCESS', + 'SHOW CREATE', + 'SHOW ENGINES', + 'SHOW FUNCTIONS', + 'SHOW GRANTS', + 'SHOW PRIVILEGES', + 'SHOW PROCESSLIST', + 'SHOW SETTING', + 'SQL SECURITY', + 'START TRANSACTION', + 'SUBPARTITION BY', + 'TABLE OVERRIDE', + 'TAGS INNER UUID', + 'TEMPORARY TABLE', + 'TO DISK', + 'TO INNER UUID', + 'TO SHARD', + 'TO TABLE', + 'TO VOLUME', + 'TRACKING ONLY', + 'UNSET FAKE TIME', + 'VALID UNTIL', + 'WITH ADMIN OPTION', + 'WITH CHECK', + 'WITH FILL', + 'WITH GRANT OPTION', + 'WITH NAME', + 'WITH REPLACE OPTION', + 'WITH TIES', +]; + +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 f89573c80c..d65159b6f6 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', From d457a9833ca9e379d2f9dad1a00a297f18853e8e Mon Sep 17 00:00:00 2001 From: Matt Basta Date: Mon, 17 Nov 2025 17:38:23 -0500 Subject: [PATCH 2/6] Get formatter working with existing features --- .../clickhouse/clickhouse.formatter.ts | 169 ++++++++- .../clickhouse/clickhouse.keywords.ts | 340 ++++-------------- test/clickhouse.test.ts | 203 +++++++++++ test/features/identifiers.ts | 30 +- test/features/strings.ts | 7 + 5 files changed, 459 insertions(+), 290 deletions(-) create mode 100644 test/clickhouse.test.ts diff --git a/src/languages/clickhouse/clickhouse.formatter.ts b/src/languages/clickhouse/clickhouse.formatter.ts index cadba79869..f7e9b550c2 100644 --- a/src/languages/clickhouse/clickhouse.formatter.ts +++ b/src/languages/clickhouse/clickhouse.formatter.ts @@ -1,23 +1,45 @@ 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, keywordPhrases } from './clickhouse.keywords.js'; +import { dataTypes, keywords } from './clickhouse.keywords.js'; const reservedSelect = expandPhrases(['SELECT [DISTINCT]']); const reservedClauses = expandPhrases([ - // https://clickhouse.com/docs/sql-reference/statements/explain - 'EXPLAIN [AST | SYNTAX | QUERY TREE | PLAN | PIPELINE | ESTIMATE | TABLE OVERRIDE]', + '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', ]); const standardOnelineClauses = expandPhrases([ // https://clickhouse.com/docs/sql-reference/statements/create 'CREATE [OR REPLACE] [TEMPORARY] TABLE [IF NOT EXISTS]', +]); +const tabularOnelineClauses = expandPhrases([ // 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} [ON CLUSTER]', - '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 | DROP FILESYSTEM CACHE}', + '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', @@ -56,8 +78,6 @@ const standardOnelineClauses = expandPhrases([ 'RENAME [TABLE | DICTIONARY | DATABASE]', // https://clickhouse.com/docs/sql-reference/statements/exchange 'EXCHANGE {TABLES | DICTIONARIES}', - // https://clickhouse.com/docs/sql-reference/statements/set - 'SET', // https://clickhouse.com/docs/sql-reference/statements/set-role 'SET ROLE [DEFAULT | NONE | ALL | ALL EXCEPT]', 'SET DEFAULT ROLE [NONE]', @@ -73,8 +93,6 @@ const standardOnelineClauses = expandPhrases([ 'CHECK GRANT', // https://clickhouse.com/docs/sql-reference/statements/undrop 'UNDROP TABLE', -]); -const tabularOnelineClauses = expandPhrases([ // https://clickhouse.com/docs/sql-reference/statements/create 'CREATE {DATABASE | NAMED COLLECTION} [IF NOT EXISTS]', 'CREATE [OR REPLACE] {VIEW | DICTIONARY} [IF NOT EXISTS]', @@ -89,8 +107,73 @@ const tabularOnelineClauses = expandPhrases([ 'ALTER {USER | ROLE | QUOTA | SETTINGS PROFILE} [IF EXISTS]', 'ALTER [ROW] POLICY [IF EXISTS]', 'ALTER NAMED COLLECTION [IF EXISTS]', + // https://clickhouse.com/docs/sql-reference/statements/alter/user + 'RENAME TO', + 'DEFAULT ROLE [ALL [EXCEPT]]', + '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]', + // 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]', + 'MODIFY STATISTICS', + '{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/alter/view + 'MODIFY QUERY', + // https://clickhouse.com/docs/sql-reference/statements/create/view#refreshable-materialized-view + 'REFRESH {EVERY | AFTER}', + 'RANDOMIZE FOR', + 'DEPENDS ON', + '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]', // https://clickhouse.com/docs/sql-reference/statements/revoke @@ -102,7 +185,7 @@ const tabularOnelineClauses = expandPhrases([ ]); const reservedSetOperations = expandPhrases([ - // https://clickhouse.com/docs/sql-reference/statements/select/set-operations + // https://clickhouse.com/docs/sql-reference/statements/select/union 'UNION [ALL | DISTINCT]', // https://clickhouse.com/docs/sql-reference/statements/parallel_with 'PARALLEL WITH', @@ -113,6 +196,8 @@ const reservedJoins = expandPhrases([ '[GLOBAL] [INNER|LEFT|RIGHT|FULL|CROSS] [OUTER|SEMI|ANTI|ANY|ALL|ASOF] JOIN', ]); +const reservedKeywordPhrases = expandPhrases(['{ROWS | RANGE} BETWEEN']); + // https://clickhouse.com/docs/sql-reference/syntax export const clickhouse: DialectOptions = { name: 'clickhouse', @@ -121,23 +206,25 @@ export const clickhouse: DialectOptions = { reservedClauses: [...reservedClauses, ...standardOnelineClauses, ...tabularOnelineClauses], reservedSetOperations, reservedJoins, - reservedKeywordPhrases: keywordPhrases, + reservedKeywordPhrases, reservedKeywords: keywords, reservedDataTypes: dataTypes, reservedFunctionNames: functions, + extraParens: ['[]'], + lineCommentTypes: ['#', '--'], nestedBlockComments: false, underscoresInNumbers: true, - stringTypes: ['$$', "''-qq", "''-qq-bs"], - identTypes: ['""-qq', '``'], + stringTypes: ['$$', "''-qq-bs"], + identTypes: ['""-qq-bs', '``'], paramTypes: { // https://clickhouse.com/docs/sql-reference/syntax#defining-and-using-query-parameters custom: [ { - regex: String.raw`\{\s*[a-zA-Z0-9_]+\s*:\s*[a-zA-Z0-9_]+\s*\}`, + regex: String.raw`\{\s*[^:]+:[^}]+\}`, key: v => { - const [key] = v.split(':'); - return key.trim(); + const match = /\{([^:]+):/.exec(v); + return match ? match[1].trim() : v; }, }, ], @@ -153,9 +240,59 @@ export const clickhouse: DialectOptions = { // Lambda creation '->', ], + postProcess, }, formatOptions: { - onelineClauses: standardOnelineClauses, + 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) => { + // 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') + ) { + const nextToken = tokens[i + 1] || EOF_TOKEN; + const prevToken = tokens[i - 1] || EOF_TOKEN; + + // 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 + } + + return token; + }); +} diff --git a/src/languages/clickhouse/clickhouse.keywords.ts b/src/languages/clickhouse/clickhouse.keywords.ts index 4f6eda91dd..938b880a46 100644 --- a/src/languages/clickhouse/clickhouse.keywords.ts +++ b/src/languages/clickhouse/clickhouse.keywords.ts @@ -157,7 +157,9 @@ export const keywords: string[] = [ 'HOUR', 'HOURS', 'HTTP', - 'ID', + // Disabling this because it's a keyword, but formats far more than + // it should. + // 'ID', 'IDENTIFIED', 'IF', 'IGNORE', @@ -433,216 +435,12 @@ export const keywords: string[] = [ 'ZKPATH', ]; -export const keywordPhrases: string[] = [ - // See documentation of `keywords` above - 'ADD COLUMN', - 'ADD CONSTRAINT', - 'ADD INDEX', - 'ADD PROJECTION', - 'ADD STATISTICS', - 'ADMIN OPTION FOR', - 'ALTER COLUMN', - 'ALTER DATABASE', - 'ALTER POLICY', - 'ALTER PROFILE', - 'ALTER QUOTA', - 'ALTER ROLE', - 'ALTER ROW POLICY', - 'ALTER SETTINGS PROFILE', - 'ALTER TABLE', - 'ALTER TEMPORARY TABLE', - 'ALTER USER', - 'AND STDOUT', - 'APPLY DELETED MASK', - 'ARRAY JOIN', - 'ATTACH PART', - 'ATTACH PARTITION', - 'ATTACH POLICY', - 'ATTACH PROFILE', - 'ATTACH QUOTA', - 'ATTACH ROLE', - 'ATTACH ROW POLICY', - 'ATTACH SETTINGS PROFILE', - 'ATTACH USER', - 'BEGIN TRANSACTION', - 'CHAR VARYING', - 'CHARACTER LARGE OBJECT', - 'CHARACTER VARYING', - 'CHECK ALL TABLES', - 'CHECK TABLE', - 'CLEAR COLUMN', - 'CLEAR INDEX', - 'CLEAR PROJECTION', - 'CLEAR STATISTICS', - 'COMMENT COLUMN', - 'CREATE POLICY', - 'CREATE PROFILE', - 'CREATE QUOTA', - 'CREATE ROLE', - 'CREATE ROW POLICY', - 'CREATE SETTINGS PROFILE', - 'CREATE TABLE', - 'CREATE TEMPORARY TABLE', - 'CREATE USER', - 'CURRENT GRANTS', - 'CURRENT QUOTA', - 'CURRENT ROLES', - 'CURRENT ROW', - 'CURRENT TRANSACTION', - 'DATA INNER UUID', - 'DEFAULT DATABASE', - 'DEFAULT ROLE', - 'DEPENDS ON', - 'DETACH PART', - 'DETACH PARTITION', - 'DISTINCT ON', - 'DROP COLUMN', - 'DROP CONSTRAINT', - 'DROP DEFAULT', - 'DROP DETACHED PART', - 'DROP DETACHED PARTITION', - 'DROP INDEX', - 'DROP PART', - 'DROP PARTITION', - 'DROP PROJECTION', - 'DROP STATISTICS', - 'DROP TABLE', - 'DROP TEMPORARY TABLE', - 'EMPTY AS', - 'ENABLED ROLES', - 'EPHEMERAL SEQUENTIAL', - 'EXCEPT DATABASE', - 'EXCEPT DATABASES', - 'EXCEPT TABLE', - 'EXCEPT TABLES', - 'EXCHANGE DICTIONARIES', - 'EXCHANGE TABLES', - 'EXTERNAL DDL FROM', - 'FETCH PART', - 'FETCH PARTITION', - 'FILESYSTEM CACHE', - 'FILESYSTEM CACHES', - 'FOREIGN KEY', - 'FORGET PARTITION', - 'FROM INFILE', - 'FROM SHARD', - 'GLOBAL IN', - 'GLOBAL NOT IN', - 'GRANT OPTION FOR', - 'GROUP BY', - 'GROUPING SETS', - 'IF EMPTY', - 'IF EXISTS', - 'IF NOT EXISTS', - 'IGNORE NULLS', - 'IN PARTITION', - 'INSERT INTO', - 'INTO OUTFILE', - 'IS NOT DISTINCT FROM', - 'IS NOT NULL', - 'IS NULL', - 'KEY BY', - 'KEYED BY', - 'LARGE OBJECT', - 'LEFT ARRAY JOIN', - 'LESS THAN', - 'MATERIALIZE COLUMN', - 'MATERIALIZE INDEX', - 'MATERIALIZE PROJECTION', - 'MATERIALIZE STATISTICS', - 'MATERIALIZE TTL', - 'METRICS INNER UUID', - 'MODIFY COLUMN', - 'MODIFY COMMENT', - 'MODIFY DEFINER', - 'MODIFY ORDER BY', - 'MODIFY QUERY', - 'MODIFY REFRESH', - 'MODIFY SAMPLE BY', - 'MODIFY SETTING', - 'MODIFY SQL SECURITY', - 'MODIFY STATISTICS', - 'MODIFY TTL', - 'MOVE PART', - 'MOVE PARTITION', - 'NAMED COLLECTION', - 'NO ACTION', - 'NO DELAY', - 'NO LIMITS', - 'NOT BETWEEN', - 'NOT IDENTIFIED', - 'NOT ILIKE', - 'NOT IN', - 'NOT KEYED', - 'NOT LIKE', - 'NOT OVERRIDABLE', - 'ON DELETE', - 'ON UPDATE', - 'ON VOLUME', - 'OPTIMIZE TABLE', - 'OR REPLACE', - 'ORDER BY', - 'PARTITION BY', - 'PERIODIC REFRESH', - 'PERSISTENT SEQUENTIAL', - 'PRIMARY KEY', - 'QUERY TREE', - 'RANDOMIZE FOR', - 'REMOVE SAMPLE BY', - 'REMOVE TTL', - 'RENAME COLUMN', - 'RENAME DATABASE', - 'RENAME DICTIONARY', - 'RENAME TABLE', - 'RENAME TO', - 'REPLACE PARTITION', - 'RESET SETTING', - 'RESPECT NULLS', - 'SAMPLE BY', - 'SET DEFAULT', - 'SET DEFAULT ROLE', - 'SET FAKE TIME', - 'SET NULL', - 'SET ROLE', - 'SET ROLE DEFAULT', - 'SET TRANSACTION SNAPSHOT', - 'SHOW ACCESS', - 'SHOW CREATE', - 'SHOW ENGINES', - 'SHOW FUNCTIONS', - 'SHOW GRANTS', - 'SHOW PRIVILEGES', - 'SHOW PROCESSLIST', - 'SHOW SETTING', - 'SQL SECURITY', - 'START TRANSACTION', - 'SUBPARTITION BY', - 'TABLE OVERRIDE', - 'TAGS INNER UUID', - 'TEMPORARY TABLE', - 'TO DISK', - 'TO INNER UUID', - 'TO SHARD', - 'TO TABLE', - 'TO VOLUME', - 'TRACKING ONLY', - 'UNSET FAKE TIME', - 'VALID UNTIL', - 'WITH ADMIN OPTION', - 'WITH CHECK', - 'WITH FILL', - 'WITH GRANT OPTION', - 'WITH NAME', - 'WITH REPLACE OPTION', - 'WITH TIES', -]; - 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', + 'AGGREGATEFUNCTION', + 'ARRAY', + 'BFLOAT16', 'BIGINT', 'BIGINT SIGNED', 'BIGINT UNSIGNED', @@ -653,7 +451,7 @@ export const dataTypes: string[] = [ 'BLOB', 'BYTE', 'BYTEA', - 'Bool', + 'BOOL', 'CHAR', 'CHAR LARGE OBJECT', 'CHAR VARYING', @@ -664,26 +462,26 @@ export const dataTypes: string[] = [ 'DEC', 'DOUBLE', 'DOUBLE PRECISION', - 'Date', - 'Date32', - 'DateTime', - 'DateTime32', - 'DateTime64', - 'Decimal', - 'Decimal128', - 'Decimal256', - 'Decimal32', - 'Decimal64', - 'Dynamic', + 'DATE', + 'DATE32', + 'DATETIME', + 'DATETIME32', + 'DATETIME64', + 'DECIMAL', + 'DECIMAL128', + 'DECIMAL256', + 'DECIMAL32', + 'DECIMAL64', + 'DYNAMIC', + 'ENUM', 'ENUM', - 'Enum', - 'Enum16', - 'Enum8', + 'ENUM16', + 'ENUM8', 'FIXED', 'FLOAT', - 'FixedString', - 'Float32', - 'Float64', + 'FIXEDSTRING', + 'FLOAT32', + 'FLOAT64', 'GEOMETRY', 'INET4', 'INET6', @@ -696,38 +494,38 @@ export const dataTypes: string[] = [ 'INTEGER', 'INTEGER SIGNED', 'INTEGER UNSIGNED', - 'IPv4', - 'IPv6', - 'Int128', - 'Int16', - 'Int256', - 'Int32', - 'Int64', - 'Int8', - 'IntervalDay', - 'IntervalHour', - 'IntervalMicrosecond', - 'IntervalMillisecond', - 'IntervalMinute', - 'IntervalMonth', - 'IntervalNanosecond', - 'IntervalQuarter', - 'IntervalSecond', - 'IntervalWeek', - 'IntervalYear', + 'IPV4', + 'IPV6', + 'INT128', + 'INT16', + 'INT256', + 'INT32', + 'INT64', + 'INT8', + 'INTERVALDAY', + 'INTERVALHOUR', + 'INTERVALMICROSECOND', + 'INTERVALMILLISECOND', + 'INTERVALMINUTE', + 'INTERVALMONTH', + 'INTERVALNANOSECOND', + 'INTERVALQUARTER', + 'INTERVALSECOND', + 'INTERVALWEEK', + 'INTERVALYEAR', 'JSON', 'LONGBLOB', 'LONGTEXT', - 'LineString', - 'LowCardinality', + 'LINESTRING', + 'LOWCARDINALITY', 'MEDIUMBLOB', 'MEDIUMINT', 'MEDIUMINT SIGNED', 'MEDIUMINT UNSIGNED', 'MEDIUMTEXT', - 'Map', - 'MultiLineString', - 'MultiPolygon', + 'MAP', + 'MULTILINESTRING', + 'MULTIPOLYGON', 'NATIONAL CHAR', 'NATIONAL CHAR VARYING', 'NATIONAL CHARACTER', @@ -738,22 +536,22 @@ export const dataTypes: string[] = [ 'NCHAR VARYING', 'NUMERIC', 'NVARCHAR', - 'Nested', - 'Nothing', - 'Nullable', - 'Object', - 'Point', - 'Polygon', + 'NESTED', + 'NOTHING', + 'NULLABLE', + 'OBJECT', + 'POINT', + 'POLYGON', 'REAL', - 'Ring', + 'RING', 'SET', 'SIGNED', 'SINGLE', 'SMALLINT', 'SMALLINT SIGNED', 'SMALLINT UNSIGNED', - 'SimpleAggregateFunction', - 'String', + 'SIMPLEAGGREGATEFUNCTION', + 'STRING', 'TEXT', 'TIMESTAMP', 'TINYBLOB', @@ -761,22 +559,22 @@ export const dataTypes: string[] = [ 'TINYINT SIGNED', 'TINYINT UNSIGNED', 'TINYTEXT', - 'Time', - 'Time64', - 'Tuple', - 'UInt128', - 'UInt16', - 'UInt256', - 'UInt32', - 'UInt64', - 'UInt8', + 'TIME', + 'TIME64', + 'TUPLE', + 'UINT128', + 'UINT16', + 'UINT256', + 'UINT32', + 'UINT64', + 'UINT8', 'UNSIGNED', 'UUID', 'VARBINARY', 'VARCHAR', 'VARCHAR2', - 'Variant', + 'VARIANT', 'YEAR', - 'bool', - 'boolean', + 'BOOL', + 'BOOLEAN', ]; diff --git a/test/clickhouse.test.ts b/test/clickhouse.test.ts new file mode 100644 index 0000000000..8eb48ba4fe --- /dev/null +++ b/test/clickhouse.test.ts @@ -0,0 +1,203 @@ +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: true, + }); + 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('SELECT\n 1 IN foo;'); + + expect(format('SELECT foo in (1,2,3);')).toBe('SELECT\n foo IN (1, 2, 3);'); + expect(format('SELECT "foo" in (1,2,3);')).toBe('SELECT\n "foo" IN (1, 2, 3);'); + expect(format('SELECT 1 in (1,2,3);')).toBe('SELECT\n 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('SELECT\n 1 = ANY foo;'); + + expect(format('SELECT foo = any (1,2,3);')).toBe('SELECT\n foo = ANY (1, 2, 3);'); + expect(format('SELECT "foo" = any (1,2,3);')).toBe('SELECT\n "foo" = ANY (1, 2, 3);'); + expect(format('SELECT 1 = any (1,2,3);')).toBe('SELECT\n 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('SELECT\n in(foo, [1, 2, 3]);'); + expect(format('SELECT in("foo", "bar");')).toBe('SELECT\n in("foo", "bar");'); + }); + it('should respect the ANY operator as a keyword when used as a function', () => { + expect(format('SELECT any(foo);')).toBe('SELECT\n any(foo);'); + expect(format('SELECT any("foo");')).toBe('SELECT\n 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( + "SELECT\n {'bar': 'baz'};" + ); + }); +}); diff --git a/test/features/identifiers.ts b/test/features/identifiers.ts index 942ef96de7..7c4a2a63b9 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 4f3ae4a2a0..0fd8a39621 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'"); From 510997c4ae5b5372ccce982b531b1549fe78dca7 Mon Sep 17 00:00:00 2001 From: Matt Basta Date: Tue, 18 Nov 2025 17:57:44 -0500 Subject: [PATCH 3/6] Checkpoint tests --- .../clickhouse/clickhouse.formatter.ts | 103 +- .../clickhouse/clickhouse.functions.ts | 27 + test/clickhouse.test.ts | 1508 ++++++++++++++++- 3 files changed, 1602 insertions(+), 36 deletions(-) diff --git a/src/languages/clickhouse/clickhouse.formatter.ts b/src/languages/clickhouse/clickhouse.formatter.ts index f7e9b550c2..4414662b0d 100644 --- a/src/languages/clickhouse/clickhouse.formatter.ts +++ b/src/languages/clickhouse/clickhouse.formatter.ts @@ -4,7 +4,11 @@ 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]']); +const reservedSelect = expandPhrases([ + 'SELECT [DISTINCT]', + // https://clickhouse.com/docs/sql-reference/statements/alter/view + 'MODIFY QUERY SELECT [DISTINCT]', +]); const reservedClauses = expandPhrases([ 'SET', @@ -28,6 +32,22 @@ const reservedClauses = expandPhrases([ // https://clickhouse.com/docs/sql-reference/statements/insert-into 'INSERT INTO', 'VALUES', + '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', + 'TYPE', ]); const standardOnelineClauses = expandPhrases([ @@ -35,10 +55,12 @@ const standardOnelineClauses = expandPhrases([ '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} [ON CLUSTER]', + '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}', @@ -56,6 +78,7 @@ const tabularOnelineClauses = expandPhrases([ '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}]', @@ -65,32 +88,28 @@ const tabularOnelineClauses = expandPhrases([ '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 | USER | ROLE | QUOTA | PROFILE | SETTINGS PROFILE | VIEW | FUNCTION | NAMED COLLECTION | ROW POLICY | POLICY} [IF EXISTS]', 'DROP [TEMPORARY] TABLE [IF EXISTS] [IF EMPTY]', // https://clickhouse.com/docs/sql-reference/statements/exists 'EXISTS [TEMPORARY] {TABLE | DICTIONARY | DATABASE}', // https://clickhouse.com/docs/sql-reference/statements/kill - 'KILL QUERY [ON CLUSTER]', + '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/set-role - 'SET ROLE [DEFAULT | NONE | ALL | ALL EXCEPT]', - 'SET DEFAULT ROLE [NONE]', // 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', - // https://clickhouse.com/docs/sql-reference/statements/move - 'MOVE {USER | ROLE | QUOTA | SETTINGS PROFILE | ROW POLICY}', - // https://clickhouse.com/docs/sql-reference/statements/check-grant - 'CHECK GRANT', + 'TO', // https://clickhouse.com/docs/sql-reference/statements/undrop 'UNDROP TABLE', // https://clickhouse.com/docs/sql-reference/statements/create @@ -109,7 +128,6 @@ const tabularOnelineClauses = expandPhrases([ 'ALTER NAMED COLLECTION [IF EXISTS]', // https://clickhouse.com/docs/sql-reference/statements/alter/user 'RENAME TO', - 'DEFAULT ROLE [ALL [EXCEPT]]', 'GRANTEES', 'NOT IDENTIFIED', 'RESET AUTHENTICATION METHODS TO NEW', @@ -120,7 +138,8 @@ const tabularOnelineClauses = expandPhrases([ '{ADD | MODIFY} SETTINGS', 'ADD PROFILES', // https://clickhouse.com/docs/sql-reference/statements/alter/apply-deleted-mask - 'APPLY DELETED MASK [IN PARTITION]', + '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 @@ -142,6 +161,11 @@ const tabularOnelineClauses = expandPhrases([ '{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]', @@ -150,7 +174,6 @@ const tabularOnelineClauses = expandPhrases([ 'REMOVE TTL', // https://clickhouse.com/docs/sql-reference/statements/alter/statistics 'ADD STATISTICS [IF NOT EXISTS]', - 'MODIFY STATISTICS', '{DROP | CLEAR} STATISTICS [IF EXISTS]', 'MATERIALIZE STATISTICS [ALL | IF EXISTS]', // https://clickhouse.com/docs/sql-reference/statements/alter/quota @@ -163,23 +186,25 @@ const tabularOnelineClauses = expandPhrases([ // 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/alter/view - 'MODIFY QUERY', // https://clickhouse.com/docs/sql-reference/statements/create/view#refreshable-materialized-view 'REFRESH {EVERY | AFTER}', 'RANDOMIZE FOR', - 'DEPENDS ON', + '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 ON CLUSTER', + 'GRANT CURRENT GRANTS', + 'WITH GRANT OPTION', // https://clickhouse.com/docs/sql-reference/statements/revoke - 'REVOKE [ON CLUSTER]', + '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', ]); @@ -196,7 +221,10 @@ const reservedJoins = expandPhrases([ '[GLOBAL] [INNER|LEFT|RIGHT|FULL|CROSS] [OUTER|SEMI|ANTI|ANY|ALL|ASOF] JOIN', ]); -const reservedKeywordPhrases = expandPhrases(['{ROWS | RANGE} BETWEEN']); +const reservedKeywordPhrases = expandPhrases([ + '{ROWS | RANGE} BETWEEN', + 'ALTER MATERIALIZE STATISTICS', +]); // https://clickhouse.com/docs/sql-reference/syntax export const clickhouse: DialectOptions = { @@ -260,15 +288,15 @@ export const clickhouse: DialectOptions = { */ 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') ) { - const nextToken = tokens[i + 1] || EOF_TOKEN; - const prevToken = tokens[i - 1] || EOF_TOKEN; - // Must be followed by ( to be a function if (nextToken.text !== '(') { // Not followed by ( means it's an operator/keyword, convert to uppercase @@ -293,6 +321,37 @@ function postProcess(tokens: Token[]): Token[] { // 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 index d2f2c296e7..281f0d45ac 100644 --- a/src/languages/clickhouse/clickhouse.functions.ts +++ b/src/languages/clickhouse/clickhouse.functions.ts @@ -292,6 +292,7 @@ export const functions: string[] = [ 'atan', 'atan2', 'atanh', + 'authenticatedUser', 'avg', 'avgWeighted', 'bar', @@ -1026,6 +1027,7 @@ export const functions: string[] = [ 'nullIf', 'nullIn', 'nullInIgnoreSet', + 'numbers', 'numericIndexedVectorAllValueSum', 'numericIndexedVectorBuild', 'numericIndexedVectorCardinality', @@ -1282,6 +1284,7 @@ export const functions: string[] = [ 'serverTimeZone', 'serverTimezone', 'serverUUID', + 'set', 'shardCount', 'shardNum', 'showCertificate', @@ -1721,4 +1724,28 @@ export const functions: string[] = [ '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/test/clickhouse.test.ts b/test/clickhouse.test.ts index 8eb48ba4fe..5c343b2369 100644 --- a/test/clickhouse.test.ts +++ b/test/clickhouse.test.ts @@ -40,12 +40,24 @@ describe('ClickhouseFormatter', () => { tableComment: true, }); supportsDropTable(format, { ifExists: true }); + supportsAlterTable(format, { addColumn: true, dropColumn: true, renameTo: true, - renameColumn: 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); @@ -170,34 +182,1502 @@ describe('ClickhouseFormatter', () => { 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('SELECT\n 1 IN foo;'); + expect(format('SELECT 1 in foo;')).toBe(dedent` + SELECT + 1 IN foo; + `); - expect(format('SELECT foo in (1,2,3);')).toBe('SELECT\n foo IN (1, 2, 3);'); - expect(format('SELECT "foo" in (1,2,3);')).toBe('SELECT\n "foo" IN (1, 2, 3);'); - expect(format('SELECT 1 in (1,2,3);')).toBe('SELECT\n 1 IN (1, 2, 3);'); + 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('SELECT\n 1 = ANY foo;'); + expect(format('SELECT 1 = any foo;')).toBe(dedent` + SELECT + 1 = ANY foo; + `); - expect(format('SELECT foo = any (1,2,3);')).toBe('SELECT\n foo = ANY (1, 2, 3);'); - expect(format('SELECT "foo" = any (1,2,3);')).toBe('SELECT\n "foo" = ANY (1, 2, 3);'); - expect(format('SELECT 1 = any (1,2,3);')).toBe('SELECT\n 1 = ANY (1, 2, 3);'); + 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('SELECT\n in(foo, [1, 2, 3]);'); - expect(format('SELECT in("foo", "bar");')).toBe('SELECT\n in("foo", "bar");'); + 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('SELECT\n any(foo);'); - expect(format('SELECT any("foo");')).toBe('SELECT\n any("foo");'); + 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( - "SELECT\n {'bar': 'baz'};" + 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/statements/select/union + describe('UNION statements', () => { + // + }); + + // https://clickhouse.com/docs/sql-reference/window-functions + describe('Window functions', () => { + // + }); + + // 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 + describe('ALTER statements', () => { + // + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/user + describe('ALTER USER statements', () => { + // + }); + + // 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', () => { + // + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/delete + describe('ALTER DELETE statements', () => { + // + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/order-by + describe('ALTER ORDER BY statements', () => { + // + }); + + // https://clickhouse.com/docs/sql-reference/statements/alter/sample-by + describe('ALTER SAMPLE BY statements', () => { + // + }); + + // 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; + `); + // NOTE: This is a little ugly because the commas separate parameters + // and not intervals. I'd prefer this to look like this: + // + // 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; + `); + // NOTE: These are a little bit ugly. Ideally this query would be + // formatted something like this: + // + // 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; + // + // That's not really in the cards here, though, without taking away + // from some of the other queries. + }); + }); + + // 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 TABLE with quoted identifiers', () => { + expect(format('DROP TABLE IF EXISTS "default"."snapshot";')).toBe(dedent` + DROP TABLE IF EXISTS "default"."snapshot"; + `); + }); + + 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 ON db1.table1;')).toBe(dedent` + DROP ROW POLICY IF EXISTS policy1 ON db1.table1; + `); + }); + + it('formats DROP POLICY short form', () => { + expect(format('DROP POLICY IF EXISTS policy1 ON db1.table1;')).toBe(dedent` + DROP POLICY IF EXISTS policy1 ON db1.table1; + `); + }); + + it('formats DROP QUOTA', () => { + expect(format('DROP QUOTA IF EXISTS quota1 ON CLUSTER my_cluster;')).toBe(dedent` + DROP QUOTA IF EXISTS quota1 + ON CLUSTER my_cluster; + `); + }); + + it('formats DROP SETTINGS PROFILE', () => { + expect(format('DROP SETTINGS PROFILE IF EXISTS profile1 ON CLUSTER my_cluster;')).toBe(dedent` + DROP SETTINGS PROFILE IF EXISTS profile1 + 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'; + `); + }); + }); + + // 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(); + `); + }); }); From be7a20d77a329689049d5761b0c205a6d09ffbfc Mon Sep 17 00:00:00 2001 From: Matt Basta Date: Wed, 19 Nov 2025 09:56:13 -0500 Subject: [PATCH 4/6] Breaking tests for consistency within Clickhouse formatter --- .../clickhouse/clickhouse.formatter.ts | 21 +- test/clickhouse.test.ts | 239 ++++++++++-------- 2 files changed, 147 insertions(+), 113 deletions(-) diff --git a/src/languages/clickhouse/clickhouse.formatter.ts b/src/languages/clickhouse/clickhouse.formatter.ts index 4414662b0d..a5a2fa1db0 100644 --- a/src/languages/clickhouse/clickhouse.formatter.ts +++ b/src/languages/clickhouse/clickhouse.formatter.ts @@ -48,6 +48,13 @@ const reservedClauses = expandPhrases([ // https://clickhouse.com/docs/sql-reference/statements/alter/statistics 'MODIFY STATISTICS', '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]', + 'DROP [TEMPORARY] TABLE [IF EXISTS] [IF EMPTY]', ]); const standardOnelineClauses = expandPhrases([ @@ -56,7 +63,7 @@ const standardOnelineClauses = expandPhrases([ ]); const tabularOnelineClauses = expandPhrases([ 'ALL EXCEPT', - 'ON CLUSTER', + // 'ON CLUSTER', // https://clickhouse.com/docs/sql-reference/statements/update 'UPDATE', // https://clickhouse.com/docs/sql-reference/statements/system @@ -91,8 +98,7 @@ const tabularOnelineClauses = expandPhrases([ 'PERMANENTLY', 'SYNC', // https://clickhouse.com/docs/sql-reference/statements/drop - 'DROP {DICTIONARY | DATABASE | USER | ROLE | QUOTA | PROFILE | SETTINGS PROFILE | VIEW | FUNCTION | NAMED COLLECTION | ROW POLICY | POLICY} [IF EXISTS]', - 'DROP [TEMPORARY] TABLE [IF EXISTS] [IF EMPTY]', + 'DROP {DICTIONARY | DATABASE | PROFILE | VIEW | FUNCTION | NAMED COLLECTION} [IF EXISTS]', // https://clickhouse.com/docs/sql-reference/statements/exists 'EXISTS [TEMPORARY] {TABLE | DICTIONARY | DATABASE}', // https://clickhouse.com/docs/sql-reference/statements/kill @@ -100,7 +106,7 @@ const tabularOnelineClauses = expandPhrases([ // https://clickhouse.com/docs/sql-reference/statements/optimize 'OPTIMIZE TABLE', // https://clickhouse.com/docs/sql-reference/statements/rename - 'RENAME [TABLE | DICTIONARY | DATABASE]', + 'RENAME {TABLE | DICTIONARY | DATABASE}', // https://clickhouse.com/docs/sql-reference/statements/exchange 'EXCHANGE {TABLES | DICTIONARIES}', // https://clickhouse.com/docs/sql-reference/statements/truncate @@ -122,12 +128,10 @@ const tabularOnelineClauses = expandPhrases([ // 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 {USER | ROLE | QUOTA | SETTINGS PROFILE} [IF EXISTS]', - 'ALTER [ROW] POLICY [IF EXISTS]', 'ALTER NAMED COLLECTION [IF EXISTS]', // https://clickhouse.com/docs/sql-reference/statements/alter/user - 'RENAME TO', 'GRANTEES', 'NOT IDENTIFIED', 'RESET AUTHENTICATION METHODS TO NEW', @@ -165,7 +169,7 @@ const tabularOnelineClauses = expandPhrases([ 'GRANULARITY', 'AFTER', 'FIRST', - + // https://clickhouse.com/docs/sql-reference/statements/alter/constraint 'ADD CONSTRAINT [IF NOT EXISTS]', 'DROP CONSTRAINT [IF EXISTS]', @@ -224,6 +228,7 @@ const reservedJoins = expandPhrases([ const reservedKeywordPhrases = expandPhrases([ '{ROWS | RANGE} BETWEEN', 'ALTER MATERIALIZE STATISTICS', + 'RENAME TO', ]); // https://clickhouse.com/docs/sql-reference/syntax diff --git a/test/clickhouse.test.ts b/test/clickhouse.test.ts index 5c343b2369..a11b03ec83 100644 --- a/test/clickhouse.test.ts +++ b/test/clickhouse.test.ts @@ -435,8 +435,7 @@ describe('ClickhouseFormatter', () => { 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 + DELETE FROM db.table ON CLUSTER foo IN PARTITION '2025-01-01' WHERE x = 1; @@ -445,14 +444,57 @@ describe('ClickhouseFormatter', () => { }); }); - // https://clickhouse.com/docs/sql-reference/statements/select/union - describe('UNION statements', () => { - // - }); - // 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 @@ -483,14 +525,20 @@ describe('ClickhouseFormatter', () => { }); }); - // https://clickhouse.com/docs/sql-reference/statements/alter - describe('ALTER statements', () => { - // - }); - // 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 @@ -562,8 +610,7 @@ describe('ClickhouseFormatter', () => { "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' + ALTER TABLE db.table_name ON CLUSTER 'my_cluster' ADD INDEX IF NOT EXISTS my_index (column1 + column2) TYPE set(100) @@ -587,8 +634,7 @@ describe('ClickhouseFormatter', () => { 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' + ALTER TABLE db.table_name ON CLUSTER 'my_cluster' DROP INDEX IF EXISTS my_index; `); }); @@ -599,8 +645,7 @@ describe('ClickhouseFormatter', () => { "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' + ALTER TABLE db.table_name ON CLUSTER 'my_cluster' MATERIALIZE INDEX IF EXISTS my_index IN PARTITION '202301'; `); @@ -612,8 +657,7 @@ describe('ClickhouseFormatter', () => { "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' + ALTER TABLE db.table_name ON CLUSTER 'my_cluster' CLEAR INDEX IF EXISTS my_index IN PARTITION '202301'; `); @@ -697,8 +741,7 @@ describe('ClickhouseFormatter', () => { '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 + 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, @@ -725,9 +768,8 @@ describe('ClickhouseFormatter', () => { '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; + ALTER ROW POLICY IF EXISTS + policy1 ON CLUSTER cluster_name1 ON database1.table1 RENAME TO new_name1; `); }); @@ -737,22 +779,10 @@ describe('ClickhouseFormatter', () => { '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; - `); - // NOTE: These are a little bit ugly. Ideally this query would be - // formatted something like this: - // - // 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; - // - // That's not really in the cards here, though, without taking away - // from some of the other queries. + 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; + `); }); }); @@ -816,8 +846,7 @@ describe('ClickhouseFormatter', () => { expect( format("ALTER TABLE visits ON CLUSTER prod APPLY DELETED MASK IN PARTITION '2025-01-01';") ).toBe(dedent` - ALTER TABLE visits - ON CLUSTER prod + ALTER TABLE visits ON CLUSTER prod APPLY DELETED MASK IN PARTITION '2025-01-01'; `); @@ -834,34 +863,30 @@ describe('ClickhouseFormatter', () => { 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 + 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; + 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; + 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 TABLE with quoted identifiers', () => { - expect(format('DROP TABLE IF EXISTS "default"."snapshot";')).toBe(dedent` - DROP TABLE IF EXISTS "default"."snapshot"; + DROP TABLE + mydb.tab1, + mydb.tab2; `); }); @@ -874,43 +899,49 @@ describe('ClickhouseFormatter', () => { 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; + 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; + DROP ROLE IF EXISTS + role1, + role2 ON CLUSTER my_cluster; `); }); it('formats DROP ROW POLICY', () => { - expect(format('DROP ROW POLICY IF EXISTS policy1 ON db1.table1;')).toBe(dedent` - DROP ROW POLICY IF EXISTS policy1 ON db1.table1; + 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 ON db1.table1;')).toBe(dedent` - DROP POLICY IF EXISTS policy1 ON db1.table1; + 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 ON CLUSTER my_cluster;')).toBe(dedent` - DROP QUOTA IF EXISTS quota1 - ON CLUSTER my_cluster; + 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 ON CLUSTER my_cluster;')).toBe(dedent` - DROP SETTINGS PROFILE IF EXISTS profile1 - ON CLUSTER my_cluster; + 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; `); }); @@ -922,24 +953,21 @@ describe('ClickhouseFormatter', () => { 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 + 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; + 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; + DROP NAMED COLLECTION IF EXISTS my_collection ON CLUSTER my_cluster; `); }); }); @@ -948,8 +976,7 @@ describe('ClickhouseFormatter', () => { 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 + TRUNCATE TABLE IF EXISTS db.table ON CLUSTER prod SYNC; `); }); @@ -959,8 +986,7 @@ describe('ClickhouseFormatter', () => { 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; + SYSTEM STOP MERGES ON CLUSTER prod; `); }); @@ -985,8 +1011,7 @@ describe('ClickhouseFormatter', () => { 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; + SYSTEM STOP FETCHES ON CLUSTER prod db.replicated_table; `); }); @@ -998,15 +1023,13 @@ describe('ClickhouseFormatter', () => { 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; + 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; + SYSTEM STOP LISTEN ON CLUSTER prod TCP SECURE; `); }); @@ -1107,8 +1130,7 @@ describe('ClickhouseFormatter', () => { 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; + ATTACH DATABASE IF NOT EXISTS test_db ON CLUSTER prod; `); }); }); @@ -1117,8 +1139,7 @@ describe('ClickhouseFormatter', () => { 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 + DETACH DATABASE test_db ON CLUSTER prod PERMANENTLY SYNC; `); @@ -1155,8 +1176,7 @@ describe('ClickhouseFormatter', () => { 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 + KILL QUERY ON CLUSTER prod WHERE elapsed > 300 FORMAT @@ -1190,8 +1210,7 @@ describe('ClickhouseFormatter', () => { 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 + OPTIMIZE TABLE logs ON CLUSTER prod DEDUPLICATE BY user_id, timestamp; @@ -1205,8 +1224,7 @@ describe('ClickhouseFormatter', () => { expect(format('RENAME DATABASE atomic_database1 TO atomic_database2 ON CLUSTER production;')) .toBe(dedent` RENAME DATABASE atomic_database1 - TO atomic_database2 - ON CLUSTER production; + TO atomic_database2 ON CLUSTER production; `); }); }); @@ -1231,8 +1249,7 @@ describe('ClickhouseFormatter', () => { 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; + AND db2.dict_B ON CLUSTER prod; `); }); }); @@ -1389,8 +1406,7 @@ describe('ClickhouseFormatter', () => { 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; + UNDROP TABLE db.my_table ON CLUSTER production; `); }); }); @@ -1504,8 +1520,7 @@ describe('ClickhouseFormatter', () => { "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 + CREATE MATERIALIZED VIEW IF NOT EXISTS mv6 ON CLUSTER prod REFRESH EVERY 1 HOUR RANDOMIZE FOR 30 MINUTE DEPENDS ON @@ -1524,6 +1539,20 @@ describe('ClickhouseFormatter', () => { }); }); + 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', () => { From 7c4f79821593cb479ce915a54866ebd5066ee02f Mon Sep 17 00:00:00 2001 From: Matt Basta Date: Thu, 4 Dec 2025 19:56:11 -0500 Subject: [PATCH 5/6] Finish getting the tests together --- .../clickhouse/clickhouse.formatter.ts | 7 +- test/clickhouse.test.ts | 181 +++++++++++++----- 2 files changed, 132 insertions(+), 56 deletions(-) diff --git a/src/languages/clickhouse/clickhouse.formatter.ts b/src/languages/clickhouse/clickhouse.formatter.ts index a5a2fa1db0..d8ae3e7e7e 100644 --- a/src/languages/clickhouse/clickhouse.formatter.ts +++ b/src/languages/clickhouse/clickhouse.formatter.ts @@ -54,7 +54,6 @@ const reservedClauses = expandPhrases([ '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]', - 'DROP [TEMPORARY] TABLE [IF EXISTS] [IF EMPTY]', ]); const standardOnelineClauses = expandPhrases([ @@ -63,7 +62,7 @@ const standardOnelineClauses = expandPhrases([ ]); const tabularOnelineClauses = expandPhrases([ 'ALL EXCEPT', - // 'ON CLUSTER', + 'ON CLUSTER', // https://clickhouse.com/docs/sql-reference/statements/update 'UPDATE', // https://clickhouse.com/docs/sql-reference/statements/system @@ -99,6 +98,9 @@ const tabularOnelineClauses = expandPhrases([ '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 @@ -228,7 +230,6 @@ const reservedJoins = expandPhrases([ const reservedKeywordPhrases = expandPhrases([ '{ROWS | RANGE} BETWEEN', 'ALTER MATERIALIZE STATISTICS', - 'RENAME TO', ]); // https://clickhouse.com/docs/sql-reference/syntax diff --git a/test/clickhouse.test.ts b/test/clickhouse.test.ts index a11b03ec83..3c49a7d722 100644 --- a/test/clickhouse.test.ts +++ b/test/clickhouse.test.ts @@ -435,7 +435,8 @@ describe('ClickhouseFormatter', () => { 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 + DELETE FROM db.table + ON CLUSTER foo IN PARTITION '2025-01-01' WHERE x = 1; @@ -534,8 +535,10 @@ describe('ClickhouseFormatter', () => { ) ).toBe(dedent` ALTER USER IF EXISTS - user1 RENAME TO user1_new, - user2 RENAME TO user2_new + user1 + RENAME TO user1_new, + user2 + RENAME TO user2_new DROP ALL SETTINGS; `); }); @@ -584,22 +587,73 @@ describe('ClickhouseFormatter', () => { // 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 @@ -610,7 +664,8 @@ describe('ClickhouseFormatter', () => { "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' + ALTER TABLE db.table_name + ON CLUSTER 'my_cluster' ADD INDEX IF NOT EXISTS my_index (column1 + column2) TYPE set(100) @@ -634,7 +689,8 @@ describe('ClickhouseFormatter', () => { 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' + ALTER TABLE db.table_name + ON CLUSTER 'my_cluster' DROP INDEX IF EXISTS my_index; `); }); @@ -645,7 +701,8 @@ describe('ClickhouseFormatter', () => { "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' + ALTER TABLE db.table_name + ON CLUSTER 'my_cluster' MATERIALIZE INDEX IF EXISTS my_index IN PARTITION '202301'; `); @@ -657,7 +714,8 @@ describe('ClickhouseFormatter', () => { "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' + ALTER TABLE db.table_name + ON CLUSTER 'my_cluster' CLEAR INDEX IF EXISTS my_index IN PARTITION '202301'; `); @@ -741,22 +799,14 @@ describe('ClickhouseFormatter', () => { '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 + 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; `); - // NOTE: This is a little ugly because the commas separate parameters - // and not intervals. I'd prefer this to look like this: - // - // 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; }); }); @@ -769,7 +819,9 @@ describe('ClickhouseFormatter', () => { ) ).toBe(dedent` ALTER ROW POLICY IF EXISTS - policy1 ON CLUSTER cluster_name1 ON database1.table1 RENAME TO new_name1; + policy1 + ON CLUSTER cluster_name1 ON database1.table1 + RENAME TO new_name1; `); }); @@ -780,8 +832,12 @@ describe('ClickhouseFormatter', () => { ) ).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; + policy1 + ON CLUSTER cluster_name1 ON database1.table1 + RENAME TO new_name1, + policy2 + ON CLUSTER cluster_name2 ON database2.table2 + RENAME TO new_name2; `); }); }); @@ -846,7 +902,8 @@ describe('ClickhouseFormatter', () => { expect( format("ALTER TABLE visits ON CLUSTER prod APPLY DELETED MASK IN PARTITION '2025-01-01';") ).toBe(dedent` - ALTER TABLE visits ON CLUSTER prod + ALTER TABLE visits + ON CLUSTER prod APPLY DELETED MASK IN PARTITION '2025-01-01'; `); @@ -863,30 +920,28 @@ describe('ClickhouseFormatter', () => { 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 + 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; + 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; + 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; + DROP TABLE mydb.tab1, + mydb.tab2; `); }); @@ -901,7 +956,8 @@ describe('ClickhouseFormatter', () => { expect(format('DROP USER IF EXISTS user1, user2 ON CLUSTER my_cluster;')).toBe(dedent` DROP USER IF EXISTS user1, - user2 ON CLUSTER my_cluster; + user2 + ON CLUSTER my_cluster; `); }); @@ -909,7 +965,8 @@ describe('ClickhouseFormatter', () => { expect(format('DROP ROLE IF EXISTS role1, role2 ON CLUSTER my_cluster;')).toBe(dedent` DROP ROLE IF EXISTS role1, - role2 ON CLUSTER my_cluster; + role2 + ON CLUSTER my_cluster; `); }); @@ -933,7 +990,8 @@ describe('ClickhouseFormatter', () => { expect(format('DROP QUOTA IF EXISTS quota1, quota2 ON CLUSTER my_cluster;')).toBe(dedent` DROP QUOTA IF EXISTS quota1, - quota2 ON CLUSTER my_cluster; + quota2 + ON CLUSTER my_cluster; `); }); @@ -941,7 +999,8 @@ describe('ClickhouseFormatter', () => { 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; + profile2 + ON CLUSTER my_cluster; `); }); @@ -953,21 +1012,24 @@ describe('ClickhouseFormatter', () => { 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 + 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; + 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; + DROP NAMED COLLECTION IF EXISTS my_collection + ON CLUSTER my_cluster; `); }); }); @@ -976,7 +1038,8 @@ describe('ClickhouseFormatter', () => { 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 + TRUNCATE TABLE IF EXISTS db.table + ON CLUSTER prod SYNC; `); }); @@ -986,7 +1049,8 @@ describe('ClickhouseFormatter', () => { 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; + SYSTEM STOP MERGES + ON CLUSTER prod; `); }); @@ -1011,7 +1075,8 @@ describe('ClickhouseFormatter', () => { 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; + SYSTEM STOP FETCHES + ON CLUSTER prod db.replicated_table; `); }); @@ -1023,13 +1088,15 @@ describe('ClickhouseFormatter', () => { 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; + 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; + SYSTEM STOP LISTEN + ON CLUSTER prod TCP SECURE; `); }); @@ -1130,7 +1197,8 @@ describe('ClickhouseFormatter', () => { 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; + ATTACH DATABASE IF NOT EXISTS test_db + ON CLUSTER prod; `); }); }); @@ -1139,7 +1207,8 @@ describe('ClickhouseFormatter', () => { 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 + DETACH DATABASE test_db + ON CLUSTER prod PERMANENTLY SYNC; `); @@ -1176,7 +1245,8 @@ describe('ClickhouseFormatter', () => { 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 + KILL QUERY + ON CLUSTER prod WHERE elapsed > 300 FORMAT @@ -1210,7 +1280,8 @@ describe('ClickhouseFormatter', () => { 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 + OPTIMIZE TABLE logs + ON CLUSTER prod DEDUPLICATE BY user_id, timestamp; @@ -1224,7 +1295,8 @@ describe('ClickhouseFormatter', () => { expect(format('RENAME DATABASE atomic_database1 TO atomic_database2 ON CLUSTER production;')) .toBe(dedent` RENAME DATABASE atomic_database1 - TO atomic_database2 ON CLUSTER production; + TO atomic_database2 + ON CLUSTER production; `); }); }); @@ -1249,7 +1321,8 @@ describe('ClickhouseFormatter', () => { 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; + AND db2.dict_B + ON CLUSTER prod; `); }); }); @@ -1406,7 +1479,8 @@ describe('ClickhouseFormatter', () => { 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; + UNDROP TABLE db.my_table + ON CLUSTER production; `); }); }); @@ -1520,7 +1594,8 @@ describe('ClickhouseFormatter', () => { "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 + CREATE MATERIALIZED VIEW IF NOT EXISTS mv6 + ON CLUSTER prod REFRESH EVERY 1 HOUR RANDOMIZE FOR 30 MINUTE DEPENDS ON From 1c19d1a31fdd42dd525d217415e5af5f25c87987 Mon Sep 17 00:00:00 2001 From: Matt Basta Date: Thu, 4 Dec 2025 19:59:27 -0500 Subject: [PATCH 6/6] Cleanup --- src/languages/clickhouse/clickhouse.formatter.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/languages/clickhouse/clickhouse.formatter.ts b/src/languages/clickhouse/clickhouse.formatter.ts index d8ae3e7e7e..beed3cdf5d 100644 --- a/src/languages/clickhouse/clickhouse.formatter.ts +++ b/src/languages/clickhouse/clickhouse.formatter.ts @@ -32,6 +32,7 @@ const reservedClauses = expandPhrases([ // 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}', @@ -47,8 +48,8 @@ const reservedClauses = expandPhrases([ '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]',