Skip to content

Commit 7312aee

Browse files
authored
Add Clickhouse formatter (#922)
Addresses #614
2 parents 3c96d06 + 1c64273 commit 7312aee

File tree

10 files changed

+4414
-3
lines changed

10 files changed

+4414
-3
lines changed

.github/workflows/webpack.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ jobs:
2222
- name: Install
2323
run: yarn install --ignore-scripts
2424

25+
- name: Format
26+
run: yarn pretty:check
27+
28+
- name: Lint
29+
run: yarn lint
30+
2531
- name: Test
2632
run: yarn test
2733

src/allDialects.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { bigquery } from './languages/bigquery/bigquery.formatter.js';
2+
export { clickhouse } from './languages/clickhouse/clickhouse.formatter.js';
23
export { db2 } from './languages/db2/db2.formatter.js';
34
export { db2i } from './languages/db2i/db2i.formatter.js';
45
export { duckdb } from './languages/duckdb/duckdb.formatter.js';

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export { ConfigError } from './validateConfig.js';
44

55
// When adding a new dialect, be sure to add it to the list of exports below.
66
export { bigquery } from './languages/bigquery/bigquery.formatter.js';
7+
export { clickhouse } from './languages/clickhouse/clickhouse.formatter.js';
78
export { db2 } from './languages/db2/db2.formatter.js';
89
export { db2i } from './languages/db2i/db2i.formatter.js';
910
export { duckdb } from './languages/duckdb/duckdb.formatter.js';
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
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

Comments
 (0)