fix(sqlserver): avoid false unknown column diagnostics

This commit is contained in:
t8y2 2026-05-27 23:07:47 +08:00
parent 9b702b86bf
commit ac4313afee
5 changed files with 132 additions and 5 deletions

View File

@ -393,6 +393,7 @@ async function ensureColumnsForTable(table: { name: string; schema?: string | nu
table.name,
table.schema ?? undefined,
);
if (columns.length === 0) return;
cachedColumnsByTable.set(cacheKey, columns);
}
@ -754,10 +755,12 @@ async function provideSqlCompletions(
completionContext.insertSchema,
);
if (epoch !== completionEpoch) return null;
const insertKey = completionContext.insertSchema
? `${completionContext.insertSchema}.${completionContext.insertTable}`
: completionContext.insertTable;
insertColumnsByTable.set(insertKey, insertCols);
if (insertCols.length > 0) {
const insertKey = completionContext.insertSchema
? `${completionContext.insertSchema}.${completionContext.insertTable}`
: completionContext.insertTable;
insertColumnsByTable.set(insertKey, insertCols);
}
} catch {
// ignore
}
@ -865,6 +868,7 @@ async function provideSqlCompletions(
refTable.schema,
);
if (epoch !== completionEpoch) return;
if (columns.length === 0) return;
cachedColumnsByTable.set(cacheKey, columns);
} catch (e) {
console.error(`[DBX] Failed to load columns for ${cacheKey}:`, e);

View File

@ -119,7 +119,9 @@ function columnsForTable(
const keys = table.schema ? [`${table.schema}.${table.name}`, table.name] : [table.name];
for (const key of keys) {
const columns = columnsByTable.get(key) ?? columnsByTable.get(normalizeName(key));
if (columns) return columns;
// Empty metadata usually means the upstream schema lookup was inconclusive,
// so avoid surfacing a false "unknown column" warning.
if (columns && columns.length > 0) return columns;
}
return null;
}

View File

@ -1419,6 +1419,9 @@ export const useConnectionStore = defineStore("connection", () => {
table: string,
schema?: string,
): Promise<SqlCompletionColumn[]> {
if (isSchemaAwareDatabase(connectionId) && !schema) {
return [];
}
const cacheKey = `${connectionId}:${database}:${schema || ""}:${table}`;
if (!completionColumnsCache.value[cacheKey]) {
await ensureConnected(connectionId);

View File

@ -21,3 +21,17 @@ fn extracts_unqualified_columns_from_single_table_select() {
analysis.columns.iter().map(|column| (column.qualifier.as_deref(), column.name.as_str())).collect();
assert_eq!(columns, vec![(None, "missing"), (None, "id")]);
}
#[test]
fn extracts_unqualified_order_by_columns_for_sqlserver_queries() {
let analysis =
analyze_sql_references("SELECT * FROM Evt_GCM_Qop_Info ORDER BY PDReceiveDatePartInfo DESC", Some("sqlserver"))
.unwrap();
assert_eq!(analysis.tables.len(), 1);
assert_eq!(analysis.tables[0].name, "Evt_GCM_Qop_Info");
let columns: Vec<_> =
analysis.columns.iter().map(|column| (column.qualifier.as_deref(), column.name.as_str())).collect();
assert_eq!(columns, vec![(None, "PDReceiveDatePartInfo")]);
}

View File

@ -0,0 +1,104 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createPinia, setActivePinia } from "pinia";
import { useConnectionStore } from "../../apps/desktop/src/stores/connectionStore.ts";
import { buildSqlSemanticDiagnostics } from "../../apps/desktop/src/lib/sqlSemanticDiagnostics.ts";
import type { ConnectionConfig, SqlReferenceAnalysis } from "../../apps/desktop/src/types/database.ts";
const span = (startColumn: number, endColumn: number) => ({
start_line: 1,
start_column: startColumn,
end_line: 1,
end_column: endColumn,
});
function installMemoryStorage(initial: Record<string, string> = {}) {
const values = new Map(Object.entries(initial));
const original = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
value: {
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => values.set(key, value),
removeItem: (key: string) => values.delete(key),
clear: () => values.clear(),
},
});
return {
restore() {
if (original) Object.defineProperty(globalThis, "localStorage", original);
else Reflect.deleteProperty(globalThis, "localStorage");
},
};
}
function sqlServerConn(): ConnectionConfig {
return {
id: "sqlserver-1",
name: "SQL Server",
db_type: "sqlserver",
host: "localhost",
port: 1433,
username: "sa",
password: "",
database: "appdb",
};
}
test("semantic diagnostics skip warnings when column metadata is inconclusive", () => {
const analysis: SqlReferenceAnalysis = {
tables: [{ name: "Evt_GCM_Qop_Info", span: span(15, 30) }],
columns: [{ name: "PDReceiveDatePartInfo", span: span(40, 60) }],
};
const diagnostics = buildSqlSemanticDiagnostics(analysis, {
tables: [{ name: "Evt_GCM_Qop_Info", type: "table" }],
columnsByTable: new Map([["Evt_GCM_Qop_Info", []]]),
});
assert.deepEqual(diagnostics, []);
});
test("sqlserver completion columns do not query using database name as schema fallback", async () => {
const storage = installMemoryStorage();
const originalFetch = globalThis.fetch;
const requests: string[] = [];
globalThis.fetch = (async (input) => {
const url = String(input);
requests.push(url);
if (url === "/api/connection/list") {
return new Response(JSON.stringify([sqlServerConn()]), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
if (url === "/api/layout/sidebar") {
return new Response("null", { status: 200, headers: { "Content-Type": "application/json" } });
}
if (url === "/api/connection/connect") {
return new Response(JSON.stringify("ok"), { status: 200, headers: { "Content-Type": "application/json" } });
}
if (url.startsWith("/api/schema/columns?")) {
throw new Error(`unexpected columns lookup: ${url}`);
}
return new Response("null", { status: 200, headers: { "Content-Type": "application/json" } });
}) as typeof fetch;
try {
setActivePinia(createPinia());
const store = useConnectionStore();
await store.initFromDisk();
const columns = await store.listCompletionColumns("sqlserver-1", "appdb", "Evt_GCM_Qop_Info");
assert.deepEqual(columns, []);
assert.equal(
requests.some((url) => url.startsWith("/api/schema/columns?")),
false,
);
} finally {
globalThis.fetch = originalFetch;
storage.restore();
}
});