feat(xugu): expose programmable object metadata
This commit is contained in:
parent
d3887c9936
commit
08cd8903f4
|
|
@ -201,6 +201,7 @@ type objectInfo struct {
|
|||
ObjectType string `json:"object_type"`
|
||||
Schema string `json:"schema"`
|
||||
Comment *string `json:"comment"`
|
||||
Valid *bool `json:"valid,omitempty"`
|
||||
}
|
||||
|
||||
type metadataListConstraints struct {
|
||||
|
|
@ -1258,10 +1259,15 @@ func (s *server) listObjects(schema string, constraints metadataListConstraints)
|
|||
var result []objectInfo
|
||||
for rows.Next() {
|
||||
var item objectInfo
|
||||
var valid any
|
||||
item.Schema = schema
|
||||
if err := rows.Scan(&item.Name, &item.ObjectType, &item.Comment); err != nil {
|
||||
if err := rows.Scan(&item.Name, &item.ObjectType, &item.Comment, &valid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if valid != nil {
|
||||
value := truthy(valid)
|
||||
item.Valid = &value
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
return emptyIfNil(result), rows.Err()
|
||||
|
|
@ -1311,19 +1317,58 @@ WHERE UPPER(s.SCHEMA_NAME) = UPPER(?)`,
|
|||
func xuguListObjectsQuery(schema string, constraints metadataListConstraints) xuguMetadataListQuery {
|
||||
return xuguConstrainedMetadataListQuery(
|
||||
`
|
||||
SELECT t.TABLE_NAME AS OBJECT_NAME, 'TABLE' AS OBJECT_TYPE, t.COMMENTS
|
||||
SELECT t.TABLE_NAME AS OBJECT_NAME, 'TABLE' AS OBJECT_TYPE, t.COMMENTS, NULL AS VALID
|
||||
FROM ALL_TABLES t
|
||||
JOIN ALL_SCHEMAS s ON s.DB_ID = t.DB_ID AND s.SCHEMA_ID = t.SCHEMA_ID
|
||||
WHERE UPPER(s.SCHEMA_NAME) = UPPER(?)
|
||||
UNION ALL
|
||||
SELECT v.VIEW_NAME AS OBJECT_NAME, 'VIEW' AS OBJECT_TYPE, v.COMMENTS
|
||||
SELECT v.VIEW_NAME AS OBJECT_NAME, 'VIEW' AS OBJECT_TYPE, v.COMMENTS, NULL AS VALID
|
||||
FROM ALL_VIEWS v
|
||||
JOIN ALL_SCHEMAS s ON s.DB_ID = v.DB_ID AND s.SCHEMA_ID = v.SCHEMA_ID
|
||||
WHERE UPPER(s.SCHEMA_NAME) = UPPER(?)`,
|
||||
"OBJECT_NAME, OBJECT_TYPE, COMMENTS",
|
||||
WHERE UPPER(s.SCHEMA_NAME) = UPPER(?)
|
||||
UNION ALL
|
||||
SELECT p.PROC_NAME AS OBJECT_NAME,
|
||||
CASE WHEN p.RET_TYPE IS NULL THEN 'PROCEDURE' ELSE 'FUNCTION' END AS OBJECT_TYPE,
|
||||
p.COMMENTS, p.VALID
|
||||
FROM ALL_PROCEDURES p
|
||||
JOIN ALL_SCHEMAS s ON s.DB_ID = p.DB_ID AND s.SCHEMA_ID = p.SCHEMA_ID
|
||||
WHERE UPPER(s.SCHEMA_NAME) = UPPER(?)
|
||||
UNION ALL
|
||||
SELECT p.PACK_NAME AS OBJECT_NAME, 'PACKAGE' AS OBJECT_TYPE, p.COMMENTS, p.VALID
|
||||
FROM ALL_PACKAGES p
|
||||
JOIN ALL_SCHEMAS s ON s.DB_ID = p.DB_ID AND s.SCHEMA_ID = p.SCHEMA_ID
|
||||
WHERE UPPER(s.SCHEMA_NAME) = UPPER(?)
|
||||
UNION ALL
|
||||
SELECT p.PACK_NAME AS OBJECT_NAME, 'PACKAGE_BODY' AS OBJECT_TYPE, p.COMMENTS, p.ALL_OK
|
||||
FROM ALL_PACKAGES p
|
||||
JOIN ALL_SCHEMAS s ON s.DB_ID = p.DB_ID AND s.SCHEMA_ID = p.SCHEMA_ID
|
||||
WHERE UPPER(s.SCHEMA_NAME) = UPPER(?)
|
||||
AND p.BODY IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT tr.TRIG_NAME AS OBJECT_NAME, 'TRIGGER' AS OBJECT_TYPE, tr.COMMENTS, tr.VALID
|
||||
FROM ALL_TRIGGERS tr
|
||||
JOIN ALL_SCHEMAS s ON s.DB_ID = tr.DB_ID AND s.SCHEMA_ID = tr.SCHEMA_ID
|
||||
WHERE UPPER(s.SCHEMA_NAME) = UPPER(?)
|
||||
UNION ALL
|
||||
SELECT q.SEQ_NAME AS OBJECT_NAME, 'SEQUENCE' AS OBJECT_TYPE, NULL AS COMMENTS, NULL AS VALID
|
||||
FROM ALL_SEQUENCES q
|
||||
JOIN ALL_SCHEMAS s ON s.DB_ID = q.DB_ID AND s.SCHEMA_ID = q.SCHEMA_ID
|
||||
WHERE UPPER(s.SCHEMA_NAME) = UPPER(?)
|
||||
UNION ALL
|
||||
SELECT u.TYPE_NAME AS OBJECT_NAME, 'TYPE' AS OBJECT_TYPE, u.COMMENTS, u.VALID
|
||||
FROM ALL_TYPES u
|
||||
JOIN ALL_SCHEMAS s ON s.DB_ID = u.DB_ID AND s.SCHEMA_ID = u.SCHEMA_ID
|
||||
WHERE UPPER(s.SCHEMA_NAME) = UPPER(?)
|
||||
UNION ALL
|
||||
SELECT u.TYPE_NAME AS OBJECT_NAME, 'TYPE_BODY' AS OBJECT_TYPE, u.COMMENTS, u.VALID
|
||||
FROM ALL_TYPES u
|
||||
JOIN ALL_SCHEMAS s ON s.DB_ID = u.DB_ID AND s.SCHEMA_ID = u.SCHEMA_ID
|
||||
WHERE UPPER(s.SCHEMA_NAME) = UPPER(?)
|
||||
AND u.BODY IS NOT NULL`,
|
||||
"OBJECT_NAME, OBJECT_TYPE, COMMENTS, VALID",
|
||||
"OBJECT_NAME",
|
||||
"OBJECT_TYPE",
|
||||
[]any{schema, schema},
|
||||
[]any{schema, schema, schema, schema, schema, schema, schema, schema, schema},
|
||||
constraints,
|
||||
)
|
||||
}
|
||||
|
|
@ -1388,6 +1433,8 @@ func normalizedXuguObjectTypes(values []string) []string {
|
|||
normalized = "TABLE"
|
||||
case "VIEW":
|
||||
normalized = "VIEW"
|
||||
case "PROCEDURE", "FUNCTION", "TRIGGER", "SEQUENCE", "PACKAGE", "PACKAGE_BODY", "TYPE", "TYPE_BODY":
|
||||
// Already normalized.
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
|
@ -1653,7 +1700,13 @@ func (s *server) getObjectSource(schema, name, objectType string) (map[string]an
|
|||
}
|
||||
builder.WriteString(line)
|
||||
}
|
||||
return map[string]any{"name": name, "object_type": objectType, "schema": schema, "source": builder.String()}, rows.Err()
|
||||
result := map[string]any{"name": name, "object_type": objectType, "schema": schema, "source": builder.String()}
|
||||
normalizedType := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(objectType), "_", " "))
|
||||
if normalizedType == "TYPE" || normalizedType == "TYPE BODY" {
|
||||
// Type source is exposed as catalog SPEC/BODY text, but cannot be safely edited as DDL.
|
||||
result["editable"] = false
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (s *server) getTableDDL(schema, table string) (string, error) {
|
||||
|
|
@ -2121,12 +2174,31 @@ SELECT TO_CHAR(p.DEFINE)
|
|||
FROM SYS_PROCEDURES p
|
||||
JOIN SYS_SCHEMAS s ON s.DB_ID = p.DB_ID AND s.SCHEMA_ID = p.SCHEMA_ID
|
||||
WHERE UPPER(s.SCHEMA_NAME) = UPPER(?) AND UPPER(p.PROC_NAME) = UPPER(?)`, []any{schema, name}, nil
|
||||
case "PACKAGE", "PACKAGE BODY":
|
||||
case "PACKAGE":
|
||||
return `
|
||||
SELECT COALESCE(TO_CHAR(k.SPEC), '') || COALESCE(TO_CHAR(k.BODY), '')
|
||||
SELECT COALESCE(TO_CHAR(k.SPEC), '')
|
||||
FROM SYS_PACKAGES k
|
||||
JOIN SYS_SCHEMAS s ON s.DB_ID = k.DB_ID AND s.SCHEMA_ID = k.SCHEMA_ID
|
||||
WHERE UPPER(s.SCHEMA_NAME) = UPPER(?) AND UPPER(k.PACK_NAME) = UPPER(?)`, []any{schema, name}, nil
|
||||
case "PACKAGE BODY", "PACKAGE_BODY":
|
||||
return `
|
||||
SELECT COALESCE(TO_CHAR(k.BODY), '')
|
||||
FROM SYS_PACKAGES k
|
||||
JOIN SYS_SCHEMAS s ON s.DB_ID = k.DB_ID AND s.SCHEMA_ID = k.SCHEMA_ID
|
||||
WHERE UPPER(s.SCHEMA_NAME) = UPPER(?) AND UPPER(k.PACK_NAME) = UPPER(?)`, []any{schema, name}, nil
|
||||
case "TYPE":
|
||||
return `
|
||||
SELECT COALESCE(TO_CHAR(u.SPEC), '')
|
||||
FROM ALL_TYPES u
|
||||
JOIN ALL_SCHEMAS s ON s.DB_ID = u.DB_ID AND s.SCHEMA_ID = u.SCHEMA_ID
|
||||
WHERE UPPER(s.SCHEMA_NAME) = UPPER(?) AND UPPER(u.TYPE_NAME) = UPPER(?)`, []any{schema, name}, nil
|
||||
case "TYPE BODY", "TYPE_BODY":
|
||||
return `
|
||||
SELECT COALESCE(TO_CHAR(u.BODY), '')
|
||||
FROM ALL_TYPES u
|
||||
JOIN ALL_SCHEMAS s ON s.DB_ID = u.DB_ID AND s.SCHEMA_ID = u.SCHEMA_ID
|
||||
WHERE UPPER(s.SCHEMA_NAME) = UPPER(?) AND UPPER(u.TYPE_NAME) = UPPER(?)
|
||||
AND u.BODY IS NOT NULL`, []any{schema, name}, nil
|
||||
default:
|
||||
return "", nil, fmt.Errorf("object source is not supported for %s", objectType)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -537,10 +537,61 @@ func TestXuguListObjectsQueryRejectsUnsupportedObjectTypes(t *testing.T) {
|
|||
t.Fatalf("unsupported object type should produce empty-result predicate:\n%s", query.SQL)
|
||||
}
|
||||
|
||||
wantArgs := []any{"APP", "APP", 10, 0}
|
||||
wantArgs := []any{"APP", "APP", "APP", "APP", "APP", "APP", "APP", "APP", "APP", 10, 0}
|
||||
assertArgs(t, query.Args, wantArgs)
|
||||
}
|
||||
|
||||
func TestXuguListObjectsQueryIncludesProgrammableObjects(t *testing.T) {
|
||||
query := xuguListObjectsQuery("APP", metadataListConstraints{
|
||||
ObjectTypes: []string{"procedure", "function", "package", "package-body", "trigger", "sequence", "type", "type-body"},
|
||||
})
|
||||
|
||||
for _, want := range []string{"ALL_PROCEDURES", "p.VALID", "ALL_PACKAGES", "p.BODY IS NOT NULL", "ALL_TRIGGERS", "ALL_SEQUENCES", "ALL_TYPES", "u.BODY IS NOT NULL", "OBJECT_NAME, OBJECT_TYPE, COMMENTS, VALID", "OBJECT_TYPE IN (?,?,?,?,?,?,?,?)"} {
|
||||
if !strings.Contains(query.SQL, want) {
|
||||
t.Fatalf("expected SQL to contain %q:\n%s", want, query.SQL)
|
||||
}
|
||||
}
|
||||
|
||||
wantArgs := []any{"APP", "APP", "APP", "APP", "APP", "APP", "APP", "APP", "APP", "FUNCTION", "PACKAGE", "PACKAGE_BODY", "PROCEDURE", "SEQUENCE", "TRIGGER", "TYPE", "TYPE_BODY"}
|
||||
assertArgs(t, query.Args, wantArgs)
|
||||
}
|
||||
|
||||
func TestXuguObjectSourceQuerySupportsSharedObjectKinds(t *testing.T) {
|
||||
for _, objectType := range []string{"TRIGGER", "PACKAGE_BODY", "TYPE", "TYPE_BODY"} {
|
||||
query, _, err := objectSourceQuery("APP", "demo", objectType)
|
||||
if err != nil {
|
||||
t.Fatalf("%s should support object source lookup: %v", objectType, err)
|
||||
}
|
||||
if strings.TrimSpace(query) == "" {
|
||||
t.Fatalf("%s should produce source SQL", objectType)
|
||||
}
|
||||
}
|
||||
|
||||
packageBodyQuery, _, err := objectSourceQuery("APP", "demo", "PACKAGE_BODY")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(packageBodyQuery, "TO_CHAR(k.BODY)") || strings.Contains(packageBodyQuery, "k.SPEC") {
|
||||
t.Fatalf("package body query must request only the body: %s", packageBodyQuery)
|
||||
}
|
||||
|
||||
typeSpecQuery, _, err := objectSourceQuery("APP", "demo", "TYPE")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(typeSpecQuery, "ALL_TYPES") || !strings.Contains(typeSpecQuery, "TO_CHAR(u.SPEC)") {
|
||||
t.Fatalf("type query must return catalog SPEC content: %s", typeSpecQuery)
|
||||
}
|
||||
|
||||
typeBodyQuery, _, err := objectSourceQuery("APP", "demo", "TYPE_BODY")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(typeBodyQuery, "ALL_TYPES") || !strings.Contains(typeBodyQuery, "TO_CHAR(u.BODY)") || !strings.Contains(typeBodyQuery, "u.BODY IS NOT NULL") {
|
||||
t.Fatalf("type body query must return catalog BODY content: %s", typeBodyQuery)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetadataListConstraintsFromParams(t *testing.T) {
|
||||
params := map[string]json.RawMessage{
|
||||
"filter": json.RawMessage(`"tab"`),
|
||||
|
|
@ -648,7 +699,6 @@ func contains(values []string, target string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
|
||||
// -- fake drivers for timeout tests --
|
||||
|
||||
func init() {
|
||||
|
|
@ -675,13 +725,13 @@ type xuguBlockingConn struct{}
|
|||
func (c *xuguBlockingConn) Prepare(query string) (driver.Stmt, error) {
|
||||
return &xuguBlockingStmt{}, nil
|
||||
}
|
||||
func (c *xuguBlockingConn) Close() error { return nil }
|
||||
func (c *xuguBlockingConn) Close() error { return nil }
|
||||
func (c *xuguBlockingConn) Begin() (driver.Tx, error) { return nil, errors.New("not supported") }
|
||||
|
||||
type xuguBlockingStmt struct{}
|
||||
|
||||
func (s *xuguBlockingStmt) Close() error { return nil }
|
||||
func (s *xuguBlockingStmt) NumInput() int { return -1 }
|
||||
func (s *xuguBlockingStmt) Close() error { return nil }
|
||||
func (s *xuguBlockingStmt) NumInput() int { return -1 }
|
||||
func (s *xuguBlockingStmt) Exec(args []driver.Value) (driver.Result, error) {
|
||||
<-xuguBlockingUnblock
|
||||
return nil, errors.New("killed")
|
||||
|
|
@ -702,13 +752,13 @@ type xuguFastConn struct{}
|
|||
func (c *xuguFastConn) Prepare(query string) (driver.Stmt, error) {
|
||||
return &xuguFastStmt{}, nil
|
||||
}
|
||||
func (c *xuguFastConn) Close() error { return nil }
|
||||
func (c *xuguFastConn) Close() error { return nil }
|
||||
func (c *xuguFastConn) Begin() (driver.Tx, error) { return nil, errors.New("not supported") }
|
||||
|
||||
type xuguFastStmt struct{}
|
||||
|
||||
func (s *xuguFastStmt) Close() error { return nil }
|
||||
func (s *xuguFastStmt) NumInput() int { return -1 }
|
||||
func (s *xuguFastStmt) Close() error { return nil }
|
||||
func (s *xuguFastStmt) NumInput() int { return -1 }
|
||||
func (s *xuguFastStmt) Exec(args []driver.Value) (driver.Result, error) {
|
||||
return driver.ResultNoRows, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1609,14 +1609,17 @@ async function handleQuickOpenSelect(item: any) {
|
|||
tableName: item.objectName || item.tableName,
|
||||
tableType: item.type === "view" ? "VIEW" : item.type === "materialized_view" ? "MATERIALIZED_VIEW" : "TABLE",
|
||||
});
|
||||
} else if (item.type === "procedure" || item.type === "function" || item.type === "sequence" || item.type === "package" || item.type === "package-body") {
|
||||
} else if (item.type === "procedure" || item.type === "function" || item.type === "trigger" || item.type === "sequence" || item.type === "package" || item.type === "package-body" || item.type === "type" || item.type === "type-body") {
|
||||
// Open the object source in a source tab
|
||||
const objectTypeMap: Record<string, ObjectSourceKind> = {
|
||||
procedure: "PROCEDURE",
|
||||
function: "FUNCTION",
|
||||
trigger: "TRIGGER",
|
||||
sequence: "SEQUENCE",
|
||||
package: "PACKAGE",
|
||||
"package-body": "PACKAGE_BODY",
|
||||
type: "TYPE",
|
||||
"type-body": "TYPE_BODY",
|
||||
};
|
||||
|
||||
const objectType = objectTypeMap[item.type];
|
||||
|
|
@ -1627,7 +1630,7 @@ async function handleQuickOpenSelect(item: any) {
|
|||
const result = await api.getObjectSource(item.connectionId, item.database, schema, item.objectName || item.tableName, objectType);
|
||||
const tabId = queryStore.createTab(item.connectionId, item.database, `Source - ${item.objectName || item.tableName}`);
|
||||
queryStore.updateSql(tabId, result.source);
|
||||
if (item.type !== "sequence") {
|
||||
if (item.type !== "sequence" && item.type !== "trigger" && item.type !== "type" && item.type !== "type-body") {
|
||||
queryStore.setObjectSource(tabId, {
|
||||
schema,
|
||||
name: item.objectName || item.tableName,
|
||||
|
|
|
|||
|
|
@ -93,13 +93,13 @@ import {
|
|||
type ObjectBrowserSortDirection,
|
||||
type ObjectBrowserSortKey,
|
||||
} from "@/lib/table/objectBrowserRows";
|
||||
import { resolveRowClickAction, shouldDeferSingleClick, type ObjectBrowserRowAction } from "@/lib/table/objectBrowserRowAction";
|
||||
import { isSourceOnlyObjectBrowserRow, resolveRowClickAction, shouldDeferSingleClick, type ObjectBrowserRowAction } from "@/lib/table/objectBrowserRowAction";
|
||||
import { filterObjectBrowserTableColumns } from "@/lib/table/objectBrowserTableInfo";
|
||||
import { createSidePanelRequestGuard } from "@/lib/table/sidePanelRequestGuard";
|
||||
import { runBatchTableTruncate } from "@/lib/table/batchTableTruncate";
|
||||
import { tableColumnDefaultDisplayValue } from "@/lib/table/tableColumnDefaultPresentation";
|
||||
|
||||
type ObjectFilter = "all" | "tables" | "views" | "materializedViews" | "procedures" | "functions" | "sequences" | "packages";
|
||||
type ObjectFilter = "all" | "tables" | "views" | "materializedViews" | "procedures" | "functions" | "triggers" | "sequences" | "packages" | "types";
|
||||
type ObjectBrowserColumnKey = "select" | "name" | "type" | "estimatedRows" | "totalBytes" | "created_at" | "updated_at" | "comment";
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -236,8 +236,10 @@ const viewCount = computed(() => rows.value.filter((row) => row.type === "VIEW")
|
|||
const materializedViewCount = computed(() => rows.value.filter((row) => row.type === "MATERIALIZED_VIEW").length);
|
||||
const procedureCount = computed(() => rows.value.filter((row) => row.type === "PROCEDURE").length);
|
||||
const functionCount = computed(() => rows.value.filter((row) => row.type === "FUNCTION").length);
|
||||
const triggerCount = computed(() => rows.value.filter((row) => row.type === "TRIGGER").length);
|
||||
const sequenceCount = computed(() => rows.value.filter((row) => row.type === "SEQUENCE").length);
|
||||
const packageCount = computed(() => rows.value.filter((row) => row.type === "PACKAGE" || row.type === "PACKAGE_BODY").length);
|
||||
const typeCount = computed(() => rows.value.filter((row) => row.type === "TYPE" || row.type === "TYPE_BODY").length);
|
||||
const canOpenStructureEditor = computed(() => supportsTableStructureEditing(tableStructureDatabaseType.value));
|
||||
const canOpenDiagram = computed(() => !!props.database && supportsSchemaDiagram(effectiveDatabaseType.value));
|
||||
const canOpenTableImport = computed(() => !!props.database && supportsTableImport(effectiveDatabaseType.value));
|
||||
|
|
@ -253,8 +255,10 @@ const objectFilters = computed<ObjectFilter[]>(() =>
|
|||
["materializedViews", materializedViewCount.value],
|
||||
["procedures", procedureCount.value],
|
||||
["functions", functionCount.value],
|
||||
["triggers", triggerCount.value],
|
||||
["sequences", sequenceCount.value],
|
||||
["packages", packageCount.value],
|
||||
["types", typeCount.value],
|
||||
] as Array<[ObjectFilter, number]>
|
||||
)
|
||||
.filter(([filter, count]) => filter === "all" || count > 0)
|
||||
|
|
@ -526,8 +530,10 @@ function iconFor(row: ObjectBrowserRow) {
|
|||
if (row.type === "VIEW" || row.type === "MATERIALIZED_VIEW") return Eye;
|
||||
if (row.type === "PROCEDURE") return ScrollText;
|
||||
if (row.type === "FUNCTION") return Braces;
|
||||
if (row.type === "TRIGGER") return RotateCcw;
|
||||
if (row.type === "SEQUENCE") return ListTree;
|
||||
if (row.type === "PACKAGE" || row.type === "PACKAGE_BODY") return Package;
|
||||
if (row.type === "TYPE" || row.type === "TYPE_BODY") return Braces;
|
||||
return Table2;
|
||||
}
|
||||
|
||||
|
|
@ -536,9 +542,12 @@ function typeLabel(type: ObjectBrowserRow["type"]) {
|
|||
if (type === "VIEW") return t("objects.view");
|
||||
if (type === "PROCEDURE") return t("objects.procedure");
|
||||
if (type === "FUNCTION") return t("objects.function");
|
||||
if (type === "TRIGGER") return t("objects.trigger");
|
||||
if (type === "SEQUENCE") return t("objects.sequence");
|
||||
if (type === "PACKAGE") return t("objects.package");
|
||||
if (type === "PACKAGE_BODY") return t("objects.packageBody");
|
||||
if (type === "TYPE") return t("objects.typeDefinition");
|
||||
if (type === "TYPE_BODY") return t("objects.typeBody");
|
||||
return t("objects.table");
|
||||
}
|
||||
|
||||
|
|
@ -635,8 +644,10 @@ function rowMatchesObjectFilter(row: ObjectBrowserRow) {
|
|||
if (objectFilter.value === "materializedViews") return row.type === "MATERIALIZED_VIEW";
|
||||
if (objectFilter.value === "procedures") return row.type === "PROCEDURE";
|
||||
if (objectFilter.value === "functions") return row.type === "FUNCTION";
|
||||
if (objectFilter.value === "triggers") return row.type === "TRIGGER";
|
||||
if (objectFilter.value === "sequences") return row.type === "SEQUENCE";
|
||||
if (objectFilter.value === "packages") return row.type === "PACKAGE" || row.type === "PACKAGE_BODY";
|
||||
if (objectFilter.value === "types") return row.type === "TYPE" || row.type === "TYPE_BODY";
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -673,8 +684,10 @@ function iconClass(type: ObjectBrowserRow["type"]) {
|
|||
if (type === "VIEW" || type === "MATERIALIZED_VIEW") return "text-purple-500";
|
||||
if (type === "PROCEDURE") return "text-blue-500";
|
||||
if (type === "FUNCTION") return "text-amber-500";
|
||||
if (type === "TRIGGER") return "text-rose-500";
|
||||
if (type === "SEQUENCE") return "text-emerald-500";
|
||||
if (type === "PACKAGE" || type === "PACKAGE_BODY") return "text-cyan-500";
|
||||
if (type === "TYPE" || type === "TYPE_BODY") return "text-violet-500";
|
||||
return "text-green-500";
|
||||
}
|
||||
|
||||
|
|
@ -682,8 +695,10 @@ function iconBgClass(type: ObjectBrowserRow["type"]) {
|
|||
if (type === "VIEW" || type === "MATERIALIZED_VIEW") return "object-browser-icon-bg object-browser-icon-bg-view";
|
||||
if (type === "PROCEDURE") return "object-browser-icon-bg object-browser-icon-bg-procedure";
|
||||
if (type === "FUNCTION") return "object-browser-icon-bg object-browser-icon-bg-function";
|
||||
if (type === "TRIGGER") return "object-browser-icon-bg object-browser-icon-bg-procedure";
|
||||
if (type === "SEQUENCE") return "object-browser-icon-bg object-browser-icon-bg-sequence";
|
||||
if (type === "PACKAGE" || type === "PACKAGE_BODY") return "object-browser-icon-bg object-browser-icon-bg-package";
|
||||
if (type === "TYPE" || type === "TYPE_BODY") return "object-browser-icon-bg object-browser-icon-bg-function";
|
||||
return "object-browser-icon-bg object-browser-icon-bg-table";
|
||||
}
|
||||
|
||||
|
|
@ -1013,14 +1028,16 @@ async function openSource(row: ObjectBrowserRow) {
|
|||
try {
|
||||
const result = await api.getObjectSource(connectionId, database, schema, row.name, row.type as ObjectSourceKind);
|
||||
if (sidePanelGuard.isStale(epoch)) return;
|
||||
sourceCanEdit.value = result.editable !== false && row.type !== "SEQUENCE";
|
||||
const editable = await api.buildEditableObjectSource({
|
||||
databaseType: effectiveDatabaseType.value,
|
||||
objectType: row.type as ObjectSourceKind,
|
||||
schema,
|
||||
name: row.name,
|
||||
source: result.source,
|
||||
});
|
||||
sourceCanEdit.value = result.editable !== false && !["SEQUENCE", "TRIGGER", "TYPE", "TYPE_BODY"].includes(row.type);
|
||||
const editable = sourceCanEdit.value
|
||||
? await api.buildEditableObjectSource({
|
||||
databaseType: effectiveDatabaseType.value,
|
||||
objectType: row.type as ObjectSourceKind,
|
||||
schema,
|
||||
name: row.name,
|
||||
source: result.source,
|
||||
})
|
||||
: result.source;
|
||||
if (sidePanelGuard.isStale(epoch)) return;
|
||||
// Viewing database source must preserve its original whitespace and comments;
|
||||
// formatting remains an explicit editor action instead of altering it on open.
|
||||
|
|
@ -2129,8 +2146,10 @@ function filterCount(filter: ObjectFilter) {
|
|||
if (filter === "materializedViews") return materializedViewCount.value;
|
||||
if (filter === "procedures") return procedureCount.value;
|
||||
if (filter === "functions") return functionCount.value;
|
||||
if (filter === "triggers") return triggerCount.value;
|
||||
if (filter === "sequences") return sequenceCount.value;
|
||||
if (filter === "packages") return packageCount.value;
|
||||
if (filter === "types") return typeCount.value;
|
||||
return rows.value.length;
|
||||
}
|
||||
|
||||
|
|
@ -2146,11 +2165,15 @@ function filterLabel(filter: ObjectFilter) {
|
|||
? "objects.procedures"
|
||||
: filter === "functions"
|
||||
? "objects.functions"
|
||||
: filter === "sequences"
|
||||
? "objects.sequences"
|
||||
: filter === "packages"
|
||||
? "objects.packages"
|
||||
: "objects.all";
|
||||
: filter === "triggers"
|
||||
? "tree.triggers"
|
||||
: filter === "sequences"
|
||||
? "objects.sequences"
|
||||
: filter === "packages"
|
||||
? "objects.packages"
|
||||
: filter === "types"
|
||||
? "tree.types"
|
||||
: "objects.all";
|
||||
return `${t(key)} ${filterCount(filter)}`;
|
||||
}
|
||||
|
||||
|
|
@ -2334,8 +2357,7 @@ function getPackageMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
|||
function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
if (item.type === "TABLE") return getTableMenuItems(item);
|
||||
if (item.type === "VIEW" || item.type === "MATERIALIZED_VIEW") return getViewMenuItems(item);
|
||||
if (item.type === "SEQUENCE") return getPackageMenuItems(item);
|
||||
if (item.type === "PACKAGE" || item.type === "PACKAGE_BODY") return getPackageMenuItems(item);
|
||||
if (isSourceOnlyObjectBrowserRow(item)) return getPackageMenuItems(item);
|
||||
return getProcFuncMenuItems(item);
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -154,9 +154,9 @@ watch(deferredSearchQuery, (newQuery, oldQuery) => {
|
|||
.catch(() => {});
|
||||
});
|
||||
|
||||
const searchableObjectGroupTypes = new Set<TreeNodeType>(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages"]);
|
||||
const searchableObjectGroupTypes = new Set<TreeNodeType>(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-sequences", "group-packages", "group-types"]);
|
||||
const simpleObjectParentTypes = new Set<TreeNodeType>(["database", "schema", "linked-server-schema"]);
|
||||
const simpleObjectChildTypes = new Set<TreeNodeType>(["table", "view", "materialized_view", "procedure", "function", "sequence", "package", "package-body", "load-more"]);
|
||||
const simpleObjectChildTypes = new Set<TreeNodeType>(["table", "view", "materialized_view", "procedure", "function", "trigger", "sequence", "package", "package-body", "type", "type-body", "load-more"]);
|
||||
|
||||
function isSimpleObjectSearchParent(node: TreeNode): boolean {
|
||||
return settingsStore.editorSettings.sidebarObjectDisplay === "simple" && simpleObjectParentTypes.has(node.type) && node.isExpanded === true && (!!node.children?.some((child) => simpleObjectChildTypes.has(child.type)) || !!store.sidebarTableSearchQueries[node.id]?.trim());
|
||||
|
|
|
|||
|
|
@ -445,7 +445,22 @@ function hasNodeDatabaseContext(node: TreeNode): node is TreeNode & { connection
|
|||
return !!node.connectionId && hasTreeNodeDatabaseContext(node);
|
||||
}
|
||||
|
||||
const groupTypes: Set<TreeNodeType> = new Set(["group-columns", "group-indexes", "group-fkeys", "group-triggers", "group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages", "group-partitions", "group-extensions"]);
|
||||
const groupTypes: Set<TreeNodeType> = new Set([
|
||||
"group-columns",
|
||||
"group-indexes",
|
||||
"group-fkeys",
|
||||
"group-triggers",
|
||||
"group-tables",
|
||||
"group-views",
|
||||
"group-materialized-views",
|
||||
"group-procedures",
|
||||
"group-functions",
|
||||
"group-sequences",
|
||||
"group-packages",
|
||||
"group-types",
|
||||
"group-partitions",
|
||||
"group-extensions",
|
||||
]);
|
||||
|
||||
function isGroupLabel(node: TreeNode): boolean {
|
||||
return groupTypes.has(node.type);
|
||||
|
|
@ -642,7 +657,7 @@ function runRowClickAction(clickDetail: number) {
|
|||
scheduleOpenData(node);
|
||||
} else if (isDocumentBrowserTreeNode(node.type)) {
|
||||
openMongoTreeData(node);
|
||||
} else if (node.type === "procedure" || node.type === "function" || node.type === "sequence" || node.type === "package" || node.type === "package-body") {
|
||||
} else if (node.type === "procedure" || node.type === "function" || node.type === "trigger" || node.type === "sequence" || node.type === "package" || node.type === "package-body" || node.type === "type" || node.type === "type-body") {
|
||||
openObjectSourceDialog(false);
|
||||
} else if (action === "toggle") {
|
||||
toggle();
|
||||
|
|
@ -2833,7 +2848,7 @@ const canOpenFieldLineage = computed(() => {
|
|||
|
||||
const hasTypeMenu = computed(() => {
|
||||
const t = activeNode.value.type;
|
||||
return t === "connection" || t === "database" || t === "schema" || t === "table" || t === "view" || t === "column" || t === "procedure" || t === "function" || t === "package" || t === "package-body" || isGroupLabel(activeNode.value);
|
||||
return t === "connection" || t === "database" || t === "schema" || t === "table" || t === "view" || t === "column" || t === "procedure" || t === "function" || t === "trigger" || t === "package" || t === "package-body" || t === "type" || t === "type-body" || isGroupLabel(activeNode.value);
|
||||
});
|
||||
|
||||
const isSelected = computed(() => connectionStore.selectedTreeNodeId === activeNode.value.id);
|
||||
|
|
@ -3871,7 +3886,7 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (node.type === "index" || node.type === "fkey" || node.type === "trigger") {
|
||||
if (node.type === "index" || node.type === "fkey" || (node.type === "trigger" && !!node.tableName)) {
|
||||
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
if (node.type === "index" && canOpenStructureEditor.value) {
|
||||
items.push({ label: "", separator: true });
|
||||
|
|
@ -3931,7 +3946,7 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (node.type === "package" || node.type === "package-body") {
|
||||
if (node.type === "trigger" || node.type === "package" || node.type === "package-body" || node.type === "type" || node.type === "type-body") {
|
||||
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
|
|
|
|||
|
|
@ -261,6 +261,10 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
|
|||
return { icon: Package, colorClass: "text-cyan-500" };
|
||||
case "package-body":
|
||||
return { icon: FileCode, colorClass: "text-cyan-400" };
|
||||
case "type":
|
||||
return { icon: Braces, colorClass: "text-violet-500" };
|
||||
case "type-body":
|
||||
return { icon: FileCode, colorClass: "text-violet-400" };
|
||||
case "group-tables":
|
||||
return { icon: Table, colorClass: "text-green-500" };
|
||||
case "group-views":
|
||||
|
|
@ -275,6 +279,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
|
|||
return { icon: ListTree, colorClass: "text-emerald-500" };
|
||||
case "group-packages":
|
||||
return { icon: Package, colorClass: "text-cyan-500" };
|
||||
case "group-types":
|
||||
return { icon: Braces, colorClass: "text-violet-500" };
|
||||
case "group-partitions":
|
||||
return { icon: node.isExpanded ? FolderOpen : FolderClosed, colorClass: "text-green-400" };
|
||||
case "group-extensions":
|
||||
|
|
@ -288,7 +294,22 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
|
|||
}
|
||||
}
|
||||
|
||||
const groupTypes: Set<TreeNodeType> = new Set(["group-columns", "group-indexes", "group-fkeys", "group-triggers", "group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages", "group-partitions", "group-extensions"]);
|
||||
const groupTypes: Set<TreeNodeType> = new Set([
|
||||
"group-columns",
|
||||
"group-indexes",
|
||||
"group-fkeys",
|
||||
"group-triggers",
|
||||
"group-tables",
|
||||
"group-views",
|
||||
"group-materialized-views",
|
||||
"group-procedures",
|
||||
"group-functions",
|
||||
"group-sequences",
|
||||
"group-packages",
|
||||
"group-types",
|
||||
"group-partitions",
|
||||
"group-extensions",
|
||||
]);
|
||||
|
||||
function isGroupLabel(node: TreeNode): boolean {
|
||||
return groupTypes.has(node.type);
|
||||
|
|
@ -304,10 +325,11 @@ function displayLabel(node: TreeNode): string {
|
|||
}
|
||||
|
||||
function visibleLabel(node: TreeNode): string {
|
||||
const withValidity = (label: string) => (node.valid === false ? `${label} · INVALID` : label);
|
||||
if (node.type === "table" || node.type === "view" || node.type === "materialized_view" || node.type === "mongo-collection" || node.type === "vector-collection" || node.type === "elasticsearch-index") {
|
||||
return sidebarDisplayTableName(node.label, settingsStore.editorSettings.sidebarHiddenTablePrefixes);
|
||||
return withValidity(sidebarDisplayTableName(node.label, settingsStore.editorSettings.sidebarHiddenTablePrefixes));
|
||||
}
|
||||
return displayLabel(node);
|
||||
return withValidity(displayLabel(node));
|
||||
}
|
||||
|
||||
type DetailTooltipRow = {
|
||||
|
|
|
|||
|
|
@ -1855,6 +1855,7 @@ export default {
|
|||
functions: "Functions",
|
||||
sequences: "Sequences",
|
||||
packages: "Packages",
|
||||
types: "Types",
|
||||
gridfs: "GridFS",
|
||||
buckets: "Buckets",
|
||||
partitions: "Partitions",
|
||||
|
|
@ -2092,9 +2093,12 @@ export default {
|
|||
view: "View",
|
||||
procedure: "Procedure",
|
||||
function: "Function",
|
||||
trigger: "Trigger",
|
||||
sequence: "Sequence",
|
||||
package: "Package",
|
||||
packageBody: "Package Body",
|
||||
typeDefinition: "Type",
|
||||
typeBody: "Type Body",
|
||||
name: "Name",
|
||||
type: "Type",
|
||||
rows: "Rows",
|
||||
|
|
|
|||
|
|
@ -1802,6 +1802,7 @@ export default withEnglishFallback({
|
|||
loadMore: "Cargar más...",
|
||||
objectBrowser: "Explorar en el navegador de objetos ({count})",
|
||||
extensions: "Extensiones",
|
||||
types: "tipo",
|
||||
},
|
||||
userAdmin: {
|
||||
title: "Usuarios y Privilegios",
|
||||
|
|
@ -1988,6 +1989,9 @@ export default withEnglishFallback({
|
|||
sortAsc: "ascendente",
|
||||
sortDesc: "descendente",
|
||||
sortBy: "ordenar por",
|
||||
trigger: "disparador",
|
||||
typeDefinition: "definición de tipo",
|
||||
typeBody: "cuerpo de tipo",
|
||||
},
|
||||
structureEditor: {
|
||||
mysqlDataTypeHelp: {
|
||||
|
|
|
|||
|
|
@ -1800,6 +1800,7 @@ export default withEnglishFallback({
|
|||
loadMore: "Carica altro...",
|
||||
objectBrowser: "Sfoglia in Esplora Oggetti ({count})",
|
||||
extensions: "Estensioni",
|
||||
types: "Tipi",
|
||||
},
|
||||
userAdmin: {
|
||||
title: "Utenti e Privilegi",
|
||||
|
|
@ -1986,6 +1987,9 @@ export default withEnglishFallback({
|
|||
sortAsc: "Crescente",
|
||||
sortDesc: "Decrescente",
|
||||
sortBy: "Ordina per",
|
||||
trigger: "Trigger",
|
||||
typeDefinition: "Definizione del tipo",
|
||||
typeBody: "Corpo del tipo",
|
||||
},
|
||||
structureEditor: {
|
||||
mysqlDataTypeHelp: {
|
||||
|
|
|
|||
|
|
@ -1801,6 +1801,7 @@ export default withEnglishFallback({
|
|||
linkedServers: "リンクサーバー",
|
||||
materializedViews: "マテリアライズドビュー",
|
||||
extensions: "拡張機能",
|
||||
types: "タイプ",
|
||||
},
|
||||
zookeeper: {
|
||||
prefixPlaceholder: "パスプレフィックス(例: /app/)",
|
||||
|
|
@ -2021,6 +2022,9 @@ export default withEnglishFallback({
|
|||
sortAsc: "昇順",
|
||||
sortDesc: "降順",
|
||||
sortBy: "並べ替え",
|
||||
trigger: "トリガー",
|
||||
typeDefinition: "タイプ",
|
||||
typeBody: "タイプ本体",
|
||||
},
|
||||
structureEditor: {
|
||||
mysqlDataTypeHelp: {
|
||||
|
|
|
|||
|
|
@ -1802,6 +1802,7 @@ export default withEnglishFallback({
|
|||
loadMore: "Carregar mais...",
|
||||
objectBrowser: "Navegar no Navegador de Objetos ({count})",
|
||||
extensions: "Extensões",
|
||||
types: "Tipos",
|
||||
},
|
||||
userAdmin: {
|
||||
title: "Usuários e Privilégios",
|
||||
|
|
@ -1988,6 +1989,9 @@ export default withEnglishFallback({
|
|||
sortAsc: "Crescente",
|
||||
sortDesc: "Decrescente",
|
||||
sortBy: "Ordenar por",
|
||||
trigger: "Gatilho",
|
||||
typeDefinition: "Definição de tipo",
|
||||
typeBody: "Corpo do tipo",
|
||||
},
|
||||
structureEditor: {
|
||||
mysqlDataTypeHelp: {
|
||||
|
|
|
|||
|
|
@ -1855,6 +1855,7 @@ export default withEnglishFallback({
|
|||
functions: "函数",
|
||||
sequences: "序列",
|
||||
packages: "包",
|
||||
types: "类型",
|
||||
partitions: "分区",
|
||||
childTables: "子表",
|
||||
loadMore: "加载更多...",
|
||||
|
|
@ -2062,9 +2063,12 @@ export default withEnglishFallback({
|
|||
view: "视图",
|
||||
procedure: "存储过程",
|
||||
function: "函数",
|
||||
trigger: "触发器",
|
||||
sequence: "序列",
|
||||
package: "包",
|
||||
packageBody: "包体",
|
||||
typeDefinition: "类型",
|
||||
typeBody: "类型体",
|
||||
name: "名称",
|
||||
type: "类型",
|
||||
rows: "行数",
|
||||
|
|
|
|||
|
|
@ -1802,6 +1802,7 @@ export default withEnglishFallback({
|
|||
loadMore: "載入更多...",
|
||||
objectBrowser: "在物件瀏覽器中檢視 ({count})",
|
||||
extensions: "擴展",
|
||||
types: "類型",
|
||||
},
|
||||
objects: {
|
||||
all: "全部",
|
||||
|
|
@ -1858,6 +1859,9 @@ export default withEnglishFallback({
|
|||
sortAsc: "升序",
|
||||
sortDesc: "降序",
|
||||
sortBy: "排序方式",
|
||||
trigger: "觸發器",
|
||||
typeDefinition: "類型",
|
||||
typeBody: "類型體",
|
||||
},
|
||||
structureEditor: {
|
||||
mysqlDataTypeHelp: {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { doubleClickRowAction, resolveRowClickAction, shouldDeferSingleClick, singleClickRowAction } from "@/lib/table/objectBrowserRowAction";
|
||||
import { doubleClickRowAction, isSourceOnlyObjectBrowserRow, resolveRowClickAction, shouldDeferSingleClick, singleClickRowAction } from "@/lib/table/objectBrowserRowAction";
|
||||
import type { ObjectBrowserRow } from "@/lib/table/objectBrowserRows";
|
||||
|
||||
function row(type: ObjectBrowserRow["type"], name = "test"): ObjectBrowserRow {
|
||||
|
|
@ -39,6 +39,10 @@ describe("singleClickRowAction", () => {
|
|||
expect(singleClickRowAction(row("PACKAGE_BODY", "pkg_body_test"))).toBe("open-source");
|
||||
});
|
||||
|
||||
it.each(["TRIGGER", "TYPE", "TYPE_BODY"] as const)("returns open-source for %s", (type) => {
|
||||
expect(singleClickRowAction(row(type, "programmable_test"))).toBe("open-source");
|
||||
});
|
||||
|
||||
it("returns none for null/undefined", () => {
|
||||
expect(singleClickRowAction(null)).toBe("none");
|
||||
expect(singleClickRowAction(undefined)).toBe("none");
|
||||
|
|
@ -63,6 +67,17 @@ describe("doubleClickRowAction", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("isSourceOnlyObjectBrowserRow", () => {
|
||||
it.each(["TRIGGER", "TYPE", "TYPE_BODY"] as const)("marks %s as source-only", (type) => {
|
||||
expect(isSourceOnlyObjectBrowserRow(row(type))).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps procedure and function mutation menus separate", () => {
|
||||
expect(isSourceOnlyObjectBrowserRow(row("PROCEDURE"))).toBe(false);
|
||||
expect(isSourceOnlyObjectBrowserRow(row("FUNCTION"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveRowClickAction", () => {
|
||||
const tableRow = row("TABLE", "users");
|
||||
const viewRow = row("VIEW", "v_users");
|
||||
|
|
|
|||
|
|
@ -29,6 +29,22 @@ describe("PostgreSQL overloaded routines", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("programmable database objects", () => {
|
||||
it("keeps Xugu trigger/type nodes distinct and preserves an invalid status", () => {
|
||||
const objects: ObjectInfo[] = [
|
||||
{ name: "TRG_AUDIT", object_type: "TRIGGER", schema: "APP", valid: false },
|
||||
{ name: "ADDRESS_T", object_type: "TYPE", schema: "APP", valid: true },
|
||||
{ name: "ADDRESS_T", object_type: "TYPE_BODY", schema: "APP", valid: true },
|
||||
];
|
||||
|
||||
const nodes = buildSimpleObjectTreeNodes({ ...context, schema: "APP", objects });
|
||||
|
||||
expect(nodes).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ type: "trigger", objectName: "TRG_AUDIT", valid: false }), expect.objectContaining({ type: "type", objectName: "ADDRESS_T", valid: true }), expect.objectContaining({ type: "type-body", objectName: "ADDRESS_T", valid: true })]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PostgreSQL table hierarchy", () => {
|
||||
it("keeps schema pagination visible at the table-group root when a page ends inside nested partitions", () => {
|
||||
const nodes = buildTableTreeNodes({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
export type SidebarObjectKind = "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY";
|
||||
export type SidebarObjectKind = "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "TRIGGER" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY" | "TYPE" | "TYPE_BODY";
|
||||
|
||||
export interface DatabaseObjectCapabilities {
|
||||
sidebarObjects: SidebarObjectKind[];
|
||||
|
|
@ -15,6 +15,7 @@ const ROUTINE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUN
|
|||
const POSTGRES_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE"];
|
||||
const POSTGRES_LIKE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION"];
|
||||
const ORACLE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY"];
|
||||
const XUGU_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "SEQUENCE", "PACKAGE", "PACKAGE_BODY", "TYPE", "TYPE_BODY"];
|
||||
|
||||
const DATABASE_TYPE_OBJECTS = new Map<DatabaseType, SidebarObjectKind[]>([
|
||||
// postgres
|
||||
|
|
@ -31,6 +32,7 @@ const DATABASE_TYPE_OBJECTS = new Map<DatabaseType, SidebarObjectKind[]>([
|
|||
["oracle", ORACLE_OBJECTS],
|
||||
["dameng", ORACLE_OBJECTS],
|
||||
["oceanbase-oracle", ORACLE_OBJECTS],
|
||||
["xugu", XUGU_OBJECTS],
|
||||
// table and view
|
||||
["sqlite", TABLE_VIEW_OBJECTS],
|
||||
["rqlite", TABLE_VIEW_OBJECTS],
|
||||
|
|
@ -74,7 +76,10 @@ export function normalizeSidebarObjectKind(type: string): SidebarObjectKind {
|
|||
const value = type.toUpperCase();
|
||||
const normalized = value.replace(/[\s-]+/g, "_");
|
||||
if (normalized.includes("PACKAGE_BODY")) return "PACKAGE_BODY";
|
||||
if (normalized.includes("TYPE_BODY")) return "TYPE_BODY";
|
||||
if (normalized.includes("PACKAGE")) return "PACKAGE";
|
||||
if (normalized.includes("TRIGGER")) return "TRIGGER";
|
||||
if (normalized.includes("TYPE")) return "TYPE";
|
||||
if (normalized.includes("MATERIALIZED_VIEW")) return "MATERIALIZED_VIEW";
|
||||
if (value.includes("VIEW")) return "VIEW";
|
||||
if (value.includes("SEQ")) return "SEQUENCE";
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ const leafTypes: Set<TreeNodeType> = new Set([
|
|||
"function",
|
||||
"package",
|
||||
"package-body",
|
||||
"type",
|
||||
"type-body",
|
||||
"object-browser",
|
||||
"redis-db",
|
||||
"mq-tenant",
|
||||
|
|
|
|||
|
|
@ -10,19 +10,22 @@ const dataNodeTypes = new Set<TreeNodeType>(["table", "view", "materialized_view
|
|||
const documentBrowserNodeTypes = new Set<TreeNodeType>(["mongo-collection", "mongo-bucket"]);
|
||||
const toggleLeafNodeTypes = new Set<TreeNodeType>(["redis-db", "mq-tenant", "etcd-root", "zookeeper-root", "mongo-gridfs", "mongo-collection", "mongo-bucket", "vector-collection", "elasticsearch-index", "user-admin"]);
|
||||
const objectBrowserNodeTypes = new Set<TreeNodeType>(["database", "schema", "object-browser"]);
|
||||
const sourceNodeTypes = new Set<TreeNodeType>(["materialized_view", "procedure", "function", "sequence", "package", "package-body"]);
|
||||
const sourceNodeTypes = new Set<TreeNodeType>(["materialized_view", "procedure", "function", "trigger", "sequence", "package", "package-body", "type", "type-body"]);
|
||||
const savedSqlNodeTypes = new Set<TreeNodeType>(["saved-sql-file"]);
|
||||
const tableChildGroupNodeTypes = new Set<TreeNodeType>(["group-columns", "group-indexes", "group-fkeys", "group-triggers", "group-partitions"]);
|
||||
const databaseChildGroupNodeTypes = new Set<TreeNodeType>(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages"]);
|
||||
const databaseChildGroupNodeTypes = new Set<TreeNodeType>(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-sequences", "group-packages", "group-types"]);
|
||||
|
||||
export function objectSourceKindForTreeNode(type: TreeNodeType): ObjectSourceKind | null {
|
||||
if (type === "view") return "VIEW";
|
||||
if (type === "materialized_view") return "MATERIALIZED_VIEW";
|
||||
if (type === "procedure") return "PROCEDURE";
|
||||
if (type === "function") return "FUNCTION";
|
||||
if (type === "trigger") return "TRIGGER";
|
||||
if (type === "sequence") return "SEQUENCE";
|
||||
if (type === "package") return "PACKAGE";
|
||||
if (type === "package-body") return "PACKAGE_BODY";
|
||||
if (type === "type") return "TYPE";
|
||||
if (type === "type-body") return "TYPE_BODY";
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export type ObjectBrowserRowAction = "table-info" | "open-table" | "open-source"
|
|||
/**
|
||||
* Determine the action for a single click on an object browser row.
|
||||
* - TABLE → table-info (show table properties panel)
|
||||
* - VIEW/MATERIALIZED_VIEW/PROCEDURE/FUNCTION/SEQUENCE/PACKAGE/PACKAGE_BODY → open-source
|
||||
* - VIEW/MATERIALIZED_VIEW/PROCEDURE/FUNCTION/TRIGGER/SEQUENCE/PACKAGE/PACKAGE_BODY/TYPE/TYPE_BODY → open-source
|
||||
* - otherwise → none
|
||||
*/
|
||||
export function singleClickRowAction(row: ObjectBrowserRow | null | undefined): ObjectBrowserRowAction {
|
||||
|
|
@ -18,7 +18,7 @@ export function singleClickRowAction(row: ObjectBrowserRow | null | undefined):
|
|||
/**
|
||||
* Determine the action for a double click on an object browser row.
|
||||
* - TABLE → open-table (open table data tab)
|
||||
* - VIEW/MATERIALIZED_VIEW/PROCEDURE/FUNCTION/SEQUENCE/PACKAGE/PACKAGE_BODY → open-source
|
||||
* - VIEW/MATERIALIZED_VIEW/PROCEDURE/FUNCTION/TRIGGER/SEQUENCE/PACKAGE/PACKAGE_BODY/TYPE/TYPE_BODY → open-source
|
||||
* - otherwise → none
|
||||
*/
|
||||
export function doubleClickRowAction(row: ObjectBrowserRow | null | undefined): ObjectBrowserRowAction {
|
||||
|
|
@ -62,6 +62,14 @@ export function shouldDeferSingleClick(row: ObjectBrowserRow | null | undefined,
|
|||
return single !== double && action === single;
|
||||
}
|
||||
|
||||
function canOpenSource(row: ObjectBrowserRow): boolean {
|
||||
return row.type === "VIEW" || row.type === "MATERIALIZED_VIEW" || row.type === "PROCEDURE" || row.type === "FUNCTION" || row.type === "SEQUENCE" || row.type === "PACKAGE" || row.type === "PACKAGE_BODY";
|
||||
/**
|
||||
* Objects with source metadata but no supported object-browser mutation API.
|
||||
* Their menu intentionally exposes only source viewing and copying.
|
||||
*/
|
||||
export function isSourceOnlyObjectBrowserRow(row: ObjectBrowserRow): boolean {
|
||||
return row.type === "TRIGGER" || row.type === "SEQUENCE" || row.type === "PACKAGE" || row.type === "PACKAGE_BODY" || row.type === "TYPE" || row.type === "TYPE_BODY";
|
||||
}
|
||||
|
||||
function canOpenSource(row: ObjectBrowserRow): boolean {
|
||||
return row.type === "VIEW" || row.type === "MATERIALIZED_VIEW" || row.type === "PROCEDURE" || row.type === "FUNCTION" || row.type === "TRIGGER" || row.type === "SEQUENCE" || row.type === "PACKAGE" || row.type === "PACKAGE_BODY" || row.type === "TYPE" || row.type === "TYPE_BODY";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ export type ObjectBrowserRow = {
|
|||
name: string;
|
||||
displayName: string;
|
||||
schema?: string;
|
||||
type: "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY";
|
||||
type: "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "TRIGGER" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY" | "TYPE" | "TYPE_BODY";
|
||||
valid?: boolean | null;
|
||||
signature?: string | null;
|
||||
comment?: string | null;
|
||||
created_at?: string | null;
|
||||
|
|
@ -27,7 +28,10 @@ export function normalizeObjectBrowserType(type: string): ObjectBrowserRow["type
|
|||
const value = type.toUpperCase();
|
||||
const normalized = value.replace(/[\s-]+/g, "_");
|
||||
if (normalized.includes("PACKAGE_BODY")) return "PACKAGE_BODY";
|
||||
if (normalized.includes("TYPE_BODY")) return "TYPE_BODY";
|
||||
if (normalized.includes("PACKAGE")) return "PACKAGE";
|
||||
if (normalized.includes("TRIGGER")) return "TRIGGER";
|
||||
if (normalized.includes("TYPE")) return "TYPE";
|
||||
if (normalized.includes("MATERIALIZED_VIEW")) return "MATERIALIZED_VIEW";
|
||||
if (value.includes("VIEW")) return "VIEW";
|
||||
if (value.includes("SEQ")) return "SEQUENCE";
|
||||
|
|
@ -57,6 +61,7 @@ export function buildObjectBrowserRows(options: { objects: ObjectInfo[]; databas
|
|||
displayName,
|
||||
schema,
|
||||
type,
|
||||
valid: object.valid,
|
||||
signature,
|
||||
comment: object.comment,
|
||||
created_at: object.created_at,
|
||||
|
|
|
|||
|
|
@ -504,7 +504,7 @@ export function buildSimpleObjectTreeNodes({ nodeId, connectionId, database, sch
|
|||
|
||||
for (const obj of objects) {
|
||||
const objectType = normalizeObjectType(obj.object_type);
|
||||
if (!["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE", "PACKAGE", "PACKAGE_BODY"].includes(objectType)) {
|
||||
if (!["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "SEQUENCE", "PACKAGE", "PACKAGE_BODY", "TYPE", "TYPE_BODY"].includes(objectType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -540,6 +540,7 @@ export function buildSimpleObjectTreeNodes({ nodeId, connectionId, database, sch
|
|||
objectName: name,
|
||||
signature: signature || undefined,
|
||||
comment: obj.comment,
|
||||
valid: obj.valid ?? undefined,
|
||||
connectionId,
|
||||
database,
|
||||
schema: childSchema,
|
||||
|
|
@ -557,9 +558,12 @@ function simpleObjectNodeType(objectType: DatabaseObjectTreeKind): TreeNodeType
|
|||
if (objectType === "MATERIALIZED_VIEW") return "materialized_view";
|
||||
if (objectType === "PROCEDURE") return "procedure";
|
||||
if (objectType === "FUNCTION") return "function";
|
||||
if (objectType === "TRIGGER") return "trigger";
|
||||
if (objectType === "SEQUENCE") return "sequence";
|
||||
if (objectType === "PACKAGE_BODY") return "package-body";
|
||||
if (objectType === "PACKAGE") return "package";
|
||||
if (objectType === "TYPE_BODY") return "type-body";
|
||||
if (objectType === "TYPE") return "type";
|
||||
return "table";
|
||||
}
|
||||
|
||||
|
|
@ -597,6 +601,13 @@ const groupDefs: Array<{
|
|||
nodeType: "group-functions",
|
||||
childType: "function",
|
||||
},
|
||||
{
|
||||
key: "__triggers",
|
||||
label: "tree.triggers",
|
||||
objectTypes: ["TRIGGER"],
|
||||
nodeType: "group-triggers",
|
||||
childType: "trigger",
|
||||
},
|
||||
{
|
||||
key: "__sequences",
|
||||
label: "tree.sequences",
|
||||
|
|
@ -611,9 +622,16 @@ const groupDefs: Array<{
|
|||
nodeType: "group-packages",
|
||||
childType: (objectType) => (objectType === "PACKAGE_BODY" ? "package-body" : "package"),
|
||||
},
|
||||
{
|
||||
key: "__types",
|
||||
label: "tree.types",
|
||||
objectTypes: ["TYPE", "TYPE_BODY"],
|
||||
nodeType: "group-types",
|
||||
childType: (objectType) => (objectType === "TYPE_BODY" ? "type-body" : "type"),
|
||||
},
|
||||
];
|
||||
|
||||
const objectGroupNodeTypes = new Set<TreeNodeType>(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages"]);
|
||||
const objectGroupNodeTypes = new Set<TreeNodeType>(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-sequences", "group-packages", "group-types"]);
|
||||
|
||||
export function buildObjectGroupPlaceholderNodes({ nodeId, connectionId, database, schema, objectTypes }: { nodeId: string; connectionId: string; database: string; schema?: string; objectTypes: DatabaseObjectTreeKind[] }): TreeNode[] {
|
||||
const supported = new Set(objectTypes);
|
||||
|
|
@ -676,12 +694,13 @@ export function buildGroupedObjectTreeNodes({ nodeId, connectionId, database, sc
|
|||
const childSchema = obj.schema ? normalizeDatabaseObjectName(obj.schema) : schema;
|
||||
const objectType = normalizeObjectType(obj.object_type);
|
||||
const childType = typeof def.childType === "function" ? def.childType(objectType) : def.childType;
|
||||
const objectTypeSuffix = objectType === "PACKAGE" || objectType === "PACKAGE_BODY" ? `:${objectType}` : "";
|
||||
const objectTypeSuffix = objectType === "PACKAGE" || objectType === "PACKAGE_BODY" || objectType === "TYPE" || objectType === "TYPE_BODY" ? `:${objectType}` : "";
|
||||
return {
|
||||
id: `${nodeId}:${def.key}:${childSchema ? `${childSchema}:` : ""}${obj.name}${objectTypeSuffix}`,
|
||||
label: obj.name,
|
||||
type: childType,
|
||||
comment: obj.comment,
|
||||
valid: obj.valid ?? undefined,
|
||||
connectionId,
|
||||
database,
|
||||
schema: childSchema,
|
||||
|
|
|
|||
|
|
@ -377,12 +377,13 @@ export interface TableInfo {
|
|||
parent_name?: string | null;
|
||||
}
|
||||
|
||||
export type DatabaseObjectType = "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY";
|
||||
export type DatabaseObjectType = "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "TRIGGER" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY" | "TYPE" | "TYPE_BODY";
|
||||
|
||||
export interface ObjectInfo {
|
||||
name: string;
|
||||
object_type: DatabaseObjectType | string;
|
||||
schema?: string | null;
|
||||
valid?: boolean | null;
|
||||
signature?: string | null;
|
||||
comment?: string | null;
|
||||
created_at?: string | null;
|
||||
|
|
@ -398,7 +399,7 @@ export interface ObjectStatistics {
|
|||
total_bytes?: number | null;
|
||||
}
|
||||
|
||||
export type ObjectSourceKind = "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY";
|
||||
export type ObjectSourceKind = "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "TRIGGER" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY" | "TYPE" | "TYPE_BODY";
|
||||
|
||||
export interface ObjectSource {
|
||||
name: string;
|
||||
|
|
@ -616,6 +617,8 @@ export type TreeNodeType =
|
|||
| "materialized_view"
|
||||
| "procedure"
|
||||
| "function"
|
||||
| "type"
|
||||
| "type-body"
|
||||
| "sequence"
|
||||
| "package"
|
||||
| "package-body"
|
||||
|
|
@ -628,6 +631,7 @@ export type TreeNodeType =
|
|||
| "group-materialized-views"
|
||||
| "group-procedures"
|
||||
| "group-functions"
|
||||
| "group-types"
|
||||
| "group-sequences"
|
||||
| "group-packages"
|
||||
| "group-partitions"
|
||||
|
|
@ -697,6 +701,7 @@ export interface TreeNode {
|
|||
signature?: string;
|
||||
tableType?: string;
|
||||
comment?: string | null;
|
||||
valid?: boolean | null;
|
||||
objectCount?: number;
|
||||
loadedKeyCount?: number;
|
||||
totalKeyCount?: number;
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ pub async fn list_objects(pool: &MySqlPool, database: &str) -> Result<Vec<Object
|
|||
name: table.name,
|
||||
object_type: "TABLE".to_string(),
|
||||
schema: Some(database.to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: table.comment,
|
||||
created_at: None,
|
||||
|
|
@ -100,6 +101,7 @@ fn plugin_object(
|
|||
name: name.to_string(),
|
||||
object_type: "FUNCTION".to_string(),
|
||||
schema: Some(database.to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: if comment_parts.is_empty() { None } else { Some(comment_parts.join(", ")) },
|
||||
created_at: None,
|
||||
|
|
|
|||
|
|
@ -2148,6 +2148,7 @@ fn row_to_object(row: &mysql_async::Row, database: &str) -> ObjectInfo {
|
|||
name: get_str_by_name(row, "object_name"),
|
||||
object_type: get_str_by_name(row, "object_type"),
|
||||
schema: Some(database.to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: get_opt_str(row, "object_comment")
|
||||
.map(|s| fix_potential_double_encoding(&s))
|
||||
|
|
@ -2283,6 +2284,7 @@ pub async fn list_table_objects_show(pool: &MySqlPool, database: &str) -> Result
|
|||
name: table.name,
|
||||
object_type: if table.table_type.eq_ignore_ascii_case("VIEW") { "VIEW" } else { "TABLE" }.to_string(),
|
||||
schema: Some(database.to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: table.comment,
|
||||
created_at: meta.and_then(|meta| meta.created_at.clone()),
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ pub async fn list_objects(pool: &mysql_async::Pool, schema: &str) -> Result<Vec<
|
|||
name: get_str(row, 0),
|
||||
object_type: get_str(row, 1),
|
||||
schema: Some(schema.to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
|
|
|
|||
|
|
@ -2125,6 +2125,7 @@ pub async fn list_objects(pool: &Pool, schema: &str) -> Result<Vec<ObjectInfo>,
|
|||
name: pg_row_try_string(row, 0),
|
||||
object_type: pg_row_try_string(row, 1),
|
||||
schema: Some(schema.to_string()),
|
||||
valid: None,
|
||||
comment: row.try_get::<_, Option<String>>(2).ok().flatten().filter(|s| !s.is_empty()),
|
||||
created_at: row.try_get::<_, Option<String>>(3).ok().flatten().filter(|s| !s.is_empty()),
|
||||
updated_at: row.try_get::<_, Option<String>>(4).ok().flatten().filter(|s| !s.is_empty()),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ pub async fn list_objects(pool: &Pool, schema: &str) -> Result<Vec<ObjectInfo>,
|
|||
name: t.name.clone(),
|
||||
object_type: t.table_type.clone(),
|
||||
schema: None,
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: t.comment.clone(),
|
||||
created_at: None,
|
||||
|
|
|
|||
|
|
@ -1330,6 +1330,7 @@ pub async fn list_objects(client: &mut SqlServerClient, schema: &str) -> Result<
|
|||
name: row.get::<&str, _>(0).unwrap_or("").to_string(),
|
||||
object_type: row.get::<&str, _>(1).unwrap_or("TABLE").to_string(),
|
||||
schema: Some(schema.to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: row.get::<&str, _>(4).filter(|s: &&str| !s.is_empty()).map(|s: &str| s.to_string()),
|
||||
created_at: row.get::<chrono::NaiveDateTime, _>(2).map(|value| value.to_string()),
|
||||
|
|
|
|||
|
|
@ -320,9 +320,12 @@ fn object_type_keyword(object_type: &ObjectSourceKind) -> &'static str {
|
|||
ObjectSourceKind::MaterializedView => "MATERIALIZED_VIEW",
|
||||
ObjectSourceKind::Procedure => "PROCEDURE",
|
||||
ObjectSourceKind::Function => "FUNCTION",
|
||||
ObjectSourceKind::Trigger => "TRIGGER",
|
||||
ObjectSourceKind::Sequence => "SEQUENCE",
|
||||
ObjectSourceKind::Package => "PACKAGE",
|
||||
ObjectSourceKind::PackageBody => "PACKAGE BODY",
|
||||
ObjectSourceKind::Type => "TYPE",
|
||||
ObjectSourceKind::TypeBody => "TYPE BODY",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -819,12 +822,18 @@ fn parse_object_source_kind(value: &str) -> Option<ObjectSourceKind> {
|
|||
Some(ObjectSourceKind::Procedure)
|
||||
} else if value.eq_ignore_ascii_case("FUNCTION") {
|
||||
Some(ObjectSourceKind::Function)
|
||||
} else if value.eq_ignore_ascii_case("TRIGGER") {
|
||||
Some(ObjectSourceKind::Trigger)
|
||||
} else if value.eq_ignore_ascii_case("SEQUENCE") {
|
||||
Some(ObjectSourceKind::Sequence)
|
||||
} else if value.eq_ignore_ascii_case("PACKAGE") {
|
||||
Some(ObjectSourceKind::Package)
|
||||
} else if value.eq_ignore_ascii_case("PACKAGE BODY") || value.eq_ignore_ascii_case("PACKAGE_BODY") {
|
||||
Some(ObjectSourceKind::PackageBody)
|
||||
} else if value.eq_ignore_ascii_case("TYPE") {
|
||||
Some(ObjectSourceKind::Type)
|
||||
} else if value.eq_ignore_ascii_case("TYPE BODY") || value.eq_ignore_ascii_case("TYPE_BODY") {
|
||||
Some(ObjectSourceKind::TypeBody)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
@ -1251,6 +1260,14 @@ mod tests {
|
|||
assert_eq!(sql, "CREATE OR REPLACE PACKAGE BODY PAYROLL AS\nEND PAYROLL;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_programmable_metadata_object_kinds() {
|
||||
assert_eq!(parse_object_source_kind("TRIGGER"), Some(ObjectSourceKind::Trigger));
|
||||
assert_eq!(parse_object_source_kind("TYPE"), Some(ObjectSourceKind::Type));
|
||||
assert_eq!(parse_object_source_kind("TYPE_BODY"), Some(ObjectSourceKind::TypeBody));
|
||||
assert_eq!(parse_object_source_kind("PACKAGE BODY"), Some(ObjectSourceKind::PackageBody));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postgres_procedure_rename_adds_drop_cleanup() {
|
||||
let statements = build_executable_object_source_statements(input(
|
||||
|
|
|
|||
|
|
@ -2219,6 +2219,7 @@ async fn external_driver_presto_like_objects(
|
|||
name: table.name,
|
||||
object_type: table.table_type,
|
||||
schema: Some(schema.to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: table.comment,
|
||||
created_at: None,
|
||||
|
|
@ -2601,6 +2602,7 @@ mod tests {
|
|||
name: name.to_string(),
|
||||
object_type: object_type.to_string(),
|
||||
schema: Some("app".to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
|
|
@ -3318,6 +3320,7 @@ mod tests {
|
|||
name: "ORDERS".to_string(),
|
||||
object_type: "TABLE".to_string(),
|
||||
schema: Some("DBX_TEST".to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
|
|
@ -3329,6 +3332,7 @@ mod tests {
|
|||
name: "ORDERS_VIEW".to_string(),
|
||||
object_type: "VIEW".to_string(),
|
||||
schema: Some("DBX_TEST".to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
|
|
@ -3340,6 +3344,7 @@ mod tests {
|
|||
name: "REFRESH_ORDERS".to_string(),
|
||||
object_type: "PROCEDURE".to_string(),
|
||||
schema: Some("DBX_TEST".to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
|
|
@ -3763,6 +3768,7 @@ async fn list_objects_once(
|
|||
name: table.name,
|
||||
object_type: table.table_type,
|
||||
schema: None,
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: table.comment,
|
||||
created_at: None,
|
||||
|
|
@ -3940,6 +3946,7 @@ async fn list_objects_once(
|
|||
name: table.name,
|
||||
object_type: table.table_type,
|
||||
schema: if schema.is_empty() { None } else { Some(schema.to_string()) },
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: table.comment,
|
||||
created_at: None,
|
||||
|
|
@ -4920,9 +4927,12 @@ fn sqlite_object_type(kind: &db::ObjectSourceKind) -> &'static str {
|
|||
db::ObjectSourceKind::View | db::ObjectSourceKind::MaterializedView => "view",
|
||||
db::ObjectSourceKind::Procedure
|
||||
| db::ObjectSourceKind::Function
|
||||
| db::ObjectSourceKind::Trigger
|
||||
| db::ObjectSourceKind::Sequence
|
||||
| db::ObjectSourceKind::Package
|
||||
| db::ObjectSourceKind::PackageBody => "routine",
|
||||
| db::ObjectSourceKind::PackageBody
|
||||
| db::ObjectSourceKind::Type
|
||||
| db::ObjectSourceKind::TypeBody => "routine",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4931,9 +4941,12 @@ fn sqlserver_object_type_filter(kind: &db::ObjectSourceKind) -> &'static str {
|
|||
db::ObjectSourceKind::View => "'V'",
|
||||
db::ObjectSourceKind::Procedure => "'P'",
|
||||
db::ObjectSourceKind::Function => "'FN','IF','TF','FS','FT'",
|
||||
db::ObjectSourceKind::Trigger => "'TR'",
|
||||
db::ObjectSourceKind::Sequence
|
||||
| db::ObjectSourceKind::Package
|
||||
| db::ObjectSourceKind::PackageBody
|
||||
| db::ObjectSourceKind::Type
|
||||
| db::ObjectSourceKind::TypeBody
|
||||
| db::ObjectSourceKind::MaterializedView => "''",
|
||||
}
|
||||
}
|
||||
|
|
@ -5055,7 +5068,11 @@ fn postgres_object_source_sql_inner(
|
|||
sql_string(name)
|
||||
)
|
||||
}
|
||||
db::ObjectSourceKind::Package | db::ObjectSourceKind::PackageBody => "SELECT NULL WHERE FALSE".to_string(),
|
||||
db::ObjectSourceKind::Trigger
|
||||
| db::ObjectSourceKind::Package
|
||||
| db::ObjectSourceKind::PackageBody
|
||||
| db::ObjectSourceKind::Type
|
||||
| db::ObjectSourceKind::TypeBody => "SELECT NULL WHERE FALSE".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5065,9 +5082,12 @@ pub fn oracle_object_source_sql(schema: &str, name: &str, kind: &db::ObjectSourc
|
|||
db::ObjectSourceKind::MaterializedView => "MATERIALIZED_VIEW",
|
||||
db::ObjectSourceKind::Procedure => "PROCEDURE",
|
||||
db::ObjectSourceKind::Function => "FUNCTION",
|
||||
db::ObjectSourceKind::Trigger => "TRIGGER",
|
||||
db::ObjectSourceKind::Sequence => "SEQUENCE",
|
||||
db::ObjectSourceKind::Package => "PACKAGE",
|
||||
db::ObjectSourceKind::PackageBody => "PACKAGE_BODY",
|
||||
db::ObjectSourceKind::Type => "TYPE",
|
||||
db::ObjectSourceKind::TypeBody => "TYPE_BODY",
|
||||
};
|
||||
if schema.trim().is_empty() {
|
||||
format!("SELECT DBMS_METADATA.GET_DDL({}, {}) FROM DUAL", sql_string(object_type), sql_string(name))
|
||||
|
|
@ -5095,9 +5115,12 @@ pub fn mysql_object_source_sql(database: &str, name: &str, kind: &db::ObjectSour
|
|||
db::ObjectSourceKind::View => format!("SHOW CREATE VIEW {qualified_name}"),
|
||||
db::ObjectSourceKind::Procedure => format!("SHOW CREATE PROCEDURE {qualified_name}"),
|
||||
db::ObjectSourceKind::Function => format!("SHOW CREATE FUNCTION {qualified_name}"),
|
||||
db::ObjectSourceKind::Sequence
|
||||
db::ObjectSourceKind::Trigger
|
||||
| db::ObjectSourceKind::Sequence
|
||||
| db::ObjectSourceKind::Package
|
||||
| db::ObjectSourceKind::PackageBody
|
||||
| db::ObjectSourceKind::Type
|
||||
| db::ObjectSourceKind::TypeBody
|
||||
| db::ObjectSourceKind::MaterializedView => String::new(),
|
||||
}
|
||||
}
|
||||
|
|
@ -5313,6 +5336,7 @@ async fn oracle_agent_list_objects(
|
|||
name,
|
||||
object_type,
|
||||
schema,
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
|
|
|
|||
|
|
@ -235,6 +235,7 @@ mod tests {
|
|||
name: "orders".to_string(),
|
||||
object_type: "BASE TABLE".to_string(),
|
||||
schema: None,
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
|
|
@ -246,6 +247,7 @@ mod tests {
|
|||
name: "active_orders".to_string(),
|
||||
object_type: "MATERIALIZED_VIEW".to_string(),
|
||||
schema: None,
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
|
|
@ -257,6 +259,7 @@ mod tests {
|
|||
name: "payroll".to_string(),
|
||||
object_type: "PACKAGE BODY".to_string(),
|
||||
schema: None,
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
|
|
@ -308,6 +311,7 @@ mod tests {
|
|||
name: "ORDERS".to_string(),
|
||||
object_type: "TABLE".to_string(),
|
||||
schema: Some("HR".to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
|
|
@ -319,6 +323,7 @@ mod tests {
|
|||
name: "bin$deleted".to_string(),
|
||||
object_type: "TABLE".to_string(),
|
||||
schema: Some("HR".to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: None,
|
||||
created_at: None,
|
||||
|
|
|
|||
|
|
@ -4594,6 +4594,9 @@ where
|
|||
db::ObjectSourceKind::Sequence | db::ObjectSourceKind::Package | db::ObjectSourceKind::PackageBody => {
|
||||
object.source.clone()
|
||||
}
|
||||
db::ObjectSourceKind::Trigger | db::ObjectSourceKind::Type | db::ObjectSourceKind::TypeBody => {
|
||||
object.source.clone()
|
||||
}
|
||||
};
|
||||
let statements = build_executable_object_source_statements(EditableObjectSourceSqlInput {
|
||||
database_type: DatabaseType::Postgres,
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ pub struct ObjectInfo {
|
|||
pub object_type: String,
|
||||
pub schema: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub valid: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub signature: Option<String>,
|
||||
pub comment: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
|
|
@ -93,9 +95,12 @@ pub enum ObjectSourceKind {
|
|||
MaterializedView,
|
||||
Procedure,
|
||||
Function,
|
||||
Trigger,
|
||||
Sequence,
|
||||
Package,
|
||||
PackageBody,
|
||||
Type,
|
||||
TypeBody,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -310,3 +315,18 @@ pub struct OwnerInfo {
|
|||
pub object_type: String,
|
||||
pub owner: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ObjectInfo;
|
||||
|
||||
#[test]
|
||||
fn list_objects_payload_preserves_optional_validity() {
|
||||
let objects: Vec<ObjectInfo> =
|
||||
serde_json::from_str(r#"[{"name":"TRG_AUDIT","object_type":"TRIGGER","schema":"APP","valid":false}]"#)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(objects[0].valid, Some(false));
|
||||
assert_eq!(objects[0].object_type, "TRIGGER");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,6 +194,7 @@ pub async fn list_objects(
|
|||
name: table.name,
|
||||
object_type: table.table_type,
|
||||
schema: Some(database.to_string()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: table.comment,
|
||||
created_at: None,
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ pub async fn list_objects(
|
|||
name: table.name,
|
||||
object_type: table.table_type,
|
||||
schema: Some(database.clone()),
|
||||
valid: None,
|
||||
signature: None,
|
||||
comment: table.comment,
|
||||
created_at: None,
|
||||
|
|
|
|||
Loading…
Reference in New Issue