feat(editor): add ClickHouse SQL highlighting

This commit is contained in:
Justin Gao 2026-07-29 19:21:56 +08:00 committed by GitHub
parent 02aea73d28
commit b4efcebd78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 911 additions and 14 deletions

View File

@ -70,7 +70,7 @@ import { trimmedSelectionLayer } from "@/lib/editor/codemirrorTrimmedSelectionLa
import { selectionMatchOccurrences } from "@/lib/editor/codemirrorSelectionMatches";
import { createInsertValueHintsExtension, requestInsertValueHintsRefresh } from "@/lib/editor/codemirrorInsertValueHints";
import { focusEditorView } from "@/lib/editor/queryEditorFocus";
import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect";
import { createDbxCodeMirrorSqlDialect, type CodeMirrorSqlDialectName } from "@/lib/editor/codemirrorSqlDialect";
import { sqlSemanticTableNameSpansForSyntaxTree } from "@/lib/editor/codemirrorSqlSemanticHighlight";
import { startsQueryEditorRectangularSelection } from "@/lib/editor/queryEditorPointerSelection";
import { LARGE_PASTE_HISTORY_USER_EVENT, normalizeQueryEditorPasteText, recoverableNativePasteSuffix, shouldRecoverLargeTauriPaste } from "@/lib/editor/queryEditorLargePaste";
@ -109,7 +109,7 @@ const props = defineProps<{
completionContextVersion?: number;
databaseType?: DatabaseType;
dialect?: "mysql" | "postgres" | "sqlserver";
syntaxDialect?: "mysql" | "postgres" | "sqlserver";
syntaxDialect?: CodeMirrorSqlDialectName;
formatDialect?: SqlFormatDialect;
formatRequestId?: number;
compressRequestId?: number;
@ -124,6 +124,10 @@ const props = defineProps<{
statementExecutionMarkers?: StatementExecutionMarker[];
}>();
function sqlBehaviorDialect(): "mysql" | "postgres" | "sqlserver" | undefined {
return props.syntaxDialect === "clickhouse" ? props.dialect : (props.syntaxDialect ?? props.dialect);
}
const COMPLETION_REMOTE_LATENCY_BUDGET_MS = 120;
const COMPLETION_DEBOUNCE_DELAY_MS = 150;
const COMPLETION_TAB_RETRY_DELAY_MS = 16;
@ -422,7 +426,7 @@ let hoverSqlHighlighter: SqlHighlighter | null = null;
function sqlCompletionDialectOptions() {
return {
databaseType: props.databaseType,
dialect: props.syntaxDialect ?? props.dialect,
dialect: sqlBehaviorDialect(),
};
}
@ -2248,7 +2252,7 @@ async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boo
const semanticModel = SEMANTIC_SQL_COMPLETION_ENABLED
? buildSqlSemanticModel(range.sql, semanticCursor, {
databaseType: props.databaseType,
dialect: props.syntaxDialect ?? props.dialect,
dialect: sqlBehaviorDialect(),
})
: null;
const semanticAnalysis = semanticModel ? mergeSqlSemanticReferenceAnalysis(analysis, semanticModel) : analysis;
@ -3679,7 +3683,7 @@ onMounted(async () => {
const ranges = windows.flatMap((window) =>
sqlSemanticTableNameSpansForSyntaxTree(sql, window, tree, {
databaseType: props.databaseType,
dialect: props.syntaxDialect ?? props.dialect,
dialect: sqlBehaviorDialect(),
}),
);
return Decoration.set(

View File

@ -77,6 +77,7 @@ import { isTableDataEditable } from "@/lib/table/tableEditing";
import { tableMetaForDataTab } from "@/lib/table/tableDataTabMeta";
import { dataTabExecutionDatabase } from "@/lib/table/dataTabExecutionDatabase";
import { formatShortcut } from "@/lib/editor/shortcutRegistry";
import type { CodeMirrorSqlDialectName } from "@/lib/editor/codemirrorSqlDialect";
import { codeMirrorSqlDialect, codeMirrorSqlDialectForConnection, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { chartableColumnIndexes } from "@/lib/dataGrid/chartData";
import { elasticsearchJsonResponseForResult } from "@/lib/elasticsearch/elasticsearchJsonResponse";
@ -293,7 +294,7 @@ const activeTabDimension = computed(() => {
const activeSqlFormatDialect = computed<SqlFormatDialect>(() => sqlFormatDialectForDbType(activeEffectiveDatabaseType.value));
const editorDialect = computed<"mysql" | "postgres" | "sqlserver">(() => codeMirrorSqlDialect(activeEffectiveDatabaseType.value));
const editorSyntaxDialect = computed<"mysql" | "postgres" | "sqlserver">(() => codeMirrorSqlDialectForConnection(props.activeConnection));
const editorSyntaxDialect = computed<CodeMirrorSqlDialectName>(() => codeMirrorSqlDialectForConnection(props.activeConnection));
const shortcutModifier = computed(() => (navigator.platform.toLowerCase().includes("mac") ? "Cmd" : "Ctrl"));

View File

@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { sqlSemanticDialectFor } from "@/lib/sql/semantic/dialect";
describe("ClickHouse semantic dialect", () => {
it("takes precedence over the legacy MySQL behavior dialect", () => {
const dialect = sqlSemanticDialectFor({
databaseType: "clickhouse",
dialect: "mysql",
});
expect(dialect.id).toBe("clickhouse");
expect(dialect.identifierQuotes).toEqual([
{ open: "`", close: "`" },
{ open: '"', close: '"' },
]);
expect(dialect.normalizeIdentifier("EventName")).toBe("EventName");
expect(dialect.quoteIdentifier("event`name")).toBe("`event``name`");
});
});

View File

@ -1,5 +1,6 @@
import type { ConnectionConfig, DatabaseType } from "@/types/database";
import { isSchemaAware, usesDatabaseObjectTreeMode, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport";
import type { CodeMirrorSqlDialectName } from "@/lib/editor/codemirrorSqlDialect";
type JdbcDialectConnection = Pick<ConnectionConfig, "db_type"> & Partial<Pick<ConnectionConfig, "driver_profile" | "driver_label" | "connection_string" | "jdbc_driver_class" | "jdbc_driver_paths" | "database_info">>;
@ -125,9 +126,11 @@ export function codeMirrorSqlDialect(dbType: DatabaseType | undefined): "mysql"
return "mysql";
}
export function codeMirrorSqlDialectForConnection(connection?: JdbcDialectConnection): "mysql" | "postgres" | "sqlserver" {
export function codeMirrorSqlDialectForConnection(connection?: JdbcDialectConnection): CodeMirrorSqlDialectName {
if (isJdbcAseProfile(connection)) return "sqlserver";
return codeMirrorSqlDialect(effectiveDatabaseTypeForConnection(connection));
const databaseType = effectiveDatabaseTypeForConnection(connection);
if (databaseType === "clickhouse") return "clickhouse";
return codeMirrorSqlDialect(databaseType);
}
function isJdbcAseProfile(connection?: JdbcDialectConnection): boolean {

View File

@ -1,7 +1,7 @@
import type { SQLDialect } from "@codemirror/lang-sql";
import type { DatabaseType } from "@/types/database";
export type CodeMirrorSqlDialectName = "mysql" | "postgres" | "sqlserver";
export type CodeMirrorSqlDialectName = "mysql" | "postgres" | "sqlserver" | "clickhouse";
type CodeMirrorSqlLanguageModule = Pick<typeof import("@codemirror/lang-sql"), "Cassandra" | "MSSQL" | "MySQL" | "PLSQL" | "PostgreSQL" | "SQLite" | "SQLDialect" | "StandardSQL">;
@ -10,6 +10,9 @@ const POSTGRES_CODEMIRROR_DATABASE_TYPES = new Set<DatabaseType>(["postgres", "r
const ORACLE_CODEMIRROR_DATABASE_TYPES = new Set<DatabaseType>(["oracle", "dameng", "yashandb", "oscar", "oceanbase-oracle"]);
const SQLITE_CODEMIRROR_DATABASE_TYPES = new Set<DatabaseType>(["sqlite", "rqlite", "turso", "cloudflare-d1"]);
const CODEMIRROR_SQLITE_EXTENSION_KEYWORDS = new Set("abort analyze attach autoincrement conflict database detach exclusive fail glob ignore index indexed instead isnull notnull offset plan pragma query raise regexp reindex rename replace temp vacuum virtual".split(" "));
const STANDARD_SQL_TYPES = "array binary bit boolean char character clob date decimal double float int integer interval large national nchar nclob numeric object precision real smallint time timestamp varchar varying";
const DBX_COMMON_SQL_KEYWORDS = [
"PIVOT",
"UNPIVOT",
@ -51,6 +54,94 @@ const POSTGRES_IDENTIFIER_LIKE_KEYWORDS = new Set("COMMENT COUNT DATA DAY HOUR I
// SQL Server table-valued parameters require READONLY in procedure/function declarations.
const SQLSERVER_KEYWORDS = "readonly";
const CLICKHOUSE_KEYWORDS = [
"ATTACH",
"DETACH",
"OPTIMIZE",
"SYSTEM",
"KILL",
"ENGINE",
"PARTITION",
"PRIMARY",
"SAMPLE",
"PREWHERE",
"ARRAY",
"GLOBAL",
"FINAL",
"TOTALS",
"ROLLUP",
"CUBE",
"LIMIT",
"BY",
"INTO",
"OUTFILE",
"COMPRESSION",
"FORMAT",
"SETTINGS",
"TTL",
"CODEC",
"MATERIALIZED",
"ALIAS",
"PROJECTION",
"INDEX",
"GRANULARITY",
]
.join(" ")
.toLowerCase();
const CLICKHOUSE_TYPES = [
"Bool",
"Int8",
"Int16",
"Int32",
"Int64",
"Int128",
"Int256",
"UInt8",
"UInt16",
"UInt32",
"UInt64",
"UInt128",
"UInt256",
"Float32",
"Float64",
"Decimal",
"Decimal32",
"Decimal64",
"Decimal128",
"Decimal256",
"String",
"FixedString",
"Date",
"Date32",
"DateTime",
"DateTime64",
"Time",
"Time64",
"Enum8",
"Enum16",
"UUID",
"IPv4",
"IPv6",
"Array",
"Tuple",
"Map",
"Nested",
"Nullable",
"LowCardinality",
"AggregateFunction",
"SimpleAggregateFunction",
"JSON",
"Object",
"Variant",
"Dynamic",
"Nothing",
]
.join(" ")
.toLowerCase();
const CLICKHOUSE_BUILTINS = ["now", "today", "toDate", "toDateTime", "toDateTime64", "toYYYYMM", "count", "sum", "avg", "min", "max", "uniq", "uniqExact", "argMin", "argMax", "groupArray", "arrayJoin", "mapKeys", "mapValues", "JSONExtract", "JSONExtractString"].join(" ").toLowerCase();
export function postgresKeywordSyntaxTerms(keywords: string): string {
return keywords
.split(/\s+/)
@ -58,8 +149,19 @@ export function postgresKeywordSyntaxTerms(keywords: string): string {
.join(" ");
}
function standardSqlKeywordSyntaxTerms(langSql: CodeMirrorSqlLanguageModule): string {
// CodeMirror keeps StandardSQL's default vocabulary internal and exposes an
// empty StandardSQL.spec. SQLite is its smallest public standard-SQL
// superset, so remove SQLite-only terms to retain the standard vocabulary.
return (langSql.SQLite.spec.keywords || "")
.split(/\s+/)
.filter((keyword) => keyword && !CODEMIRROR_SQLITE_EXTENSION_KEYWORDS.has(keyword))
.join(" ");
}
function codeMirrorBaseDialect(langSql: CodeMirrorSqlLanguageModule, dialectName: CodeMirrorSqlDialectName, databaseType?: DatabaseType): SQLDialect {
if (databaseType) {
if (databaseType === "clickhouse") return langSql.StandardSQL;
if (MYSQL_CODEMIRROR_DATABASE_TYPES.has(databaseType)) return langSql.MySQL;
if (POSTGRES_CODEMIRROR_DATABASE_TYPES.has(databaseType)) return langSql.PostgreSQL;
if (ORACLE_CODEMIRROR_DATABASE_TYPES.has(databaseType)) return langSql.PLSQL;
@ -69,6 +171,7 @@ function codeMirrorBaseDialect(langSql: CodeMirrorSqlLanguageModule, dialectName
if (databaseType === "jdbc" && dialectName === "sqlserver") return langSql.MSSQL;
return langSql.StandardSQL;
}
if (dialectName === "clickhouse") return langSql.StandardSQL;
return dialectName === "postgres" ? langSql.PostgreSQL : dialectName === "sqlserver" ? langSql.MSSQL : langSql.MySQL;
}
@ -76,13 +179,23 @@ export function createDbxCodeMirrorSqlDialect(langSql: CodeMirrorSqlLanguageModu
const baseDialect = codeMirrorBaseDialect(langSql, dialectName, databaseType);
const isPostgres = baseDialect === langSql.PostgreSQL;
const isSqlServer = baseDialect === langSql.MSSQL;
const baseKeywords = isPostgres ? postgresKeywordSyntaxTerms(baseDialect.spec.keywords || "") : baseDialect.spec.keywords || "";
const isClickHouse = databaseType === "clickhouse" || dialectName === "clickhouse";
const baseKeywords = isClickHouse ? standardSqlKeywordSyntaxTerms(langSql) : isPostgres ? postgresKeywordSyntaxTerms(baseDialect.spec.keywords || "") : baseDialect.spec.keywords || "";
const baseTypes = isClickHouse ? STANDARD_SQL_TYPES : baseDialect.spec.types || "";
const commonKeywords = isClickHouse ? DBX_COMMON_SQL_KEYWORDS.toLowerCase() : DBX_COMMON_SQL_KEYWORDS;
return langSql.SQLDialect.define({
...baseDialect.spec,
keywords: [baseKeywords, DBX_COMMON_SQL_KEYWORDS, isPostgres ? POSTGRES_PLPGSQL_KEYWORDS : "", isSqlServer ? SQLSERVER_KEYWORDS : ""].filter(Boolean).join(" "),
types: [baseDialect.spec.types || "", isPostgres ? POSTGRES_PLPGSQL_TYPES : ""].filter(Boolean).join(" ") || undefined,
builtin: [baseDialect.spec.builtin || "", isPostgres ? POSTGRES_PLPGSQL_BUILTIN : ""].filter(Boolean).join(" ") || undefined,
keywords: [baseKeywords, commonKeywords, isClickHouse ? CLICKHOUSE_KEYWORDS : "", isPostgres ? POSTGRES_PLPGSQL_KEYWORDS : "", isSqlServer ? SQLSERVER_KEYWORDS : ""].filter(Boolean).join(" "),
types: [baseTypes, isClickHouse ? CLICKHOUSE_TYPES : "", isPostgres ? POSTGRES_PLPGSQL_TYPES : ""].filter(Boolean).join(" ") || undefined,
builtin: [baseDialect.spec.builtin || "", isClickHouse ? CLICKHOUSE_BUILTINS : "", isPostgres ? POSTGRES_PLPGSQL_BUILTIN : ""].filter(Boolean).join(" ") || undefined,
...(isClickHouse
? {
identifierQuotes: '"`',
backslashEscapes: true,
spaceAfterDashes: false,
}
: {}),
doubleDollarQuotedStrings: false,
});
}

View File

@ -83,6 +83,22 @@ export const SQL_SEMANTIC_DIALECTS: Record<string, SqlSemanticDialectAdapter> =
return parts.length >= 1 ? "schema" : "unknown";
},
},
clickhouse: {
id: "clickhouse",
identifierQuotes: [
{ open: "`", close: "`" },
{ open: '"', close: '"' },
],
supportsAsForTableAlias: true,
projectionAliasVisibility: { where: false, groupBy: true, having: true, orderBy: true },
normalizeIdentifier: defaultNormalize,
quoteIdentifier: (identifier) => quoteWith(identifier, "`"),
qualifierRole(parts, context) {
if (context === "column") return parts.length >= 2 ? "table" : "table";
if (context === "routine") return parts.length >= 2 ? "package" : "schema";
return parts.length >= 1 ? "schema" : "unknown";
},
},
sqlserver: {
id: "sqlserver",
identifierQuotes: [
@ -143,6 +159,7 @@ export function sqlReferenceAnalysisDialectFor(options: { databaseType?: Databas
}
export function sqlSemanticDialectFor(options: { databaseType?: DatabaseType; dialect?: "mysql" | "postgres" | "sqlserver" }): SqlSemanticDialectAdapter {
if (options.databaseType === "clickhouse") return SQL_SEMANTIC_DIALECTS.clickhouse;
if (options.dialect && SQL_SEMANTIC_DIALECTS[options.dialect]) return SQL_SEMANTIC_DIALECTS[options.dialect];
switch (options.databaseType) {
case "postgres":

View File

@ -0,0 +1,542 @@
# ClickHouse SQL Syntax Highlighting Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a dedicated ClickHouse CodeMirror dialect so ClickHouse keywords, types, built-ins, comments, and table names receive correct editor highlighting.
**Architecture:** Keep DBX's existing CodeMirror/Lezer pipeline and define a ClickHouse dialect by extending `StandardSQL` with a curated ClickHouse vocabulary and lexical options. Route ClickHouse connections to that syntax dialect without widening the existing completion/formatting dialect, and add a ClickHouse semantic adapter selected before the legacy MySQL behavior dialect.
**Tech Stack:** TypeScript, Vue 3, CodeMirror 6 `@codemirror/lang-sql@6.10.0`, Lezer SQL parser, Vitest.
## Global Constraints
- Cover the main CodeMirror query editor and CodeMirror-based DDL/SQL viewers.
- Do not change Shiki-rendered static SQL blocks in AI messages.
- Do not change SQL formatting, execution, completion, snippets, or editor theme colors.
- Use `StandardSQL`, not `MySQL`, as the ClickHouse parser base.
- Preserve ClickHouse's compact `--SELECT 1` line-comment behavior.
- Add no runtime dependency and no persisted-setting migration.
---
## File Map
- Modify `apps/desktop/src/lib/editor/codemirrorSqlDialect.ts`: own the ClickHouse CodeMirror dialect name, vocabulary, lexical configuration, and parser factory selection.
- Modify `apps/desktop/src/lib/database/jdbcDialect.ts`: return the dedicated syntax dialect for native and inferred JDBC ClickHouse connections while retaining the existing three-value behavior dialect.
- Modify `apps/desktop/src/components/editor/QueryEditor.vue`: allow `clickhouse` only for the syntax-dialect prop.
- Modify `apps/desktop/src/components/layout/ContentArea.vue`: type the syntax dialect with the shared CodeMirror dialect type.
- Modify `packages/app-tests/codemirrorSqlDialect.test.ts`: verify vocabulary parser nodes, comments, and database-to-syntax propagation.
- Modify `packages/app-tests/jdbcDialect.test.ts`: verify inferred JDBC ClickHouse syntax selection.
- Modify `apps/desktop/src/lib/sql/semantic/dialect.ts`: own the ClickHouse semantic adapter and prioritize it for ClickHouse database types.
- Create `apps/desktop/src/lib/__tests__/sql/semantic/dialect.spec.ts`: verify adapter selection, quoting, and normalization.
### Task 1: Dedicated ClickHouse CodeMirror dialect
**Files:**
- Modify: `packages/app-tests/codemirrorSqlDialect.test.ts`
- Modify: `packages/app-tests/jdbcDialect.test.ts`
- Modify: `apps/desktop/src/lib/editor/codemirrorSqlDialect.ts`
- Modify: `apps/desktop/src/lib/database/jdbcDialect.ts`
- Modify: `apps/desktop/src/components/editor/QueryEditor.vue`
- Modify: `apps/desktop/src/components/layout/ContentArea.vue`
**Interfaces:**
- Produces: `CodeMirrorSqlDialectName = "mysql" | "postgres" | "sqlserver" | "clickhouse"`.
- Produces: `codeMirrorSqlDialectForConnection(connection): CodeMirrorSqlDialectName`.
- Preserves: `codeMirrorSqlDialect(dbType): "mysql" | "postgres" | "sqlserver"` for completion and formatting behavior.
- Consumes: `createDbxCodeMirrorSqlDialect(langSql, dialectName, databaseType)`.
- [ ] **Step 1: Write failing parser and mapping tests**
Extend `packages/app-tests/codemirrorSqlDialect.test.ts` with:
```ts
import { codeMirrorSqlDialect, codeMirrorSqlDialectForConnection } from "../../apps/desktop/src/lib/database/jdbcDialect.ts";
test("maps ClickHouse connections to the dedicated editor syntax dialect", () => {
assert.equal(codeMirrorSqlDialectForConnection({ db_type: "clickhouse" }), "clickhouse");
assert.equal(
codeMirrorSqlDialectForConnection({
db_type: "jdbc",
connection_string: "jdbc:clickhouse://127.0.0.1:8123/default",
}),
"clickhouse",
);
});
test("classifies ClickHouse-specific syntax", () => {
const dialect = createDbxCodeMirrorSqlDialect(langSql, "clickhouse", "clickhouse");
const sql = `
CREATE TABLE events
(
id UInt64,
created_at DateTime64(3),
category LowCardinality(String),
attributes Map(String, String)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(created_at)
ORDER BY id
TTL created_at + INTERVAL 30 DAY
SETTINGS index_granularity = 8192;
SELECT uniqExact(id), argMax(category, created_at)
FROM events
PREWHERE created_at >= now() - INTERVAL 1 DAY
ARRAY JOIN mapKeys(attributes) AS attribute_key
LIMIT 10 BY category
FORMAT JSONEachRow;
`;
for (const keyword of ["ENGINE", "PARTITION", "TTL", "SETTINGS", "PREWHERE", "ARRAY", "FORMAT"]) {
assert.ok(countParsedNodes(dialect, sql, "Keyword", keyword) >= 1, keyword);
}
for (const type of ["UInt64", "DateTime64", "LowCardinality", "Map"]) {
assert.equal(countParsedNodes(dialect, sql, "Type", type), 1, type);
}
for (const builtin of ["toYYYYMM", "uniqExact", "argMax", "mapKeys"]) {
assert.equal(countParsedNodes(dialect, sql, "Builtin", builtin), 1, builtin);
}
assert.equal(countParsedNodes(dialect, "--SELECT 1", "LineComment", "--SELECT 1"), 1);
});
```
Extend `packages/app-tests/jdbcDialect.test.ts` with:
```ts
test("uses dedicated ClickHouse editor syntax for inferred JDBC connections", () => {
const connection = {
db_type: "jdbc" as const,
connection_string: "jdbc:clickhouse://127.0.0.1:8123/default",
};
assert.equal(inferJdbcDialect(connection), "clickhouse");
assert.equal(codeMirrorSqlDialectForConnection(connection), "clickhouse");
});
```
- [ ] **Step 2: Run the tests and verify the red state**
Run:
```bash
pnpm vitest run packages/app-tests/codemirrorSqlDialect.test.ts packages/app-tests/jdbcDialect.test.ts
```
Expected: FAIL because `"clickhouse"` is not accepted as a CodeMirror dialect name and ClickHouse connections currently return `"mysql"`.
- [ ] **Step 3: Add the ClickHouse dialect definition**
In `apps/desktop/src/lib/editor/codemirrorSqlDialect.ts`, extend the shared name and add focused vocabulary constants:
```ts
export type CodeMirrorSqlDialectName = "mysql" | "postgres" | "sqlserver" | "clickhouse";
const CLICKHOUSE_KEYWORDS = [
"ATTACH",
"DETACH",
"OPTIMIZE",
"SYSTEM",
"KILL",
"ENGINE",
"PARTITION",
"PRIMARY",
"SAMPLE",
"PREWHERE",
"ARRAY",
"GLOBAL",
"FINAL",
"WITH",
"TOTALS",
"ROLLUP",
"CUBE",
"LIMIT",
"BY",
"INTO",
"OUTFILE",
"COMPRESSION",
"FORMAT",
"SETTINGS",
"TTL",
"CODEC",
"MATERIALIZED",
"ALIAS",
"PROJECTION",
"INDEX",
"GRANULARITY",
].join(" ");
const CLICKHOUSE_TYPES = [
"Bool",
"Int8",
"Int16",
"Int32",
"Int64",
"Int128",
"Int256",
"UInt8",
"UInt16",
"UInt32",
"UInt64",
"UInt128",
"UInt256",
"Float32",
"Float64",
"Decimal",
"Decimal32",
"Decimal64",
"Decimal128",
"Decimal256",
"String",
"FixedString",
"Date",
"Date32",
"DateTime",
"DateTime64",
"Time",
"Time64",
"Enum8",
"Enum16",
"UUID",
"IPv4",
"IPv6",
"Array",
"Tuple",
"Map",
"Nested",
"Nullable",
"LowCardinality",
"AggregateFunction",
"SimpleAggregateFunction",
"JSON",
"Object",
"Variant",
"Dynamic",
"Nothing",
].join(" ");
const CLICKHOUSE_BUILTINS = [
"now",
"today",
"toDate",
"toDateTime",
"toDateTime64",
"toYYYYMM",
"count",
"sum",
"avg",
"min",
"max",
"uniq",
"uniqExact",
"argMin",
"argMax",
"groupArray",
"arrayJoin",
"mapKeys",
"mapValues",
"JSONExtract",
"JSONExtractString",
].join(" ");
```
Make `codeMirrorBaseDialect()` select `StandardSQL` for either an explicit
ClickHouse syntax name or `databaseType === "clickhouse"`:
```ts
if (databaseType) {
if (databaseType === "clickhouse") return langSql.StandardSQL;
// Existing database family checks remain in their current order.
}
if (dialectName === "clickhouse") return langSql.StandardSQL;
```
In `createDbxCodeMirrorSqlDialect()`, build ClickHouse-specific spec fields
without changing other dialects:
```ts
const isClickHouse = databaseType === "clickhouse" || dialectName === "clickhouse";
return langSql.SQLDialect.define({
...baseDialect.spec,
keywords: [
baseKeywords,
DBX_COMMON_SQL_KEYWORDS,
isClickHouse ? CLICKHOUSE_KEYWORDS : "",
isPostgres ? POSTGRES_PLPGSQL_KEYWORDS : "",
isSqlServer ? SQLSERVER_KEYWORDS : "",
]
.filter(Boolean)
.join(" "),
types: [baseDialect.spec.types || "", isClickHouse ? CLICKHOUSE_TYPES : "", isPostgres ? POSTGRES_PLPGSQL_TYPES : ""]
.filter(Boolean)
.join(" ") || undefined,
builtin: [baseDialect.spec.builtin || "", isClickHouse ? CLICKHOUSE_BUILTINS : "", isPostgres ? POSTGRES_PLPGSQL_BUILTIN : ""]
.filter(Boolean)
.join(" ") || undefined,
identifierQuotes: isClickHouse ? '"`' : baseDialect.spec.identifierQuotes,
backslashEscapes: isClickHouse ? true : baseDialect.spec.backslashEscapes,
spaceAfterDashes: isClickHouse ? false : baseDialect.spec.spaceAfterDashes,
doubleDollarQuotedStrings: false,
});
```
- [ ] **Step 4: Route ClickHouse only through the syntax-dialect channel**
In `apps/desktop/src/lib/database/jdbcDialect.ts`, import the type and keep the
existing behavior-dialect function narrow:
```ts
import type { CodeMirrorSqlDialectName } from "@/lib/editor/codemirrorSqlDialect";
export function codeMirrorSqlDialectForConnection(connection?: JdbcDialectConnection): CodeMirrorSqlDialectName {
if (isJdbcAseProfile(connection)) return "sqlserver";
const databaseType = effectiveDatabaseTypeForConnection(connection);
if (databaseType === "clickhouse") return "clickhouse";
return codeMirrorSqlDialect(databaseType);
}
```
In `apps/desktop/src/components/editor/QueryEditor.vue`, import the type and
change only the syntax prop:
```ts
import { createDbxCodeMirrorSqlDialect, type CodeMirrorSqlDialectName } from "@/lib/editor/codemirrorSqlDialect";
syntaxDialect?: CodeMirrorSqlDialectName;
```
In `apps/desktop/src/components/layout/ContentArea.vue`, import the same type and
change only the syntax computed value:
```ts
import type { CodeMirrorSqlDialectName } from "@/lib/editor/codemirrorSqlDialect";
const editorSyntaxDialect = computed<CodeMirrorSqlDialectName>(() => codeMirrorSqlDialectForConnection(props.activeConnection));
```
The existing `editorDialect` remains `"mysql" | "postgres" | "sqlserver"` so
completion, identifier quoting, and formatting behavior stay out of scope.
- [ ] **Step 5: Run focused tests and typecheck**
Run:
```bash
pnpm vitest run packages/app-tests/codemirrorSqlDialect.test.ts packages/app-tests/jdbcDialect.test.ts
pnpm typecheck
```
Expected: both test files PASS and `vue-tsc` exits with code 0.
- [ ] **Step 6: Commit the dedicated parser**
```bash
git add apps/desktop/src/lib/editor/codemirrorSqlDialect.ts \
apps/desktop/src/lib/database/jdbcDialect.ts \
apps/desktop/src/components/editor/QueryEditor.vue \
apps/desktop/src/components/layout/ContentArea.vue \
packages/app-tests/codemirrorSqlDialect.test.ts \
packages/app-tests/jdbcDialect.test.ts
git commit -m "feat(editor): add ClickHouse SQL highlighting"
```
### Task 2: ClickHouse semantic table-name adapter
**Files:**
- Create: `apps/desktop/src/lib/__tests__/sql/semantic/dialect.spec.ts`
- Modify: `apps/desktop/src/lib/sql/semantic/dialect.ts`
**Interfaces:**
- Produces: `SQL_SEMANTIC_DIALECTS.clickhouse: SqlSemanticDialectAdapter`.
- Preserves: `sqlSemanticDialectFor(options)` input type; ClickHouse is selected by `databaseType`, not by widening completion's three-value dialect.
- Consumes: `SqlSemanticDialectAdapter` and `defaultNormalize`.
- [ ] **Step 1: Write the failing semantic adapter test**
Create `apps/desktop/src/lib/__tests__/sql/semantic/dialect.spec.ts`:
```ts
import { describe, expect, it } from "vitest";
import { sqlSemanticDialectFor } from "@/lib/sql/semantic/dialect";
describe("ClickHouse semantic dialect", () => {
it("takes precedence over the legacy MySQL behavior dialect", () => {
const dialect = sqlSemanticDialectFor({
databaseType: "clickhouse",
dialect: "mysql",
});
expect(dialect.id).toBe("clickhouse");
expect(dialect.identifierQuotes).toEqual([
{ open: "`", close: "`" },
{ open: '"', close: '"' },
]);
expect(dialect.normalizeIdentifier("EventName")).toBe("EventName");
expect(dialect.quoteIdentifier("event`name")).toBe("`event``name`");
});
});
```
- [ ] **Step 2: Run the test and verify the red state**
Run:
```bash
pnpm vitest run apps/desktop/src/lib/__tests__/sql/semantic/dialect.spec.ts
```
Expected: FAIL because the returned adapter id is currently `"mysql"`.
- [ ] **Step 3: Implement the minimal ClickHouse semantic adapter**
Add this entry to `SQL_SEMANTIC_DIALECTS` in
`apps/desktop/src/lib/sql/semantic/dialect.ts`:
```ts
clickhouse: {
id: "clickhouse",
identifierQuotes: [
{ open: "`", close: "`" },
{ open: '"', close: '"' },
],
supportsAsForTableAlias: true,
projectionAliasVisibility: { where: false, groupBy: true, having: true, orderBy: true },
normalizeIdentifier: defaultNormalize,
quoteIdentifier: (identifier) => quoteWith(identifier, "`"),
qualifierRole(parts, context) {
if (context === "column") return parts.length >= 2 ? "table" : "table";
if (context === "routine") return parts.length >= 2 ? "package" : "schema";
return parts.length >= 1 ? "schema" : "unknown";
},
},
```
Select it before the explicit three-value behavior dialect:
```ts
export function sqlSemanticDialectFor(options: { databaseType?: DatabaseType; dialect?: "mysql" | "postgres" | "sqlserver" }): SqlSemanticDialectAdapter {
if (options.databaseType === "clickhouse") return SQL_SEMANTIC_DIALECTS.clickhouse;
if (options.dialect && SQL_SEMANTIC_DIALECTS[options.dialect]) return SQL_SEMANTIC_DIALECTS[options.dialect];
// Existing switch remains unchanged.
}
```
- [ ] **Step 4: Run semantic and parser regression tests**
Run:
```bash
pnpm vitest run \
apps/desktop/src/lib/__tests__/sql/semantic/dialect.spec.ts \
apps/desktop/src/lib/__tests__/sql/semantic/model.spec.ts \
packages/app-tests/codemirrorSqlDialect.test.ts
```
Expected: all tests PASS.
- [ ] **Step 5: Commit the semantic adapter**
```bash
git add apps/desktop/src/lib/sql/semantic/dialect.ts \
apps/desktop/src/lib/__tests__/sql/semantic/dialect.spec.ts
git commit -m "feat(editor): add ClickHouse semantic SQL dialect"
```
### Task 3: Final verification and PR preparation
**Files:**
- Verify: all files changed by Tasks 1 and 2
- Verify: `docs/superpowers/specs/2026-07-29-clickhouse-syntax-highlighting-design.md`
- Verify: `docs/superpowers/plans/2026-07-29-clickhouse-syntax-highlighting.md`
**Interfaces:**
- Consumes: the dedicated ClickHouse parser and semantic adapter.
- Produces: a reviewed, tested branch ready for a GitHub pull request.
- [ ] **Step 1: Run the full relevant automated checks**
Run:
```bash
pnpm vitest run \
packages/app-tests/codemirrorSqlDialect.test.ts \
packages/app-tests/jdbcDialect.test.ts \
apps/desktop/src/lib/__tests__/editor/codemirrorSqlDialect.spec.ts \
apps/desktop/src/lib/__tests__/sql/semantic/dialect.spec.ts \
apps/desktop/src/lib/__tests__/sql/semantic/model.spec.ts
pnpm typecheck
```
Expected: every Vitest file passes and typecheck exits with code 0.
- [ ] **Step 2: Review parser output and scope boundaries**
Run:
```bash
git diff main...HEAD --check
git diff main...HEAD --stat
git diff main...HEAD -- \
apps/desktop/src/lib/editor/codemirrorSqlDialect.ts \
apps/desktop/src/lib/database/jdbcDialect.ts \
apps/desktop/src/lib/sql/semantic/dialect.ts
rg -n "clickhouse" apps/desktop/src/lib/sql/sqlHighlighter.ts
```
Expected: no whitespace errors; changes stay in the planned CodeMirror and
semantic paths; `sqlHighlighter.ts` has no new ClickHouse/Shiki modification.
- [ ] **Step 3: Perform a local correctness review**
Check each diff hunk against this exact checklist:
```text
[ ] ClickHouse uses StandardSQL, never MySQL, as its parser base.
[ ] Compact -- comments remain LineComment nodes.
[ ] Keywords, types, and built-ins are stored in separate spec fields.
[ ] Only syntaxDialect accepts the new clickhouse value.
[ ] Existing completion/formatting dialect types remain unchanged.
[ ] databaseType=clickhouse wins over dialect=mysql in semantic selection.
[ ] No dependency, persistence, theme, Shiki, execution, or formatter changes.
```
Expected: every item is satisfied; correct any violation and rerun Step 1.
- [ ] **Step 4: Push and create the pull request**
Run:
```bash
git status --short
git push -u origin codex/clickhouse-syntax-highlighting
gh pr create \
--base main \
--head codex/clickhouse-syntax-highlighting \
--title "feat(editor): add dedicated ClickHouse SQL highlighting" \
--body-file /tmp/dbx-clickhouse-highlighting-pr.md
```
Before the last command, create `/tmp/dbx-clickhouse-highlighting-pr.md` with:
```markdown
## Summary
- add a dedicated ClickHouse CodeMirror SQL dialect
- classify ClickHouse keywords, data types, and common built-in functions
- select a ClickHouse semantic adapter for table-name highlighting
- preserve existing completion, formatting, Shiki, and theme behavior
## Test plan
- `pnpm vitest run packages/app-tests/codemirrorSqlDialect.test.ts packages/app-tests/jdbcDialect.test.ts apps/desktop/src/lib/__tests__/editor/codemirrorSqlDialect.spec.ts apps/desktop/src/lib/__tests__/sql/semantic/dialect.spec.ts apps/desktop/src/lib/__tests__/sql/semantic/model.spec.ts`
- `pnpm typecheck`
```
Expected: the branch is pushed and GitHub returns a new PR URL.

View File

@ -0,0 +1,141 @@
# ClickHouse SQL Syntax Highlighting Design
**Date:** 2026-07-29
## Goal
Give ClickHouse connections a dedicated SQL highlighting dialect in DBX's
CodeMirror query editors and DDL viewers. ClickHouse keywords, data types, and
common built-in functions should receive the same semantic colors that the
existing editor themes already assign to those token classes.
## Background
DBX currently has separate dialect-selection paths for SQL formatting and SQL
highlighting:
- `sqlFormatter.ts` maps ClickHouse to the formatter's native `clickhouse`
language.
- `jdbcDialect.ts` maps ClickHouse to the generic `mysql` CodeMirror dialect
name.
- `codemirrorSqlDialect.ts` does not recognize the ClickHouse database type and
therefore selects `StandardSQL` as its parser base.
- the semantic table-name overlay receives the explicit `mysql` dialect and
consequently applies MySQL rules to ClickHouse SQL.
This explains why formatting can look correct while highlighting does not.
Editor themes already distinguish keywords, types, functions, strings, and
identifiers; the parser is producing incomplete or incorrect token classes.
## Scope
Included:
- the main CodeMirror query editor
- CodeMirror-based DDL and SQL viewers that use the shared DBX dialect factory
- ClickHouse keyword, data-type, built-in-function, comment, and table-name
classification
- focused parser and propagation tests
Not included:
- Shiki-rendered static SQL blocks in AI messages
- SQL formatting, execution, completion, or snippet behavior
- a complete ClickHouse grammar or query validator
- theme color changes
## Chosen Approach
Add `clickhouse` as a first-class DBX CodeMirror dialect name. Define its
CodeMirror dialect from `StandardSQL`, then extend the dialect specification
with ClickHouse-specific keywords, data types, and common built-in functions.
`StandardSQL` is the safer parser base than `MySQL` because ClickHouse and MySQL
do not share all lexical behavior. In particular, DBX already tests that
ClickHouse accepts `--` comments without the whitespace requirement used by the
CodeMirror MySQL dialect.
The vocabulary will be curated from the official ClickHouse SQL reference and
kept as named constants next to the existing PostgreSQL and SQL Server
extensions. Representative coverage includes:
- clauses and DDL terms such as `PREWHERE`, `ARRAY JOIN`, `ENGINE`,
`PARTITION BY`, `SAMPLE BY`, `TTL`, `SETTINGS`, `CODEC`, and `FORMAT`
- types such as `UInt64`, `DateTime64`, `LowCardinality`, `Nullable`, `Map`,
`Tuple`, `Nested`, `AggregateFunction`, and `SimpleAggregateFunction`
- common functions such as `toYYYYMM`, `uniqExact`, `argMax`, `groupArray`,
`arrayJoin`, and `JSONExtractString`
This is vocabulary-driven syntax highlighting, not validation. Unknown future
functions will still parse as callable identifiers instead of breaking editor
input.
## Data Flow
1. A ClickHouse connection resolves to the new `clickhouse` CodeMirror dialect
name in `jdbcDialect.ts`.
2. QueryEditor and DDL viewers pass that name and the ClickHouse database type
into `createDbxCodeMirrorSqlDialect()`.
3. The dialect factory selects the dedicated ClickHouse specification.
4. CodeMirror's Lezer SQL parser assigns keyword, type, built-in, comment, and
identifier tags.
5. Existing editor themes render those tags with their current colors.
6. The semantic overlay selects a ClickHouse adapter for table-name
decorations instead of inheriting MySQL behavior accidentally.
## Semantic Adapter
Add a ClickHouse semantic dialect adapter rather than silently treating
ClickHouse as MySQL. It will support ClickHouse's backtick and double-quote
identifier quoting and use the project's existing default identifier
normalization unless a verified ClickHouse-specific rule is required.
This adapter is limited to tokenization and table-reference decoration. It does
not change query execution or attempt to model every ClickHouse alias-resolution
extension.
## Compatibility And Failure Behavior
- Existing MySQL, PostgreSQL, SQL Server, Oracle, SQLite, and generic database
mappings remain unchanged.
- ClickHouse keeps its current `--SELECT 1` line-comment behavior.
- An unlisted ClickHouse keyword or function remains readable as an identifier;
highlighting degrades locally without blocking editing.
- All changes remain frontend-only and require no persisted-setting migration.
## Testing
Use test-driven development:
1. Add parser tests that initially fail under the current StandardSQL fallback.
2. Assert representative ClickHouse terms produce keyword, type, and built-in
parser nodes.
3. Retain and strengthen the line-comment regression test.
4. Assert the JDBC database-type mapping returns `clickhouse`.
5. Assert QueryEditor and DDL viewers pass the dedicated dialect through the
shared factory.
6. Add semantic-dialect coverage for ClickHouse identifier quoting and
selection.
7. Run focused tests, the relevant desktop test suite, and frontend typecheck.
## Alternatives Considered
### Extend the MySQL dialect
This is smaller but preserves known lexical mismatches and continues to make
ClickHouse behavior depend on unrelated MySQL changes.
### Integrate the ClickHouse lexer or WebAssembly
ClickHouse exposes native query-highlighting capabilities, including recent
lexer/WASM work. That route could eventually provide higher fidelity, but it
adds runtime, bundle-size, and CodeMirror syntax-tree integration costs that are
disproportionate to this focused highlighting fix.
## Success Criteria
- ClickHouse-specific clauses, types, and common functions are visually
differentiated in query and DDL editors.
- ClickHouse no longer resolves to MySQL or generic StandardSQL by accident.
- existing dialect behavior and editor themes do not regress.
- the implementation is covered by focused parser, mapping, and semantic tests.

View File

@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
import { test } from "vitest";
import * as langSql from "@codemirror/lang-sql";
import { createDbxCodeMirrorSqlDialect } from "../../apps/desktop/src/lib/editor/codemirrorSqlDialect.ts";
import { codeMirrorSqlDialect } from "../../apps/desktop/src/lib/database/jdbcDialect.ts";
import { codeMirrorSqlDialect, codeMirrorSqlDialectForConnection } from "../../apps/desktop/src/lib/database/jdbcDialect.ts";
import type { DatabaseType } from "../../apps/desktop/src/types/database.ts";
function hasKeyword(keywords: string | undefined, keyword: string): boolean {
@ -47,6 +47,53 @@ test("keeps DBX PostgreSQL procedural dialect extensions", () => {
assert.equal(hasKeyword(dialect.spec.builtin, "TG_NAME"), true);
});
test("maps ClickHouse connections to the dedicated editor syntax dialect", () => {
assert.equal(codeMirrorSqlDialectForConnection({ db_type: "clickhouse" }), "clickhouse");
assert.equal(
codeMirrorSqlDialectForConnection({
db_type: "jdbc",
connection_string: "jdbc:clickhouse://127.0.0.1:8123/default",
}),
"clickhouse",
);
});
test("classifies ClickHouse-specific syntax", () => {
const dialect = createDbxCodeMirrorSqlDialect(langSql, "clickhouse", "clickhouse");
const sql = `
CREATE TABLE events
(
id UInt64,
created_at DateTime64(3),
category LowCardinality(String),
attributes Map(String, String)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(created_at)
ORDER BY id
TTL created_at + INTERVAL 30 DAY
SETTINGS index_granularity = 8192;
SELECT uniqExact(id), argMax(category, created_at)
FROM events
PREWHERE created_at >= now() - INTERVAL 1 DAY
ARRAY JOIN mapKeys(attributes) AS attribute_key
LIMIT 10 BY category
FORMAT JSONEachRow;
`;
for (const keyword of ["SELECT", "FROM", "ENGINE", "PARTITION", "TTL", "SETTINGS", "PREWHERE", "FORMAT"]) {
assert.ok(countParsedNodes(dialect, sql, "Keyword", keyword) >= 1, keyword);
}
for (const type of ["UInt64", "DateTime64", "LowCardinality", "Map"]) {
assert.equal(countParsedNodes(dialect, sql, "Type", type), 1, type);
}
for (const builtin of ["toYYYYMM", "uniqExact", "argMax", "mapKeys"]) {
assert.equal(countParsedNodes(dialect, sql, "Builtin", builtin), 1, builtin);
}
assert.equal(countParsedNodes(dialect, "--SELECT 1", "LineComment", "--SELECT 1"), 1);
});
test("treats compact double-dash comments as comments in non-MySQL SQL dialects", () => {
const databaseTypes: DatabaseType[] = [
"oracle",

View File

@ -29,6 +29,16 @@ test("infers JDBC dialect from driver profile", () => {
);
});
test("uses dedicated ClickHouse editor syntax for inferred JDBC connections", () => {
const connection = {
db_type: "jdbc" as const,
connection_string: "jdbc:clickhouse://127.0.0.1:8123/default",
};
assert.equal(inferJdbcDialect(connection), "clickhouse");
assert.equal(codeMirrorSqlDialectForConnection(connection), "clickhouse");
});
test("infers GaussDB-compatible JDBC connections as schema-aware", () => {
const gaussdbConnection = {
db_type: "jdbc" as const,