mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-23 04:55:21 +00:00
feat(editor): add ClickHouse SQL highlighting
This commit is contained in:
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user