forked from ClickHouse/ClickHouse
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocs.html
More file actions
1819 lines (1670 loc) · 111 KB
/
Copy pathdocs.html
File metadata and controls
1819 lines (1670 loc) · 111 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ClickHouse Reference</title>
<link rel="icon" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1NCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDExIDEwIj48c3R5bGU+LmJne2ZpbGw6I2ZmMH0ub3tmaWxsOiMwMDB9PC9zdHlsZT48cmVjdCBjbGFzcz0iYmciIHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiLz48cGF0aCBkPSJNMSwxIGgxIHY4IGgtMSB6IiBjbGFzcz0ibyIvPjxwYXRoIGQ9Ik0zLDEgaDEgdjggaC0xIHoiIGNsYXNzPSJvIi8+PHBhdGggZD0iTTUsMSBoMSB2OCBoLTEgeiIgY2xhc3M9Im8iLz48cGF0aCBkPSJNNywxIGgxIHY4IGgtMSB6IiBjbGFzcz0ibyIvPjxwYXRoIGQ9Ik05LDQuMjUgaDEgdjEuNSBoLTEgeiIgY2xhc3M9Im8iLz48L3N2Zz4K">
<style>
* {
box-sizing: border-box;
}
/* Light theme (default) and dark theme, switchable like in `play.html`. Only a handful of
variables differ; everything else is derived from them (often via `color-mix`). */
:root {
--bg: #F8F8F8;
--fg: #1A1A1A;
--panel-bg: #FFFFFF;
--border: #DDDDDD;
--border-soft: #EEEEEE;
--muted: #888888;
--hover-bg: #FFF7DA;
--code-bg: #F0F0F0;
--pre-bg: #1A1A1A;
--link: #0088FF;
}
[data-theme="dark"] {
--bg: #1A1A1A;
--fg: #DDDDDD;
--panel-bg: #242424;
--border: #3A3A3A;
--border-soft: #333333;
--muted: #999999;
--hover-bg: #3A3A26;
--code-bg: #2E2E2E;
--pre-bg: #141414;
--link: #FF0;
}
html, body {
height: 100%;
margin: 0;
background: var(--bg);
color: var(--fg);
/* Theme-aware scrollbars, as in `play.html`. */
scrollbar-color: var(--border) var(--bg);
/* Same typefaces as `play.html`. */
font-family: Roboto Mono, Liberation Sans, DejaVu Sans, sans-serif, Noto Color Emoji, Apple Color Emoji, Segoe UI Emoji;
}
body {
display: flex;
flex-direction: column;
height: 100%;
}
a {
color: var(--link);
}
#header {
padding: 0.5rem 1rem;
background: #1A1A1A;
color: #FF0;
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
#header h1 {
font-size: 14pt;
margin: 0;
font-weight: bold;
white-space: nowrap;
}
#header h1 .accent {
color: white;
}
#connection-params {
margin-left: auto;
display: flex;
gap: 0.25rem;
}
#connection-params input {
border: 1px solid #555;
background: #2A2A2A;
color: #EEE;
font-size: 10pt;
padding: 0.2rem 0.4rem;
}
#url { width: 16rem; }
#user, #password { width: 6rem; }
#search-bar {
padding: 1rem;
background: #FF0;
}
#search {
width: 100%;
border: 1px solid black;
font-size: 18pt;
font-weight: bold;
padding: 0.6rem 0.9rem;
outline: none;
}
#main {
flex: 1;
display: flex;
min-height: 0;
}
#results {
width: 24rem;
/* Cap the result list at 30% of the width so it does not dominate narrow (mobile) screens.
On wide screens 24rem is the smaller value and wins, so the layout is unchanged there. */
max-width: 30%;
overflow-y: auto;
border-right: 1px solid var(--border);
background: var(--panel-bg);
scrollbar-width: thin;
scrollbar-color: var(--border) var(--panel-bg);
}
.result {
padding: 0.5rem 0.9rem;
border-bottom: 1px solid var(--border-soft);
cursor: pointer;
/* Each entity type gets a faint tint, blended into the panel background so it adapts to
the theme. The `:hover`/`.selected` rules below set `background` directly and so win
regardless of source order. */
background: color-mix(in srgb, var(--type-accent, transparent) 14%, var(--panel-bg));
}
.result:hover {
background: var(--hover-bg);
}
.result.selected {
background: #FF0;
color: #1A1A1A;
}
.result .name {
font-weight: bold;
font-family: DejaVu Sans Mono, Liberation Mono, MonoLisa, Consolas, monospace;
font-size: 12pt;
word-break: break-all;
}
.result .type {
font-size: 9pt;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.result.selected .type {
color: #663D00;
}
/* A small badge next to the name marks an entity that is just an alias of another, so it is
recognizable in the list without opening it. It is tinted with the type accent like the row. */
.result .name .alias-badge {
display: inline-block;
margin-left: 0.5em;
padding: 0 0.4em;
font-size: 7.5pt;
font-weight: normal;
line-height: 1.6;
vertical-align: middle;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
background: color-mix(in srgb, var(--type-accent, var(--muted)) 16%, var(--panel-bg));
border: 1px solid color-mix(in srgb, var(--type-accent, var(--muted)) 45%, var(--border));
border-radius: 4px;
}
.result.selected .name .alias-badge {
color: #663D00;
background: transparent;
border-color: #663D00;
}
#doc {
flex: 1;
overflow-y: auto;
padding: 1.5rem 2rem;
background: var(--panel-bg);
line-height: 1.5;
scrollbar-width: thin;
scrollbar-color: var(--border) var(--panel-bg);
}
#doc.empty {
color: var(--muted);
display: flex;
align-items: center;
justify-content: center;
font-size: 13pt;
}
/* Rendered Markdown styling */
#doc h1 { font-size: 20pt; margin-top: 0; border-bottom: 2px solid #FF0; padding-bottom: 0.3rem; }
#doc h2 { font-size: 16pt; border-bottom: 1px solid var(--border); padding-bottom: 0.2rem; }
#doc h3 { font-size: 13pt; }
#doc code {
background: var(--code-bg);
padding: 0.1rem 0.3rem;
font-size: 90%;
font-family: DejaVu Sans Mono, Liberation Mono, MonoLisa, Consolas, monospace;
}
#doc pre {
position: relative;
background: var(--pre-bg);
color: #EEE;
padding: 0.8rem 1rem;
overflow-x: auto;
/* Keep code blocks tight: the relaxed prose line-height (1.5 on `#doc`) would space out
code lines, so reset it to 1 here. */
line-height: 1;
font-family: DejaVu Sans Mono, Liberation Mono, MonoLisa, Consolas, monospace;
/* Code blocks are always dark, so their scrollbar is dark in both themes. */
scrollbar-width: thin;
scrollbar-color: #555 transparent;
}
#doc pre code {
background: none;
color: inherit;
padding: 0;
}
/* The code block lives in a non-scrolling wrapper so the Copy button can be pinned to the
visible right edge, independent of the code's horizontal scroll. */
#doc .code-wrapper {
position: relative;
}
/* Copy button, shown at the top-right of a code block while the cursor is over it. */
#doc .code-wrapper .copy-button {
position: absolute;
top: 0.4rem;
right: 0.4rem;
opacity: 0;
transition: opacity 0.1s;
background: rgba(255, 255, 255, 0.12);
color: #EEE;
border: 1px solid rgba(255, 255, 255, 0.25);
font-size: 9pt;
padding: 0.15rem 0.5rem;
cursor: pointer;
font-family: inherit;
}
#doc .code-wrapper:hover .copy-button {
opacity: 1;
}
#doc .code-wrapper .copy-button:hover {
background: rgba(255, 255, 255, 0.25);
}
/* Per-token SQL syntax highlighting, produced by the embedded ClickHouse lexer
(compiled to WebAssembly). The palette is the dark-background variant from
`play.html`, since our code blocks render on a dark background. */
#doc pre .q-kw { color: #EEE; font-weight: bold; }
#doc pre .q-id { color: #00CDCD; }
#doc pre .q-fn { color: #CDCD00; }
#doc pre .q-num { color: #00D700; }
#doc pre .q-str { color: #00CD00; }
#doc pre .q-qid { color: #00D7D7; }
#doc pre .q-com { color: #9E9E9E; font-style: italic; }
#doc pre .q-op { color: #EEE; }
#doc pre .q-err { color: #FF6E40; }
#doc table {
border-collapse: collapse;
}
#doc th, #doc td {
border: 1px solid var(--border);
padding: 0.3rem 0.6rem;
}
#doc blockquote {
border-left: 4px solid #FF0;
margin-left: 0;
padding-left: 1rem;
color: var(--muted);
}
#doc .entity-type {
display: inline-block;
background: #FF0;
color: black;
font-size: 9pt;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.15rem 0.5rem;
margin-bottom: 0.5rem;
}
/* A pencil link next to the entity title that opens the entity's source file on GitHub
(from the `source` column of `system.documentation`). Muted by default, accented on hover. */
#doc .source-link {
color: var(--muted);
text-decoration: none;
margin-left: 0.6rem;
opacity: 0.7;
transition: color 0.1s, opacity 0.1s;
}
#doc h1:hover .source-link {
opacity: 1;
}
#doc .source-link:hover {
color: var(--link);
opacity: 1;
}
#doc .source-link svg {
width: 0.7em;
height: 0.7em;
fill: currentColor;
vertical-align: baseline;
}
#logo {
height: 1.6rem;
display: block;
}
/* Heading anchors: a "#" symbol that appears on hover and links to the section. */
#doc h1, #doc h2, #doc h3, #doc h4, #doc h5, #doc h6 {
scroll-margin-top: 1rem;
}
#doc .heading-anchor {
color: var(--muted);
text-decoration: none;
margin-left: 0.4rem;
font-weight: normal;
opacity: 0;
transition: opacity 0.1s;
}
#doc h1:hover .heading-anchor, #doc h2:hover .heading-anchor,
#doc h3:hover .heading-anchor, #doc h4:hover .heading-anchor,
#doc h5:hover .heading-anchor, #doc h6:hover .heading-anchor {
opacity: 1;
}
#doc .heading-anchor:hover {
color: var(--fg);
}
/* Internal links (to another documented entity) get a subtle dotted underline. */
#doc a.doc-internal-link {
text-decoration: underline dotted;
}
/* `:::note`-style admonitions, modelled on the documentation website. The tinted background
is blended into the panel background so it works in both light and dark themes. */
#doc .admonition {
--adm-color: #888;
border-left: 4px solid var(--adm-color);
background: color-mix(in srgb, var(--adm-color) 12%, var(--panel-bg));
padding: 0.6rem 1rem;
margin: 1rem 0;
}
#doc .admonition-title {
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 9pt;
margin-bottom: 0.3rem;
color: var(--adm-color);
}
#doc .admonition > :last-child {
margin-bottom: 0;
}
#doc .admonition-note { --adm-color: #2F7FF7; }
#doc .admonition-tip { --adm-color: #18A974; }
#doc .admonition-info { --adm-color: #0099B8; }
#doc .admonition-warning { --adm-color: #D79400; }
#doc .admonition-danger { --adm-color: #E5403A; }
/* Theme switcher (sun / moon), like in `play.html`: only the icon for the *other* theme is
shown, and clicking it switches and memoizes the choice. */
#theme {
cursor: pointer;
font-size: 14pt;
user-select: none;
line-height: 1;
}
[data-theme="light"] #toggle-light, [data-theme="dark"] #toggle-dark {
display: none;
}
#status {
font-size: 9pt;
color: var(--muted);
padding: 0.3rem 0.9rem;
}
.error {
color: #E5484D;
white-space: pre-wrap;
font-family: DejaVu Sans Mono, Liberation Mono, MonoLisa, Consolas, monospace;
}
</style>
</head>
<body>
<div id="header">
<!-- The ClickHouse logo, matching the one in `play.html`. -->
<svg id="logo" viewBox="0 0 11 10" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="11" height="10" fill="#FF0"/>
<path d="M1,1 h1 v8 h-1 z" fill="#000"/>
<path d="M3,1 h1 v8 h-1 z" fill="#000"/>
<path d="M5,1 h1 v8 h-1 z" fill="#000"/>
<path d="M7,1 h1 v8 h-1 z" fill="#000"/>
<path d="M9,4.25 h1 v1.5 h-1 z" fill="#000"/>
</svg>
<h1>ClickHouse <span class="accent">Reference</span></h1>
<form id="connection-params" onsubmit="return false">
<input spellcheck="false" id="url" type="text" placeholder="URL" />
<input spellcheck="false" id="user" type="text" placeholder="user" />
<input spellcheck="false" id="password" type="password" placeholder="password" />
</form>
<div id="theme" title="Switch color theme"><span id="toggle-dark">🌘</span><span id="toggle-light">☀️</span></div>
</div>
<div id="search-bar">
<input spellcheck="false" autocomplete="off" autofocus id="search" type="text"
placeholder="Search functions, settings, table engines, data types…" />
</div>
<div id="main">
<div id="results"></div>
<div id="doc" class="empty">Start typing to search the documentation.</div>
</div>
<div id="status"></div>
<!-- Marked (Markdown renderer) and KaTeX (TeX math renderer).
When this page is served by ClickHouse, these CDN links are rewritten to embedded,
same-origin copies under "/js/" (see DocsWebUIRequestHandler), so no third-party code
runs in the ClickHouse HTTP origin. The CDN URLs are kept here so the page still works
when opened as a local file (file://) pointed at a remote server. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
<script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
<script>
const $search = document.getElementById('search');
const $results = document.getElementById('results');
const $doc = document.getElementById('doc');
const $status = document.getElementById('status');
const $url = document.getElementById('url');
const $user = document.getElementById('user');
const $password = document.getElementById('password');
if (!$url.value) {
$url.value = location.protocol != 'file:' ? location.origin : 'http://localhost:8123/';
}
{
/// Pick up the user name from the query string if present, like the other built-in UI pages.
const user_from_url = new URL(window.location).searchParams.get('user');
if (user_from_url) {
$user.value = user_from_url;
}
}
/* ----------------------------------------------------------------------------------------
Color theme. Precedence matches `play.html`: the `?theme=` query parameter, then the value
memoized in `localStorage`, then the operating-system preference. The user's choice (made
with the switcher) is remembered in `localStorage`.
---------------------------------------------------------------------------------------- */
function setColorTheme(new_theme, update_preference) {
if (update_preference) {
window.localStorage.setItem('theme', new_theme);
}
document.documentElement.setAttribute('data-theme', new_theme);
}
{
let theme = new URL(window.location).searchParams.get('theme');
if (['dark', 'light'].indexOf(theme) === -1) {
theme = window.localStorage.getItem('theme');
}
if (theme) {
setColorTheme(theme);
} else {
/// Autodetect from the OS preference, and keep following it if it changes later
/// (some systems switch automatically between day and night).
const media_query_list = window.matchMedia('(prefers-color-scheme: dark)');
setColorTheme(media_query_list.matches ? 'dark' : 'light');
media_query_list.addEventListener('change', e => setColorTheme(e.matches ? 'dark' : 'light'));
}
}
document.getElementById('toggle-light').onclick = () => setColorTheme('light', true);
document.getElementById('toggle-dark').onclick = () => setColorTheme('dark', true);
/// The current set of results and which one is selected.
let current = [];
let selected = -1;
/// The id of the in-page section the user navigated to inside the open entity, if any.
let current_section = '';
/// A lazily-loaded map from lower-cased entity name to the list of {name, type} entities that
/// share it, used to turn relative documentation links into in-app links when they point to
/// another documented entity. The value is a list because names are not unique across types
/// (e.g. `JSON` is both a data type and a format); a link is disambiguated by its target route.
let all_names = null;
/// Used to discard responses of queries that were superseded by a newer keystroke.
let request_seq = 0;
/// Bumped whenever the connection (server URL, user, or password) changes. `loadAllNames`
/// snapshots it before the request and only publishes the result into `all_names` if it is
/// still current, so an in-flight request that resolves after a connection change cannot
/// repopulate the cross-link cache with names from the previous server or user.
let connection_seq = 0;
/// A distinct accent color per entity type, so the kinds of entities are visually
/// distinguishable in the result list (used for the left stripe and a faint, theme-blended
/// row tint). Keyed by the `type` enum values of `system.documentation`.
const TYPE_ACCENT = {
'Function': '#4F8DF7',
'Aggregate Function': '#3F6FE0',
'Aggregate Function Combinator': '#6A6AE0',
'Table Function': '#3FB0E0',
'Table Engine': '#4FB04F',
'Database Engine': '#3F9F66',
'Data Type': '#9B59D0',
'Dictionary Layout': '#2FB39A',
'Dictionary Source': '#1F9E88',
'Data Skipping Index': '#E08A2F',
'Disk Type': '#A07A4F',
'Setting': '#D9B600',
'MergeTree Setting': '#C7A400',
'Server Setting': '#B89600',
'Format': '#E05A93',
'Compression Codec': '#D95F4F',
'Profile Event': '#7C8FB8',
'Current Metric': '#5FA88F',
'Asynchronous Metric': '#9A8FCF',
'System Table': '#8A8F99',
};
function buildURL() {
/// Always request CORS headers, exactly like `/play` and `/schema`. When this page is opened
/// as a local file (`file://`) and pointed at a remote server, the queries are cross-origin
/// (the browser sends `Origin: null`), and ClickHouse only adds `Access-Control-Allow-Origin`
/// when `add_http_cors_header=1` is set; without it the searches would be blocked by CORS.
/// The configured URL may already carry query parameters (e.g. `http://host:8123/?database=db`),
/// so append with `&` in that case instead of a second `?`, exactly like `play.html`; otherwise
/// `default_format` would be folded into the existing parameter and the response would not be JSON.
const sep = $url.value.indexOf('?') >= 0 ? '&' : '?';
let url = `${$url.value}${sep}default_format=JSONEachRow&enable_http_compression=1&add_http_cors_header=1`;
if ($user.value) {
url += `&user=${encodeURIComponent($user.value)}`;
}
if ($password.value) {
url += `&password=${encodeURIComponent($password.value)}`;
}
return url;
}
/* ----------------------------------------------------------------------------------------
Markdown rendering setup.
---------------------------------------------------------------------------------------- */
/// `:::note ... :::` admonitions (as used on the documentation website). Implemented as a
/// block-level Marked extension so that the body is itself rendered as Markdown.
const admonitionExtension = {
name: 'admonition',
level: 'block',
start(src) {
const i = src.search(/^:::/m);
return i < 0 ? undefined : i;
},
tokenizer(src) {
const rule = /^:::([a-zA-Z]+)[ \t]*(.*)\n([\s\S]*?)\n:::[ \t]*(?:\n|$)/;
const match = rule.exec(src);
if (match) {
const token = {
type: 'admonition',
raw: match[0],
variant: match[1].toLowerCase(),
title: (match[2] || match[1]).trim(),
tokens: [],
};
this.lexer.blockTokens(match[3], token.tokens);
return token;
}
},
renderer(token) {
const inner = this.parser.parse(token.tokens);
return `<div class="admonition admonition-${escapeHTML(token.variant)}">`
+ `<div class="admonition-title">${escapeHTML(token.title)}</div>`
+ inner + `</div>`;
},
};
/// Render a TeX fragment with KaTeX. The math is captured raw by the tokenizers below (before
/// Markdown's inline rules can mangle backslashes, underscores, etc.), so it renders faithfully.
function renderTeX(tex, display_mode) {
if (typeof katex === 'undefined') {
/// KaTeX is not loaded (e.g. offline): show the original TeX source.
const fence = display_mode ? '$$' : '$';
return escapeHTML(fence + tex + fence);
}
try {
return katex.renderToString(tex, { displayMode: display_mode, throwOnError: false });
} catch (e) {
return escapeHTML(tex);
}
}
/// TeX math: `$$ ... $$` (and `\[ ... \]`) as display math, `$ ... $` (and `\( ... \)`) inline.
const mathBlockExtension = {
name: 'mathBlock',
level: 'block',
start(src) {
const i = src.search(/\$\$|\\\[/);
return i < 0 ? undefined : i;
},
tokenizer(src) {
const match = /^\$\$([\s\S]+?)\$\$/.exec(src) || /^\\\[([\s\S]+?)\\\]/.exec(src);
if (match) {
return { type: 'mathBlock', raw: match[0], text: match[1].trim() };
}
},
renderer(token) {
return renderTeX(token.text, /*display_mode=*/ true);
},
};
const mathInlineExtension = {
name: 'mathInline',
level: 'inline',
start(src) {
const i = src.search(/\$|\\\(/);
return i < 0 ? undefined : i;
},
tokenizer(src) {
/// `$ ... $` on a single line (not `$$`), or `\( ... \)`.
const match = /^\$(?!\$)([^\n$]+?)\$/.exec(src) || /^\\\(([\s\S]+?)\\\)/.exec(src);
if (match) {
return { type: 'mathInline', raw: match[0], text: match[1].trim() };
}
},
renderer(token) {
return renderTeX(token.text, /*display_mode=*/ false);
},
};
marked.use({ extensions: [admonitionExtension, mathBlockExtension, mathInlineExtension] });
/// Content of documentation snippets that converted pages `import` and use as a self-closing tag
/// (e.g. `import PrettyFormatSettings from '/snippets/common-pretty-format-settings.mdx'; ...
/// <PrettyFormatSettings/>`). Unlike a decorative badge, these carry real content (a settings
/// table, a data-type mapping) that a converted entry would otherwise lose entirely on this page;
/// keyed by the imported path's suffix so any local binding name resolves. Kept in sync with the
/// corresponding files under `docs/snippets/`.
const DOC_SNIPPETS = {
"_snippet_dictionary_in_cloud.mdx": "<Tip>\nIf you are using a dictionary with ClickHouse Cloud please use the DDL query option to create your dictionaries, and create your dictionary as user `default`.\nAlso, verify the list of supported dictionary sources in the [Cloud Compatibility guide](/products/cloud/guides/cloud-compatibility).\n</Tip>",
"_when-to-use-json.mdx": "## When to use the `JSON` Type {#when-to-use-json-type}\n\nThe `JSON` type is designed for querying, filtering, and aggregating specific fields within JSON objects that have dynamic or unpredictable structures. It achieves this by splitting JSON objects into separate sub-columns, which dramatically reduces data read and speeds up queries on selected fields compared to alternatives like `Map` or parsing strings.\n\n**However, this comes with important trade-offs:**\n\n- Slower `INSERT`s - Splitting JSON into sub-columns, performing type inference, and managing flexible storage structures makes inserts slower compared to storing JSON as a simple `String` column.\n- Slower when reading entire objects - If you need to retrieve complete JSON documents (rather than specific fields), the `JSON` type is slower than reading from a `String` column. The overhead of reconstructing objects from separate sub-columns provides no benefit when you're not doing field-level queries.\n- Storage overhead - Maintaining separate sub-columns adds structural overhead compared to storing JSON as a single string value.\n\n### Use the `JSON` type when: {#use-json-type}\n\n- Your data has a dynamic or unpredictable structure with varying keys across documents\n- Field types or schemas change over time or vary between records\n- You need to query, filter, or aggregate on specific paths within JSON objects whose structure you can't predict upfront\n- Your use case involves semi-structured data like logs, events, or user-generated content with inconsistent schemas\n\n### Use a `String` column (or structured types) when: {#use-string-type}\n- Your data structure is known and consistent - in this case, use normal columns, `Tuple`, `Array`, `Dynamic`, or `Variant` types instead\n- `JSON` documents are treated as opaque blobs that are only stored and retrieved in their entirety without field-level analysis\n- You don't need to query or filter on individual JSON fields within the database\n- The `JSON` is simply a transport/storage format, not analyzed within ClickHouse\n\n<Tip>\nIf `JSON` is an opaque document that isn't analyzed inside the database, and only stored and retrieved back, it should be stored as a `String` field. The `JSON` type's benefits only materialize when you need to efficiently query, filter, or aggregate on specific fields within dynamic `JSON` structures.\n\nYou can also mix approaches—use standard columns for predictable top-level fields and a `JSON` column for dynamic sections of the payload.\n</Tip>",
"common-pretty-format-settings.mdx": "The following settings are common to all `Pretty` formats:\n\n| Setting | Description | Default |\n|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|\n| [`output_format_pretty_max_rows`](/reference/settings/formats/output-format#output_format_pretty_max_rows) | Row limit for Pretty formats. | `10000` |\n| [`output_format_pretty_max_column_pad_width`](/reference/settings/formats/output-format#output_format_pretty_max_column_pad_width) | Maximum width to pad all values in a column in Pretty formats. | `250` |\n| [`output_format_pretty_max_value_width`](/reference/settings/formats/output-format#output_format_pretty_max_value_width) | Maximum width of value to display in Pretty formats. If greater - it will be cut. | `10000` |\n| [`output_format_pretty_color`](/reference/settings/formats/output-format#output_format_pretty_color) | Use ANSI escape sequences to paint colors in Pretty formats. | `true` |\n| [`output_format_pretty_grid_charset`](/reference/settings/formats/output-format#output_format_pretty_grid_charset) | Charset for printing grid borders. Available charsets: ASCII, UTF-8. | `UTF-8` |\n| [`output_format_pretty_row_numbers`](/reference/settings/formats/output-format#output_format_pretty_row_numbers) | Add row numbers before each row for pretty output format. | `true` |\n| [`output_format_pretty_display_footer_column_names`](/reference/settings/formats/output-format#output_format_pretty_display_footer_column_names) | Display column names in the footer if table contains many rows. | `true` |\n| [`output_format_pretty_display_footer_column_names_min_rows`](/reference/settings/formats/output-format#output_format_pretty_display_footer_column_names_min_rows) | Sets the minimum number of rows for which a footer will be displayed if [`output_format_pretty_display_footer_column_names`](/reference/settings/formats/output-format#output_format_pretty_display_footer_column_names) is enabled. | `50` |",
"common-row-binary-format-settings.mdx": "The following settings are common to all `RowBinary` type formats.\n\n| Setting | Description | Default |\n|------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|\n| [`format_binary_max_string_size`](/reference/settings/formats/format-binary#format_binary_max_string_size) | The maximum allowed size for String in RowBinary format. | `1GiB` |\n| [`output_format_binary_encode_types_in_binary_format`](/reference/settings/formats/output-format#output_format_binary_encode_types_in_binary_format) | Allows to write types in header using [`binary encoding`](/reference/data-types/data-types-binary-encoding) instead of strings with type names in [`RowBinaryWithNamesAndTypes`](/reference/formats/RowBinary/RowBinaryWithNamesAndTypes) output format. | `false` |\n| [`input_format_binary_decode_types_in_binary_format`](/reference/settings/formats/input-format#input_format_binary_decode_types_in_binary_format) | Allows to read types in header using [`binary encoding`](/reference/data-types/data-types-binary-encoding) instead of strings with type names in [`RowBinaryWithNamesAndTypes`](/reference/formats/RowBinary/RowBinaryWithNamesAndTypes) input format. | `false` |\n| [`output_format_binary_write_json_as_string`](/reference/settings/formats/output-format#output_format_binary_write_json_as_string) | Allows to write values of the [`JSON`](/reference/data-types/newjson) data type as `JSON` [String](/reference/data-types/string) values in [`RowBinary`](/reference/formats/RowBinary/RowBinary) output format. | `false` |\n| [`input_format_binary_read_json_as_string`](/reference/settings/formats/input-format#input_format_binary_read_json_as_string) | Allows to read values of the [`JSON`](/reference/data-types/newjson) data type as `JSON` [String](/reference/data-types/string) values in [`RowBinary`](/reference/formats/RowBinary/RowBinary) input format. | `false` |",
"data-types-matching.mdx": "The table below shows all data types supported by the Apache Avro format, and their corresponding ClickHouse [data types](/reference/data-types/index) in `INSERT` and `SELECT` queries.\n\n| Avro data type `INSERT` | ClickHouse data type | Avro data type `SELECT` |\n|---------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|---------------------------------|\n| `boolean`, `int`, `long`, `float`, `double` | [Int(8\\16\\32)](/reference/data-types/int-uint), [UInt(8\\16\\32)](/reference/data-types/int-uint) | `int` |\n| `boolean`, `int`, `long`, `float`, `double` | [Int64](/reference/data-types/int-uint), [UInt64](/reference/data-types/int-uint) | `long` |\n| `boolean`, `int`, `long`, `float`, `double` | [Float32](/reference/data-types/float) | `float` |\n| `boolean`, `int`, `long`, `float`, `double` | [Float64](/reference/data-types/float) | `double` |\n| `bytes`, `string`, `fixed`, `enum` | [String](/reference/data-types/string) | `bytes` or `string` \\* |\n| `bytes`, `string`, `fixed` | [FixedString(N)](/reference/data-types/fixedstring) | `fixed(N)` |\n| `enum` | [Enum(8\\16)](/reference/data-types/enum) | `enum` |\n| `array(T)` | [Array(T)](/reference/data-types/array) | `array(T)` |\n| `map(V, K)` | [Map(V, K)](/reference/data-types/map) | `map(string, K)` |\n| `union(null, T)`, `union(T, null)` | [Nullable(T)](/reference/data-types/date) | `union(null, T)` |\n| `union(T1, T2, …)` \\** | [Variant(T1, T2, …)](/reference/data-types/variant) | `union(T1, T2, …)` \\** |\n| `null` | [Nullable(Nothing)](/reference/data-types/special-data-types/nothing) | `null` |\n| `int (date)` \\**\\* | [Date](/reference/data-types/date), [Date32](/reference/data-types/date32) | `int (date)` \\**\\* |\n| `long (timestamp-millis)` \\**\\* | [DateTime64(3)](/reference/data-types/datetime) | `long (timestamp-millis)` \\**\\* |\n| `long (timestamp-micros)` \\**\\* | [DateTime64(6)](/reference/data-types/datetime) | `long (timestamp-micros)` \\**\\* |\n| `bytes (decimal)` \\**\\* | [DateTime64(N)](/reference/data-types/datetime) | `bytes (decimal)` \\**\\* |\n| `int` | [IPv4](/reference/data-types/ipv4) | `int` |\n| `fixed(16)` | [IPv6](/reference/data-types/ipv6) | `fixed(16)` |\n| `bytes (decimal)` \\**\\* | [Decimal(P, S)](/reference/data-types/decimal) | `bytes (decimal)` \\**\\* |\n| `string (uuid)` \\**\\* | [UUID](/reference/data-types/uuid) | `string (uuid)` \\**\\* |\n| `fixed(16)` | [Int128/UInt128](/reference/data-types/int-uint) | `fixed(16)` |\n| `fixed(32)` | [Int256/UInt256](/reference/data-types/int-uint) | `fixed(32)` |\n| `record` | [Tuple](/reference/data-types/tuple) | `record` |\n\n\\* `bytes` is default, controlled by setting [`output_format_avro_string_column_pattern`](/reference/settings/formats/output-format#output_format_avro_string_column_pattern)\n\n\\** The [Variant type](/reference/data-types/variant) implicitly accepts `null` as a field value, so for example the Avro `union(T1, T2, null)` will be converted to `Variant(T1, T2)`.\nAs a result, when producing Avro from ClickHouse, we have to always include the `null` type to the Avro `union` type set as we don't know if any value is actually `null` during the schema inference.\n\n\\**\\* [Avro logical types](https://avro.apache.org/docs/1.12.0/specification/#logical-types)\n\nUnsupported Avro logical data types:\n- `time-millis`\n- `time-micros`\n- `duration`",
};
/// Known MDX components from the documentation toolchain (status badges and a few layout
/// wrappers). The layout wrappers are build machinery, not content, and are stripped whether or not
/// the Markdown that uses them also `import`s them; a status `*Badge`, in contrast, carries real
/// availability information and is rendered as a readable label by `preprocessMarkdown` (some
/// embedded entries use badges without a local import, e.g. `transactionID` opens with a bare
/// `<ExperimentalBadge/>` / `<CloudNotSupportedBadge/>`). Only these known names, plus any `*Badge`
/// component (see `preprocessMarkdown`), are recognized, so literal `<...>` placeholders in the prose
/// (e.g. `<SearchPhrase>` in an XML example) are preserved.
const MDX_COMPONENTS = [
'ExperimentalBadge', 'BetaBadge', 'DeprecatedBadge',
'CloudNotSupportedBadge', 'CloudAvailableBadge', 'PrivatePreviewBadge',
'Tabs', 'TabItem', 'VerticalStepper', 'Image',
/// Mintlify components used by embedded pages converted from the website's Mintlify sources.
/// Admonitions (`<Note>` etc.) and titled wrappers (`<Tab>`, `<Card>`) are first rewritten by
/// `preprocessMarkdown` into renderable Markdown; the names here catch any leftover tags.
'Tab', 'Card', 'CardGroup',
'Note', 'Warning', 'Tip', 'Info', 'Check', 'Danger', 'Caution',
];
/// Human-readable text for an MDX `*Badge` component such as `<ExperimentalBadge/>`, mirroring the
/// terminal `help` renderer's `badgeLabel` (`src/Client/TerminalMarkdownRenderer.cpp`) so both help
/// surfaces convey the same status. Known badges get a descriptive label; any other `*Badge` (the
/// website adds new ones over time) falls back to its name with the camel case split (e.g.
/// `CommunityMaintainedBadge` -> `Community Maintained`).
function badgeLabel(name) {
switch (name) {
case 'ExperimentalBadge': return 'Experimental';
case 'BetaBadge': return 'Beta';
case 'CloudNotSupportedBadge': return 'Not supported in ClickHouse Cloud';
case 'CloudAvailableBadge': return 'Available in ClickHouse Cloud';
case 'CloudOnlyBadge': return 'ClickHouse Cloud only';
case 'PrivatePreviewBadge': return 'Private Preview';
case 'ScalePlanFeatureBadge': return 'Scale plan feature';
case 'EnterprisePlanFeatureBadge': return 'Enterprise plan feature';
}
const stem = name.endsWith('Badge') ? name.slice(0, -5) : name;
return stem.replace(/([^A-Z])([A-Z])/g, '$1 $2');
}
/// The substantive message that a plan-gating badge renders from its attributes on the website
/// (`docs/snippets/components/ScalePlanFeatureBadge`, `.../EnterprisePlanFeatureBadge`): which plan
/// a feature requires and how to get it. Unlike a status badge, collapsing these to a label would
/// lose that message, so `preprocessMarkdown` renders it after the label. An attribute is truthy
/// when present with a non-empty value, matching how the website's JSX treats the string attributes
/// the documentation actually uses (`support="true"`, `linking_verb_are="True"`); kept in sync with
/// the identical `badgePayload` in the terminal `help` renderer (`src/Client/TerminalMarkdownRenderer.cpp`).
function badgePayload(name, attributes) {
if (name !== 'ScalePlanFeatureBadge' && name !== 'EnterprisePlanFeatureBadge')
return '';
const attribute = (attr) => {
const m = new RegExp('(?:^|\\s)' + attr + '\\s*=\\s*"([^"]*)"').exec(attributes || '');
return m ? m[1] : '';
};
const feature = attribute('feature') || 'This feature';
const verb = attribute('linking_verb_are') ? 'are' : 'is';
if (name === 'ScalePlanFeatureBadge')
return feature + ' ' + verb + ' available in the Scale and Enterprise plans. To upgrade, visit the plans page in the cloud console.';
return feature + ' ' + verb + ' available in the Enterprise plan. '
+ (attribute('support') ? 'Contact support to enable this feature.' : 'To upgrade, visit the plans page in the cloud console.');
}
/// Strip MDX-isms that occasionally leak into the documentation text: `import`/`export`
/// statements and `<import ... >` directives are machinery for the website build, not content.
/// The layout components brought in by those `import`s (e.g. a `<Tabs>` wrapper), as well as the
/// known `MDX_COMPONENTS` above even when they are not imported, are stripped as well; a status
/// `<ExperimentalBadge/>` is instead rendered as a readable label (see `badgeLabel`). Either way the
/// tag must not survive verbatim: a self-closing custom tag like `<Tabs/>` is otherwise parsed by
/// the HTML parser as an unclosed element — the self-closing slash is ignored for non-void
/// elements — that swallows the rest of the document as its children, so the sanitizer then drops
/// the whole body (the visible symptom: an entity rendered as an empty page).
function preprocessMarkdown(md) {
/// Seed with the known component names, then add the local names of imported MDX components:
/// default imports (`import ExperimentalBadge from '...'`) and named/aliased imports
/// (`import {Tabs, TabItem as Item} from '...'`). An import from a known `DOC_SNIPPETS` path
/// is also recorded under its local name, whatever that happens to be, so its usage can be
/// resolved to real content below instead of falling through to the generic tag-stripping.
const components = new Set(MDX_COMPONENTS);
const snippetContentByLocalName = new Map();
const importRe = /^[ \t]*import\s+([\s\S]*?)\s+from\s+['"]([^'"]+)['"];?[ \t]*$/gm;
for (const m of md.matchAll(importRe)) {
const clause = m[1].trim();
const path = m[2];
const snippetEntry = Object.entries(DOC_SNIPPETS).find(([suffix]) => path.endsWith(suffix));
const braced = /\{([^}]*)\}/.exec(clause);
const names = braced ? braced[1].split(',') : [clause];
for (const name of names) {
/// The local binding is the identifier after `as` when the import is aliased.
const id = /([A-Za-z_$][\w$]*)\s*$/.exec(name.trim());
if (id) {
components.add(id[1]);
if (snippetEntry)
snippetContentByLocalName.set(id[1], snippetEntry[1]);
}
}
}
let out = md
.replace(importRe, '')
.replace(/^[ \t]*export\s+default\b.*$/gm, '')
.replace(/<import\b[^>]*\/?>/gi, '');
/// Resolve self-closing usages of a known documentation snippet import (see `DOC_SNIPPETS`)
/// to its actual content, before the generic tag-stripping below discards it as machinery
/// with nothing to render.
for (const [name, content] of snippetContentByLocalName)
out = out.replace(new RegExp('<' + name + '(?:\\s[^>]*)?/?>', 'g'), '\n\n' + content + '\n\n');
/// Mintlify admonition components (`<Note>` ... `</Note>`), used by embedded pages converted
/// from the website's Mintlify sources: mapped onto the `:::type` admonition syntax so they
/// render as admonitions instead of being dropped as unknown elements. The tags stand on
/// their own lines in those sources; any leftover inline tag is stripped by the loop below.
out = out
.replace(/^[ \t]*<(Note|Warning|Tip|Info|Check|Danger|Caution)>[ \t]*$/gm, (m, name) => ':::' + name.toLowerCase())
.replace(/^[ \t]*<\/(?:Note|Warning|Tip|Info|Check|Danger|Caution)>[ \t]*$/gm, ':::');
/// A `<Tab title="...">` / `<TabItem label="...">` / `<Card title="...">` introduces an
/// alternative (e.g. a syntax variant) or a callout whose heading carries meaning: keep the
/// title as a bold line; the wrapper tags themselves are stripped below.
out = out.replace(/<(?:Tab|TabItem|Card)(\s[^>]*)>/g, (m, attrs) => {
const title = /(?:^|\s)(?:title|label)="([^"]*)"/.exec(attrs);
return title ? '\n**' + title[1] + '**\n' : '';
});
/// Remove the opening, closing, and self-closing tags of those components, keeping any
/// content nested between an open/close pair. A `*Badge` is handled separately below — it is
/// rendered as a readable label rather than dropped — so it is skipped here even when imported.
for (const name of components)
if (!name.endsWith('Badge'))
out = out.replace(new RegExp('</?' + name + '(?:\\s[^>]*)?/?>', 'g'), '');
/// Render any `*Badge` MDX component as a readable label instead of dropping it, mirroring the
/// terminal `help` renderer (see `badgeLabel`). A status badge such as `<ExperimentalBadge/>`,
/// `<CloudNotSupportedBadge/>`, or `<ScalePlanFeatureBadge/>` carries real availability
/// information (experimental, not supported in ClickHouse Cloud, plan-gated), so the browser help
/// surface shows it as a bold label rather than silently omitting it. A plan-gating badge also
/// carries a substantive message built from its attributes (see `badgePayload`), rendered after
/// the label. Matching by the `*Badge` name (not the local `import`) removes the dependency on
/// the import being present, and a self-closing badge no longer swallows the rest of the
/// document into its subtree for the sanitizer to drop. A `*Badge` is a PascalCase component
/// name that does not occur in documentation prose, so this is safe. A closing `</...Badge>`
/// (badges are self-closing in practice) is simply removed.
out = out.replace(/<(\/?)([A-Z][A-Za-z0-9]*Badge)((?:\s[^>]*)?)\/?>/g,
(m, closing, name, attributes) => {
if (closing)
return '';
const payload = badgePayload(name, attributes);
return '**[' + badgeLabel(name) + ']**' + (payload ? ' ' + payload : '');
});
/// Likewise strip any remaining *self-closing* PascalCase component that was neither imported
/// nor named in the lists above, nor resolved as a known snippet import — the website adds new
/// components and snippets over time. A self-closing custom tag is unambiguously JSX — prose
/// placeholders like `<SearchPhrase>` are never self-closing — and if left in place it swallows
/// the rest of the document as its subtree (see the comment above `preprocessMarkdown`).
out = out.replace(/<[A-Z][A-Za-z0-9]*(?:\s[^>]*)?\/>/g, '');
return out;
}
/* ----------------------------------------------------------------------------------------
Sanitization of rendered Markdown.
`marked.parse` is not a sanitizer: it passes raw HTML in the Markdown through verbatim and
keeps any URL scheme in links and images. The documentation rendered here comes from the
server the user points this page at, over a connection whose credentials live in this same
origin, so an untrusted or compromised `system.documentation` could otherwise inject
`<img src=x onerror=...>` or `[x](javascript:...)` that runs in this origin, reads the
`user`/`password` fields, and issues same-origin queries. Every rendered document is
therefore sanitized against an allowlist before it reaches the DOM: elements not on the
list are dropped with their subtree, attributes not on the list are removed, `href`/`src`
values are kept only for safe URL schemes, and resource sinks that auto-load when the
document renders — a same-origin `src`, or a `url(...)` in a `style` — are rejected so that
untrusted documentation cannot make the browser fetch `/?query=...` from this origin on its
own. The allowlist covers what Markdown, the admonition and math extensions, and KaTeX (its
HTML and the parallel MathML) legitimately produce.
---------------------------------------------------------------------------------------- */
const ALLOWED_TAGS = new Set([
/// Markdown block- and inline-level elements.
'p', 'br', 'hr', 'div', 'span', 'a', 'img', 'code', 'pre', 'blockquote',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'dl', 'dt', 'dd',
'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td',
'strong', 'em', 'b', 'i', 'u', 's', 'del', 'ins', 'sub', 'sup', 'kbd', 'samp',
'abbr', 'mark', 'small', 'wbr',
/// Disclosure widgets: some embedded docs wrap notes in `<details><summary>...`, e.g. the
/// "Implementation details" sections of the `uniq` and `uniqCombined` aggregate functions.
/// Dropping them (with their subtree) made those sections disappear from the rendered page.
'details', 'summary',
/// MathML, as emitted by KaTeX alongside its visible HTML rendering.
'math', 'semantics', 'annotation', 'mrow', 'mi', 'mo', 'mn', 'ms', 'mtext', 'mspace',
'msup', 'msub', 'msubsup', 'mfrac', 'msqrt', 'mroot', 'mover', 'munder', 'munderover',
'mtable', 'mtr', 'mtd', 'mlabeledtr', 'mpadded', 'mphantom', 'menclose', 'mstyle',
'merror', 'mglyph', 'mmultiscripts', 'mprescripts', 'none',
]);
const ALLOWED_ATTRS = new Set([
/// `class` and `style` are needed by KaTeX (inline layout) and our admonitions; the rest are
/// harmless presentational or accessibility attributes. `href`/`src` are additionally
/// scheme-checked below.
'class', 'style', 'id', 'title', 'colspan', 'rowspan', 'align', 'start', 'aria-hidden',
'href', 'src', 'alt', 'target', 'rel', 'open',
/// MathML presentation attributes used by KaTeX.
'mathvariant', 'displaystyle', 'scriptlevel', 'stretchy', 'accent', 'accentunder',
'width', 'height', 'depth', 'lspace', 'rspace', 'voffset', 'fence', 'separator',
'mathcolor', 'mathbackground', 'linethickness', 'encoding', 'xmlns',
]);
/// Whether a URL is unsafe to keep in an `href`/`src`. Only relative URLs, in-page anchors, and
/// the `http`/`https`/`mailto` schemes are allowed; `javascript:`, `data:`, `vbscript:`, and the
/// like are rejected. Browsers ignore ASCII whitespace and control characters when resolving the
/// scheme, so those are stripped first to catch tricks like `java\tscript:`.
function isUnsafeURL(value) {
const match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(value.replace(/[\u0000-\u0020]+/g, ''));
if (!match) {
return false; /// No scheme: a relative URL or a `#` fragment, which is safe.
}
const scheme = match[1].toLowerCase();
return scheme !== 'http' && scheme !== 'https' && scheme !== 'mailto';
}
/// Whether a URL resolves to this page's own origin. `src` values that do are rejected: unlike a
/// link (`href`), a `src` is fetched automatically when the document renders, so a same-origin
/// `src` such as `` would make the browser issue an unintended query
/// against the very ClickHouse server whose credentials live in this origin. Cross-origin images
/// remain allowed; only the credentialed same origin is special.
function isSameOriginURL(value) {
try {
return new URL(value, location.href).origin === location.origin;
} catch (e) {
return false; /// Unparseable (e.g. a bare fragment): not a resource load against this origin.
}
}
/// Sanitize the value of a `style` attribute, or return `null` to drop the attribute entirely.
/// CSS can also auto-load resources — `url(...)`, `image-set(...)`, `cross-fade(...)` — so a
/// `style="background:url('/?query=SELECT%201')"` is the same hazard as a same-origin `src`.
/// KaTeX's inline styles are plain layout declarations (dimensions, offsets, colors) that never
/// contain those functions, nor CSS escapes; so the safe and robust rule is to drop any `style`
/// that uses a resource-loading function or hides one behind a backslash escape (e.g. `u\72l(`).
function sanitizeStyle(value) {
if (value.includes('\\'))
return null;
if (/(?:^|[^\w-])(?:url|image-set|-webkit-image-set|cross-fade)\s*\(/i.test(value))
return null;
return value;
}
/// Sanitize a parsed DOM subtree in place (see the block comment above).
function sanitizeNode(node) {
/// Snapshot the children first: the loop removes nodes and attributes as it goes.
for (const child of [...node.childNodes]) {
if (child.nodeType === Node.ELEMENT_NODE) {
if (!ALLOWED_TAGS.has(child.tagName.toLowerCase())) {
child.remove();
continue;
}
for (const attr of [...child.attributes]) {
const name = attr.name.toLowerCase();
if (!ALLOWED_ATTRS.has(name)
|| (name === 'href' && isUnsafeURL(attr.value))
|| (name === 'src' && (isUnsafeURL(attr.value) || isSameOriginURL(attr.value)))
|| (name === 'style' && sanitizeStyle(attr.value) === null)) {
child.removeAttribute(attr.name);
}
}
sanitizeNode(child);
} else if (child.nodeType === Node.COMMENT_NODE) {
child.remove();
}
}
}
/// Render rendered-Markdown HTML into `element`, sanitized first (see the block comment above).
function setSanitizedHTML(element, html) {
const template = document.createElement('template');
template.innerHTML = html;
sanitizeNode(template.content);
element.replaceChildren(template.content);
}
/// Search both the name and the body of the documentation, but rank exact and prefix
/// name matches first, so the most relevant entities are at the top.
const QUERY = `
SELECT name, type, description, source
FROM system.documentation
WHERE positionCaseInsensitive(name, {q:String}) > 0
OR positionCaseInsensitive(description, {q:String}) > 0
ORDER BY
(lowerUTF8(name) = lowerUTF8({q:String})) DESC,
startsWithUTF8(lowerUTF8(name), lowerUTF8({q:String})) DESC,
positionCaseInsensitive(name, {q:String}) > 0 DESC,
length(name) ASC,
name ASC
LIMIT 200`;
/// Run a search and render the results. `desired`, if given, is `{name, type, section}` of the
/// entry that should be selected once the results arrive (used when restoring from the URL);
/// otherwise the first result is selected. This function never touches the browser history —
/// the callers decide whether to push or replace a history entry.
async function runSearch(term, desired) {
const seq = ++request_seq;
/// Snapshot the connection generation this search belongs to. A response is fetched, parsed,
/// and rendered across two `await`s, and during either of them the user may change the server
/// URL, user, or password (which bumps `connection_seq` and clears `all_names`). Re-checking
/// this snapshot after each `await`, alongside `request_seq`, keeps a response fetched from the
/// previous connection from being rendered or mixed into the rebuilt cross-link cache.
const conn = connection_seq;
if (!term) {
current = [];
selected = -1;
renderResults();
$doc.className = 'empty';
$doc.innerText = 'Start typing to search the documentation.';
$status.innerText = '';
return;
}
const url = buildURL() + `¶m_q=${encodeURIComponent(term)}`;
try {
/// `Authorization: never` keeps the browser from attaching stored HTTP Basic
/// credentials, so the empty `user`/`password` fields connect as the default user,
/// exactly as `play.html` does, instead of authenticating as a cached `default` login.
const response = await fetch(url, { method: 'POST', body: QUERY, headers: { 'Authorization': 'never' } });
if (seq !== request_seq || conn !== connection_seq) {
return; /// A newer query or a connection change has superseded this; drop the response.
}
const text = await response.text();
if (seq !== request_seq || conn !== connection_seq) {
return; /// Re-check after the body arrives: either could have changed while it streamed.
}
if (!response.ok) {
$status.innerText = '';
$doc.className = 'empty';
$doc.innerHTML = `<div class="error"></div>`;
$doc.querySelector('.error').innerText = text;
return;
}
current = text.split('\n').filter(l => l).map(l => JSON.parse(l));
if (desired && desired.name) {
const idx = current.findIndex(r =>
r.name === desired.name && (!desired.type || r.type === desired.type));
selected = idx >= 0 ? idx : (current.length ? 0 : -1);