mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-02 02:34:42 +08:00
feat(sql): add Doris/SelectDB completion and semantic diagnostics
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildSqlCompletionItems, getSqlFunctionSignatureHelp } from "@/lib/sql/sqlCompletion";
|
||||
import { buildSqlSemanticModel } from "@/lib/sql/semantic/model";
|
||||
import { tokenizeSqlSemantic } from "@/lib/sql/semantic/tokens";
|
||||
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { resolveSqlDialectId, sqlReferenceAnalysisDialectFor } from "@/lib/sql/semantic/dialect";
|
||||
|
||||
describe("Doris and SelectDB SQL assistance", () => {
|
||||
it.each(["doris", "selectdb"])("routes the %s profile to Doris assistance", (driver_profile) => {
|
||||
expect(effectiveDatabaseTypeForConnection({ db_type: "mysql", driver_profile })).toBe("doris");
|
||||
});
|
||||
|
||||
it.each(["BITMAP_UNION_COUNT", "HLL_UNION_AGG", "ARRAY_MAP", "JSON_EXTRACT_STRING", "DATE_TRUNC", "PERCENTILE_APPROX", "EXPLODE_SPLIT"])("offers %s with an insertable signature", (name) => {
|
||||
const sql = `SELECT ${name.slice(0, -1)}`;
|
||||
const items = buildSqlCompletionItems(sql, sql.length, { databaseType: "doris", tables: [], columnsByTable: new Map() });
|
||||
expect(items).toContainEqual(expect.objectContaining({ label: name, type: "function", apply: expect.stringContaining(`${name}(`) }));
|
||||
const call = `SELECT ${name}(`;
|
||||
expect(getSqlFunctionSignatureHelp(call, call.length, "doris")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("ranks a matching column ahead of a function prefix match", () => {
|
||||
const sql = "SELECT bitmap_ FROM events";
|
||||
const items = buildSqlCompletionItems(sql, sql.indexOf(" FROM"), {
|
||||
databaseType: "doris",
|
||||
tables: [{ name: "events" }],
|
||||
columnsByTable: new Map([["events", [{ name: "bitmap_value", table: "events", dataType: "BITMAP" }]]]),
|
||||
});
|
||||
expect(items.findIndex((item) => item.label === "bitmap_value")).toBeGreaterThanOrEqual(0);
|
||||
expect(items.findIndex((item) => item.label === "BITMAP_UNION_COUNT")).toBeGreaterThan(items.findIndex((item) => item.label === "bitmap_value"));
|
||||
});
|
||||
|
||||
it("retains a Doris parser dialect instead of falling back to MySQL", () => {
|
||||
expect(sqlReferenceAnalysisDialectFor({ databaseType: "doris", fallbackDialect: "mysql" })).toBe("doris");
|
||||
});
|
||||
|
||||
it("models chained LATERAL VIEW outputs as local columns", () => {
|
||||
const sql = "SELECT e, part FROM events t LATERAL VIEW explode(t.tags) a AS e LATERAL VIEW explode_split(t.name, ',') b AS part";
|
||||
const model = buildSqlSemanticModel(sql, sql.indexOf("e,") + 1, { databaseType: "doris", dialect: "doris" });
|
||||
expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ name: "a", kind: "table_function", columns: ["e"] }), expect.objectContaining({ name: "b", kind: "table_function", columns: ["part"] })]));
|
||||
});
|
||||
|
||||
it("keeps the Doris semantic adapter on the real editor dialect shape", () => {
|
||||
// QueryEditor passes the MySQL fallback dialect for Doris connections; the databaseType must
|
||||
// still win so the doris adapter (LATERAL VIEW modeling, ...) is not masked by "mysql".
|
||||
expect(resolveSqlDialectId({ databaseType: "doris", dialect: "mysql" })).toBe("doris");
|
||||
expect(resolveSqlDialectId({ databaseType: "starrocks", dialect: "mysql" })).toBe("doris");
|
||||
});
|
||||
|
||||
it("treats # as a line comment for Doris", () => {
|
||||
const tokens = tokenizeSqlSemantic("SELECT 1 # trailing note\nFROM t", "doris");
|
||||
expect(tokens).toContainEqual(expect.objectContaining({ kind: "comment", text: "# trailing note" }));
|
||||
});
|
||||
|
||||
it("models LATERAL VIEW columns under the editor dialect shape", () => {
|
||||
const sql = "SELECT e FROM events t LATERAL VIEW explode(t.tags) a AS e";
|
||||
const model = buildSqlSemanticModel(sql, sql.indexOf("e") + 1, { databaseType: "doris", dialect: "mysql" });
|
||||
expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ name: "a", kind: "table_function", columns: ["e"] })]));
|
||||
});
|
||||
|
||||
it("models LATERAL VIEW OUTER like the plain form", () => {
|
||||
const sql = "SELECT e FROM events t LATERAL VIEW OUTER explode(t.tags) a AS e";
|
||||
const model = buildSqlSemanticModel(sql, sql.indexOf("e") + 1, { databaseType: "doris", dialect: "mysql" });
|
||||
expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ name: "a", kind: "table_function", columns: ["e"] })]));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
# Doris and SelectDB SQL assistance
|
||||
|
||||
The function catalogue is derived from the Apache Doris 2.1 and 3.x SQL manuals.
|
||||
SelectDB connections use the Doris capability family through `jdbcDialect.ts`.
|
||||
|
||||
Regenerate the catalogue from an Apache `doris-website` checkout:
|
||||
|
||||
```sh
|
||||
node scripts/generate-doris-functions.mjs /path/to/doris-website
|
||||
```
|
||||
|
||||
The generated file records the source revision and per-function documentation
|
||||
links. `documentedIn` records documentation coverage, not a minimum server
|
||||
version: the 3.x manuals also contain functions added after 3.0.
|
||||
|
||||
Source: https://github.com/apache/doris-website (Apache License 2.0).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
import catalog from "@/lib/sql/doris/functions.generated.json";
|
||||
|
||||
export interface DorisFunctionDefinition {
|
||||
name: string;
|
||||
category: string;
|
||||
signatures: string[];
|
||||
docs: string;
|
||||
documentedIn: string[];
|
||||
}
|
||||
|
||||
const definitions = catalog.functions as DorisFunctionDefinition[];
|
||||
|
||||
function splitParameters(value: string): string[] {
|
||||
const parameters: string[] = [];
|
||||
let depth = 0;
|
||||
let start = 0;
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const character = value[index];
|
||||
if (character === "(" || character === "<" || character === "[") depth += 1;
|
||||
if (character === ")" || character === ">" || character === "]") depth = Math.max(0, depth - 1);
|
||||
if (character === "," && depth === 0) {
|
||||
parameters.push(value.slice(start, index).trim());
|
||||
start = index + 1;
|
||||
}
|
||||
}
|
||||
const last = value.slice(start).trim();
|
||||
if (last) parameters.push(last);
|
||||
return parameters;
|
||||
}
|
||||
|
||||
function parameterName(value: string, index: number): string {
|
||||
const placeholder = /<([^>]+)>/.exec(value)?.[1] ?? value;
|
||||
const cleaned = placeholder
|
||||
.replace(/\[.*$/g, "")
|
||||
.replace(/\s+/g, "_")
|
||||
.replace(/[^A-Za-z0-9_]/g, "")
|
||||
.replace(/^_+|_+$/g, "");
|
||||
return cleaned || `arg${index + 1}`;
|
||||
}
|
||||
|
||||
function parametersFor(definition: DorisFunctionDefinition): string[] {
|
||||
const signature = definition.signatures.find((item) => item.includes("("));
|
||||
if (!signature) return ["expression"];
|
||||
const open = signature.indexOf("(");
|
||||
const close = signature.lastIndexOf(")");
|
||||
if (open < 0 || close <= open) return ["expression"];
|
||||
return splitParameters(signature.slice(open + 1, close))
|
||||
.filter((item) => item && !/^\.\.\.$/.test(item))
|
||||
.map(parameterName);
|
||||
}
|
||||
|
||||
export const DORIS_FUNCTION_DEFINITIONS = definitions;
|
||||
const compatibilityOverrides: DorisFunctionDefinition[] = [
|
||||
{
|
||||
name: "JSON_EXTRACT_STRING",
|
||||
category: "JSON",
|
||||
signatures: ["JSON_EXTRACT_STRING(json_string, path)"],
|
||||
docs: "Extracts a string value from a JSON string by path.",
|
||||
documentedIn: ["Apache Doris JSON function compatibility"],
|
||||
},
|
||||
];
|
||||
for (const definition of compatibilityOverrides) {
|
||||
if (!DORIS_FUNCTION_DEFINITIONS.some((item) => item.name === definition.name)) DORIS_FUNCTION_DEFINITIONS.push(definition);
|
||||
}
|
||||
export const DORIS_FUNCTION_SIGNATURES = new Map(DORIS_FUNCTION_DEFINITIONS.map((definition) => [definition.name, parametersFor(definition)]));
|
||||
export const DORIS_FUNCTION_DOCS = new Map(DORIS_FUNCTION_DEFINITIONS.map((definition) => [definition.name, `${definition.category} · ${definition.docs}`]));
|
||||
@@ -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
|
||||
|
||||
@@ -18,6 +18,7 @@ import { identifierMatchScore, matchesIdentifierSearch } from "@/lib/sql/identif
|
||||
import { containsHan, orderedSubsequenceSpan, pinyinFirstLetters } from "@/lib/common/pinyin";
|
||||
import { quoteTableIdentifier } from "@/lib/table/tableSelectSql";
|
||||
import { driverProfileCompletionObjects, driverProfileCompletionTableMetadata, driverProfileCompletionTables, driverProfileRoutineSignatures } from "@/lib/database/driverProfileExtensions";
|
||||
import { DORIS_FUNCTION_DOCS, DORIS_FUNCTION_SIGNATURES } from "@/lib/sql/doris/functions";
|
||||
|
||||
export { DEFAULT_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
|
||||
|
||||
@@ -585,8 +586,8 @@ const DATABASE_SQL_KEYWORDS: Partial<Record<DatabaseType, string[]>> = {
|
||||
manticoresearch: MANTICORESEARCH_SQL_KEYWORDS,
|
||||
duckdb: ["COMMENT"],
|
||||
clickhouse: ["COMMENT"],
|
||||
doris: ["COMMENT", "MATERIALIZED", "MATERIALIZED VIEW"],
|
||||
starrocks: ["COMMENT", "MATERIALIZED", "MATERIALIZED VIEW"],
|
||||
doris: ["COMMENT", "MATERIALIZED", "MATERIALIZED VIEW", "DISTRIBUTED BY HASH", "DUPLICATE KEY", "AGGREGATE KEY", "UNIQUE KEY", "PRIMARY KEY", "PROPERTIES", "PARTITION BY", "BUCKETS", "LATERAL VIEW", "EXPLODE", "ARRAY", "MAP", "STRUCT", "BITMAP", "HLL"],
|
||||
starrocks: ["COMMENT", "MATERIALIZED", "MATERIALIZED VIEW", "DISTRIBUTED BY HASH", "DUPLICATE KEY", "PROPERTIES", "PARTITION BY", "BUCKETS", "LATERAL VIEW"],
|
||||
dameng: ["COMMENT"],
|
||||
kingbase: ["COMMENT"],
|
||||
vastbase: ["COMMENT"],
|
||||
@@ -648,7 +649,8 @@ const EXACT_LABEL_MATCH_BOOST = 10000;
|
||||
const EXACT_CUSTOM_SNIPPET_TRIGGER_BOOST = 40000;
|
||||
|
||||
function isTableTriggerKeyword(keyword: string, options: SqlSemanticBuildOptions): boolean {
|
||||
return TABLE_TRIGGER_KEYWORDS.has(keyword) || (keyword === "desc" && resolveSqlDialectId(options) === "mysql");
|
||||
const dialectId = resolveSqlDialectId(options);
|
||||
return TABLE_TRIGGER_KEYWORDS.has(keyword) || (keyword === "desc" && (dialectId === "mysql" || dialectId === "doris"));
|
||||
}
|
||||
|
||||
// Keywords that only make sense in DDL / statement-start contexts (not inside SELECT/INSERT/UPDATE/DELETE)
|
||||
@@ -1131,6 +1133,8 @@ const DATABASE_FUNCTION_SIGNATURES: Partial<Record<DatabaseType, Map<string, str
|
||||
"cloudflare-d1": CLOUDFLARE_D1_FUNCTION_SIGNATURES,
|
||||
sqlserver: SQLSERVER_FUNCTION_SIGNATURES,
|
||||
manticoresearch: MANTICORESEARCH_FUNCTION_SIGNATURES,
|
||||
doris: DORIS_FUNCTION_SIGNATURES,
|
||||
starrocks: DORIS_FUNCTION_SIGNATURES,
|
||||
};
|
||||
|
||||
const MYSQL_FUNCTION_APPLY_TEMPLATES = new Map<string, string>([
|
||||
@@ -5039,6 +5043,13 @@ function activeFunctionSignatures(databaseType?: DatabaseType): Map<string, stri
|
||||
return signatures;
|
||||
}
|
||||
|
||||
const DORIS_FUNCTION_DESCRIPTIONS = new Map(DORIS_FUNCTION_DOCS);
|
||||
const EMPTY_FUNCTION_DESCRIPTIONS = new Map<string, string>();
|
||||
|
||||
function activeFunctionDescriptions(databaseType?: DatabaseType): Map<string, string> {
|
||||
return databaseType === "doris" || databaseType === "starrocks" ? DORIS_FUNCTION_DESCRIPTIONS : EMPTY_FUNCTION_DESCRIPTIONS;
|
||||
}
|
||||
|
||||
function formatFunctionSignatureApply(definition: ClickHouseFunctionDefinition, omitOpeningParen: boolean): string {
|
||||
if (omitOpeningParen) return definition.name;
|
||||
const signature = definition.signatures[definition.preferredSignature ?? 0];
|
||||
@@ -5089,7 +5100,7 @@ function buildFunctionSnippetItems(prefix: string, functionDescriptions: Map<str
|
||||
items.push({
|
||||
label: functionName,
|
||||
type: "function" as const,
|
||||
detail: functionDescriptions.get(name) ?? "function",
|
||||
detail: activeFunctionDescriptions(databaseType).get(name) ?? functionDescriptions.get(name) ?? "function",
|
||||
apply: mysqlApply ? `${functionName}${applyGeneratedSqlTemplateKeywordCase(mysqlApply.slice(name.length), keywordCase)}` : `${functionName}(${paramStr})`,
|
||||
boost: computeBoost(name, prefix) + 300,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, relative, resolve } from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parse } from "yaml";
|
||||
|
||||
// Input: an Apache doris-website checkout containing the versioned SQL manuals.
|
||||
const docsRoot = process.argv[2];
|
||||
if (!docsRoot) throw new Error("Usage: node scripts/generate-doris-functions.mjs /path/to/doris-website");
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const revision = execFileSync("git", ["-C", docsRoot, "rev-parse", "HEAD"], { encoding: "utf8" }).trim();
|
||||
|
||||
function files(root) {
|
||||
return readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
|
||||
const path = join(root, entry.name);
|
||||
return entry.isDirectory() ? files(path) : path.endsWith(".md") ? [path] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function definitions(version) {
|
||||
const root = join(docsRoot, `versioned_docs/version-${version}/sql-manual/sql-functions`);
|
||||
return files(root).flatMap((path) => {
|
||||
const text = readFileSync(path, "utf8");
|
||||
const frontmatter = /^---\s*\n([\s\S]*?)\n---/.exec(text)?.[1];
|
||||
const metadata = frontmatter ? parse(frontmatter) : {};
|
||||
const name = String(metadata?.title ?? "")
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
if (!/^[A-Z][A-Z0-9_]*$/.test(name) || metadata?.draft || name === "OVERVIEW" || relative(root, path).startsWith("combinators/")) return [];
|
||||
const syntax = /##+\s+Syntax\s*\n([\s\S]*?)(?=\n##|$)/i.exec(text)?.[1] ?? "";
|
||||
const code = [...syntax.matchAll(/```[^\n]*\n([\s\S]*?)```|`([^`\n]+)`/g)].map((match) => match[1] ?? match[2]);
|
||||
const signatures = [
|
||||
...new Set(
|
||||
code.flatMap((block) => {
|
||||
const matches = [...block.matchAll(new RegExp(`\\b${name}\\s*\\(`, "gi"))];
|
||||
return matches.flatMap((match) => {
|
||||
const start = match.index;
|
||||
let depth = 1;
|
||||
let end = start + match[0].length;
|
||||
while (end < block.length && depth > 0) {
|
||||
if (block[end] === "(") depth++;
|
||||
if (block[end] === ")") depth--;
|
||||
end++;
|
||||
}
|
||||
return depth === 0 ? [block.slice(start, end).replace(/\s+/g, " ")] : [];
|
||||
});
|
||||
}),
|
||||
),
|
||||
];
|
||||
const category = relative(root, dirname(path)).replaceAll("\\", "/");
|
||||
const document = relative(root, path).replaceAll("\\", "/").replace(/\.md$/, "");
|
||||
return [{ name, category, signatures, docs: `https://doris.apache.org/docs/${version}/sql-manual/sql-functions/${document}/` }];
|
||||
});
|
||||
}
|
||||
|
||||
const older = new Map(definitions("2.1").map((definition) => [definition.name, definition]));
|
||||
const current = new Map(definitions("3.x").map((definition) => [definition.name, definition]));
|
||||
const entries = [...current.values()].map((definition) => ({ ...definition, documentedIn: older.has(definition.name) ? ["2.1", "3.x"] : ["3.x"] }));
|
||||
for (const definition of older.values()) {
|
||||
if (!current.has(definition.name)) entries.push({ ...definition, documentedIn: ["2.1"] });
|
||||
}
|
||||
entries.sort((left, right) => left.name.localeCompare(right.name, "en"));
|
||||
const output = { source: "https://github.com/apache/doris-website", revision, license: "Apache-2.0", functions: entries };
|
||||
writeFileSync(join(repoRoot, "apps/desktop/src/lib/sql/doris/functions.generated.json"), JSON.stringify(output, null, 2) + "\n");
|
||||
console.log(`Generated ${entries.length} Doris functions from ${revision}`);
|
||||
Reference in New Issue
Block a user