Skip to content

Commit 9a282b2

Browse files
add support for selecting protocol version in native format
1 parent 74fb74c commit 9a282b2

19 files changed

Lines changed: 1236 additions & 99 deletions

e2e/electron.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ test.describe('Electron app', () => {
2323
await expect(window.locator('.query-textarea')).toBeVisible();
2424
await expect(window.locator('.query-btn.primary')).toBeVisible();
2525
await expect(window.locator('#format-select')).toBeVisible();
26+
await expect(window.locator('#protocol-version-select')).toHaveCount(0);
2627

2728
// In Electron mode, the host input should be visible and Share button hidden
2829
await expect(window.locator('#host-input')).toBeVisible();
@@ -32,6 +33,10 @@ test.describe('Electron app', () => {
3233
const shareButtons = window.locator('button', { hasText: 'Share' });
3334
await expect(shareButtons).toHaveCount(0);
3435

36+
await window.locator('#format-select').selectOption('Native');
37+
await expect(window.locator('#protocol-version-select')).toBeVisible();
38+
await expect(window.locator('#protocol-version-select')).toHaveValue('0');
39+
3540
// Verify the window title
3641
const title = await window.title();
3742
expect(title).toBeTruthy();

electron/main.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { app, BrowserWindow, ipcMain } from 'electron';
22
import { fileURLToPath } from 'node:url';
33
import path from 'node:path';
44
import fs from 'node:fs';
5+
import { appendClickHouseRequestParams } from '../src/core/clickhouse/request-params';
6+
import { DEFAULT_NATIVE_PROTOCOL_VERSION } from '../src/core/types/native-protocol';
57

68
const __dirname = path.dirname(fileURLToPath(import.meta.url));
79

@@ -66,12 +68,14 @@ function createWindow(): void {
6668
}
6769

6870
// IPC: Execute a ClickHouse query
69-
ipcMain.handle('execute-query', async (_event, options: { query: string; format: string }) => {
71+
ipcMain.handle('execute-query', async (_event, options: { query: string; format: string; nativeProtocolVersion?: number }) => {
7072
const config = loadConfig();
71-
const params = new URLSearchParams({
72-
default_format: options.format,
73-
...CLICKHOUSE_SETTINGS,
74-
});
73+
const params = new URLSearchParams(CLICKHOUSE_SETTINGS);
74+
appendClickHouseRequestParams(
75+
params,
76+
options.format,
77+
options.nativeProtocolVersion ?? DEFAULT_NATIVE_PROTOCOL_VERSION
78+
);
7579

7680
const response = await fetch(`${config.host}/?${params}`, {
7781
method: 'POST',

electron/preload.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { contextBridge, ipcRenderer } from 'electron';
22

33
contextBridge.exposeInMainWorld('electronAPI', {
4-
executeQuery: (options: { query: string; format: string }): Promise<ArrayBuffer> =>
4+
executeQuery: (options: { query: string; format: string; nativeProtocolVersion?: number }): Promise<ArrayBuffer> =>
55
ipcRenderer.invoke('execute-query', options),
66
getConfig: (): Promise<{ host: string }> =>
77
ipcRenderer.invoke('get-config'),

src/components/App.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,23 @@ import { QueryInput } from './QueryInput';
66
import { decodeBase64Url } from '../core/base64url';
77
import { useStore } from '../store/store';
88
import { ClickHouseFormat } from '../core/types/formats';
9+
import {
10+
DEFAULT_NATIVE_PROTOCOL_VERSION,
11+
isNativeProtocolVersion,
12+
} from '../core/types/native-protocol';
913
import logo from '../assets/clickhouse-yellow-badge.svg';
1014
import '../styles/app.css';
1115

1216
function App() {
1317
const setQuery = useStore((s) => s.setQuery);
1418
const setFormat = useStore((s) => s.setFormat);
19+
const setNativeProtocolVersion = useStore((s) => s.setNativeProtocolVersion);
1520

1621
useEffect(() => {
1722
const params = new URLSearchParams(window.location.search);
1823
const q = params.get('q');
1924
const f = params.get('f');
25+
const pv = params.get('pv');
2026

2127
if (q) {
2228
try {
@@ -28,8 +34,16 @@ function App() {
2834
if (f && Object.values(ClickHouseFormat).includes(f as ClickHouseFormat)) {
2935
setFormat(f as ClickHouseFormat);
3036
}
37+
if (pv) {
38+
const parsed = Number(pv);
39+
if (Number.isInteger(parsed) && isNativeProtocolVersion(parsed)) {
40+
setNativeProtocolVersion(parsed);
41+
} else {
42+
setNativeProtocolVersion(DEFAULT_NATIVE_PROTOCOL_VERSION);
43+
}
44+
}
3145

32-
if (q || f) {
46+
if (q || f || pv) {
3347
window.history.replaceState({}, '', window.location.pathname);
3448
}
3549
}, []); // eslint-disable-line react-hooks/exhaustive-deps

src/components/AstTree/AstTree.tsx

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ export function AstTree() {
419419
setActiveNode(blockHeaderId, 'Block metadata (Header)');
420420
toggleExpanded(blockHeaderId);
421421
}}
422-
onDoubleClick={() => scrollToHex(block.header.numColumnsRange.start)}
422+
onDoubleClick={() => scrollToHex(block.header.byteRange.start)}
423423
onMouseEnter={() => setHoveredNode(blockHeaderId)}
424424
onMouseLeave={() => setHoveredNode(null)}
425425
style={{ '--depth': 1 } as React.CSSProperties}
@@ -428,12 +428,53 @@ export function AstTree() {
428428
<span className="ast-metadata-badge">Header</span>
429429
<span className="ast-metadata-label">Block metadata</span>
430430
<span className="ast-metadata-bytes">
431-
[{block.header.numColumnsRange.start}:{block.header.numRowsRange.end}] (
432-
{block.header.numRowsRange.end - block.header.numColumnsRange.start}B)
431+
[{block.header.byteRange.start}:{block.header.byteRange.end}] (
432+
{block.header.byteRange.end - block.header.byteRange.start}B)
433433
</span>
434434
</div>
435435
{isHeaderExpanded && (
436436
<div className="ast-children">
437+
{block.header.blockInfo && (
438+
<div className="ast-metadata-section">
439+
<div
440+
className={`ast-metadata-header ${activeNodeId === `block-${blockIndex}-blockinfo` ? 'active' : ''} ${hoveredNodeId === `block-${blockIndex}-blockinfo` ? 'hovered' : ''}`}
441+
onClick={() => setActiveNode(`block-${blockIndex}-blockinfo`, 'BlockInfo')}
442+
onDoubleClick={() => scrollToHex(block.header.blockInfo!.byteRange.start)}
443+
onMouseEnter={() => setHoveredNode(`block-${blockIndex}-blockinfo`)}
444+
onMouseLeave={() => setHoveredNode(null)}
445+
style={{ '--depth': 2 } as React.CSSProperties}
446+
>
447+
<span className="ast-metadata-badge">BlockInfo</span>
448+
<span className="ast-metadata-label">Field-based metadata</span>
449+
<span className="ast-metadata-bytes">
450+
[{block.header.blockInfo.byteRange.start}:{block.header.blockInfo.byteRange.end}] (
451+
{block.header.blockInfo.byteRange.end - block.header.blockInfo.byteRange.start}B)
452+
</span>
453+
</div>
454+
{block.header.blockInfo.fields.map((field) => {
455+
const fieldId = `block-${blockIndex}-blockinfo-field-${field.fieldNumber}`;
456+
return (
457+
<div
458+
key={fieldId}
459+
className={`ast-metadata-item ${activeNodeId === fieldId ? 'active' : ''} ${hoveredNodeId === fieldId ? 'hovered' : ''}`}
460+
style={{ '--depth': 3 } as React.CSSProperties}
461+
onClick={() => setActiveNode(fieldId, `${field.fieldName}: ${field.displayValue}`)}
462+
onDoubleClick={() => scrollToHex(field.byteRange.start)}
463+
onMouseEnter={() => setHoveredNode(fieldId)}
464+
onMouseLeave={() => setHoveredNode(null)}
465+
>
466+
<span className="ast-metadata-badge">Field {field.fieldNumber}</span>
467+
<span className="ast-metadata-label">{field.fieldName}:</span>
468+
<span className="ast-metadata-value">{field.displayValue}</span>
469+
<span className="ast-metadata-bytes">
470+
[{field.byteRange.start}:{field.byteRange.end}] (
471+
{field.byteRange.end - field.byteRange.start}B)
472+
</span>
473+
</div>
474+
);
475+
})}
476+
</div>
477+
)}
437478
<div
438479
className={`ast-metadata-item ${activeNodeId === numColsId ? 'active' : ''} ${hoveredNodeId === numColsId ? 'hovered' : ''}`}
439480
style={{ '--depth': 2 } as React.CSSProperties}
@@ -525,8 +566,8 @@ export function AstTree() {
525566
<span className="ast-metadata-badge">Meta</span>
526567
<span className="ast-metadata-label">Column definition</span>
527568
<span className="ast-metadata-bytes">
528-
[{col.nameByteRange.start}:{col.typeByteRange.end}] (
529-
{col.typeByteRange.end - col.nameByteRange.start}B)
569+
[{col.metadataByteRange.start}:{col.metadataByteRange.end}] (
570+
{col.metadataByteRange.end - col.metadataByteRange.start}B)
530571
</span>
531572
</div>
532573
{isColMetaExpanded && (
@@ -563,6 +604,66 @@ export function AstTree() {
563604
{col.typeByteRange.end - col.typeByteRange.start}B)
564605
</span>
565606
</div>
607+
{col.serializationInfo && (
608+
<>
609+
<div
610+
className={`ast-metadata-item ${activeNodeId === `${col.id}-serialization` ? 'active' : ''} ${hoveredNodeId === `${col.id}-serialization` ? 'hovered' : ''}`}
611+
style={{ '--depth': 3 } as React.CSSProperties}
612+
onClick={() => setActiveNode(`${col.id}-serialization`, 'Serialization info')}
613+
onDoubleClick={() => scrollToHex(col.serializationInfo!.byteRange.start)}
614+
onMouseEnter={() => setHoveredNode(`${col.id}-serialization`)}
615+
onMouseLeave={() => setHoveredNode(null)}
616+
>
617+
<span className="ast-metadata-badge">Meta</span>
618+
<span className="ast-metadata-label">serialization:</span>
619+
<span className="ast-metadata-value">
620+
{col.serializationInfo.hasCustomSerialization ? 'custom' : 'default'}
621+
</span>
622+
<span className="ast-metadata-bytes">
623+
[{col.serializationInfo.byteRange.start}:{col.serializationInfo.byteRange.end}] (
624+
{col.serializationInfo.byteRange.end - col.serializationInfo.byteRange.start}B)
625+
</span>
626+
</div>
627+
<div
628+
className={`ast-metadata-item ${activeNodeId === `${col.id}-serialization-has-custom` ? 'active' : ''} ${hoveredNodeId === `${col.id}-serialization-has-custom` ? 'hovered' : ''}`}
629+
style={{ '--depth': 3 } as React.CSSProperties}
630+
onClick={() => setActiveNode(`${col.id}-serialization-has-custom`, `has_custom: ${col.serializationInfo!.hasCustomSerialization}`)}
631+
onDoubleClick={() => scrollToHex(col.serializationInfo!.hasCustomRange.start)}
632+
onMouseEnter={() => setHoveredNode(`${col.id}-serialization-has-custom`)}
633+
onMouseLeave={() => setHoveredNode(null)}
634+
>
635+
<span className="ast-metadata-badge">UInt8</span>
636+
<span className="ast-metadata-label">has_custom:</span>
637+
<span className="ast-metadata-value">
638+
{col.serializationInfo.hasCustomSerialization ? '1' : '0'}
639+
</span>
640+
<span className="ast-metadata-bytes">
641+
[{col.serializationInfo.hasCustomRange.start}:{col.serializationInfo.hasCustomRange.end}] (
642+
{col.serializationInfo.hasCustomRange.end - col.serializationInfo.hasCustomRange.start}B)
643+
</span>
644+
</div>
645+
{col.serializationInfo.kindStackRange && (
646+
<div
647+
className={`ast-metadata-item ${activeNodeId === `${col.id}-serialization-kinds` ? 'active' : ''} ${hoveredNodeId === `${col.id}-serialization-kinds` ? 'hovered' : ''}`}
648+
style={{ '--depth': 3 } as React.CSSProperties}
649+
onClick={() => setActiveNode(`${col.id}-serialization-kinds`, `kind stack: ${col.serializationInfo!.kindStack.join(' -> ')}`)}
650+
onDoubleClick={() => scrollToHex(col.serializationInfo!.kindStackRange!.start)}
651+
onMouseEnter={() => setHoveredNode(`${col.id}-serialization-kinds`)}
652+
onMouseLeave={() => setHoveredNode(null)}
653+
>
654+
<span className="ast-metadata-badge">Kinds</span>
655+
<span className="ast-metadata-label">kindStack:</span>
656+
<span className="ast-metadata-value">
657+
{col.serializationInfo.kindStack.join(' -> ')}
658+
</span>
659+
<span className="ast-metadata-bytes">
660+
[{col.serializationInfo.kindStackRange.start}:{col.serializationInfo.kindStackRange.end}] (
661+
{col.serializationInfo.kindStackRange.end - col.serializationInfo.kindStackRange.start}B)
662+
</span>
663+
</div>
664+
)}
665+
</>
666+
)}
566667
</div>
567668
)}
568669
</div>

src/components/HexViewer/HexViewer.tsx

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,7 @@ function buildHighlightMap(
125125
const blockHeaderId = `block-${blockIndex}-header`;
126126
if (activeNodeId === blockHeaderId || hoveredNodeId === blockHeaderId) {
127127
const isActive = activeNodeId === blockHeaderId;
128-
// Highlight entire header range (numColumns + numRows)
129-
for (let i = block.header.numColumnsRange.start; i < block.header.numRowsRange.end; i++) {
128+
for (let i = block.header.byteRange.start; i < block.header.byteRange.end; i++) {
130129
const existing = map.get(i);
131130
if (!existing || isActive || !existing.isActive) {
132131
map.set(i, { color: metadataColor, isActive, isHovered: !isActive });
@@ -158,6 +157,30 @@ function buildHighlightMap(
158157
}
159158
}
160159

160+
const blockInfoId = `block-${blockIndex}-blockinfo`;
161+
if (block.header.blockInfo && (activeNodeId === blockInfoId || hoveredNodeId === blockInfoId)) {
162+
const isActive = activeNodeId === blockInfoId;
163+
for (let i = block.header.blockInfo.byteRange.start; i < block.header.blockInfo.byteRange.end; i++) {
164+
const existing = map.get(i);
165+
if (!existing || isActive || !existing.isActive) {
166+
map.set(i, { color: metadataColor, isActive, isHovered: !isActive });
167+
}
168+
}
169+
}
170+
171+
block.header.blockInfo?.fields.forEach((field) => {
172+
const fieldId = `block-${blockIndex}-blockinfo-field-${field.fieldNumber}`;
173+
if (activeNodeId === fieldId || hoveredNodeId === fieldId) {
174+
const isActive = activeNodeId === fieldId;
175+
for (let i = field.byteRange.start; i < field.byteRange.end; i++) {
176+
const existing = map.get(i);
177+
if (!existing || isActive || !existing.isActive) {
178+
map.set(i, { color: metadataColor, isActive, isHovered: !isActive });
179+
}
180+
}
181+
}
182+
});
183+
161184
block.columns.forEach((col) => {
162185
// Check if the column itself is active/hovered
163186
const isColActive = col.id === activeNodeId;
@@ -181,8 +204,7 @@ function buildHighlightMap(
181204

182205
if (activeNodeId === colMetaId || hoveredNodeId === colMetaId) {
183206
const isActive = activeNodeId === colMetaId;
184-
// Highlight both name and type ranges
185-
for (let i = col.nameByteRange.start; i < col.typeByteRange.end; i++) {
207+
for (let i = col.metadataByteRange.start; i < col.metadataByteRange.end; i++) {
186208
const existing = map.get(i);
187209
if (!existing || isActive || !existing.isActive) {
188210
map.set(i, { color: metadataColor, isActive, isHovered: !isActive });
@@ -210,6 +232,41 @@ function buildHighlightMap(
210232
}
211233
}
212234

235+
if (col.serializationInfo) {
236+
const serializationId = `${col.id}-serialization`;
237+
if (activeNodeId === serializationId || hoveredNodeId === serializationId) {
238+
const isActive = activeNodeId === serializationId;
239+
for (let i = col.serializationInfo.byteRange.start; i < col.serializationInfo.byteRange.end; i++) {
240+
const existing = map.get(i);
241+
if (!existing || isActive || !existing.isActive) {
242+
map.set(i, { color: metadataColor, isActive, isHovered: !isActive });
243+
}
244+
}
245+
}
246+
247+
const hasCustomId = `${col.id}-serialization-has-custom`;
248+
if (activeNodeId === hasCustomId || hoveredNodeId === hasCustomId) {
249+
const isActive = activeNodeId === hasCustomId;
250+
for (let i = col.serializationInfo.hasCustomRange.start; i < col.serializationInfo.hasCustomRange.end; i++) {
251+
const existing = map.get(i);
252+
if (!existing || isActive || !existing.isActive) {
253+
map.set(i, { color: metadataColor, isActive, isHovered: !isActive });
254+
}
255+
}
256+
}
257+
258+
const kindsId = `${col.id}-serialization-kinds`;
259+
if (col.serializationInfo.kindStackRange && (activeNodeId === kindsId || hoveredNodeId === kindsId)) {
260+
const isActive = activeNodeId === kindsId;
261+
for (let i = col.serializationInfo.kindStackRange.start; i < col.serializationInfo.kindStackRange.end; i++) {
262+
const existing = map.get(i);
263+
if (!existing || isActive || !existing.isActive) {
264+
map.set(i, { color: metadataColor, isActive, isHovered: !isActive });
265+
}
266+
}
267+
}
268+
}
269+
213270
// Also visit individual values
214271
col.values.forEach((node) => visitNode(node, 0));
215272
});

0 commit comments

Comments
 (0)