feat(sql): add Doris/SelectDB completion and semantic diagnostics
This commit is contained in:
10 files changed
+6273
-17
No files matched your search
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user