parent
be7b9ac021
commit
ad9ca8400b
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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" }]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1160,6 +1160,7 @@ export interface SqlCompletionTable {
|
|||
database?: string;
|
||||
schema?: string;
|
||||
type?: SqlObjectNavigationType;
|
||||
tableType?: string;
|
||||
detail?: string;
|
||||
applyName?: string;
|
||||
boost?: number;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" }],
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) }],
|
||||
|
|
|
|||
Loading…
Reference in New Issue