4 files changed
+111
-4
No files matched your search
@@ -70,7 +70,7 @@ import type { ColumnInfo, ConnectionConfig, ConstraintInfo, ForeignKeyInfo, Inde
|
||||
import { sortTablesByFkDependency, type TableWithFk } from "@/lib/table/tableDependencySort";
|
||||
import { isSchemaAware, supportsTableVacuum, supportsTransfer } from "@/lib/database/databaseCapabilities";
|
||||
import { supportsSchemaDiagram, supportsTableImport, supportsTableStructureEditing, supportsTableTruncate } from "@/lib/database/databaseFeatureSupport";
|
||||
import { codeMirrorSqlDialect, connectionObjectTreeNodeSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { codeMirrorSqlDialect, connectionObjectTreeNodeSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, objectListSchemaForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { getTableMetadataCapabilities, type TableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities";
|
||||
import { constraintsForConstraintsTab } from "@/lib/table/constraintPresentation";
|
||||
import { buildTableSelectSql } from "@/lib/table/tableSelectSql";
|
||||
@@ -2817,6 +2817,10 @@ watch([() => props.initialEventName, () => props.initialEventOpenRequestId, () =
|
||||
openInitialEventIfNeeded();
|
||||
});
|
||||
|
||||
// 达梦的对象列表 SQL 固定 `WHERE o.OWNER = ?`(DamengAgent),空 schema 必然
|
||||
// 匹配 0 行;schema 解析失败(loadSchemas 抛错或返回空列表)时标签会整体空白
|
||||
// (#8301)。经 objectListSchemaForConnection 回退到连接用户名(大写),仅限
|
||||
// 达梦;oracle/oceanbase-oracle 维持空 schema 由后端解析当前 schema。
|
||||
async function loadObjects(options?: { allowCached?: boolean; preserveExistingRows?: boolean }) {
|
||||
error.value = "";
|
||||
// A new load supersedes any in-flight one, so reset the transient refresh flags
|
||||
@@ -2830,7 +2834,7 @@ async function loadObjects(options?: { allowCached?: boolean; preserveExistingRo
|
||||
// same-instance refresh) — a failure then keeps the rows and raises a non-blocking
|
||||
// banner instead of replacing the whole list with a full-area error.
|
||||
let scaffoldRefresh = false;
|
||||
const schema = needsSchema.value ? selectedSchema.value || "" : props.database;
|
||||
const schema = needsSchema.value ? objectListSchemaForConnection(props.connection, selectedSchema.value) : props.database;
|
||||
const request = objectBrowserRowsLoadGuard.start(objectBrowserRowsCacheScope(schema));
|
||||
const cacheWriteToken = createObjectBrowserRowsCacheWriteToken(request.scope);
|
||||
|
||||
@@ -2943,7 +2947,15 @@ function normalizeStatisticNumber(value: number | null | undefined): number | nu
|
||||
|
||||
async function reload(options?: { allowCachedObjects?: boolean; contextEpoch?: number; preserveExistingRows?: boolean }) {
|
||||
const epoch = options?.contextEpoch ?? objectBrowserRowsLoadGuard.invalidate();
|
||||
if (!(await loadSchemas(epoch))) return;
|
||||
try {
|
||||
if (!(await loadSchemas(epoch))) return;
|
||||
} catch (e) {
|
||||
// listSchemas 失败(如共享连接上 SET SCHEMA 后的二次元数据请求)时,
|
||||
// loadSchemas 不会触碰 selectedSchema,这里也不清空它:保留 props.schema
|
||||
// 或用户已选值,继续尝试加载对象列表(达梦走用户名回退),避免标签静默
|
||||
// 空白 (#8301)。对象列表若同样失败,loadObjects 自身的 catch 会展示错误。
|
||||
console.warn("[ObjectBrowser] loadSchemas failed, keeping selected schema for", props.connection.id, e);
|
||||
}
|
||||
if (!objectBrowserRowsLoadGuard.isEpochCurrent(epoch)) return;
|
||||
await loadObjects({ allowCached: options?.allowCachedObjects, preserveExistingRows: options?.preserveExistingRows });
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const objectBrowserSource = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8");
|
||||
|
||||
function functionBody(name: string, source: string): string {
|
||||
const signature = new RegExp(`(?:async\\s+)?function\\s+${name}\\s*\\([^)]*\\)\\s*(?::\\s*[^\\{]+)?\\{`, "m").exec(source);
|
||||
if (!signature) throw new Error(`Missing function ${name}`);
|
||||
const bodyStart = signature.index + signature[0].length;
|
||||
let depth = 1;
|
||||
for (let index = bodyStart; index < source.length; index += 1) {
|
||||
if (source[index] === "{") depth += 1;
|
||||
else if (source[index] === "}") depth -= 1;
|
||||
if (depth === 0) return source.slice(bodyStart, index);
|
||||
}
|
||||
throw new Error(`Unclosed function ${name}`);
|
||||
}
|
||||
|
||||
describe("ObjectBrowser object-list schema fallback (#8301)", () => {
|
||||
it("resolves the listObjects schema through the shared Dameng-aware helper", () => {
|
||||
const loadObjects = functionBody("loadObjects", objectBrowserSource);
|
||||
expect(loadObjects).toContain("needsSchema.value ? objectListSchemaForConnection(props.connection, selectedSchema.value) : props.database");
|
||||
expect(objectBrowserSource).toContain('objectListSchemaForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"');
|
||||
});
|
||||
|
||||
it("routes the schema through the request scope used by listObjects", () => {
|
||||
const loadSqlObjectBrowserRows = functionBody("loadSqlObjectBrowserRows", objectBrowserSource);
|
||||
expect(loadSqlObjectBrowserRows).toContain("api.listObjects(request.scope.connectionId, request.scope.database, request.scope.schema");
|
||||
const loadObjects = functionBody("loadObjects", objectBrowserSource);
|
||||
expect(loadObjects).toContain("objectBrowserRowsCacheScope(schema)");
|
||||
});
|
||||
|
||||
it("keeps the reload chain alive when loadSchemas rejects", () => {
|
||||
const reload = functionBody("reload", objectBrowserSource);
|
||||
expect(reload).toMatch(/try \{\s*if \(!\(await loadSchemas\(epoch\)\)\) return;\s*\} catch/);
|
||||
const catchClause = reload.slice(reload.indexOf("} catch"));
|
||||
// The failure path must not clear the selection (props.schema / current value)
|
||||
// and must still load objects — Dameng then falls back to the username.
|
||||
expect(catchClause).not.toContain("selectedSchema.value =");
|
||||
expect(catchClause).toContain("console.warn");
|
||||
expect(reload.lastIndexOf("await loadObjects")).toBeGreaterThan(reload.indexOf("} catch"));
|
||||
});
|
||||
|
||||
it("leaves the selected schema untouched when listSchemas itself rejects", () => {
|
||||
const loadSchemas = functionBody("loadSchemas", objectBrowserSource);
|
||||
// Every selectedSchema assignment in the try block sits behind the awaited
|
||||
// listSchemas call, so a rejection keeps the current selection intact.
|
||||
const tryStart = loadSchemas.indexOf("try {");
|
||||
const apiCall = loadSchemas.indexOf("await api.listSchemas", tryStart);
|
||||
const firstAssignment = loadSchemas.indexOf("selectedSchema.value =", apiCall);
|
||||
expect(apiCall).toBeGreaterThan(tryStart);
|
||||
expect(firstAssignment).toBeGreaterThan(apiCall);
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
gaussdbTargetServerType,
|
||||
inferJdbcDialect,
|
||||
metadataSchemaForConnection,
|
||||
objectListSchemaForConnection,
|
||||
setGaussdbConnectionMode,
|
||||
setGaussdbCountQueryDop,
|
||||
setGaussdbIdentifierQuoteStyle,
|
||||
@@ -488,3 +489,28 @@ describe("object tree node schema", () => {
|
||||
expect(connectionDatabaseMetadataSchema({ db_type: "spanner" }, "projects/p/instances/i/databases/db", "public")).toBe("public");
|
||||
});
|
||||
});
|
||||
|
||||
describe("object list schema", () => {
|
||||
it("falls back to the uppercased Dameng connection username when no schema is selected", () => {
|
||||
// Dameng's object SQL filters on a fixed WHERE o.OWNER = ?, so a blank schema
|
||||
// matches nothing and the object tab renders empty (#8301).
|
||||
expect(objectListSchemaForConnection({ db_type: "dameng", username: "sales_app" })).toBe("SALES_APP");
|
||||
});
|
||||
|
||||
it("applies the Dameng fallback to generic jdbc:dm connections", () => {
|
||||
expect(objectListSchemaForConnection({ db_type: "jdbc", connection_string: "jdbc:dm://localhost:5236", username: "sysdba" })).toBe("SYSDBA");
|
||||
});
|
||||
|
||||
it("keeps an explicitly selected schema for Dameng", () => {
|
||||
expect(objectListSchemaForConnection({ db_type: "dameng", username: "sales_app" }, "OTHER_SCHEMA")).toBe("OTHER_SCHEMA");
|
||||
});
|
||||
|
||||
it("returns a blank schema for Dameng when the connection has no usable username", () => {
|
||||
expect(objectListSchemaForConnection({ db_type: "dameng" })).toBe("");
|
||||
expect(objectListSchemaForConnection({ db_type: "dameng", username: " " })).toBe("");
|
||||
});
|
||||
|
||||
it.each(["oracle", "oceanbase-oracle", "postgres", "mysql"] as const)("does not fall back to the username for %s", (dbType) => {
|
||||
expect(objectListSchemaForConnection({ db_type: dbType, username: "app_user" })).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import type { ConnectionConfig, DatabaseType } from "@/types/database";
|
||||
import { isSchemaAware, spannerObjectTreeSchema, usesDatabaseObjectTreeMode, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport";
|
||||
import type { CodeMirrorSqlDialectName } from "@/lib/editor/codemirrorSqlDialect";
|
||||
|
||||
type JdbcDialectConnection = Partial<Pick<ConnectionConfig, "db_type" | "driver_profile" | "driver_label" | "connection_string" | "url_params" | "jdbc_driver_class" | "jdbc_driver_paths" | "database_info" | "external_config">>;
|
||||
type JdbcDialectConnection = Partial<Pick<ConnectionConfig, "db_type" | "driver_profile" | "driver_label" | "connection_string" | "url_params" | "jdbc_driver_class" | "jdbc_driver_paths" | "database_info" | "external_config" | "username">>;
|
||||
|
||||
export type GaussdbIdentifierQuoteStyle = "auto" | "double" | "backtick";
|
||||
export type GaussdbConnectionMode = "native" | "m-jdbc";
|
||||
@@ -264,6 +264,21 @@ export function connectionObjectTreeQuerySchema(connection: JdbcDialectConnectio
|
||||
return schema || database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema sent with object-list (listObjects) requests. Dameng's object SQL
|
||||
* filters on a fixed `WHERE o.OWNER = ?`, so a blank schema matches nothing and
|
||||
* the object tab renders empty when the schema could not be resolved (#8301).
|
||||
* Mirror the completion path (connectionStore.listCompletionColumns) and fall
|
||||
* back to the connection username — uppercased, the Oracle-family storage
|
||||
* convention for unquoted Dameng users. Other Oracle-family types keep the
|
||||
* blank schema so the backend resolves the current schema itself.
|
||||
*/
|
||||
export function objectListSchemaForConnection(connection: JdbcDialectConnection | undefined, schema?: string): string {
|
||||
if (schema) return schema;
|
||||
if (effectiveDatabaseTypeForConnection(connection) !== "dameng") return "";
|
||||
return connection?.username?.trim().toUpperCase() || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata schema for query paths that treat the database name as the schema when a
|
||||
* connection exposes no schema level (MySQL, HBase, and the other flat engines).
|
||||
|
||||
Reference in new issue
Block a user