feat(xugu): add object dependency inspection

This commit is contained in:
Elias authored and GitHub committed 2026-09-07 14:06:55 +08:00
1 parent dc7b905220
commit c0c97ef0e4
13 files changed
+252

No files matched your search

@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import { canViewDatabaseObjectDependencies, databaseDependencyProviderFor } from "@/lib/database/databaseObjectDependencies";
const node = (overrides: Record<string, unknown> = {}) => ({
type: "table" as const,
schema: "APP",
objectName: "ORDERS",
label: "ORDERS",
...overrides,
});
describe("database dependency providers", () => {
it("resolves only providers registered for the requested database", () => {
expect(databaseDependencyProviderFor("xugu")?.databaseType).toBe("xugu");
expect(databaseDependencyProviderFor("postgres")).toBeNull();
expect(databaseDependencyProviderFor(undefined)).toBeNull();
});
it("keeps the UI capability check database-agnostic", () => {
expect(canViewDatabaseObjectDependencies("xugu", node())).toBe(true);
expect(canViewDatabaseObjectDependencies("postgres", node())).toBe(false);
});
it("does not expose package members or incomplete nodes", () => {
const provider = databaseDependencyProviderFor("xugu");
expect(provider).not.toBeNull();
expect(provider!.supports(node({ type: "procedure", parentType: "package", parentName: "PKG" }))).toBe(false);
expect(provider!.supports(node({ type: "package-body" }))).toBe(false);
expect(provider!.buildQuery(node({ schema: undefined }))).toBeNull();
expect(provider!.buildQuery(node({ type: "sequence" }))).toBeNull();
});
});
@@ -0,0 +1,47 @@
import { describe, expect, it } from "vitest";
import { xuguDependencyObjectTypeForTreeNode, xuguObjectDependenciesSql } from "@/lib/database/xuguObjectDependencies";
describe("xuguObjectDependenciesSql", () => {
it("reads both directions from the accessible dependency dictionary", () => {
const sql = xuguObjectDependenciesSql({ schema: "APP", objectName: "ORDERS", objectType: "table" });
expect(sql).toContain("JOIN ALL_DEPENDS");
expect(sql).toContain("WHERE o.DB_ID = CURRENT_DB_ID");
expect(sql).toContain("'DEPENDS_ON'");
expect(sql).toContain("'REFERENCED_BY'");
expect(sql).toContain("AND o.OBJ_TYPE = 5");
expect(sql).toContain("d.OWNER_ID2 = t.SCHEMA_ID AND d.OBJ_ID2 = t.OBJ_ID AND d.OBJ_TYPE2 = t.OBJ_TYPE");
expect(sql).toContain("SELECT 'DEPENDS_ON' AS DIRECTION, d.OWNER_ID1 AS OWNER_ID, d.OBJ_ID1 AS OBJ_ID, d.OBJ_TYPE1 AS OBJ_TYPE");
expect(sql).toContain("d.OWNER_ID1 = t.SCHEMA_ID AND d.OBJ_ID1 = t.OBJ_ID AND d.OBJ_TYPE1 = t.OBJ_TYPE");
expect(sql).toContain("SELECT 'REFERENCED_BY' AS DIRECTION, d.OWNER_ID2 AS OWNER_ID, d.OBJ_ID2 AS OBJ_ID, d.OBJ_TYPE2 AS OBJ_TYPE");
expect(sql).toContain("o.SCHEMA_ID = r.OWNER_ID AND o.OBJ_ID = r.OBJ_ID AND o.OBJ_TYPE = r.OBJ_TYPE");
expect(sql).not.toContain("t.USER_ID");
expect(sql).not.toContain("o.USER_ID = r.OWNER_ID");
expect(sql).toContain("ORDER BY r.DIRECTION, s.SCHEMA_NAME, o.OBJ_NAME");
});
it("uses the Xugu routine object type and escapes dictionary lookup literals", () => {
const sql = xuguObjectDependenciesSql({ schema: "O'REILLY", objectName: "RUN'JOB", objectType: "function" });
expect(sql).toContain("UPPER('O''REILLY')");
expect(sql).toContain("UPPER('RUN''JOB')");
expect(sql).toContain("AND o.OBJ_TYPE = 7");
});
});
describe("xuguDependencyObjectTypeForTreeNode", () => {
it.each([
["table", "table"],
["view", "view"],
["procedure", "procedure"],
["function", "function"],
["trigger", "trigger"],
["package", "package"],
] as const)("maps %s to a supported dependency object", (nodeType, expected) => {
expect(xuguDependencyObjectTypeForTreeNode(nodeType)).toBe(expected);
});
it.each(["materialized_view", "package-body", "type", "sequence", "synonym"] as const)("does not expose unsupported %s objects", (nodeType) => {
expect(xuguDependencyObjectTypeForTreeNode(nodeType)).toBeNull();
});
});
@@ -0,0 +1,33 @@
import type { DatabaseType, TreeNode } from "@/types/database";
import { xuguDependencyProvider } from "@/lib/database/xuguObjectDependencies";
/**
* The subset of a sidebar node needed by a database dependency provider.
* Keeping this contract small lets providers remain independent of Vue state
* and makes it possible to test them without constructing a full tree node.
*/
export type DependencyTreeNode = Pick<TreeNode, "type" | "schema" | "objectName" | "label" | "parentType" | "parentName">;
/**
* Database-specific dependency inspection contract.
*
* Providers own catalog SQL and object-type mappings. The sidebar only asks
* whether a node is supported and executes the returned read-only query.
*/
export interface DatabaseDependencyProvider {
readonly databaseType: DatabaseType;
supports(node: DependencyTreeNode): boolean;
buildQuery(node: DependencyTreeNode): string | null;
}
const PROVIDERS: Partial<Record<DatabaseType, DatabaseDependencyProvider>> = {
xugu: xuguDependencyProvider,
};
export function databaseDependencyProviderFor(databaseType?: DatabaseType): DatabaseDependencyProvider | null {
return databaseType ? (PROVIDERS[databaseType] ?? null) : null;
}
export function canViewDatabaseObjectDependencies(databaseType: DatabaseType | undefined, node: DependencyTreeNode): boolean {
return databaseDependencyProviderFor(databaseType)?.supports(node) ?? false;
}
@@ -0,0 +1,100 @@
import type { TreeNodeType } from "@/types/database";
import type { DatabaseDependencyProvider, DependencyTreeNode } from "@/lib/database/databaseObjectDependencies";
export type XuguDependencyObjectType = "table" | "view" | "procedure" | "function" | "trigger" | "package";
const OBJECT_TYPE_CODES: Record<XuguDependencyObjectType, number> = {
table: 5,
procedure: 7,
function: 7,
view: 9,
trigger: 11,
package: 18,
};
/**
* Returns the XuguDB dictionary object kind supported by the dependency
* inspector. Package members and package bodies intentionally return null:
* ALL_DEPENDS records dependencies for their owning schema object instead.
*/
export function xuguDependencyObjectTypeForTreeNode(type: TreeNodeType): XuguDependencyObjectType | null {
switch (type) {
case "table":
case "view":
case "procedure":
case "function":
case "trigger":
case "package":
return type;
default:
return null;
}
}
/**
* Builds a read-only query over XuguDB's accessible dependency dictionary.
*
* ALL_* views deliberately preserve the server's permission boundary: a
* regular user sees only dependencies of objects it is allowed to inspect,
* while administrative connections can see the broader catalog. Both
* directions are returned so callers can assess an object's prerequisites and
* the impact of changing it.
*/
export function xuguObjectDependenciesSql(options: { schema: string; objectName: string; objectType: XuguDependencyObjectType }): string {
const schema = quoteXuguString(options.schema);
const objectName = quoteXuguString(options.objectName);
const objectType = OBJECT_TYPE_CODES[options.objectType];
return `WITH target AS (
SELECT o.DB_ID, o.SCHEMA_ID, o.OBJ_ID, o.OBJ_TYPE
FROM ALL_OBJECTS o
JOIN ALL_SCHEMAS s ON s.DB_ID = o.DB_ID AND s.SCHEMA_ID = o.SCHEMA_ID
WHERE o.DB_ID = CURRENT_DB_ID
AND UPPER(s.SCHEMA_NAME) = UPPER(${schema})
AND UPPER(o.OBJ_NAME) = UPPER(${objectName})
AND o.OBJ_TYPE = ${objectType}
), dependency_rows AS (
SELECT 'DEPENDS_ON' AS DIRECTION, d.OWNER_ID1 AS OWNER_ID, d.OBJ_ID1 AS OBJ_ID, d.OBJ_TYPE1 AS OBJ_TYPE, d.DB_ID
FROM target t
JOIN ALL_DEPENDS d ON d.DB_ID = t.DB_ID AND d.OWNER_ID2 = t.SCHEMA_ID AND d.OBJ_ID2 = t.OBJ_ID AND d.OBJ_TYPE2 = t.OBJ_TYPE
UNION ALL
SELECT 'REFERENCED_BY' AS DIRECTION, d.OWNER_ID2 AS OWNER_ID, d.OBJ_ID2 AS OBJ_ID, d.OBJ_TYPE2 AS OBJ_TYPE, d.DB_ID
FROM target t
JOIN ALL_DEPENDS d ON d.DB_ID = t.DB_ID AND d.OWNER_ID1 = t.SCHEMA_ID AND d.OBJ_ID1 = t.OBJ_ID AND d.OBJ_TYPE1 = t.OBJ_TYPE
)
SELECT r.DIRECTION,
s.SCHEMA_NAME,
o.OBJ_NAME AS OBJECT_NAME,
CASE o.OBJ_TYPE
WHEN 5 THEN 'TABLE'
WHEN 7 THEN 'PROCEDURE_OR_FUNCTION'
WHEN 9 THEN 'VIEW'
WHEN 11 THEN 'TRIGGER'
WHEN 18 THEN 'PACKAGE'
ELSE 'OBJECT_' || o.OBJ_TYPE
END AS OBJECT_TYPE
FROM dependency_rows r
JOIN ALL_OBJECTS o ON o.DB_ID = r.DB_ID AND o.SCHEMA_ID = r.OWNER_ID AND o.OBJ_ID = r.OBJ_ID AND o.OBJ_TYPE = r.OBJ_TYPE
JOIN ALL_SCHEMAS s ON s.DB_ID = o.DB_ID AND s.SCHEMA_ID = o.SCHEMA_ID
ORDER BY r.DIRECTION, s.SCHEMA_NAME, o.OBJ_NAME;`;
}
/** XuguDB implementation of the shared dependency-inspection contract. */
export const xuguDependencyProvider: DatabaseDependencyProvider = {
databaseType: "xugu",
supports(node: DependencyTreeNode): boolean {
// Package members and package bodies do not have independent ALL_DEPENDS
// records in XuguDB; exposing the action for them would be misleading.
if (node.parentType === "package" || node.type === "package-body") return false;
return xuguDependencyObjectTypeForTreeNode(node.type) !== null;
},
buildQuery(node: DependencyTreeNode): string | null {
const objectType = xuguDependencyObjectTypeForTreeNode(node.type);
const objectName = node.objectName || node.label;
if (!objectType || !node.schema || !objectName) return null;
return xuguObjectDependenciesSql({ schema: node.schema, objectName, objectType });
},
};
function quoteXuguString(value: string): string {
return `'${value.replaceAll("'", "''")}'`;
}