fix(tdengine): recognize tbname in stable diagnostics

Closes #5685
This commit is contained in:
t8y2 2026-08-08 18:28:11 +08:00
parent be7b9ac021
commit ad9ca8400b
No known key found for this signature in database
9 changed files with 167 additions and 2 deletions

View File

@ -1894,7 +1894,10 @@ async function findExactSemanticDiagnosticTable(table: SqlTableReference): Promi
if (!target) return null;
const localMatches = connectionStore.lookupLocalCompletionTables(props.connectionId, target.database, table.name, MAX_COMPLETION_TABLES, target.schema, target.catalog);
const localExact = localMatches.find((item) => completionTablesMatch(item, table));
if (localExact) return localExact;
if (localExact) {
cachedTables = mergeCompletionTables(cachedTables, [localExact]);
return localExact;
}
const remoteMatches = await connectionStore.listCompletionTables(props.connectionId, target.database, table.name, MAX_COMPLETION_TABLES, target.schema, false, props.schema, target.catalog);
cachedTables = mergeCompletionTables(cachedTables, remoteMatches);

View File

@ -100,4 +100,19 @@ describe("completionTreeIndex", () => {
expect(completionTablesFromTree(tree, "doris", "sales")).toEqual([{ name: "orders", schema: undefined, type: "table" }]);
expect(completionTablesFromTree(tree, "doris", "sales", undefined, "hive_catalog")).toEqual([{ name: "orders", catalog: "hive_catalog", schema: undefined, type: "table" }]);
});
it("preserves TDengine stable metadata from the sidebar tree", () => {
const tree: TreeNode[] = [
{
id: "tdengine-stable",
label: "test_tb",
type: "table",
tableType: "STABLE",
connectionId: "tdengine",
database: "issue_5685",
},
];
expect(completionTablesFromTree(tree, "tdengine", "issue_5685")).toEqual([{ name: "test_tb", schema: undefined, type: "table", tableType: "STABLE" }]);
});
});

View File

@ -1,5 +1,6 @@
import type { TreeNode } from "@/types/database";
import type { SqlCompletionTable } from "@/lib/sql/sqlCompletion";
import { isTdengineStableTableType } from "@/lib/table/tableEditing";
const TABLE_NODE_TYPES = new Set(["table", "view", "materialized_view"]);
@ -31,6 +32,7 @@ export function completionTablesFromTree(nodes: readonly TreeNode[], connectionI
catalog: node.catalog,
schema: node.schema,
type: node.type === "materialized_view" ? "materialized_view" : node.type === "view" ? "view" : "table",
...(isTdengineStableTableType(node.tableType) ? { tableType: node.tableType } : {}),
});
});
return dedupeCompletionTables(tables);

View File

@ -1,6 +1,7 @@
import type { SqlCompletionColumn, SqlCompletionTable } from "@/lib/sql/sqlCompletion";
import { getSqlCompletionContext, isOracleSystemValueName } from "@/lib/sql/sqlCompletion";
import { executableStatementRanges, isOraclePlSqlStatement, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
import { DBX_TDENGINE_TBNAME_COLUMN, isTdengineStableTableType } from "@/lib/table/tableEditing";
import type { DatabaseType, SqlColumnReference, SqlReferenceAnalysis, SqlReferenceScope, SqlTableReference, SqlTextSpan } from "@/types/database";
export interface SqlSemanticDiagnostic {
@ -45,6 +46,7 @@ export function buildSqlSemanticDiagnostics(analysis: SqlReferenceAnalysis, sche
const tables = analysis.tables.filter((table) => table.name.trim());
const knownTables = new Map<string, SqlTableReference>();
const scopesById = scopesByIdMap(analysis.scopes);
let tdengineStableTables: Set<string> | undefined;
for (const table of tables) {
knownTables.set(normalizeName(table.name), table);
@ -73,6 +75,10 @@ export function buildSqlSemanticDiagnostics(analysis: SqlReferenceAnalysis, sche
const columnNames = new Set(columns.map((item) => normalizeName(item.name)));
if (columnNames.has(normalizeName(column.name))) continue;
if (schema.databaseType === "tdengine" && normalizeName(column.name) === DBX_TDENGINE_TBNAME_COLUMN) {
tdengineStableTables ??= tdengineStableTableKeys(schema.tables);
if (tdengineStableTables.has(tableReferenceKey(table))) continue;
}
const displayName = column.qualifier ? `${column.qualifier}.${column.name}` : column.name;
diagnostics.push({
@ -85,6 +91,20 @@ export function buildSqlSemanticDiagnostics(analysis: SqlReferenceAnalysis, sche
return diagnostics;
}
function tdengineStableTableKeys(tables: readonly SqlCompletionTable[]): Set<string> {
const keys = new Set<string>();
for (const table of tables) {
if (!isTdengineStableTableType(table.tableType)) continue;
keys.add(completionTableReferenceKey(table));
}
return keys;
}
function completionTableReferenceKey(table: Pick<SqlCompletionTable, "name" | "database" | "schema">): string {
if (table.schema) return normalizeName(`${table.database ? `${table.database}.` : ""}${table.schema}.${table.name}`);
return normalizeName(table.name);
}
function isUnquotedOracleSystemValueReference(column: SqlColumnReference, schema: SqlSemanticDiagnosticSchema): boolean {
if (column.qualifier || !isOracleSystemValueName(column.name, schema.databaseType)) return false;
if (!schema.sql) return false;

View File

@ -1160,6 +1160,7 @@ export interface SqlCompletionTable {
database?: string;
schema?: string;
type?: SqlObjectNavigationType;
tableType?: string;
detail?: string;
applyName?: string;
boost?: number;

View File

@ -9,7 +9,7 @@ function isViewTableType(tableType?: string): boolean {
return tableType?.toUpperCase().includes("VIEW") === true;
}
function isTdengineStableTableType(tableType?: string): boolean {
export function isTdengineStableTableType(tableType?: string): boolean {
const normalized = tableType?.trim().toUpperCase();
return normalized === "STABLE" || normalized === "SUPER TABLE" || normalized === "SUPERTABLE";
}

View File

@ -96,6 +96,18 @@ function dorisConnection(): ConnectionConfig {
} as ConnectionConfig;
}
function tdengineConnection(): ConnectionConfig {
return {
...postgresConnection(),
id: "tdengine-1",
name: "TDengine",
db_type: "tdengine",
port: 6041,
username: "root",
database: "issue_5685",
} as ConnectionConfig;
}
function deferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((res) => {
@ -141,6 +153,31 @@ describe("connectionStore completion assistant", () => {
expect(tables).toEqual([{ name: "users", schema: "public", type: "table" }]);
});
it("preserves TDengine stable type in completion metadata", async () => {
const listTables = vi.fn().mockResolvedValue([
{ name: "test_tb", table_type: "STABLE", comment: null },
{ name: "ordinary_table", table_type: "TABLE", comment: null },
]);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
listTables,
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.connections = [tdengineConnection()];
store.connectedIds.add("tdengine-1");
const tables = await store.listCompletionTables("tdengine-1", "issue_5685");
expect(tables).toEqual([
{ name: "test_tb", type: "table", tableType: "STABLE" },
{ name: "ordinary_table", type: "table" },
]);
});
it("deduplicates in-flight assistant table requests", async () => {
const completionAssistantSearch = vi.fn().mockResolvedValue({
candidates: [{ name: "accounts", kind: "table", schema: "public" }],

View File

@ -125,6 +125,7 @@ import { RABBITMQ_MQ_TENANT, resolveMqSystemKindFromConnection } from "@/lib/mq/
import { applySidebarDatabaseStorage, applySidebarTableStorage, sidebarDatabaseNames, supportsSidebarDatabaseStorage, supportsSidebarTableStorage, type SidebarTableStorageScope } from "@/lib/sidebar/sidebarDatabaseStorage";
import { connectionHasConfiguredSidebarVisibleFilter, nacosVisibleNamespaceSummary, sidebarVisibleFilterSummary } from "@/lib/sidebar/sidebarVisibleFilterSummary";
import { connectionCanConfigureSidebarVisibleDatabases } from "@/lib/sidebar/sidebarVisibleFilterMenu";
import { isTdengineStableTableType } from "@/lib/table/tableEditing";
const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes";
const ACTIVE_CONNECTION_STORAGE_KEY = "dbx-active-connection";
@ -1602,9 +1603,15 @@ export const useConnectionStore = defineStore("connection", () => {
name: table.name,
schema,
type: sqlObjectNavigationTypeFromTableType(table.table_type),
...completionStableTableType(table.table_type),
}));
}
function completionStableTableType(tableType: string | null | undefined): Partial<Pick<SqlCompletionTable, "tableType">> {
if (!tableType || !isTdengineStableTableType(tableType)) return {};
return { tableType: tableType.trim() };
}
function sameSidebarObjectName(left: string | undefined, right: string | undefined): boolean {
return (left || "").toLowerCase() === (right || "").toLowerCase();
}
@ -5542,6 +5549,7 @@ export const useConnectionStore = defineStore("connection", () => {
name: candidate.name,
schema: candidate.schema ?? undefined,
type: sqlObjectNavigationTypeFromTableType(candidate.data_type || candidate.kind),
...completionStableTableType(candidate.data_type),
};
if (!withOracleMetadata) return table;
return {
@ -6022,6 +6030,7 @@ export const useConnectionStore = defineStore("connection", () => {
catalog,
schema,
type: sqlObjectNavigationTypeFromTableType(table.table_type),
...completionStableTableType(table.table_type),
}));
} else {
results = lookupLocalCompletionTables(connectionId, database, normalizedFilter, limit, undefined, catalog);
@ -6042,6 +6051,7 @@ export const useConnectionStore = defineStore("connection", () => {
catalog,
schema,
type: sqlObjectNavigationTypeFromTableType(table.table_type),
...completionStableTableType(table.table_type),
}));
} catch {
results = [];
@ -6064,6 +6074,7 @@ export const useConnectionStore = defineStore("connection", () => {
catalog,
schema,
type: sqlObjectNavigationTypeFromTableType(table.table_type),
...completionStableTableType(table.table_type),
}));
} else {
completionTablesCache.value[cacheKey] = lookupLocalCompletionTables(connectionId, database, normalizedFilter, limit, undefined, catalog);
@ -6082,6 +6093,7 @@ export const useConnectionStore = defineStore("connection", () => {
name: table.name,
catalog,
type: sqlObjectNavigationTypeFromTableType(table.table_type),
...completionStableTableType(table.table_type),
}));
completionTablesCache.value[cacheKey] = limit ? completionTablesCache.value[cacheKey].slice(0, limit) : completionTablesCache.value[cacheKey];
indexCompletionTables(connectionId, database, schema, completionTablesCache.value[cacheKey], catalog);

View File

@ -119,6 +119,81 @@ test("flags missing columns when loaded column metadata is empty", () => {
);
});
test.each(["STABLE", "SUPER TABLE", "SUPERTABLE"])("recognizes TDengine tbname for %s metadata", (tableType) => {
const analysis: SqlReferenceAnalysis = {
tables: [{ name: "test_tb", span: span(53, 59) }],
columns: [{ name: "tbname", span: span(8, 13) }],
};
const diagnostics = buildSqlSemanticDiagnostics(analysis, {
tables: [{ name: "test_tb", type: "table", tableType }],
columnsByTable: new Map([
[
"test_tb",
[
{ name: "ts", table: "test_tb" },
{ name: "reading", table: "test_tb" },
{ name: "device_id", table: "test_tb" },
],
],
]),
databaseType: "tdengine",
});
assert.deepEqual(diagnostics, []);
});
test("still flags tbname for ordinary TDengine tables", () => {
const analysis: SqlReferenceAnalysis = {
tables: [{ name: "ordinary_table", span: span(20, 33) }],
columns: [{ name: "tbname", span: span(8, 13) }],
};
const diagnostics = buildSqlSemanticDiagnostics(analysis, {
tables: [{ name: "ordinary_table", type: "table", tableType: "TABLE" }],
columnsByTable: new Map([["ordinary_table", [{ name: "ts", table: "ordinary_table" }]]]),
databaseType: "tdengine",
});
assert.deepEqual(
diagnostics.map((diagnostic) => diagnostic.message),
["Unknown column tbname"],
);
});
test("recognizes qualified TDengine stable tbname references", () => {
const analysis: SqlReferenceAnalysis = {
tables: [{ name: "test_tb", span: span(27, 33) }],
columns: [{ name: "tbname", qualifier: "test_tb", span: span(8, 21) }],
};
const diagnostics = buildSqlSemanticDiagnostics(analysis, {
tables: [{ name: "test_tb", type: "table", tableType: "STABLE" }],
columnsByTable: new Map([["test_tb", [{ name: "ts", table: "test_tb" }]]]),
databaseType: "tdengine",
});
assert.deepEqual(diagnostics, []);
});
test("does not treat tbname as virtual outside TDengine", () => {
const analysis: SqlReferenceAnalysis = {
tables: [{ name: "test_tb", span: span(20, 26) }],
columns: [{ name: "tbname", span: span(8, 13) }],
};
const diagnostics = buildSqlSemanticDiagnostics(analysis, {
tables: [{ name: "test_tb", type: "table", tableType: "STABLE" }],
columnsByTable: new Map([["test_tb", [{ name: "ts", table: "test_tb" }]]]),
databaseType: "postgres",
});
assert.deepEqual(
diagnostics.map((diagnostic) => diagnostic.message),
["Unknown column tbname"],
);
});
test("flags where-clause columns missing from a single referenced table", () => {
const analysis: SqlReferenceAnalysis = {
tables: [{ name: "t_0001", span: span(15, 22) }],