diff --git a/agents/drivers/xugu/main.go b/agents/drivers/xugu/main.go index 218147bd9..75adada99 100644 --- a/agents/drivers/xugu/main.go +++ b/agents/drivers/xugu/main.go @@ -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) } diff --git a/agents/drivers/xugu/main_test.go b/agents/drivers/xugu/main_test.go index 17e61cf45..aebcbc397 100644 --- a/agents/drivers/xugu/main_test.go +++ b/agents/drivers/xugu/main_test.go @@ -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 } diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 2c541a5df..745ed0cbd 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -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 = { 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, diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index 13949ed99..7936f80cb 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -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(() => ["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); } diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index dfa56c402..afab9270c 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -154,9 +154,9 @@ watch(deferredSearchQuery, (newQuery, oldQuery) => { .catch(() => {}); }); -const searchableObjectGroupTypes = new Set(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages"]); +const searchableObjectGroupTypes = new Set(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-sequences", "group-packages", "group-types"]); const simpleObjectParentTypes = new Set(["database", "schema", "linked-server-schema"]); -const simpleObjectChildTypes = new Set(["table", "view", "materialized_view", "procedure", "function", "sequence", "package", "package-body", "load-more"]); +const simpleObjectChildTypes = new Set(["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()); diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index 08d82c392..ea594e1fa 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -445,7 +445,22 @@ function hasNodeDatabaseContext(node: TreeNode): node is TreeNode & { connection return !!node.connectionId && hasTreeNodeDatabaseContext(node); } -const groupTypes: Set = 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 = 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 }); diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index befc8fb09..61017e6dc 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -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 = 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 = 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 = { diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 8b473ba54..eff5fe172 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -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", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 13cb9bcf5..4ca23a2ab 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -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: { diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 2f59f3b6f..d435467b4 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -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: { diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 98da1d245..8b9a505fb 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -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: { diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 7011807f6..740499f67 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -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: { diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index b7c3c6adc..0d01710dc 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -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: "行数", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index fbea2c09b..c416c875f 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -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: { diff --git a/apps/desktop/src/lib/__tests__/table/objectBrowserRowAction.spec.ts b/apps/desktop/src/lib/__tests__/table/objectBrowserRowAction.spec.ts index c595ef2ec..4dc91001f 100644 --- a/apps/desktop/src/lib/__tests__/table/objectBrowserRowAction.spec.ts +++ b/apps/desktop/src/lib/__tests__/table/objectBrowserRowAction.spec.ts @@ -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"); diff --git a/apps/desktop/src/lib/__tests__/table/tableTree.spec.ts b/apps/desktop/src/lib/__tests__/table/tableTree.spec.ts index 367883e5c..24f4db7c5 100644 --- a/apps/desktop/src/lib/__tests__/table/tableTree.spec.ts +++ b/apps/desktop/src/lib/__tests__/table/tableTree.spec.ts @@ -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({ diff --git a/apps/desktop/src/lib/database/databaseObjectCapabilities.ts b/apps/desktop/src/lib/database/databaseObjectCapabilities.ts index dc4e134a2..e915aa0b3 100644 --- a/apps/desktop/src/lib/database/databaseObjectCapabilities.ts +++ b/apps/desktop/src/lib/database/databaseObjectCapabilities.ts @@ -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([ // postgres @@ -31,6 +32,7 @@ const DATABASE_TYPE_OBJECTS = new Map([ ["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"; diff --git a/apps/desktop/src/lib/sidebar/sidebarTreeItemLayout.ts b/apps/desktop/src/lib/sidebar/sidebarTreeItemLayout.ts index 3a960c13e..3c47ced40 100644 --- a/apps/desktop/src/lib/sidebar/sidebarTreeItemLayout.ts +++ b/apps/desktop/src/lib/sidebar/sidebarTreeItemLayout.ts @@ -9,6 +9,8 @@ const leafTypes: Set = new Set([ "function", "package", "package-body", + "type", + "type-body", "object-browser", "redis-db", "mq-tenant", diff --git a/apps/desktop/src/lib/sidebar/treeNodeClick.ts b/apps/desktop/src/lib/sidebar/treeNodeClick.ts index dd1328eee..4ff5d46d0 100644 --- a/apps/desktop/src/lib/sidebar/treeNodeClick.ts +++ b/apps/desktop/src/lib/sidebar/treeNodeClick.ts @@ -10,19 +10,22 @@ const dataNodeTypes = new Set(["table", "view", "materialized_view const documentBrowserNodeTypes = new Set(["mongo-collection", "mongo-bucket"]); const toggleLeafNodeTypes = new Set(["redis-db", "mq-tenant", "etcd-root", "zookeeper-root", "mongo-gridfs", "mongo-collection", "mongo-bucket", "vector-collection", "elasticsearch-index", "user-admin"]); const objectBrowserNodeTypes = new Set(["database", "schema", "object-browser"]); -const sourceNodeTypes = new Set(["materialized_view", "procedure", "function", "sequence", "package", "package-body"]); +const sourceNodeTypes = new Set(["materialized_view", "procedure", "function", "trigger", "sequence", "package", "package-body", "type", "type-body"]); const savedSqlNodeTypes = new Set(["saved-sql-file"]); const tableChildGroupNodeTypes = new Set(["group-columns", "group-indexes", "group-fkeys", "group-triggers", "group-partitions"]); -const databaseChildGroupNodeTypes = new Set(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages"]); +const databaseChildGroupNodeTypes = new Set(["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; } diff --git a/apps/desktop/src/lib/table/objectBrowserRowAction.ts b/apps/desktop/src/lib/table/objectBrowserRowAction.ts index 59e463807..2d141f974 100644 --- a/apps/desktop/src/lib/table/objectBrowserRowAction.ts +++ b/apps/desktop/src/lib/table/objectBrowserRowAction.ts @@ -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"; } diff --git a/apps/desktop/src/lib/table/objectBrowserRows.ts b/apps/desktop/src/lib/table/objectBrowserRows.ts index efdcaa81d..f153a49e2 100644 --- a/apps/desktop/src/lib/table/objectBrowserRows.ts +++ b/apps/desktop/src/lib/table/objectBrowserRows.ts @@ -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, diff --git a/apps/desktop/src/lib/table/tableTree.ts b/apps/desktop/src/lib/table/tableTree.ts index 8f6a3db3b..d45de7699 100644 --- a/apps/desktop/src/lib/table/tableTree.ts +++ b/apps/desktop/src/lib/table/tableTree.ts @@ -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(["group-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages"]); +const objectGroupNodeTypes = new Set(["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, diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index c3088274e..ed110cd08 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -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; diff --git a/crates/dbx-core/src/db/manticoresearch.rs b/crates/dbx-core/src/db/manticoresearch.rs index 3e5abad49..b0faec754 100644 --- a/crates/dbx-core/src/db/manticoresearch.rs +++ b/crates/dbx-core/src/db/manticoresearch.rs @@ -40,6 +40,7 @@ pub async fn list_objects(pool: &MySqlPool, database: &str) -> Result 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()), diff --git a/crates/dbx-core/src/db/ob_oracle.rs b/crates/dbx-core/src/db/ob_oracle.rs index cd1b5d226..f6df03477 100644 --- a/crates/dbx-core/src/db/ob_oracle.rs +++ b/crates/dbx-core/src/db/ob_oracle.rs @@ -104,6 +104,7 @@ pub async fn list_objects(pool: &mysql_async::Pool, schema: &str) -> Result Result, 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>(2).ok().flatten().filter(|s| !s.is_empty()), created_at: row.try_get::<_, Option>(3).ok().flatten().filter(|s| !s.is_empty()), updated_at: row.try_get::<_, Option>(4).ok().flatten().filter(|s| !s.is_empty()), diff --git a/crates/dbx-core/src/db/questdb.rs b/crates/dbx-core/src/db/questdb.rs index 1894c3ac9..672290b2b 100644 --- a/crates/dbx-core/src/db/questdb.rs +++ b/crates/dbx-core/src/db/questdb.rs @@ -12,6 +12,7 @@ pub async fn list_objects(pool: &Pool, schema: &str) -> Result, name: t.name.clone(), object_type: t.table_type.clone(), schema: None, + valid: None, signature: None, comment: t.comment.clone(), created_at: None, diff --git a/crates/dbx-core/src/db/sqlserver.rs b/crates/dbx-core/src/db/sqlserver.rs index 09cb37c08..fb7147c77 100644 --- a/crates/dbx-core/src/db/sqlserver.rs +++ b/crates/dbx-core/src/db/sqlserver.rs @@ -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::(2).map(|value| value.to_string()), diff --git a/crates/dbx-core/src/object_source_sql.rs b/crates/dbx-core/src/object_source_sql.rs index c41af54e4..21f7fdd52 100644 --- a/crates/dbx-core/src/object_source_sql.rs +++ b/crates/dbx-core/src/object_source_sql.rs @@ -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 { 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( diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs index c7b414578..2f2804f29 100644 --- a/crates/dbx-core/src/schema.rs +++ b/crates/dbx-core/src/schema.rs @@ -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, diff --git a/crates/dbx-core/src/schema/normalization.rs b/crates/dbx-core/src/schema/normalization.rs index 3bee666d1..c1d4c1d7d 100644 --- a/crates/dbx-core/src/schema/normalization.rs +++ b/crates/dbx-core/src/schema/normalization.rs @@ -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, diff --git a/crates/dbx-core/src/transfer.rs b/crates/dbx-core/src/transfer.rs index 9f36d151c..3f76ee726 100644 --- a/crates/dbx-core/src/transfer.rs +++ b/crates/dbx-core/src/transfer.rs @@ -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, diff --git a/crates/dbx-core/src/types.rs b/crates/dbx-core/src/types.rs index 108277443..7515676d3 100644 --- a/crates/dbx-core/src/types.rs +++ b/crates/dbx-core/src/types.rs @@ -62,6 +62,8 @@ pub struct ObjectInfo { pub object_type: String, pub schema: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub valid: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub signature: Option, pub comment: Option, pub created_at: Option, @@ -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 = + 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"); + } +} diff --git a/crates/dbx-web/src/routes/schema.rs b/crates/dbx-web/src/routes/schema.rs index 39c30a34d..865f44806 100644 --- a/crates/dbx-web/src/routes/schema.rs +++ b/crates/dbx-web/src/routes/schema.rs @@ -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, diff --git a/src-tauri/src/commands/schema.rs b/src-tauri/src/commands/schema.rs index 6b0fbc37d..5f667be71 100644 --- a/src-tauri/src/commands/schema.rs +++ b/src-tauri/src/commands/schema.rs @@ -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,