diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index f8edd23a47..388d7a52ae 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -2161,12 +2161,13 @@ 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 === "trigger" || item.type === "sequence" || item.type === "package" || item.type === "package-body" || item.type === "type" || item.type === "type-body") { + } else if (item.type === "procedure" || item.type === "function" || item.type === "trigger" || item.type === "event" || 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", + event: "EVENT", sequence: "SEQUENCE", package: "PACKAGE", "package-body": "PACKAGE_BODY", @@ -2193,7 +2194,7 @@ async function handleQuickOpenSelect(item: any) { }); const tabId = queryStore.createTab(item.connectionId, item.database, `Source - ${objectName}`); queryStore.updateSql(tabId, editableSource); - if (item.type !== "sequence" && item.type !== "trigger" && item.type !== "type" && item.type !== "type-body") { + if (item.type !== "sequence" && item.type !== "trigger" && item.type !== "event" && item.type !== "type" && item.type !== "type-body") { queryStore.setObjectSource(tabId, { schema, name: objectName, diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index 141c57faea..c94a455ce5 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -317,9 +317,9 @@ watch([deferredSearchQuery, regexMode], ([newQuery, isRegexMode], [oldQuery, was }); }); -const searchableObjectGroupTypes = new Set(["group-tables", "group-dolt-system-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-sequences", "group-synonyms", "group-packages", "group-types"]); +const searchableObjectGroupTypes = new Set(["group-tables", "group-dolt-system-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-events", "group-sequences", "group-synonyms", "group-packages", "group-types"]); const simpleObjectParentTypes = new Set(["database", "schema", "linked-server-schema"]); -const simpleObjectChildTypes = new Set(["table", "view", "materialized_view", "procedure", "function", "trigger", "sequence", "synonym", "package", "package-body", "type", "type-body", "load-more"]); +const simpleObjectChildTypes = new Set(["table", "view", "materialized_view", "procedure", "function", "trigger", "event", "sequence", "synonym", "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 14fa4c96a2..0a59a8e9ec 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -593,6 +593,7 @@ const groupTypes: Set = new Set([ "group-indexes", "group-fkeys", "group-triggers", + "group-events", "group-constraints", "group-table-partitions", "group-table-subpartitions", @@ -5252,7 +5253,7 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean { return true; } - if (node.type === "sequence") { + if (node.type === "sequence" || node.type === "event") { items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 }); items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value }); items.push({ label: t("contextMenu.changeOpenMode"), action: () => emit("open-settings", "navigation"), icon: Settings2 }); diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 8bbd9a78cc..56d614c1c8 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -16,6 +16,7 @@ import { Link, Link2, Zap, + Clock, ListTree, FileCode, Network, @@ -252,6 +253,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null { return { icon: Link, colorClass: "text-blue-400" }; case "group-triggers": return { icon: Zap, colorClass: "text-orange-400" }; + case "group-events": + return { icon: Clock, colorClass: "text-orange-400" }; case "group-constraints": return { icon: Key, colorClass: "text-amber-500" }; case "group-table-partitions": @@ -279,6 +282,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null { return { icon: Link, colorClass: "text-blue-300" }; case "trigger": return { icon: Zap, colorClass: "text-orange-300" }; + case "event": + return { icon: Clock, colorClass: "text-orange-300" }; case "redis-db": return { icon: Database, colorClass: "text-red-400" }; case "mq-tenant": @@ -1418,6 +1423,7 @@ function onKeydown(event: KeyboardEvent) { node.type === 'group-procedures' || node.type === 'group-functions' || node.type === 'group-triggers' || + node.type === 'group-events' || node.type === 'group-sequences' || node.type === 'group-synonyms' || node.type === 'group-packages' || diff --git a/apps/desktop/src/components/sidebar/__tests__/TreeItem.loadMoreActivation.spec.ts b/apps/desktop/src/components/sidebar/__tests__/TreeItem.loadMoreActivation.spec.ts index f68731e4d9..dfe052b7c3 100644 --- a/apps/desktop/src/components/sidebar/__tests__/TreeItem.loadMoreActivation.spec.ts +++ b/apps/desktop/src/components/sidebar/__tests__/TreeItem.loadMoreActivation.spec.ts @@ -431,6 +431,24 @@ describe("TreeItem load-more activation", () => { }); }); +describe("TreeItem object counts", () => { + it("renders the event group count", async () => { + const node: TreeNode = { + id: "connection-1:app:__events", + label: "tree.events", + type: "group-events", + connectionId: "connection-1", + database: "app", + objectCount: 17, + children: [], + }; + + const { row } = await mountTreeItem(node); + + expect(row.textContent).toContain("17"); + }); +}); + describe("TreeItem searched connection activation", () => { function searchedConnection(): TreeNode { const filtered = filterSidebarTree( diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 7929f88b03..88f0596357 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -3027,6 +3027,7 @@ export default { indexes: "Indexes", foreignKeys: "Foreign Keys", triggers: "Triggers", + events: "Events", constraints: "Constraints", tables: "Tables", doltSystemTables: "Dolt System Tables", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 9ff6df759e..0006a45848 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -2956,6 +2956,7 @@ export default withEnglishFallback({ indexes: "Índices", foreignKeys: "Claves foráneas", triggers: "Disparadores", + events: "Eventos", constraints: "Restricciones", tables: "Tablas", tableNameFilterActive: "filtrado", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 7bedbb4daf..cfa2df850f 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -2954,6 +2954,7 @@ export default withEnglishFallback({ indexes: "Indici", foreignKeys: "Chiavi Esterne", triggers: "Trigger", + events: "Eventi", constraints: "Vincoli", tables: "Tabelle", tableNameFilterActive: "filtrato", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 6f8b68f8b0..08a4828da2 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -2980,6 +2980,7 @@ export default withEnglishFallback({ indexes: "インデックス", foreignKeys: "外部キー", triggers: "トリガー", + events: "イベント", constraints: "制約", tables: "テーブル", tableNameFilterActive: "フィルターあり", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index b44901e160..b7d2ac9c17 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -2859,6 +2859,7 @@ export default withEnglishFallback({ indexes: "인덱스", foreignKeys: "외래 키", triggers: "트리거", + events: "이벤트", constraints: "제약 조건", tables: "테이블", tableNameFilterActive: "필터됨", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 7d1e6d9503..41757dd704 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -2957,6 +2957,7 @@ export default withEnglishFallback({ indexes: "Índices", foreignKeys: "Chaves estrangeiras", triggers: "Triggers", + events: "Eventos", constraints: "Restrições", tables: "Tabelas", tableNameFilterActive: "filtrado", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index aa800a6878..14acaf22bf 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -2951,6 +2951,7 @@ export default withEnglishFallback({ indexes: "索引", foreignKeys: "外键", triggers: "触发器", + events: "事件", constraints: "约束", tables: "表", doltSystemTables: "Dolt 系统表", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 1cff1c284d..d3c168c474 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -2956,6 +2956,7 @@ export default withEnglishFallback({ indexes: "索引", foreignKeys: "外鍵", triggers: "觸發器", + events: "事件", constraints: "約束", tables: "資料表", doltSystemTables: "Dolt 系統資料表", diff --git a/apps/desktop/src/lib/__tests__/database/databaseObjectCapabilities.spec.ts b/apps/desktop/src/lib/__tests__/database/databaseObjectCapabilities.spec.ts index b11f3bc5d1..0e1b6e7863 100644 --- a/apps/desktop/src/lib/__tests__/database/databaseObjectCapabilities.spec.ts +++ b/apps/desktop/src/lib/__tests__/database/databaseObjectCapabilities.spec.ts @@ -39,8 +39,8 @@ describe("databaseObjectCapabilities", () => { }); it("exposes triggers only for native MySQL among MySQL-compatible paths", () => { - expect(sidebarObjectKindsForDatabase("mysql")).toEqual(["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "TRIGGER"]); - expect(databaseObjectCapabilities("mysql").sourceReadable).toEqual(["VIEW", "PROCEDURE", "FUNCTION", "TRIGGER"]); + expect(sidebarObjectKindsForDatabase("mysql")).toEqual(["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "EVENT"]); + expect(databaseObjectCapabilities("mysql").sourceReadable).toEqual(["VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "EVENT"]); expect(sidebarObjectKindsForDatabase("doris")).toEqual(["TABLE", "VIEW"]); expect(sidebarObjectKindsForDatabase("starrocks")).toEqual(["TABLE", "VIEW", "MATERIALIZED_VIEW"]); diff --git a/apps/desktop/src/lib/__tests__/table/tableTree.spec.ts b/apps/desktop/src/lib/__tests__/table/tableTree.spec.ts index d2a499abee..b42e560133 100644 --- a/apps/desktop/src/lib/__tests__/table/tableTree.spec.ts +++ b/apps/desktop/src/lib/__tests__/table/tableTree.spec.ts @@ -248,6 +248,16 @@ describe("programmable database objects", () => { ]); }); + it("groups MySQL scheduled events under a dedicated Events group", () => { + const objects: ObjectInfo[] = [{ name: "event_daily_middle_db_sync", object_type: "EVENT", schema: "shop" }]; + + const groups = buildGroupedObjectTreeNodes({ ...context, schema: "shop", objects, databaseType: "mysql" }); + const eventGroup = groups.find((node) => node.type === "group-events"); + + expect(eventGroup).toEqual(expect.objectContaining({ objectCount: 1, label: "tree.events" })); + expect(eventGroup?.children).toEqual([expect.objectContaining({ type: "event", objectName: "event_daily_middle_db_sync" })]); + }); + it("keeps table-scoped Kingbase triggers distinct and source-addressable", () => { const objects: ObjectInfo[] = [ { name: "audit_before", object_type: "TRIGGER", schema: "public", parent_schema: "public", parent_name: "items" }, diff --git a/apps/desktop/src/lib/database/databaseObjectCapabilities.ts b/apps/desktop/src/lib/database/databaseObjectCapabilities.ts index f0bc8e4690..9e76473ca5 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" | "TRIGGER" | "SEQUENCE" | "SYNONYM" | "PACKAGE" | "PACKAGE_BODY" | "TYPE" | "TYPE_BODY"; +export type SidebarObjectKind = "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "TRIGGER" | "EVENT" | "SEQUENCE" | "SYNONYM" | "PACKAGE" | "PACKAGE_BODY" | "TYPE" | "TYPE_BODY"; export interface DatabaseObjectCapabilities { sidebarObjects: SidebarObjectKind[]; @@ -12,7 +12,7 @@ const TABLE_VIEW_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW"]; const TABLE_VIEW_MV_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW"]; const ROUTINE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION"]; -const MYSQL_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "TRIGGER"]; +const MYSQL_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "EVENT"]; // PostgreSQL-family databases with a verified pg_type listing path. TYPE only // covers user-created types (enum/domain/composite/range/multirange/base); @@ -157,6 +157,7 @@ export function normalizeSidebarObjectKind(type: string): SidebarObjectKind { if (normalized.includes("TYPE_BODY")) return "TYPE_BODY"; if (normalized.includes("PACKAGE")) return "PACKAGE"; if (normalized.includes("TRIGGER")) return "TRIGGER"; + if (normalized.includes("EVENT")) return "EVENT"; if (normalized.includes("TYPE")) return "TYPE"; if (normalized.includes("MATERIALIZED_VIEW")) return "MATERIALIZED_VIEW"; if (value.includes("VIEW")) return "VIEW"; diff --git a/apps/desktop/src/lib/sidebar/treeNodeClick.ts b/apps/desktop/src/lib/sidebar/treeNodeClick.ts index 1033751386..4283a2782a 100644 --- a/apps/desktop/src/lib/sidebar/treeNodeClick.ts +++ b/apps/desktop/src/lib/sidebar/treeNodeClick.ts @@ -54,11 +54,11 @@ const objectBrowserNodeTypes = new Set(["database", "schema", "obj export function shouldOpenObjectBrowserOnSingleClick(type: TreeNodeType, enabled: boolean): boolean { return enabled && objectBrowserNodeTypes.has(type); } -const sourceNodeTypes = new Set(["materialized_view", "procedure", "function", "trigger", "sequence", "synonym", "package", "package-body", "type", "type-body"]); +const sourceNodeTypes = new Set(["materialized_view", "procedure", "function", "trigger", "event", "sequence", "synonym", "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-constraints", "group-partitions", "group-table-partitions", "group-table-subpartitions"]); -const databaseChildGroupNodeTypes = new Set(["group-tables", "group-dolt-system-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-sequences", "group-synonyms", "group-packages", "group-types"]); -const displayPathObjectNodeTypes = new Set(["table", "view", "materialized_view", "procedure", "function", "trigger"]); +const databaseChildGroupNodeTypes = new Set(["group-tables", "group-dolt-system-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-events", "group-sequences", "group-synonyms", "group-packages", "group-types"]); +const displayPathObjectNodeTypes = new Set(["table", "view", "materialized_view", "procedure", "function", "trigger", "event"]); export function objectSourceKindForTreeNode(type: TreeNodeType): ObjectSourceKind | null { if (type === "view") return "VIEW"; @@ -66,6 +66,7 @@ export function objectSourceKindForTreeNode(type: TreeNodeType): ObjectSourceKin if (type === "procedure") return "PROCEDURE"; if (type === "function") return "FUNCTION"; if (type === "trigger") return "TRIGGER"; + if (type === "event") return "EVENT"; if (type === "sequence") return "SEQUENCE"; if (type === "synonym") return "SYNONYM"; if (type === "package") return "PACKAGE"; diff --git a/apps/desktop/src/lib/sidebar/treeNodeGroup.ts b/apps/desktop/src/lib/sidebar/treeNodeGroup.ts index f6162c852c..543e1aa0ad 100644 --- a/apps/desktop/src/lib/sidebar/treeNodeGroup.ts +++ b/apps/desktop/src/lib/sidebar/treeNodeGroup.ts @@ -5,6 +5,7 @@ const treeGroupNodeTypes = new Set([ "group-indexes", "group-fkeys", "group-triggers", + "group-events", "group-constraints", "group-table-partitions", "group-table-subpartitions", diff --git a/apps/desktop/src/lib/sidebar/treeNodeIcon.ts b/apps/desktop/src/lib/sidebar/treeNodeIcon.ts index 70cbd30a6d..46f1a99bf5 100644 --- a/apps/desktop/src/lib/sidebar/treeNodeIcon.ts +++ b/apps/desktop/src/lib/sidebar/treeNodeIcon.ts @@ -1,5 +1,5 @@ import type { Component } from "vue"; -import { Archive, Braces, Columns3, Database, Eye, FileCode, FolderClosed, FolderOpen, Gauge, Key, Link, Link2, ListTree, Network, Package, Plus, ScrollText, Server, ShieldCheck, Table, TableProperties, UsersRound, Zap } from "@lucide/vue"; +import { Archive, Braces, Clock, Columns3, Database, Eye, FileCode, FolderClosed, FolderOpen, Gauge, Key, Link, Link2, ListTree, Network, Package, Plus, ScrollText, Server, ShieldCheck, Table, TableProperties, UsersRound, Zap } from "@lucide/vue"; import type { ColumnInfo, TreeNode } from "@/types/database"; export type TreeNodeIconInfo = { @@ -68,6 +68,10 @@ export function getTreeNodeIconInfo(node: TreeNode): TreeNodeIconInfo | null { return { icon: Zap, colorClass: "text-orange-400" }; case "trigger": return { icon: Zap, colorClass: "text-orange-300" }; + case "group-events": + return { icon: Clock, colorClass: "text-orange-400" }; + case "event": + return { icon: Clock, colorClass: "text-orange-300" }; case "group-constraints": case "constraint": return { icon: Key, colorClass: "text-amber-500" }; diff --git a/apps/desktop/src/lib/table/tableTree.ts b/apps/desktop/src/lib/table/tableTree.ts index 42f7999661..a250e159fa 100644 --- a/apps/desktop/src/lib/table/tableTree.ts +++ b/apps/desktop/src/lib/table/tableTree.ts @@ -634,7 +634,7 @@ export function buildSimpleObjectTreeNodes({ nodeId, connectionId, database, sch for (const obj of coalesceXuguPackageObjects(objects, databaseType)) { const objectType = normalizeObjectType(obj.object_type); - if (!["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "SEQUENCE", "SYNONYM", "PACKAGE", "PACKAGE_BODY", "TYPE", "TYPE_BODY"].includes(objectType)) { + if (!["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "EVENT", "SEQUENCE", "SYNONYM", "PACKAGE", "PACKAGE_BODY", "TYPE", "TYPE_BODY"].includes(objectType)) { continue; } @@ -700,6 +700,7 @@ function simpleObjectNodeType(objectType: DatabaseObjectTreeKind): TreeNodeType if (objectType === "PROCEDURE") return "procedure"; if (objectType === "FUNCTION") return "function"; if (objectType === "TRIGGER") return "trigger"; + if (objectType === "EVENT") return "event"; if (objectType === "SEQUENCE") return "sequence"; if (objectType === "SYNONYM") return "synonym"; if (objectType === "PACKAGE_BODY") return "package-body"; @@ -752,6 +753,13 @@ const groupDefs: Array<{ nodeType: "group-triggers", childType: "trigger", }, + { + key: "__events", + label: "tree.events", + objectTypes: ["EVENT"], + nodeType: "group-events", + childType: "event", + }, { key: "__sequences", label: "tree.sequences", @@ -782,7 +790,7 @@ const groupDefs: Array<{ }, ]; -const objectGroupNodeTypes = new Set(["group-tables", "group-dolt-system-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-sequences", "group-synonyms", "group-packages", "group-types"]); +const objectGroupNodeTypes = new Set(["group-tables", "group-dolt-system-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-events", "group-sequences", "group-synonyms", "group-packages", "group-types"]); export function buildObjectGroupPlaceholderNodes({ nodeId, diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index dc18c4038d..c78ea6fa49 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -1303,7 +1303,7 @@ export const useConnectionStore = defineStore("connection", () => { // recurse into raw objects, its `meta` too), mirroring the markRaw() treatment // queryStore already applies to result rows. Containers stay reactive so their // children / isExpanded / isLoading mutations still drive the UI. - const LEAF_TREE_NODE_TYPES = new Set(["column", "index", "fkey", "trigger", "type-member"]); + const LEAF_TREE_NODE_TYPES = new Set(["column", "index", "fkey", "trigger", "event", "type-member"]); function markRawLeafTreeNodes(nodes: TreeNode[]): TreeNode[] { for (const node of nodes) { diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 2c50ba3557..dadb0bf115 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -470,7 +470,7 @@ export interface ObjectStatistics { total_bytes?: number | null; } -export type ObjectSourceKind = "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "TRIGGER" | "SEQUENCE" | "SYNONYM" | "PACKAGE" | "PACKAGE_BODY" | "TYPE" | "TYPE_BODY"; +export type ObjectSourceKind = "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "TRIGGER" | "EVENT" | "SEQUENCE" | "SYNONYM" | "PACKAGE" | "PACKAGE_BODY" | "TYPE" | "TYPE_BODY"; export interface ObjectSource { name: string; @@ -911,6 +911,7 @@ export type TreeNodeType = | "group-indexes" | "group-fkeys" | "group-triggers" + | "group-events" | "group-constraints" | "group-table-partitions" | "group-table-subpartitions" @@ -945,6 +946,7 @@ export type TreeNodeType = | "index" | "fkey" | "trigger" + | "event" | "constraint" | "partition" | "subpartition" diff --git a/crates/dbx-core/src/object_source_sql.rs b/crates/dbx-core/src/object_source_sql.rs index a8b28f5ff4..2264154e96 100644 --- a/crates/dbx-core/src/object_source_sql.rs +++ b/crates/dbx-core/src/object_source_sql.rs @@ -347,6 +347,7 @@ fn object_type_keyword(object_type: &ObjectSourceKind) -> &'static str { ObjectSourceKind::Procedure => "PROCEDURE", ObjectSourceKind::Function => "FUNCTION", ObjectSourceKind::Trigger => "TRIGGER", + ObjectSourceKind::Event => "EVENT", ObjectSourceKind::Sequence => "SEQUENCE", ObjectSourceKind::Synonym => "SYNONYM", ObjectSourceKind::Package => "PACKAGE", diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs index dd2027f6ae..ff6d3a6ee6 100644 --- a/crates/dbx-core/src/schema.rs +++ b/crates/dbx-core/src/schema.rs @@ -2771,16 +2771,16 @@ mod tests { dameng_object_statistics_rows_only_sql, dameng_object_statistics_user_segments_sql, deduplicate_column_infos, ephemeral_agent_metadata_session_id, external_driver_uses_mysql_ddl, filter_mongodb_agent_collections, filter_mysql_system_databases_for_config, filter_object_infos, filter_table_infos, filter_visible_schema_names, - gaussdb_m_view_object_source_sql, gbase8a_object_statistics_sql, is_agent_postgres_metadata_fallback_config, - is_mysql_external_driver_config, is_retryable_metadata_error, metadata_error_action, - metadata_name_or_comment_matches, mysql_database_list_timeout, mysql_external_driver_ddl_from_query_result, - mysql_external_driver_ddl_sql, mysql_object_source_ddl_column_index, mysql_object_source_sql, - mysql_table_list_source_for_config, mysql_table_metadata_catalog, normalize_information_schema_table_type, - oracle_columns_from_query_result, oracle_columns_sql, oracle_object_statistics_dba_segments_sql, - oracle_object_statistics_from_query_result, oracle_object_statistics_rows_only_sql, - oracle_object_statistics_sql, oracle_object_statistics_user_segments_sql, - oracle_table_comment_from_query_result, oracle_table_comment_sql, oracle_table_comments_sql, - presto_like_columns_from_query_result, presto_like_information_schema_columns_sql, + finalize_object_source, gaussdb_m_view_object_source_sql, gbase8a_object_statistics_sql, + is_agent_postgres_metadata_fallback_config, is_mysql_external_driver_config, is_retryable_metadata_error, + metadata_error_action, metadata_name_or_comment_matches, mysql_database_list_timeout, + mysql_external_driver_ddl_from_query_result, mysql_external_driver_ddl_sql, + mysql_object_source_ddl_column_index, mysql_object_source_sql, mysql_table_list_source_for_config, + mysql_table_metadata_catalog, normalize_information_schema_table_type, oracle_columns_from_query_result, + oracle_columns_sql, oracle_object_statistics_dba_segments_sql, oracle_object_statistics_from_query_result, + oracle_object_statistics_rows_only_sql, oracle_object_statistics_sql, + oracle_object_statistics_user_segments_sql, oracle_table_comment_from_query_result, oracle_table_comment_sql, + oracle_table_comments_sql, presto_like_columns_from_query_result, presto_like_information_schema_columns_sql, presto_like_information_schema_tables_sql, presto_like_tables_from_query_result, replace_metadata_runtime, should_query_oracle_columns_via_sql_first, table_comments_from_query_result, table_name_filter_matches, tdengine_table_comment_like_pattern, tdengine_table_comment_sql, tdengine_table_comments_sql, @@ -3345,6 +3345,27 @@ mod tests { ); } + #[test] + fn mysql_object_source_sql_emits_show_create_event() { + assert_eq!( + mysql_object_source_sql("tenant_db", "event_daily_sync", &db::ObjectSourceKind::Event), + "SHOW CREATE EVENT `tenant_db`.`event_daily_sync`" + ); + } + + #[test] + fn mysql_event_object_source_is_read_only() { + let source = finalize_object_source(db::ObjectSource { + name: "event_daily_sync".to_string(), + object_type: db::ObjectSourceKind::Event, + schema: None, + source: "CREATE EVENT event_daily_sync ON SCHEDULE EVERY 1 DAY DO SELECT 1".to_string(), + editable: None, + }); + + assert_eq!(source.editable, Some(false)); + } + #[test] fn mysql_object_source_sql_emits_show_create_materialized_view() { // Regression for the review comment: Doris / StarRocks ride on the MySQL @@ -3373,6 +3394,10 @@ mod tests { assert_eq!(mysql_object_source_ddl_column_index(&db::ObjectSourceKind::Procedure), 2); assert_eq!(mysql_object_source_ddl_column_index(&db::ObjectSourceKind::Function), 2); assert_eq!(mysql_object_source_ddl_column_index(&db::ObjectSourceKind::Trigger), 2); + // SHOW CREATE EVENT returns (Event, sql_mode, time_zone, Create Event, …) — + // one extra time_zone column before the DDL, verified against a real MySQL + // 8.0 instance (`SHOW CREATE EVENT` for a live event). + assert_eq!(mysql_object_source_ddl_column_index(&db::ObjectSourceKind::Event), 3); } #[test] @@ -7235,6 +7260,7 @@ fn sqlite_object_type(kind: &db::ObjectSourceKind) -> &'static str { db::ObjectSourceKind::Procedure | db::ObjectSourceKind::Function | db::ObjectSourceKind::Trigger + | db::ObjectSourceKind::Event | db::ObjectSourceKind::Sequence | db::ObjectSourceKind::Synonym | db::ObjectSourceKind::Package @@ -7250,7 +7276,8 @@ fn sqlserver_object_type_filter(kind: &db::ObjectSourceKind) -> &'static str { db::ObjectSourceKind::Procedure => "'P'", db::ObjectSourceKind::Function => "'FN','IF','TF','FS','FT'", db::ObjectSourceKind::Trigger => "'TR'", - db::ObjectSourceKind::Sequence + db::ObjectSourceKind::Event + | db::ObjectSourceKind::Sequence | db::ObjectSourceKind::Synonym | db::ObjectSourceKind::Package | db::ObjectSourceKind::PackageBody @@ -7488,6 +7515,7 @@ fn postgres_object_source_sql_inner( ) } db::ObjectSourceKind::Trigger + | db::ObjectSourceKind::Event | db::ObjectSourceKind::Synonym | db::ObjectSourceKind::Package | db::ObjectSourceKind::PackageBody @@ -7503,6 +7531,7 @@ pub fn oracle_object_source_sql(schema: &str, name: &str, kind: &db::ObjectSourc db::ObjectSourceKind::Procedure => "PROCEDURE", db::ObjectSourceKind::Function => "FUNCTION", db::ObjectSourceKind::Trigger => "TRIGGER", + db::ObjectSourceKind::Event => "EVENT", db::ObjectSourceKind::Sequence => "SEQUENCE", db::ObjectSourceKind::Synonym => "SYNONYM", db::ObjectSourceKind::Package => "PACKAGE", @@ -7559,6 +7588,7 @@ pub fn mysql_object_source_sql(database: &str, name: &str, kind: &db::ObjectSour db::ObjectSourceKind::Procedure => format!("SHOW CREATE PROCEDURE {qualified_name}"), db::ObjectSourceKind::Function => format!("SHOW CREATE FUNCTION {qualified_name}"), db::ObjectSourceKind::Trigger => format!("SHOW CREATE TRIGGER {qualified_name}"), + db::ObjectSourceKind::Event => format!("SHOW CREATE EVENT {qualified_name}"), db::ObjectSourceKind::Sequence | db::ObjectSourceKind::Synonym | db::ObjectSourceKind::Package @@ -7585,6 +7615,8 @@ pub fn mysql_object_source_sql(database: &str, name: &str, kind: &db::ObjectSour /// `(Name, DDL)` → DDL at index `1`. /// - `SHOW CREATE PROCEDURE`, `SHOW CREATE FUNCTION`, `SHOW CREATE TRIGGER` → /// `(Name, sql_mode, DDL, …)` → DDL at index `2`. +/// - `SHOW CREATE EVENT` → `(Event, sql_mode, time_zone, Create Event, …)` → +/// DDL at index `3` (it has an extra `time_zone` column before the DDL). /// /// Encoded as a function so the index can be unit-tested without a live DB. pub(crate) fn mysql_object_source_ddl_column_index(kind: &db::ObjectSourceKind) -> usize { @@ -7599,6 +7631,7 @@ pub(crate) fn mysql_object_source_ddl_column_index(kind: &db::ObjectSourceKind) | db::ObjectSourceKind::PackageBody | db::ObjectSourceKind::Type | db::ObjectSourceKind::TypeBody => 2, + db::ObjectSourceKind::Event => 3, } } @@ -7804,7 +7837,7 @@ pub async fn get_object_source_core( signature: Option<&str>, relation_name: Option<&str>, ) -> Result { - let mut source = retry_metadata_connection(state, connection_id, Some(database), || { + let source = retry_metadata_connection(state, connection_id, Some(database), || { get_object_source_once( state, connection_id, @@ -7817,10 +7850,17 @@ pub async fn get_object_source_core( ) }) .await?; + Ok(finalize_object_source(source)) +} + +fn finalize_object_source(mut source: db::ObjectSource) -> db::ObjectSource { if matches!(source.object_type, db::ObjectSourceKind::Procedure | db::ObjectSourceKind::Function) { source.source = normalize_routine_object_source(source.source); } - Ok(source) + if matches!(source.object_type, db::ObjectSourceKind::Event) { + source.editable = Some(false); + } + source } async fn get_object_source_once( diff --git a/crates/dbx-core/src/transfer.rs b/crates/dbx-core/src/transfer.rs index fc1ba3b7b4..fb5382f745 100644 --- a/crates/dbx-core/src/transfer.rs +++ b/crates/dbx-core/src/transfer.rs @@ -7468,9 +7468,10 @@ where | db::ObjectSourceKind::Synonym | db::ObjectSourceKind::Package | db::ObjectSourceKind::PackageBody => object.source.clone(), - db::ObjectSourceKind::Trigger | db::ObjectSourceKind::Type | db::ObjectSourceKind::TypeBody => { - object.source.clone() - } + db::ObjectSourceKind::Trigger + | db::ObjectSourceKind::Event + | 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 6ec52a568b..02e4925071 100644 --- a/crates/dbx-core/src/types.rs +++ b/crates/dbx-core/src/types.rs @@ -122,6 +122,7 @@ pub enum ObjectSourceKind { Procedure, Function, Trigger, + Event, Sequence, Synonym, Package,