feat(sql): add Doris/SelectDB completion and semantic diagnostics

This commit is contained in:
zipg authored and GitHub committed 2026-09-12 17:36:58 +08:00
1 parent 20f6092cf2
commit f8ce8ab353
10 files changed
+6273 -17

No files matched your search

+27 -9
View File
@@ -48,6 +48,12 @@ function roleForGenericQualifier(parts: string[], context: "table" | "column" |
return "schema";
}
function roleForMysqlLikeQualifier(parts: string[], context: "table" | "column" | "routine"): "catalog" | "schema" | "table" | "package" | "unknown" {
if (context === "column") return "table";
if (context === "routine") return parts.length >= 2 ? "package" : "schema";
return parts.length >= 1 ? "schema" : "unknown";
}
export const SQL_SEMANTIC_DIALECTS: Record<string, SqlSemanticDialectAdapter> = {
generic: {
id: "generic",
@@ -77,11 +83,19 @@ export const SQL_SEMANTIC_DIALECTS: Record<string, SqlSemanticDialectAdapter> =
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";
},
qualifierRole: roleForMysqlLikeQualifier,
},
doris: {
id: "doris",
identifierQuotes: [
{ open: "`", close: "`" },
{ open: '"', close: '"' },
],
supportsAsForTableAlias: true,
projectionAliasVisibility: { where: false, groupBy: true, having: true, orderBy: true },
normalizeIdentifier: defaultNormalize,
quoteIdentifier: (identifier) => quoteWith(identifier, "`"),
qualifierRole: roleForMysqlLikeQualifier,
},
clickhouse: {
id: "clickhouse",
@@ -156,11 +170,17 @@ export const SQL_SEMANTIC_DIALECTS: Record<string, SqlSemanticDialectAdapter> =
export function sqlReferenceAnalysisDialectFor(options: { databaseType?: DatabaseType; identifierQuote?: string; fallbackDialect: string }): string {
if (options.databaseType === "kyuubi") return "spark";
if (options.databaseType === "kingbase" && options.identifierQuote === "`") return "mysql";
if (options.databaseType === "doris" || options.databaseType === "starrocks") return "doris";
return options.fallbackDialect;
}
export function sqlSemanticDialectFor(options: { databaseType?: DatabaseType; dialect?: "mysql" | "postgres" | "sqlserver" | "clickhouse" }): SqlSemanticDialectAdapter {
export function sqlSemanticDialectFor(options: { databaseType?: DatabaseType; dialect?: "mysql" | "postgres" | "sqlserver" | "clickhouse" | "doris" }): SqlSemanticDialectAdapter {
if (options.databaseType === "clickhouse") return SQL_SEMANTIC_DIALECTS.clickhouse;
// Doris/StarRocks connections ride the editor's MySQL fallback dialect (codeMirrorSqlDialect maps
// them to "mysql"), so the explicit-dialect branch below would otherwise mask the doris adapter
// (LATERAL VIEW modeling, etc.) on the real editor path. Like clickhouse, they win on
// databaseType regardless of the passed dialect.
if (options.databaseType === "doris" || options.databaseType === "starrocks") return SQL_SEMANTIC_DIALECTS.doris;
if (options.dialect && SQL_SEMANTIC_DIALECTS[options.dialect]) return SQL_SEMANTIC_DIALECTS[options.dialect];
switch (options.databaseType) {
case "postgres":
@@ -171,8 +191,6 @@ export function sqlSemanticDialectFor(options: { databaseType?: DatabaseType; di
case "uxdb":
return SQL_SEMANTIC_DIALECTS.postgres;
case "mysql":
case "doris":
case "starrocks":
return SQL_SEMANTIC_DIALECTS.mysql;
case "sqlserver":
return SQL_SEMANTIC_DIALECTS.sqlserver;
@@ -204,6 +222,6 @@ export function sqlSemanticDialectFor(options: { databaseType?: DatabaseType; di
* "generic" default), matching tokenizeSqlSemantic's default and preserving the behavior callers
* had before dialect-aware scanning existed.
*/
export function resolveSqlDialectId(options: { databaseType?: DatabaseType; dialect?: "mysql" | "postgres" | "sqlserver" | "clickhouse" }): string {
export function resolveSqlDialectId(options: { databaseType?: DatabaseType; dialect?: "mysql" | "postgres" | "sqlserver" | "clickhouse" | "doris" }): string {
return options.databaseType || options.dialect ? sqlSemanticDialectFor(options).id : "mysql";
}
+53 -2
View File
@@ -464,6 +464,46 @@ function parseRowSourceList(state: ParseState, target: number, introducer: strin
return parsed ? { sources: [parsed.source], nextIndex: parsed.nextIndex } : null;
}
function parseDorisLateralView(state: ParseState, index: number, sourceIndex: number): { source: SqlSemanticRowSource; nextIndex: number } | null {
if (state.dialect.id !== "doris" || state.tokens[index]?.normalized !== "lateral" || state.tokens[index + 1]?.normalized !== "view") return null;
// Doris's `LATERAL VIEW [OUTER] fn(...) alias AS col` -- skip the optional OUTER marker so the
// OUTER form models the same function columns as the plain form.
let functionIndex = index + 2;
if (state.tokens[functionIndex]?.normalized === "outer") functionIndex += 1;
const functionName = readQualifiedName(state.tokens, functionIndex, state.dialect);
if (!functionName || state.tokens[functionName.nextIndex]?.text !== "(") return null;
const close = findMatchingParenToken(state.tokens, functionName.nextIndex);
if (close < 0) return null;
let aliasIndex = close + 1;
if (state.tokens[aliasIndex]?.normalized === "as") aliasIndex += 1;
const alias = state.tokens[aliasIndex];
if (!alias || alias.kind !== "word") return null;
let columnIndex = aliasIndex + 1;
if (state.tokens[columnIndex]?.normalized === "as") columnIndex += 1;
const columns: string[] = [];
while (state.tokens[columnIndex]?.kind === "word") {
columns.push(state.tokens[columnIndex].text);
columnIndex += 1;
if (state.tokens[columnIndex]?.text !== ",") break;
columnIndex += 1;
}
const endToken = state.tokens[Math.max(aliasIndex, columnIndex - 1)] ?? alias;
const name = alias.text;
return {
source: {
id: `table-function:${sourceIndex}:${name}`,
kind: "table_function",
name,
alias: name,
qualifierParts: [name],
qualifiedName: functionName.name,
sourceSpan: { start: state.tokens[index].span.start, end: endToken.span.end },
columns: columns.length ? columns : undefined,
},
nextIndex: columnIndex,
};
}
function parseRowSourcesAtDepth(state: ParseState, sourceDepth: number, sourceIndexOffset = 0): SqlSemanticRowSource[] {
const sources: SqlSemanticRowSource[] = [];
let inSelectFromClause = false;
@@ -497,9 +537,20 @@ function parseRowSourcesAtDepth(state: ParseState, sourceDepth: number, sourceIn
sources.push(...parsed.sources);
index = parsed.nextIndex - 1;
const separator = state.tokens[parsed.nextIndex];
// Track the position past any LATERAL VIEW clauses so the FROM-list separator check sees
// the real comma after them (parsed.nextIndex points at the first "lateral" token itself).
let afterSources = parsed.nextIndex;
let lateral = parseDorisLateralView(state, afterSources, sourceIndexOffset + sources.length);
while (lateral) {
sources.push(lateral.source);
index = lateral.nextIndex - 1;
afterSources = lateral.nextIndex;
lateral = parseDorisLateralView(state, afterSources, sourceIndexOffset + sources.length);
}
const separator = state.tokens[afterSources];
if (normalized !== "from" || separator?.text !== "," || separator.depth !== sourceDepth) break;
target = parsed.nextIndex + 1;
target = afterSources + 1;
}
}
return dedupeSources(sources);
+1 -1
View File
@@ -128,7 +128,7 @@ export function tokenizeSqlSemantic(input: string, dialectId = "mysql", options?
continue;
}
if (ch === "#" && dialectId === "mysql") {
if (ch === "#" && (dialectId === "mysql" || dialectId === "doris")) {
index += 1;
while (index < input.length && input[index] !== "\n" && input[index] !== "\r") index += 1;
tokens.push(token("comment", input.slice(start, index), start, index, depth));
+1 -1
View File
@@ -129,7 +129,7 @@ export interface SqlSemanticModel {
export interface SqlSemanticBuildOptions {
databaseType?: DatabaseType;
dialect?: "mysql" | "postgres" | "sqlserver";
dialect?: "mysql" | "postgres" | "sqlserver" | "clickhouse" | "doris";
/**
* Live CodeMirror state for the document being edited, when the caller has one (i.e. this is a
* real editor completion request, not a pure-string test/utility call). When present and its