Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
127 changes: 127 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

QuestDB UI is a monorepo hosting the implementation of QuestDB user interface and surrounding tooling using TypeScript, React, and Yarn 3 with PnP (Plug and Play).

### Package Structure

- `@questdb/web-console` - The main GUI application for QuestDB
- `@questdb/react-components` - Shared component library for internal reuse
- `browser-tests` - Cypress-based browser tests

## Common Development Commands

### Initial Setup
```bash
# Clone and bootstrap (dependencies are committed via Yarn PnP)
yarn

# Build react-components first (required dependency)
yarn workspace @questdb/react-components build
```

### Web Console Development
```bash
# Start development server (runs on localhost:9999)
yarn workspace @questdb/web-console start

# Build production version
yarn workspace @questdb/web-console build

# Run unit tests (watch mode)
yarn workspace @questdb/web-console test

# Run unit tests (CI mode)
yarn workspace @questdb/web-console test:prod
```

### React Components Development
```bash
# Start Storybook
yarn workspace @questdb/react-components storybook

# Build library
yarn workspace @questdb/react-components build
```

### Browser Tests
```bash
# Run browser tests (requires web-console running)
yarn workspace browser-tests test

# Run auth tests
yarn workspace browser-tests test:auth

# Run enterprise tests
yarn workspace browser-tests test:enterprise
```

### Running QuestDB Backend
The web console requires QuestDB running in the background:
```bash
docker run -p 9000:9000 -p 9009:9009 -p 8812:8812 questdb/questdb
```

## Architecture Overview

### Web Console Structure

The web console (`packages/web-console/src/`) follows a layered architecture:

1. **Entry Points**
- `index.tsx` - React application bootstrap
- `index.html` - HTML template

2. **State Management**
- Redux store with epics (redux-observable)
- Store modules: `Console`, `Query`, `Telemetry`
- Database persistence with Dexie.js

3. **Core Scenes/Features**
- `Console` - Main SQL query interface
- `Editor` - Monaco-based SQL editor with QuestDB-specific language support
- `Schema` - Database schema browser with virtual table support
- `Import` - CSV file import functionality
- `Result` - Query result visualization
- `Metrics` - System metrics visualization with uPlot

4. **Provider Hierarchy**
- `QuestProvider` - QuestDB client services
- `LocalStorageProvider` - Persistent settings
- `AuthProvider` - Authentication handling
- `SettingsProvider` - Application settings
- `PosthogProviderWrapper` - Analytics

5. **Component Organization**
- Reusable components in `components/`
- Scene-specific components within each scene directory
- Shared hooks in `Hooks/`
- Form components with Joi validation schemas

### Key Technologies

- **UI Framework**: React 17 with TypeScript
- **Styling**: Styled-components + SCSS
- **State**: Redux + Redux-Observable (RxJS)
- **Editor**: Monaco Editor with custom QuestDB SQL language
- **Charts**: uPlot for time-series, ECharts for general visualizations
- **Forms**: React Hook Form with Joi validation
- **Storage**: Dexie.js for IndexedDB persistence
- **Build**: Webpack 5 with Babel

### Development Notes

- Node version: 16.13.1 (use fnm/nvm)
- Yarn version: 3.x (enabled via corepack)
- All dependencies are committed (Yarn Zero-Installs)
- TypeScript version locked at 4.4.4
- Bundle size limits enforced via BundleWatch

### Testing Approach

- Unit tests: Jest with React Testing Library
- Browser tests: Cypress for E2E testing
- Time zone: Tests run with TZ=UTC
16 changes: 16 additions & 0 deletions packages/browser-tests/cypress/integration/console/editor.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,22 @@ describe("handling comments", () => {
`select\n\n --line;\n 2`
);
});

it("should ignore quotes inside comments", () => {
cy.typeQueryDirectly(
"/*\n * Today's intraday EURUSD market activity.\n */\nselect timestamp, open, high, low, close, total_volume\nfrom market_data_ohlc_15m\nwhere date_trunc('day', now()) < timestamp and symbol = 'EURUSD';\nselect 1;\n"
);
cy.getCursorQueryGlyph().should("have.length", 2);

cy.clickLine(1);
cy.getCursorQueryDecoration().should("not.exist");

cy.clickLine(4);
cy.getCursorQueryDecoration().should("have.length", 3);

cy.clickLine(7);
cy.getCursorQueryDecoration().should("have.length", 1);
});
});

describe("multiple run buttons with dynamic query log", () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-tests/questdb
Submodule questdb updated 96 files
+80 −91 README.md
+178 −0 benchmarks/src/main/java/org/questdb/Decimal128AddBenchmark.java
+157 −0 benchmarks/src/main/java/org/questdb/Decimal128DivideBenchmark.java
+167 −0 benchmarks/src/main/java/org/questdb/Decimal128ModuloBenchmark.java
+171 −0 benchmarks/src/main/java/org/questdb/Decimal128MultiplyBenchmark.java
+195 −0 benchmarks/src/main/java/org/questdb/Decimal128RoundBenchmark.java
+185 −0 benchmarks/src/main/java/org/questdb/Decimal128SubtractBenchmark.java
+185 −0 benchmarks/src/main/java/org/questdb/Decimal256AddBenchmark.java
+175 −0 benchmarks/src/main/java/org/questdb/Decimal256DivideBenchmark.java
+183 −0 benchmarks/src/main/java/org/questdb/Decimal256ModuloBenchmark.java
+177 −0 benchmarks/src/main/java/org/questdb/Decimal256MultiplyBenchmark.java
+191 −0 benchmarks/src/main/java/org/questdb/Decimal256RoundBenchmark.java
+176 −0 benchmarks/src/main/java/org/questdb/Decimal256SubtractBenchmark.java
+160 −0 benchmarks/src/main/java/org/questdb/Decimal64AddBenchmark.java
+152 −0 benchmarks/src/main/java/org/questdb/Decimal64DivideBenchmark.java
+152 −0 benchmarks/src/main/java/org/questdb/Decimal64ModuloBenchmark.java
+152 −0 benchmarks/src/main/java/org/questdb/Decimal64MultiplyBenchmark.java
+147 −0 benchmarks/src/main/java/org/questdb/Decimal64RoundBenchmark.java
+152 −0 benchmarks/src/main/java/org/questdb/Decimal64SubtractBenchmark.java
+1 −2 core/pom.xml
+36 −27 core/rust/qdb-core/src/col_driver/mod.rs
+14 −15 core/rust/qdb-core/src/col_type.rs
+77 −70 core/rust/qdbr/src/parquet_read/decode.rs
+28 −33 core/rust/qdbr/src/parquet_read/meta.rs
+10 −8 core/rust/qdbr/src/parquet_write/array.rs
+11 −3 core/rust/qdbr/src/parquet_write/file.rs
+6 −0 core/rust/qdbr/src/parquet_write/schema.rs
+2 −2 core/src/main/java/io/questdb/cairo/TableWriter.java
+17 −4 core/src/main/java/io/questdb/cairo/sql/PageFrameAddressCache.java
+17 −12 core/src/main/java/io/questdb/cairo/sql/PageFrameCursor.java
+16 −3 core/src/main/java/io/questdb/cairo/sql/PageFrameMemoryPool.java
+0 −50 core/src/main/java/io/questdb/cairo/sql/TablePageFrameCursor.java
+1 −1 core/src/main/java/io/questdb/cairo/sql/async/PageFrameSequence.java
+27 −4 core/src/main/java/io/questdb/griffin/RecordToRowCopierUtils.java
+6 −2 core/src/main/java/io/questdb/griffin/SqlCompilerImpl.java
+13 −13 core/src/main/java/io/questdb/griffin/SqlOptimiser.java
+43 −43 core/src/main/java/io/questdb/griffin/SqlUtil.java
+5 −0 core/src/main/java/io/questdb/griffin/engine/functions/eq/EqTimestampCursorFunctionFactory.java
+4 −0 core/src/main/java/io/questdb/griffin/engine/functions/lt/GtTimestampCursorFunctionFactory.java
+4 −0 core/src/main/java/io/questdb/griffin/engine/functions/lt/LtTimestampCursorFunctionFactory.java
+5 −1 core/src/main/java/io/questdb/griffin/engine/functions/table/ReadParquetFunctionFactory.java
+5 −0 core/src/main/java/io/questdb/griffin/engine/functions/table/ReadParquetPageFrameCursor.java
+26 −15 core/src/main/java/io/questdb/griffin/engine/functions/table/ReadParquetRecordCursor.java
+2 −2 core/src/main/java/io/questdb/griffin/engine/groupby/SampleByFirstLastRecordCursorFactory.java
+1 −1 core/src/main/java/io/questdb/griffin/engine/groupby/vect/GroupByNotKeyedVectorRecordCursorFactory.java
+1 −1 core/src/main/java/io/questdb/griffin/engine/groupby/vect/GroupByRecordCursorFactory.java
+9 −2 core/src/main/java/io/questdb/griffin/engine/table/AbstractPageFrameRecordCursor.java
+5 −0 core/src/main/java/io/questdb/griffin/engine/table/SelectedRecordCursorFactory.java
+8 −0 core/src/main/java/io/questdb/griffin/engine/table/TablePageFrameCursor.java
+4 −3 core/src/main/java/io/questdb/griffin/engine/table/TimeFrameRecordCursorImpl.java
+18 −1 core/src/main/java/io/questdb/griffin/engine/table/parquet/PartitionDecoder.java
+1,131 −0 core/src/main/java/io/questdb/std/Decimal128.java
+1,961 −0 core/src/main/java/io/questdb/std/Decimal256.java
+593 −0 core/src/main/java/io/questdb/std/Decimal64.java
+442 −0 core/src/main/java/io/questdb/std/DecimalKnuthDivider.java
+4 −0 core/src/main/java/io/questdb/std/LowerCaseCharSequenceHashSet.java
+96 −1 core/src/main/java/io/questdb/std/NumericException.java
+119 −24 core/src/main/java/io/questdb/std/Rnd.java
+ core/src/main/resources/io/questdb/bin/darwin-aarch64/libquestdbr.dylib
+ core/src/main/resources/io/questdb/bin/darwin-x86-64/libquestdbr.dylib
+ core/src/main/resources/io/questdb/bin/linux-aarch64/libquestdbr.so
+ core/src/main/resources/io/questdb/bin/linux-x86-64/libquestdbr.so
+ core/src/main/resources/io/questdb/bin/windows-x86-64/questdbr.dll
+71 −2 core/src/test/java/io/questdb/test/cairo/mv/MatViewTest.java
+838 −0 core/src/test/java/io/questdb/test/griffin/DistinctJoinAliasTest.java
+119 −0 core/src/test/java/io/questdb/test/griffin/DistinctTest.java
+23 −0 core/src/test/java/io/questdb/test/griffin/InsertCastTest.java
+26 −0 core/src/test/java/io/questdb/test/griffin/ParquetTest.java
+82 −16 core/src/test/java/io/questdb/test/griffin/SqlUtilTest.java
+29 −0 core/src/test/java/io/questdb/test/griffin/engine/functions/eq/EqTimestampCursorFunctionFactoryTest.java
+28 −0 core/src/test/java/io/questdb/test/griffin/engine/functions/lt/GtTimestampCursorFunctionFactoryTest.java
+28 −0 core/src/test/java/io/questdb/test/griffin/engine/functions/lt/LtTimestampCursorFunctionFactoryTest.java
+1,732 −0 core/src/test/java/io/questdb/test/std/Decimal128Test.java
+1,606 −0 core/src/test/java/io/questdb/test/std/Decimal256Test.java
+1,083 −0 core/src/test/java/io/questdb/test/std/Decimal64Test.java
+2 −1 core/src/test/java/io/questdb/test/std/LowerCaseCharSequenceHashSetTest.java
+91 −0 core/src/test/java/io/questdb/test/std/NumericExceptionTest.java
+ core/src/test/resources/sqllogictest/data/parquet-testing/map_duckdb.parquet
+10 −0 core/src/test/resources/sqllogictest/test/parquet/map_duckdb.test
+224 −115 i18n/README.ar-dz.md
+417 −0 i18n/README.de-de.md
+201 −116 i18n/README.es-es.md
+432 −0 i18n/README.fr-fr.md
+430 −0 i18n/README.he-il.md
+205 −112 i18n/README.hn-in.md
+199 −107 i18n/README.it-it.md
+201 −107 i18n/README.ja-ja.md
+409 −0 i18n/README.ko-kr.md
+416 −0 i18n/README.ms-my.md
+432 −0 i18n/README.nl-nl.md
+208 −106 i18n/README.pt.md
+205 −115 i18n/README.tr-tr.md
+203 −91 i18n/README.ua-ua.md
+193 −93 i18n/README.vi-vn.md
+189 −123 i18n/README.zh-cn.md
+188 −123 i18n/README.zh-hk.md
1 change: 1 addition & 0 deletions packages/web-console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"test:prod": "TZ=UTC && jest --ci --runInBand"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.57.0",
"@date-fns/tz": "^1.2.0",
"@docsearch/css": "^3.5.2",
"@docsearch/react": "^3.5.2",
Expand Down
210 changes: 210 additions & 0 deletions packages/web-console/src/components/ExplainErrorButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import React, { useState, useContext } from "react"
import styled from "styled-components"
import { Button, Loader } from "@questdb/react-components"
import { InfoCircle } from "@styled-icons/boxicons-regular"
import { useSelector } from "react-redux"
import { useLocalStorage } from "../../providers/LocalStorageProvider"
import type { ClaudeAPIError, ClaudeExplanation } from "../../utils/claude"
import { isClaudeError, explainError, createSchemaClient } from "../../utils/claude"
import { toast } from "../Toast"
import { QuestContext } from "../../providers"
import { selectors } from "../../store"

const StyledExplainErrorButton = styled(Button)`
background-color: ${({ theme }) => theme.color.orange};
border-color: ${({ theme }) => theme.color.orange};
color: ${({ theme }) => theme.color.foreground};

&:hover:not(:disabled) {
background-color: ${({ theme }) => theme.color.orange};
border-color: ${({ theme }) => theme.color.orange};
filter: brightness(1.2);
}

&:disabled {
background-color: ${({ theme }) => theme.color.orange};
border-color: ${({ theme }) => theme.color.orange};
opacity: 0.6;
}

svg {
color: ${({ theme }) => theme.color.foreground};
}
`

const ExplanationDialog = styled.div`
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: ${({ theme }) => theme.color.backgroundDarker};
border: 1px solid ${({ theme }) => theme.color.gray1};
border-radius: 0.8rem;
padding: 2rem;
max-width: 60rem;
max-height: 70vh;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.3);
`

const Overlay = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
`

const DialogHeader = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid ${({ theme }) => theme.color.gray1};
`

const DialogTitle = styled.h3`
margin: 0;
color: ${({ theme }) => theme.color.foreground};
font-size: 1.8rem;
`

const CloseButton = styled.button`
background: none;
border: none;
color: ${({ theme }) => theme.color.gray2};
font-size: 1.8rem;
cursor: pointer;
padding: 0.5rem;
border-radius: 0.4rem;

&:hover {
color: ${({ theme }) => theme.color.foreground};
background: ${({ theme }) => theme.color.selection};
}
`

const ErrorSection = styled.div`
margin-bottom: 1.5rem;
`

const SectionTitle = styled.h4`
margin: 0 0 0.8rem 0;
color: ${({ theme }) => theme.color.foreground};
font-size: 1.4rem;
`

const CodeBlock = styled.pre`
background: ${({ theme }) => theme.color.selection};
border: 1px solid ${({ theme }) => theme.color.gray1};
border-radius: 0.4rem;
padding: 1rem;
margin: 0.8rem 0;
overflow-x: auto;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 1.3rem;
line-height: 1.4;
color: ${({ theme }) => theme.color.foreground};
`

const ExplanationText = styled.div`
color: ${({ theme }) => theme.color.foreground};
font-size: 1.4rem;
line-height: 1.6;
white-space: pre-wrap;
`

type Props = {
query: string
errorMessage: string
disabled?: boolean
}

export const ExplainErrorButton = ({ query, errorMessage, disabled }: Props) => {
const { aiAssistantSettings } = useLocalStorage()
const { quest } = useContext(QuestContext)
const tables = useSelector(selectors.query.getTables)
const [isExplaining, setIsExplaining] = useState(false)
const [showDialog, setShowDialog] = useState(false)
const [explanation, setExplanation] = useState<string>('')

const handleExplainError = async () => {
const schemaClient = aiAssistantSettings.grantSchemaAccess ? createSchemaClient(tables, quest) : undefined

setIsExplaining(true)
const response = await explainError(query, errorMessage, aiAssistantSettings, schemaClient)

if (isClaudeError(response)) {
const error = response as ClaudeAPIError
toast.error(error.message)
return
}

const result = response as ClaudeExplanation
if (!result.explanation) {
toast.error("No explanation received from Anthropic API")
return
}

setExplanation(result.explanation)
setShowDialog(true)
setIsExplaining(false)
}

const handleCloseDialog = () => {
setShowDialog(false)
setExplanation('')
}

if (!aiAssistantSettings.apiKey) {
return null
}

return (
<>
<StyledExplainErrorButton
size="sm"
onClick={handleExplainError}
disabled={disabled || isExplaining}
prefixIcon={isExplaining ? <Loader size="14px" /> : <InfoCircle size="16px" />}
title="Get AI explanation for this error"
data-hook="button-explain-error"
>
{isExplaining ? "Getting help..." : "Why did this fail?"}
</StyledExplainErrorButton>

{showDialog && (
<>
<Overlay onClick={handleCloseDialog} />
<ExplanationDialog>
<DialogHeader>
<DialogTitle>🤖 AI Error Explanation</DialogTitle>
<CloseButton onClick={handleCloseDialog} title="Close">
×
</CloseButton>
</DialogHeader>

<ErrorSection>
<SectionTitle>SQL Query</SectionTitle>
<CodeBlock>{query}</CodeBlock>
</ErrorSection>

<ErrorSection>
<SectionTitle>Error Message</SectionTitle>
<CodeBlock>{errorMessage}</CodeBlock>
</ErrorSection>

<ErrorSection>
<SectionTitle>AI Explanation</SectionTitle>
<ExplanationText>{explanation}</ExplanationText>
</ErrorSection>
</ExplanationDialog>
</>
)}
</>
)
}
Loading
Loading