|
| 1 | +import { DialectOptions } from '../../dialect.js'; |
| 2 | +import { expandPhrases } from '../../expandPhrases.js'; |
| 3 | +import { EOF_TOKEN, isToken, Token, TokenType } from '../../lexer/token.js'; |
| 4 | +import { functions } from './clickhouse.functions.js'; |
| 5 | +import { dataTypes, keywords } from './clickhouse.keywords.js'; |
| 6 | + |
| 7 | +const reservedSelect = expandPhrases([ |
| 8 | + 'SELECT [DISTINCT]', |
| 9 | + // https://clickhouse.com/docs/sql-reference/statements/alter/view |
| 10 | + 'MODIFY QUERY SELECT [DISTINCT]', |
| 11 | +]); |
| 12 | + |
| 13 | +const reservedClauses = expandPhrases([ |
| 14 | + 'SET', |
| 15 | + // https://clickhouse.com/docs/sql-reference/statements/select |
| 16 | + 'WITH', |
| 17 | + 'FROM', |
| 18 | + 'SAMPLE', |
| 19 | + 'PREWHERE', |
| 20 | + 'WHERE', |
| 21 | + 'GROUP BY', |
| 22 | + 'HAVING', |
| 23 | + 'QUALIFY', |
| 24 | + 'ORDER BY', |
| 25 | + 'LIMIT', // Note: Clickhouse has no OFFSET clause |
| 26 | + 'SETTINGS', |
| 27 | + 'INTO OUTFILE', |
| 28 | + 'FORMAT', |
| 29 | + // https://clickhouse.com/docs/sql-reference/window-functions |
| 30 | + 'WINDOW', |
| 31 | + 'PARTITION BY', |
| 32 | + // https://clickhouse.com/docs/sql-reference/statements/insert-into |
| 33 | + 'INSERT INTO', |
| 34 | + 'VALUES', |
| 35 | + // https://clickhouse.com/docs/sql-reference/statements/create/view#refreshable-materialized-view |
| 36 | + 'DEPENDS ON', |
| 37 | + // https://clickhouse.com/docs/sql-reference/statements/move |
| 38 | + 'MOVE {USER | ROLE | QUOTA | SETTINGS PROFILE | ROW POLICY}', |
| 39 | + // https://clickhouse.com/docs/sql-reference/statements/grant |
| 40 | + 'GRANT', |
| 41 | + // https://clickhouse.com/docs/sql-reference/statements/revoke |
| 42 | + 'REVOKE', |
| 43 | + // https://clickhouse.com/docs/sql-reference/statements/check-grant |
| 44 | + 'CHECK GRANT', |
| 45 | + // https://clickhouse.com/docs/sql-reference/statements/set-role |
| 46 | + 'SET [DEFAULT] ROLE [NONE | ALL | ALL EXCEPT]', |
| 47 | + // https://clickhouse.com/docs/sql-reference/statements/optimize |
| 48 | + 'DEDUPLICATE BY', |
| 49 | + // https://clickhouse.com/docs/sql-reference/statements/alter/statistics |
| 50 | + 'MODIFY STATISTICS', |
| 51 | + // Used for ALTER INDEX ... TYPE and ALTER STATISTICS ... TYPE |
| 52 | + 'TYPE', |
| 53 | + // https://clickhouse.com/docs/sql-reference/statements/alter |
| 54 | + 'ALTER USER [IF EXISTS]', |
| 55 | + 'ALTER [ROW] POLICY [IF EXISTS]', |
| 56 | + // https://clickhouse.com/docs/sql-reference/statements/drop |
| 57 | + 'DROP {USER | ROLE | QUOTA | PROFILE | SETTINGS PROFILE | ROW POLICY | POLICY} [IF EXISTS]', |
| 58 | +]); |
| 59 | + |
| 60 | +const standardOnelineClauses = expandPhrases([ |
| 61 | + // https://clickhouse.com/docs/sql-reference/statements/create |
| 62 | + 'CREATE [OR REPLACE] [TEMPORARY] TABLE [IF NOT EXISTS]', |
| 63 | +]); |
| 64 | +const tabularOnelineClauses = expandPhrases([ |
| 65 | + 'ALL EXCEPT', |
| 66 | + 'ON CLUSTER', |
| 67 | + // https://clickhouse.com/docs/sql-reference/statements/update |
| 68 | + 'UPDATE', |
| 69 | + // https://clickhouse.com/docs/sql-reference/statements/system |
| 70 | + 'SYSTEM RELOAD {DICTIONARIES | DICTIONARY | FUNCTIONS | FUNCTION | ASYNCHRONOUS METRICS}', |
| 71 | + '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}', |
| 72 | + 'SYSTEM FLUSH LOGS', |
| 73 | + 'SYSTEM RELOAD {CONFIG | USERS}', |
| 74 | + 'SYSTEM SHUTDOWN', |
| 75 | + 'SYSTEM KILL', |
| 76 | + 'SYSTEM FLUSH DISTRIBUTED', |
| 77 | + 'SYSTEM START DISTRIBUTED SENDS', |
| 78 | + 'SYSTEM {STOP | START} {LISTEN | MERGES | TTL MERGES | MOVES | FETCHES | REPLICATED SENDS | REPLICATION QUEUES | PULLING REPLICATION LOG}', |
| 79 | + 'SYSTEM {SYNC | RESTART | RESTORE} REPLICA', |
| 80 | + 'SYSTEM {SYNC | RESTORE} DATABASE REPLICA', |
| 81 | + 'SYSTEM RESTART REPLICAS', |
| 82 | + 'SYSTEM UNFREEZE', |
| 83 | + 'SYSTEM WAIT LOADING PARTS', |
| 84 | + 'SYSTEM {LOAD | UNLOAD} PRIMARY KEY', |
| 85 | + 'SYSTEM {STOP | START} [REPLICATED] VIEW', |
| 86 | + 'SYSTEM {STOP | START} VIEWS', |
| 87 | + 'SYSTEM {REFRESH | CANCEL | WAIT} VIEW', |
| 88 | + 'WITH NAME', |
| 89 | + // https://clickhouse.com/docs/sql-reference/statements/show |
| 90 | + 'SHOW [CREATE] {TABLE | TEMPORARY TABLE | DICTIONARY | VIEW | DATABASE}', |
| 91 | + 'SHOW DATABASES [[NOT] {LIKE | ILIKE}]', |
| 92 | + 'SHOW [FULL] [TEMPORARY] TABLES [FROM | IN]', |
| 93 | + 'SHOW [EXTENDED] [FULL] COLUMNS {FROM | IN}', |
| 94 | + // https://clickhouse.com/docs/sql-reference/statements/attach |
| 95 | + 'ATTACH {TABLE | DICTIONARY | DATABASE} [IF NOT EXISTS]', |
| 96 | + // https://clickhouse.com/docs/sql-reference/statements/detach |
| 97 | + 'DETACH {TABLE | DICTIONARY | DATABASE} [IF EXISTS]', |
| 98 | + 'PERMANENTLY', |
| 99 | + 'SYNC', |
| 100 | + // https://clickhouse.com/docs/sql-reference/statements/drop |
| 101 | + 'DROP {DICTIONARY | DATABASE | PROFILE | VIEW | FUNCTION | NAMED COLLECTION} [IF EXISTS]', |
| 102 | + 'DROP [TEMPORARY] TABLE [IF EXISTS] [IF EMPTY]', |
| 103 | + // https://clickhouse.com/docs/sql-reference/statements/alter/table#rename |
| 104 | + 'RENAME TO', |
| 105 | + // https://clickhouse.com/docs/sql-reference/statements/exists |
| 106 | + 'EXISTS [TEMPORARY] {TABLE | DICTIONARY | DATABASE}', |
| 107 | + // https://clickhouse.com/docs/sql-reference/statements/kill |
| 108 | + 'KILL QUERY', |
| 109 | + // https://clickhouse.com/docs/sql-reference/statements/optimize |
| 110 | + 'OPTIMIZE TABLE', |
| 111 | + // https://clickhouse.com/docs/sql-reference/statements/rename |
| 112 | + 'RENAME {TABLE | DICTIONARY | DATABASE}', |
| 113 | + // https://clickhouse.com/docs/sql-reference/statements/exchange |
| 114 | + 'EXCHANGE {TABLES | DICTIONARIES}', |
| 115 | + // https://clickhouse.com/docs/sql-reference/statements/truncate |
| 116 | + 'TRUNCATE TABLE [IF EXISTS]', |
| 117 | + // https://clickhouse.com/docs/sql-reference/statements/execute_as |
| 118 | + 'EXECUTE AS', |
| 119 | + // https://clickhouse.com/docs/sql-reference/statements/use |
| 120 | + 'USE', |
| 121 | + 'TO', |
| 122 | + // https://clickhouse.com/docs/sql-reference/statements/undrop |
| 123 | + 'UNDROP TABLE', |
| 124 | + // https://clickhouse.com/docs/sql-reference/statements/create |
| 125 | + 'CREATE {DATABASE | NAMED COLLECTION} [IF NOT EXISTS]', |
| 126 | + 'CREATE [OR REPLACE] {VIEW | DICTIONARY} [IF NOT EXISTS]', |
| 127 | + 'CREATE MATERIALIZED VIEW [IF NOT EXISTS]', |
| 128 | + 'CREATE FUNCTION', |
| 129 | + 'CREATE {USER | ROLE | QUOTA | SETTINGS PROFILE} [IF NOT EXISTS | OR REPLACE]', |
| 130 | + 'CREATE [ROW] POLICY [IF NOT EXISTS | OR REPLACE]', |
| 131 | + // https://clickhouse.com/docs/sql-reference/statements/create/table#replace-table |
| 132 | + 'REPLACE [TEMPORARY] TABLE [IF NOT EXISTS]', |
| 133 | + // https://clickhouse.com/docs/sql-reference/statements/alter |
| 134 | + 'ALTER {ROLE | QUOTA | SETTINGS PROFILE} [IF EXISTS]', |
| 135 | + 'ALTER [TEMPORARY] TABLE', |
| 136 | + 'ALTER NAMED COLLECTION [IF EXISTS]', |
| 137 | + // https://clickhouse.com/docs/sql-reference/statements/alter/user |
| 138 | + 'GRANTEES', |
| 139 | + 'NOT IDENTIFIED', |
| 140 | + 'RESET AUTHENTICATION METHODS TO NEW', |
| 141 | + '{IDENTIFIED | ADD IDENTIFIED} [WITH | BY]', |
| 142 | + '[ADD | DROP] HOST {LOCAL | NAME | REGEXP | IP | LIKE}', |
| 143 | + 'VALID UNTIL', |
| 144 | + 'DROP [ALL] {PROFILES | SETTINGS}', |
| 145 | + '{ADD | MODIFY} SETTINGS', |
| 146 | + 'ADD PROFILES', |
| 147 | + // https://clickhouse.com/docs/sql-reference/statements/alter/apply-deleted-mask |
| 148 | + 'APPLY DELETED MASK', |
| 149 | + 'IN PARTITION', |
| 150 | + // https://clickhouse.com/docs/sql-reference/statements/alter/column |
| 151 | + '{ADD | DROP | RENAME | CLEAR | COMMENT | MODIFY | ALTER | MATERIALIZE} COLUMN', |
| 152 | + // https://clickhouse.com/docs/sql-reference/statements/alter/partition |
| 153 | + '{DETACH | DROP | ATTACH | FETCH | MOVE} {PART | PARTITION}', |
| 154 | + 'DROP DETACHED {PART | PARTITION}', |
| 155 | + '{FORGET | REPLACE} PARTITION', |
| 156 | + 'CLEAR COLUMN', |
| 157 | + '{FREEZE | UNFREEZE} [PARTITION]', |
| 158 | + 'CLEAR INDEX', |
| 159 | + 'TO {DISK | VOLUME}', |
| 160 | + '[DELETE | REWRITE PARTS] IN PARTITION', |
| 161 | + // https://clickhouse.com/docs/sql-reference/statements/alter/setting |
| 162 | + '{MODIFY | RESET} SETTING', |
| 163 | + // https://clickhouse.com/docs/sql-reference/statements/alter/delete |
| 164 | + 'DELETE WHERE', |
| 165 | + // https://clickhouse.com/docs/sql-reference/statements/alter/order-by |
| 166 | + 'MODIFY ORDER BY', |
| 167 | + // https://clickhouse.com/docs/sql-reference/statements/alter/sample-by |
| 168 | + '{MODIFY | REMOVE} SAMPLE BY', |
| 169 | + // https://clickhouse.com/docs/sql-reference/statements/alter/skipping-index |
| 170 | + '{ADD | MATERIALIZE | CLEAR} INDEX [IF NOT EXISTS]', |
| 171 | + 'DROP INDEX [IF EXISTS]', |
| 172 | + 'GRANULARITY', |
| 173 | + 'AFTER', |
| 174 | + 'FIRST', |
| 175 | + |
| 176 | + // https://clickhouse.com/docs/sql-reference/statements/alter/constraint |
| 177 | + 'ADD CONSTRAINT [IF NOT EXISTS]', |
| 178 | + 'DROP CONSTRAINT [IF EXISTS]', |
| 179 | + // https://clickhouse.com/docs/sql-reference/statements/alter/ttl |
| 180 | + 'MODIFY TTL', |
| 181 | + 'REMOVE TTL', |
| 182 | + // https://clickhouse.com/docs/sql-reference/statements/alter/statistics |
| 183 | + 'ADD STATISTICS [IF NOT EXISTS]', |
| 184 | + '{DROP | CLEAR} STATISTICS [IF EXISTS]', |
| 185 | + 'MATERIALIZE STATISTICS [ALL | IF EXISTS]', |
| 186 | + // https://clickhouse.com/docs/sql-reference/statements/alter/quota |
| 187 | + 'KEYED BY', |
| 188 | + 'NOT KEYED', |
| 189 | + 'FOR [RANDOMIZED] INTERVAL', |
| 190 | + // https://clickhouse.com/docs/sql-reference/statements/alter/row-policy |
| 191 | + 'AS {PERMISSIVE | RESTRICTIVE}', |
| 192 | + 'FOR SELECT', |
| 193 | + // https://clickhouse.com/docs/sql-reference/statements/alter/projection |
| 194 | + 'ADD PROJECTION [IF NOT EXISTS]', |
| 195 | + '{DROP | MATERIALIZE | CLEAR} PROJECTION [IF EXISTS]', |
| 196 | + // https://clickhouse.com/docs/sql-reference/statements/create/view#refreshable-materialized-view |
| 197 | + 'REFRESH {EVERY | AFTER}', |
| 198 | + 'RANDOMIZE FOR', |
| 199 | + 'APPEND', |
| 200 | + 'APPEND TO', |
| 201 | + // https://clickhouse.com/docs/sql-reference/statements/delete |
| 202 | + 'DELETE FROM', |
| 203 | + // https://clickhouse.com/docs/sql-reference/statements/explain |
| 204 | + 'EXPLAIN [AST | SYNTAX | QUERY TREE | PLAN | PIPELINE | ESTIMATE | TABLE OVERRIDE]', |
| 205 | + // https://clickhouse.com/docs/sql-reference/statements/grant |
| 206 | + 'GRANT ON CLUSTER', |
| 207 | + 'GRANT CURRENT GRANTS', |
| 208 | + 'WITH GRANT OPTION', |
| 209 | + // https://clickhouse.com/docs/sql-reference/statements/revoke |
| 210 | + 'REVOKE ON CLUSTER', |
| 211 | + 'ADMIN OPTION FOR', |
| 212 | + // https://clickhouse.com/docs/sql-reference/statements/check-table |
| 213 | + 'CHECK TABLE', |
| 214 | + 'PARTITION ID', |
| 215 | + // https://clickhouse.com/docs/sql-reference/statements/describe-table |
| 216 | + '{DESC | DESCRIBE} TABLE', |
| 217 | +]); |
| 218 | + |
| 219 | +const reservedSetOperations = expandPhrases([ |
| 220 | + // https://clickhouse.com/docs/sql-reference/statements/select/union |
| 221 | + 'UNION [ALL | DISTINCT]', |
| 222 | + // https://clickhouse.com/docs/sql-reference/statements/parallel_with |
| 223 | + 'PARALLEL WITH', |
| 224 | +]); |
| 225 | + |
| 226 | +const reservedJoins = expandPhrases([ |
| 227 | + // https://clickhouse.com/docs/sql-reference/statements/select/join |
| 228 | + '[GLOBAL] [INNER|LEFT|RIGHT|FULL|CROSS] [OUTER|SEMI|ANTI|ANY|ALL|ASOF] JOIN', |
| 229 | + // https://clickhouse.com/docs/sql-reference/statements/select/array-join |
| 230 | + '[LEFT] ARRAY JOIN', |
| 231 | +]); |
| 232 | + |
| 233 | +const reservedKeywordPhrases = expandPhrases([ |
| 234 | + '{ROWS | RANGE} BETWEEN', |
| 235 | + 'ALTER MATERIALIZE STATISTICS', |
| 236 | +]); |
| 237 | + |
| 238 | +// https://clickhouse.com/docs/sql-reference/syntax |
| 239 | +export const clickhouse: DialectOptions = { |
| 240 | + name: 'clickhouse', |
| 241 | + tokenizerOptions: { |
| 242 | + reservedSelect, |
| 243 | + reservedClauses: [...reservedClauses, ...standardOnelineClauses, ...tabularOnelineClauses], |
| 244 | + reservedSetOperations, |
| 245 | + reservedJoins, |
| 246 | + reservedKeywordPhrases, |
| 247 | + |
| 248 | + reservedKeywords: keywords, |
| 249 | + reservedDataTypes: dataTypes, |
| 250 | + reservedFunctionNames: functions, |
| 251 | + extraParens: ['[]', '{}'], |
| 252 | + lineCommentTypes: ['#', '--'], |
| 253 | + nestedBlockComments: false, |
| 254 | + underscoresInNumbers: true, |
| 255 | + stringTypes: ['$$', "''-qq-bs"], |
| 256 | + identTypes: ['""-qq-bs', '``'], |
| 257 | + paramTypes: { |
| 258 | + // https://clickhouse.com/docs/sql-reference/syntax#defining-and-using-query-parameters |
| 259 | + custom: [ |
| 260 | + { |
| 261 | + // Parameters are like {foo:Uint64} or {foo:Map(String, String)} |
| 262 | + // We include `'` in the negated character class to be a little sneaky: |
| 263 | + // map literals have quoted keys, and we use this to avoid confusing |
| 264 | + // them for named parameters. This means that the map literal `{'foo':1}` |
| 265 | + // will be formatted as `{'foo': 1}` rather than `{foo: 1}`. |
| 266 | + regex: String.raw`\{[^:']+:[^}]+\}`, |
| 267 | + key: v => { |
| 268 | + const match = /\{([^:]+):/.exec(v); |
| 269 | + return match ? match[1].trim() : v; |
| 270 | + }, |
| 271 | + }, |
| 272 | + ], |
| 273 | + }, |
| 274 | + operators: [ |
| 275 | + // Strings, arithmetic |
| 276 | + '%', // modulo |
| 277 | + '||', // string concatenation |
| 278 | + |
| 279 | + // Ternary |
| 280 | + '?', |
| 281 | + ':', |
| 282 | + |
| 283 | + // Comparison |
| 284 | + '==', |
| 285 | + '<=>', // null-safe equal |
| 286 | + |
| 287 | + // Lambda creation |
| 288 | + '->', |
| 289 | + ], |
| 290 | + postProcess, |
| 291 | + }, |
| 292 | + formatOptions: { |
| 293 | + onelineClauses: [...standardOnelineClauses, ...tabularOnelineClauses], |
| 294 | + tabularOnelineClauses, |
| 295 | + }, |
| 296 | +}; |
| 297 | + |
| 298 | +/** |
| 299 | + * 1. Formats GRANT statements to use RESERVED_KEYWORD instead of RESERVED_SELECT |
| 300 | + * for SELECT GRANTs |
| 301 | + * 2. Formats SET(100) as RESERVED_FUNCTION_NAME instead of RESERVED_KEYWORD |
| 302 | + * so it appears as a function rather than a statement. |
| 303 | + */ |
| 304 | +function postProcess(tokens: Token[]): Token[] { |
| 305 | + return tokens.map((token, i) => { |
| 306 | + const nextToken = tokens[i + 1] || EOF_TOKEN; |
| 307 | + const prevToken = tokens[i - 1] || EOF_TOKEN; |
| 308 | + |
| 309 | + // If we have queries like |
| 310 | + // > GRANT SELECT, INSERT ON db.table TO john |
| 311 | + // > GRANT SELECT(a, b), SELECT(c) ON db.table TO john |
| 312 | + // we want to format them as |
| 313 | + // > GRANT |
| 314 | + // > SELECT, |
| 315 | + // > INSERT ON db.table |
| 316 | + // > TO john |
| 317 | + // > GRANT |
| 318 | + // > SELECT(a, b), |
| 319 | + // > SELECT(c) ON db.table |
| 320 | + // > TO john |
| 321 | + // To do this we need to convert the SELECT keyword to a RESERVED_KEYWORD. |
| 322 | + if ( |
| 323 | + token.type === TokenType.RESERVED_SELECT && |
| 324 | + (nextToken.type === TokenType.COMMA || |
| 325 | + prevToken.type === TokenType.RESERVED_CLAUSE || |
| 326 | + prevToken.type === TokenType.COMMA) |
| 327 | + ) { |
| 328 | + return { ...token, type: TokenType.RESERVED_KEYWORD }; |
| 329 | + } |
| 330 | + |
| 331 | + // We should format `set(100)` as-is rather than `SET (100)` |
| 332 | + if (isToken.SET(token) && nextToken.type === TokenType.OPEN_PAREN) { |
| 333 | + return { ...token, type: TokenType.RESERVED_FUNCTION_NAME }; |
| 334 | + } |
| 335 | + |
| 336 | + return token; |
| 337 | + }); |
| 338 | +} |
0 commit comments