feat(editor): add ClickHouse SQL highlighting
This commit is contained in:
parent
02aea73d28
commit
b4efcebd78
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
||||
|
|
|
|||
|
|
@ -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`");
|
||||
});
|
||||
});
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue