|
| 1 | +# CLAUDE.md - ClickHouse Format Explorer |
| 2 | + |
| 3 | +## Project Overview |
| 4 | + |
| 5 | +A tool for visualizing ClickHouse RowBinary and Native wire format data. Features an interactive hex viewer with AST-based type visualization, similar to ImHex. Available as a web app (Docker) or an Electron desktop app that connects to your existing ClickHouse server. |
| 6 | + |
| 7 | +**Current scope**: RowBinaryWithNamesAndTypes and Native formats. |
| 8 | + |
| 9 | +## Tech Stack |
| 10 | + |
| 11 | +- **Frontend**: React 18 + TypeScript + Vite |
| 12 | +- **State**: Zustand |
| 13 | +- **UI**: react-window (virtualized hex viewer), react-resizable-panels (split panes) |
| 14 | +- **Desktop**: Electron (optional, connects to user's ClickHouse) |
| 15 | +- **Testing**: Vitest + testcontainers (integration), Playwright (Electron e2e) |
| 16 | +- **Deployment**: Docker (bundles ClickHouse + nginx) or Electron desktop app |
| 17 | + |
| 18 | +## Commands |
| 19 | + |
| 20 | +```bash |
| 21 | +npm run dev # Start web dev server (requires ClickHouse at localhost:8123) |
| 22 | +npm run build # Build web app for production |
| 23 | +npm run test # Run integration tests (uses testcontainers) |
| 24 | +npm run lint # ESLint check |
| 25 | +npm run test:e2e # Build Electron + run Playwright e2e tests |
| 26 | + |
| 27 | +# Electron desktop app |
| 28 | +npm run electron:dev # Dev mode with hot reload |
| 29 | +npm run electron:build # Package desktop installer for current platform |
| 30 | + |
| 31 | +# Docker (self-contained with bundled ClickHouse) |
| 32 | +docker build -t rowbinary-explorer . |
| 33 | +docker run -d -p 8080:80 rowbinary-explorer |
| 34 | +``` |
| 35 | + |
| 36 | +## Directory Structure |
| 37 | + |
| 38 | +``` |
| 39 | +src/ |
| 40 | +├── components/ # React components |
| 41 | +│ ├── App.tsx # Main layout with resizable panels |
| 42 | +│ ├── QueryInput.tsx # SQL query input + run button + connection settings |
| 43 | +│ ├── HexViewer/ # Virtualized hex viewer with highlighting |
| 44 | +│ └── AstTree/ # Collapsible AST tree view |
| 45 | +├── core/ |
| 46 | +│ ├── types/ |
| 47 | +│ │ ├── ast.ts # AstNode, ByteRange, ParsedData interfaces |
| 48 | +│ │ └── clickhouse-types.ts # ClickHouseType discriminated union |
| 49 | +│ ├── decoder/ |
| 50 | +│ │ ├── rowbinary-decoder.ts # RowBinaryWithNamesAndTypes decoder |
| 51 | +│ │ ├── native-decoder.ts # Native format decoder |
| 52 | +│ │ ├── reader.ts # BinaryReader with byte-range tracking |
| 53 | +│ │ ├── leb128.ts # LEB128 varint decoder |
| 54 | +│ │ ├── test-helpers.ts # Shared test utilities |
| 55 | +│ │ ├── smoke-cases.ts # Smoke test case definitions |
| 56 | +│ │ └── validation-cases.ts # Validation test case definitions |
| 57 | +│ ├── parser/ |
| 58 | +│ │ ├── type-lexer.ts # Tokenizer for type strings |
| 59 | +│ │ └── type-parser.ts # Parser: string -> ClickHouseType |
| 60 | +│ └── clickhouse/ |
| 61 | +│ └── client.ts # HTTP client (fetch for web, IPC for Electron) |
| 62 | +├── store/ |
| 63 | +│ └── store.ts # Zustand store (query, parsed data, UI state) |
| 64 | +└── styles/ # CSS files |
| 65 | +electron/ |
| 66 | +├── main.ts # Electron main process (window, IPC handlers) |
| 67 | +└── preload.ts # Preload script (contextBridge → electronAPI) |
| 68 | +e2e/ |
| 69 | +└── electron.spec.ts # Playwright Electron e2e tests |
| 70 | +docs/ |
| 71 | +├── rowbinaryspec.md # RowBinary wire format specification |
| 72 | +├── nativespec.md # Native wire format specification |
| 73 | +└── jsonspec.md # JSON type specification |
| 74 | +docker/ |
| 75 | +├── nginx.conf # Proxies /clickhouse to ClickHouse server |
| 76 | +├── users.xml # Read-only ClickHouse user |
| 77 | +└── supervisord.conf # Runs nginx + ClickHouse together |
| 78 | +``` |
| 79 | + |
| 80 | +## Wire Format Docs |
| 81 | + |
| 82 | + * RowBinary: docs/rowbinaryspec.md |
| 83 | + * Native: docs/nativespec.md |
| 84 | + * JSON: docs/jsonspec.md |
| 85 | + |
| 86 | +## Key Concepts |
| 87 | + |
| 88 | +### AstNode |
| 89 | +Every decoded value is represented as an `AstNode` (`src/core/types/ast.ts:12`): |
| 90 | +- `id` - Unique identifier for selection/highlighting |
| 91 | +- `type` - ClickHouse type name string |
| 92 | +- `byteRange` - `{start, end}` byte offsets (exclusive end) |
| 93 | +- `value` - Decoded JavaScript value |
| 94 | +- `displayValue` - Human-readable string |
| 95 | +- `children` - Child nodes for composite types (Array, Tuple, etc.) |
| 96 | + |
| 97 | +### ClickHouseType |
| 98 | +A discriminated union representing all ClickHouse types (`src/core/types/clickhouse-types.ts:4`): |
| 99 | +- Primitives: `UInt8`-`UInt256`, `Int8`-`Int256`, `Float32/64`, `String`, etc. |
| 100 | +- Composites: `Array`, `Tuple`, `Map`, `Nullable`, `LowCardinality` |
| 101 | +- Advanced: `Variant`, `Dynamic`, `JSON` |
| 102 | +- Geo: `Point`, `Ring`, `Polygon`, `MultiPolygon`, `LineString`, `MultiLineString`, `Geometry` |
| 103 | +- Intervals: `IntervalSecond`, `IntervalMinute`, `IntervalHour`, `IntervalDay`, `IntervalWeek`, `IntervalMonth`, `IntervalQuarter`, `IntervalYear` (stored as Int64) |
| 104 | +- Other: `Enum8/16`, `Nested`, `QBit`, `AggregateFunction` |
| 105 | + |
| 106 | +### Decoding Flow |
| 107 | +1. User enters SQL query, clicks "Run Query" |
| 108 | +2. `ClickHouseClient` (`src/core/clickhouse/client.ts`) sends query: |
| 109 | + - **Web mode**: `fetch()` via Vite proxy or nginx |
| 110 | + - **Electron mode**: IPC to main process → `fetch()` to ClickHouse (no CORS) |
| 111 | +3. Decoder parses the binary response: |
| 112 | + - **RowBinary** (`rowbinary-decoder.ts`): Row-oriented, header + rows |
| 113 | + - **Native** (`native-decoder.ts`): Column-oriented with blocks |
| 114 | +4. Type strings parsed via `parseType()` into `ClickHouseType` |
| 115 | +5. Each decoded value returns an `AstNode` with byte tracking |
| 116 | +6. UI renders hex view (left) and AST tree (right) |
| 117 | + |
| 118 | +### Electron Architecture |
| 119 | +``` |
| 120 | +Renderer (React) Main Process (Node.js) |
| 121 | + │ │ |
| 122 | + ├─ window.electronAPI │ |
| 123 | + │ .executeQuery(opts) ────────►├─ fetch(clickhouseUrl + query) |
| 124 | + │ │ → ArrayBuffer |
| 125 | + │◄── IPC response ──────────────┤ |
| 126 | + │ │ |
| 127 | + ├─ Uint8Array → decoders │ |
| 128 | + └─ render hex view + AST tree │ |
| 129 | +``` |
| 130 | + |
| 131 | +- Runtime detection: `window.electronAPI` exists → IPC path, otherwise → `fetch()` |
| 132 | +- `vite-plugin-electron` activates only when `ELECTRON=true` env var is set |
| 133 | +- Connection config in `config.json` (project root in dev, next to executable in prod) |
| 134 | +- Experimental ClickHouse settings (Variant, Dynamic, JSON, etc.) sent as query params |
| 135 | + |
| 136 | +### Interactive Highlighting |
| 137 | +- Click a node in AST tree → highlights corresponding bytes in hex view |
| 138 | +- Click a byte in hex view → selects the deepest AST node containing that byte |
| 139 | +- State managed in Zustand store: `activeNodeId`, `hoveredNodeId` |
| 140 | + |
| 141 | +## Adding a New ClickHouse Type |
| 142 | + |
| 143 | +1. Add type variant to `ClickHouseType` in `src/core/types/clickhouse-types.ts` |
| 144 | +2. Add `typeToString()` case for serialization back to string |
| 145 | +3. Add `getTypeColor()` case for UI coloring |
| 146 | +4. Add parser case in `src/core/parser/type-parser.ts` |
| 147 | +5. Add decoder method in `RowBinaryDecoder` (`src/core/decoder/rowbinary-decoder.ts`): |
| 148 | + - Add case in `decodeValue()` switch |
| 149 | + - Implement `decode{TypeName}()` method returning `AstNode` |
| 150 | +6. Add decoder method in `NativeDecoder` (`src/core/decoder/native-decoder.ts`): |
| 151 | + - Add case in `decodeValue()` switch |
| 152 | + - For columnar types, may need `decode{TypeName}Column()` method |
| 153 | +7. If type has binary type index (for Dynamic), add to `decodeDynamicType()` |
| 154 | +8. Add test cases to `smoke-cases.ts` and `validation-cases.ts` |
| 155 | + |
| 156 | +## Important Implementation Details |
| 157 | + |
| 158 | +- **LEB128**: Variable-length integers used for string lengths, array sizes, column counts |
| 159 | +- **UUID byte order**: ClickHouse uses a special byte ordering (see `decodeUUID()` at `decoder.ts:629`) |
| 160 | +- **IPv4**: Stored as little-endian UInt32, displayed in reverse order |
| 161 | +- **Dynamic type**: Uses BinaryTypeIndex encoding; type is encoded in the data itself |
| 162 | +- **LowCardinality**: Does not affect wire format in RowBinary (transparent wrapper) |
| 163 | +- **Nested**: Encoded as parallel arrays, one per field |
| 164 | + |
| 165 | +## Testing |
| 166 | + |
| 167 | +### Integration Tests (Vitest + testcontainers) |
| 168 | + |
| 169 | +Tests use testcontainers to spin up a real ClickHouse instance: |
| 170 | +```bash |
| 171 | +npm run test # Runs all integration tests |
| 172 | +``` |
| 173 | + |
| 174 | +Tests are organized into three categories with shared test case definitions: |
| 175 | + |
| 176 | +1. **Smoke Tests** (`smoke.integration.test.ts`) |
| 177 | + - Verify parsing succeeds without value validation |
| 178 | + - Test cases defined in `smoke-cases.ts` |
| 179 | + - Parametrized for both RowBinary and Native formats |
| 180 | + |
| 181 | +2. **Validation Tests** (`validation.integration.test.ts`) |
| 182 | + - Verify decoded values and AST structure |
| 183 | + - Test cases defined in `validation-cases.ts` with format-specific callbacks |
| 184 | + - Check values, children counts, byte ranges, metadata |
| 185 | + |
| 186 | +3. **Coverage Tests** (`coverage.integration.test.ts`) |
| 187 | + - Analyze byte coverage of AST leaf nodes |
| 188 | + - Report uncovered byte ranges |
| 189 | + |
| 190 | +### Electron e2e Tests (Playwright) |
| 191 | + |
| 192 | +```bash |
| 193 | +npm run test:e2e # Builds Electron app + runs Playwright tests |
| 194 | +``` |
| 195 | + |
| 196 | +Tests in `e2e/electron.spec.ts` launch the actual Electron app and verify: |
| 197 | +- App window opens and UI renders |
| 198 | +- Host input is visible (Electron mode) and Share button is hidden |
| 199 | +- Connection settings can be edited |
| 200 | +- Upload button is present and functional |
| 201 | + |
| 202 | +### Test Case Interface |
| 203 | +```typescript |
| 204 | +interface ValidationTestCase { |
| 205 | + name: string; |
| 206 | + query: string; |
| 207 | + settings?: Record<string, string | number>; |
| 208 | + rowBinaryValidator?: (result: DecodedResult) => void; |
| 209 | + nativeValidator?: (result: DecodedResult) => void; |
| 210 | +} |
| 211 | +``` |
| 212 | + |
| 213 | +### Adding New Test Cases |
| 214 | +1. Add query to `smoke-cases.ts` for basic parsing verification |
| 215 | +2. Add to `validation-cases.ts` with validator callbacks for detailed checks |
| 216 | +3. Use `bothFormats(validator)` helper when validation logic is identical |
0 commit comments