diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index 1d4fc90fc..d67d4355c 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -17,6 +17,7 @@ import { Eraser, Eye, FileCode, + ListTree, Upload, Loader2, Network, @@ -93,7 +94,7 @@ import { type ObjectBrowserSortKey, } from "@/lib/objectBrowserRows"; -type ObjectFilter = "all" | "tables" | "views" | "procedures" | "functions" | "packages"; +type ObjectFilter = "all" | "tables" | "views" | "procedures" | "functions" | "sequences" | "packages"; const props = defineProps<{ connection: ConnectionConfig; @@ -170,6 +171,7 @@ const tableCount = computed(() => rows.value.filter((row) => row.type === "TABLE const viewCount = computed(() => rows.value.filter((row) => row.type === "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 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, ); @@ -213,6 +215,7 @@ const objectFilters = computed(() => ["views", viewCount.value], ["procedures", procedureCount.value], ["functions", functionCount.value], + ["sequences", sequenceCount.value], ["packages", packageCount.value], ] as Array<[ObjectFilter, number]> ) @@ -258,6 +261,7 @@ function iconFor(row: ObjectBrowserRow) { if (row.type === "VIEW") return Eye; if (row.type === "PROCEDURE") return ScrollText; if (row.type === "FUNCTION") return Braces; + if (row.type === "SEQUENCE") return ListTree; if (row.type === "PACKAGE" || row.type === "PACKAGE_BODY") return Package; return Table2; } @@ -266,6 +270,7 @@ 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 === "SEQUENCE") return t("objects.sequence"); if (type === "PACKAGE") return t("objects.package"); if (type === "PACKAGE_BODY") return t("objects.packageBody"); return t("objects.table"); @@ -290,6 +295,7 @@ function rowMatchesObjectFilter(row: ObjectBrowserRow) { if (objectFilter.value === "views") return row.type === "VIEW"; if (objectFilter.value === "procedures") return row.type === "PROCEDURE"; if (objectFilter.value === "functions") return row.type === "FUNCTION"; + if (objectFilter.value === "sequences") return row.type === "SEQUENCE"; if (objectFilter.value === "packages") return row.type === "PACKAGE" || row.type === "PACKAGE_BODY"; return true; } @@ -330,6 +336,7 @@ function iconClass(type: ObjectBrowserRow["type"]) { if (type === "VIEW") return "text-purple-500"; if (type === "PROCEDURE") return "text-blue-500"; if (type === "FUNCTION") return "text-amber-500"; + if (type === "SEQUENCE") return "text-emerald-500"; if (type === "PACKAGE" || type === "PACKAGE_BODY") return "text-cyan-500"; return "text-green-500"; } @@ -351,6 +358,7 @@ function canOpenSource(row: ObjectBrowserRow) { row.type === "VIEW" || row.type === "PROCEDURE" || row.type === "FUNCTION" || + row.type === "SEQUENCE" || row.type === "PACKAGE" || row.type === "PACKAGE_BODY" ); @@ -405,7 +413,7 @@ async function openSource(row: ObjectBrowserRow) { ); sourceContent.value = result.source; sourceDraft.value = result.source; - sourceEditing.value = true; + sourceEditing.value = row.type !== "SEQUENCE"; } catch (e: any) { sourceError.value = e?.message || String(e); } finally { @@ -1145,6 +1153,7 @@ function filterCount(filter: ObjectFilter) { if (filter === "views") return viewCount.value; if (filter === "procedures") return procedureCount.value; if (filter === "functions") return functionCount.value; + if (filter === "sequences") return sequenceCount.value; if (filter === "packages") return packageCount.value; return rows.value.length; } @@ -1159,9 +1168,11 @@ function filterLabel(filter: ObjectFilter) { ? "objects.procedures" : filter === "functions" ? "objects.functions" - : filter === "packages" - ? "objects.packages" - : "objects.all"; + : filter === "sequences" + ? "objects.sequences" + : filter === "packages" + ? "objects.packages" + : "objects.all"; return `${t(key)} ${filterCount(filter)}`; } @@ -1320,6 +1331,7 @@ function getPackageMenuItems(item: ObjectBrowserRow): ContextMenuItem[] { function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] { if (item.type === "TABLE") return getTableMenuItems(item); if (item.type === "VIEW") return getViewMenuItems(item); + if (item.type === "SEQUENCE") return getPackageMenuItems(item); if (item.type === "PACKAGE" || item.type === "PACKAGE_BODY") return getPackageMenuItems(item); return getProcFuncMenuItems(item); } diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 52f5e6bfb..c3397d3be 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -270,6 +270,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null { return { icon: ScrollText, colorClass: "text-blue-500" }; case "function": return { icon: Braces, colorClass: "text-amber-500" }; + case "sequence": + return { icon: ListTree, colorClass: "text-emerald-500" }; case "package": return { icon: Package, colorClass: "text-cyan-500" }; case "package-body": @@ -282,6 +284,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null { return { icon: ScrollText, colorClass: "text-blue-500" }; case "group-functions": return { icon: Braces, colorClass: "text-amber-500" }; + case "group-sequences": + return { icon: ListTree, colorClass: "text-emerald-500" }; case "group-packages": return { icon: Package, colorClass: "text-cyan-500" }; case "group-partitions": @@ -300,6 +304,7 @@ const groupTypes: Set = new Set([ "group-views", "group-procedures", "group-functions", + "group-sequences", "group-packages", "group-partitions", "saved-sql-root", @@ -368,6 +373,7 @@ async function toggle() { node.type === "group-views" || node.type === "group-procedures" || node.type === "group-functions" || + node.type === "group-sequences" || node.type === "group-packages"; if (databaseObjectGroup && connectionStore.isTreeNodeChildrenLoaded(node.id)) { node.isExpanded = !node.isExpanded; @@ -473,6 +479,7 @@ function runRowClickAction() { } else if ( node.type === "procedure" || node.type === "function" || + node.type === "sequence" || node.type === "package" || node.type === "package-body" ) { @@ -1225,11 +1232,13 @@ function viewObjectSource() { .then(async (result) => { const tabId = queryStore.createTab(node.connectionId!, node.database!, `Source - ${node.label}`); queryStore.updateSql(tabId, result.source); - queryStore.setObjectSource(tabId, { - schema, - name: node.label, - objectType, - }); + if (objectType !== "SEQUENCE") { + queryStore.setObjectSource(tabId, { + schema, + name: node.label, + objectType, + }); + } }) .catch((e: any) => { toast(e?.message || String(e), 5000); @@ -3248,6 +3257,13 @@ function treeItemMenuItems(): ContextMenuItem[] { return items; } + if (node.type === "sequence") { + items.push({ label: t("contextMenu.viewSource"), action: viewObjectSource, icon: Code2 }); + items.push({ label: "", separator: true }); + items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value }); + return items; + } + if (node.type === "package" || node.type === "package-body") { items.push({ label: t("contextMenu.viewSource"), action: viewObjectSource, icon: Code2 }); items.push({ label: "", separator: true }); @@ -3410,6 +3426,7 @@ function treeItemMenuItems(): ContextMenuItem[] { node.type === 'group-views' || node.type === 'group-procedures' || node.type === 'group-functions' || + node.type === 'group-sequences' || node.type === 'group-packages' || node.type === 'group-partitions') && node.objectCount != null diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 103842624..67579a801 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1150,6 +1150,7 @@ export default { views: "Views", procedures: "Procedures", functions: "Functions", + sequences: "Sequences", packages: "Packages", partitions: "Partitions", objectBrowser: "Browse in Object Browser ({count})", @@ -1207,12 +1208,14 @@ export default { views: "Views", procedures: "Procedures", functions: "Functions", + sequences: "Sequences", packages: "Packages", table: "Table", partitions: "{count} partitions", view: "View", procedure: "Procedure", function: "Function", + sequence: "Sequence", package: "Package", packageBody: "Package Body", name: "Name", @@ -1228,7 +1231,7 @@ export default { comment: "Comment", loadingSchemas: "Loading schemas...", schema: "Schema", - search: "Search tables, views, functions, or procedures...", + search: "Search tables, views, functions, sequences, or procedures...", loading: "Loading objects...", empty: "No objects found", selectedTables: "{count} tables selected", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 3a9cf925b..dbe475acb 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1006,6 +1006,7 @@ export default { views: "Vistas", procedures: "Procedimientos", functions: "Funciones", + sequences: "Secuencias", packages: "Paquetes", partitions: "Particiones", objectBrowser: "Explorar en el navegador de objetos ({count})", @@ -1016,12 +1017,14 @@ export default { views: "Vistas", procedures: "Procedimientos", functions: "Funciones", + sequences: "Secuencias", packages: "Paquetes", table: "Tabla", partitions: "{count} particiones", view: "Vista", procedure: "Procedimiento", function: "Función", + sequence: "Secuencia", package: "Paquete", packageBody: "Cuerpo del paquete", name: "Nombre", @@ -1037,7 +1040,7 @@ export default { comment: "Comentario", loadingSchemas: "Cargando esquemas...", schema: "Esquema", - search: "Buscar tablas, vistas, funciones o procedimientos...", + search: "Buscar tablas, vistas, funciones, secuencias o procedimientos...", loading: "Cargando objetos...", empty: "No se encontraron objetos", selectedTables: "{count} tablas seleccionadas", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 04a8af928..db5fc2eb5 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1139,6 +1139,7 @@ export default { views: "Viste", procedures: "Procedure", functions: "Funzioni", + sequences: "Sequenze", packages: "Pacchetti", partitions: "Partizioni", objectBrowser: "Sfoglia in Esplora Oggetti ({count})", @@ -1149,12 +1150,14 @@ export default { views: "Viste", procedures: "Procedure", functions: "Funzioni", + sequences: "Sequenze", packages: "Pacchetti", table: "Tabella", partitions: "{count} partizioni", view: "Vista", procedure: "Procedura", function: "Funzione", + sequence: "Sequenza", package: "Pacchetto", packageBody: "Corpo del Pacchetto", name: "Nome", @@ -1170,7 +1173,7 @@ export default { comment: "Commento", loadingSchemas: "Caricamento schemi...", schema: "Schema", - search: "Cerca tabelle, viste, funzioni o procedure...", + search: "Cerca tabelle, viste, funzioni, sequenze o procedure...", loading: "Caricamento oggetti...", empty: "Nessun oggetto trovato", selectedTables: "{count} tabelle selezionate", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 7523a3503..c6480d4c8 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1134,6 +1134,7 @@ export default { views: "Visões", procedures: "Procedimentos", functions: "Funções", + sequences: "Sequências", packages: "Pacotes", partitions: "Partições", objectBrowser: "Navegar no Navegador de Objetos ({count})", @@ -1144,12 +1145,14 @@ export default { views: "Visões", procedures: "Procedimentos", functions: "Funções", + sequences: "Sequências", packages: "Pacotes", table: "Tabela", partitions: "{count} partições", view: "Visão", procedure: "Procedimento", function: "Função", + sequence: "Sequência", package: "Pacote", packageBody: "Corpo do pacote", name: "Nome", @@ -1165,7 +1168,7 @@ export default { comment: "Comentário", loadingSchemas: "Carregando schemas...", schema: "Schema", - search: "Pesquisar tabelas, visões, funções ou procedimentos...", + search: "Pesquisar tabelas, visões, funções, sequências ou procedimentos...", loading: "Carregando objetos...", empty: "Nenhum objeto encontrado", selectedTables: "{count} tabelas selecionadas", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 12c4ad40f..60f552833 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1125,6 +1125,7 @@ export default { views: "视图", procedures: "存储过程", functions: "函数", + sequences: "序列", packages: "包", partitions: "分区", objectBrowser: "在对象浏览器中查看 ({count})", @@ -1180,12 +1181,14 @@ export default { views: "视图", procedures: "存储过程", functions: "函数", + sequences: "序列", packages: "包", table: "表", partitions: "{count} 个分区", view: "视图", procedure: "存储过程", function: "函数", + sequence: "序列", package: "包", packageBody: "包体", name: "名称", @@ -1201,7 +1204,7 @@ export default { comment: "注释", loadingSchemas: "加载 Schema...", schema: "Schema", - search: "搜索表、视图、函数或存储过程...", + search: "搜索表、视图、函数、序列或存储过程...", loading: "正在加载对象...", empty: "没有找到对象", selectedTables: "已选择 {count} 张表", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 535ba5d4f..aa5eb251f 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1071,6 +1071,7 @@ export default { views: "檢視", procedures: "預存程序", functions: "函式", + sequences: "序列", packages: "套件", partitions: "分割區", objectBrowser: "在物件瀏覽器中檢視 ({count})", @@ -1081,12 +1082,14 @@ export default { views: "檢視", procedures: "預存程序", functions: "函式", + sequences: "序列", packages: "套件", table: "資料表", partitions: "{count} 個分割區", view: "檢視", procedure: "預存程序", function: "函式", + sequence: "序列", package: "套件", packageBody: "套件主體", name: "名稱", @@ -1102,7 +1105,7 @@ export default { comment: "註解", loadingSchemas: "載入 Schema……", schema: "Schema", - search: "搜尋資料表、檢視、函式或預存程序……", + search: "搜尋資料表、檢視、函式、序列或預存程序……", loading: "正在載入物件……", empty: "沒有找到物件", selectedTables: "已選擇 {count} 張資料表", diff --git a/apps/desktop/src/lib/databaseObjectCapabilities.ts b/apps/desktop/src/lib/databaseObjectCapabilities.ts index 95456b0e0..effba95e2 100644 --- a/apps/desktop/src/lib/databaseObjectCapabilities.ts +++ b/apps/desktop/src/lib/databaseObjectCapabilities.ts @@ -1,6 +1,6 @@ import type { DatabaseType } from "@/types/database"; -export type SidebarObjectKind = "TABLE" | "VIEW" | "PROCEDURE" | "FUNCTION" | "PACKAGE" | "PACKAGE_BODY"; +export type SidebarObjectKind = "TABLE" | "VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY"; export interface DatabaseObjectCapabilities { sidebarObjects: SidebarObjectKind[]; @@ -10,6 +10,7 @@ export interface DatabaseObjectCapabilities { const TABLE_VIEW_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW"]; const ROUTINE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION"]; +const POSTGRES_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE"]; const ORACLE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY"]; const TABLE_VIEW_ONLY_TYPES = new Set([ @@ -31,6 +32,7 @@ const TABLE_VIEW_ONLY_TYPES = new Set([ ]); const ORACLE_PACKAGE_TYPES = new Set(["oracle", "oceanbase-oracle"]); +const POSTGRES_SEQUENCE_TYPES = new Set(["postgres", "gaussdb", "kwdb", "opengauss"]); export function databaseObjectCapabilities(dbType?: DatabaseType): DatabaseObjectCapabilities { const sidebarObjects = sidebarObjectKindsForDatabase(dbType); @@ -45,6 +47,7 @@ export function sidebarObjectKindsForDatabase(dbType?: DatabaseType): SidebarObj if (!dbType) return [...TABLE_VIEW_OBJECTS]; if (ORACLE_PACKAGE_TYPES.has(dbType)) return [...ORACLE_OBJECTS]; if (TABLE_VIEW_ONLY_TYPES.has(dbType)) return [...TABLE_VIEW_OBJECTS]; + if (POSTGRES_SEQUENCE_TYPES.has(dbType)) return [...POSTGRES_OBJECTS]; return [...ROUTINE_OBJECTS]; } @@ -53,6 +56,7 @@ export function normalizeSidebarObjectKind(type: string): SidebarObjectKind { if (value.includes("PACKAGE BODY") || value.includes("PACKAGE_BODY")) return "PACKAGE_BODY"; if (value.includes("PACKAGE")) return "PACKAGE"; if (value.includes("VIEW")) return "VIEW"; + if (value.includes("SEQ")) return "SEQUENCE"; if (value.includes("PROC")) return "PROCEDURE"; if (value.includes("FUNC")) return "FUNCTION"; return "TABLE"; diff --git a/apps/desktop/src/lib/objectBrowserRows.ts b/apps/desktop/src/lib/objectBrowserRows.ts index 961569169..bd8ee8721 100644 --- a/apps/desktop/src/lib/objectBrowserRows.ts +++ b/apps/desktop/src/lib/objectBrowserRows.ts @@ -6,7 +6,7 @@ export type ObjectBrowserRow = { id: string; name: string; schema?: string; - type: "TABLE" | "VIEW" | "PROCEDURE" | "FUNCTION" | "PACKAGE" | "PACKAGE_BODY"; + type: "TABLE" | "VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY"; comment?: string | null; created_at?: string | null; updated_at?: string | null; @@ -24,6 +24,7 @@ export function normalizeObjectBrowserType(type: string): ObjectBrowserRow["type if (value.includes("PACKAGE BODY") || value.includes("PACKAGE_BODY")) return "PACKAGE_BODY"; if (value.includes("PACKAGE")) return "PACKAGE"; if (value.includes("VIEW")) return "VIEW"; + if (value.includes("SEQ")) return "SEQUENCE"; if (value.includes("PROC")) return "PROCEDURE"; if (value.includes("FUNC")) return "FUNCTION"; return "TABLE"; diff --git a/apps/desktop/src/lib/tableTree.ts b/apps/desktop/src/lib/tableTree.ts index 46b82ce1f..a224ca11f 100644 --- a/apps/desktop/src/lib/tableTree.ts +++ b/apps/desktop/src/lib/tableTree.ts @@ -374,7 +374,9 @@ export function buildSimpleObjectTreeNodes({ for (const obj of objects) { const objectType = normalizeObjectType(obj.object_type); - if (!["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY"].includes(objectType)) continue; + if (!["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE", "PACKAGE", "PACKAGE_BODY"].includes(objectType)) { + continue; + } const name = normalizeDatabaseObjectName(obj.name); if (!name) continue; @@ -426,6 +428,7 @@ function simpleObjectNodeType(objectType: DatabaseObjectTreeKind): TreeNodeType if (objectType === "VIEW") return "view"; if (objectType === "PROCEDURE") return "procedure"; if (objectType === "FUNCTION") return "function"; + if (objectType === "SEQUENCE") return "sequence"; if (objectType === "PACKAGE_BODY") return "package-body"; if (objectType === "PACKAGE") return "package"; return "table"; @@ -458,6 +461,13 @@ const groupDefs: Array<{ nodeType: "group-functions", childType: "function", }, + { + key: "__sequences", + label: "tree.sequences", + objectTypes: ["SEQUENCE"], + nodeType: "group-sequences", + childType: "sequence", + }, { key: "__packages", label: "tree.packages", @@ -472,6 +482,7 @@ const objectGroupNodeTypes = new Set([ "group-views", "group-procedures", "group-functions", + "group-sequences", "group-packages", ]); diff --git a/apps/desktop/src/lib/treeNodeClick.ts b/apps/desktop/src/lib/treeNodeClick.ts index c374c5e0e..78b70441c 100644 --- a/apps/desktop/src/lib/treeNodeClick.ts +++ b/apps/desktop/src/lib/treeNodeClick.ts @@ -16,7 +16,7 @@ export type SidebarActivation = "single" | "double"; const dataNodeTypes = new Set(["table", "view"]); const toggleLeafNodeTypes = new Set(["redis-db", "mongo-collection", "user-admin"]); const objectBrowserNodeTypes = new Set(["database", "schema", "object-browser"]); -const sourceNodeTypes = new Set(["procedure", "function", "package", "package-body"]); +const sourceNodeTypes = new Set(["procedure", "function", "sequence", "package", "package-body"]); const tableChildGroupNodeTypes = new Set([ "group-columns", "group-indexes", @@ -29,6 +29,7 @@ const databaseChildGroupNodeTypes = new Set([ "group-views", "group-procedures", "group-functions", + "group-sequences", "group-packages", ]); @@ -36,6 +37,7 @@ export function objectSourceKindForTreeNode(type: TreeNodeType): ObjectSourceKin if (type === "view") return "VIEW"; if (type === "procedure") return "PROCEDURE"; if (type === "function") return "FUNCTION"; + if (type === "sequence") return "SEQUENCE"; if (type === "package") return "PACKAGE"; if (type === "package-body") return "PACKAGE_BODY"; return null; diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 2b176eb50..138cee186 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -1593,6 +1593,7 @@ export const useConnectionStore = defineStore("connection", () => { node.type === "group-views" || node.type === "group-procedures" || node.type === "group-functions" || + node.type === "group-sequences" || node.type === "group-packages" ) { await loadObjectGroupChildren(node, options); diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index eb9db36f4..d8bfd712e 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -178,7 +178,7 @@ export interface TableInfo { parent_name?: string | null; } -export type DatabaseObjectType = "TABLE" | "VIEW" | "PROCEDURE" | "FUNCTION" | "PACKAGE" | "PACKAGE_BODY"; +export type DatabaseObjectType = "TABLE" | "VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY"; export interface ObjectInfo { name: string; @@ -191,7 +191,7 @@ export interface ObjectInfo { parent_name?: string | null; } -export type ObjectSourceKind = "VIEW" | "PROCEDURE" | "FUNCTION" | "PACKAGE" | "PACKAGE_BODY"; +export type ObjectSourceKind = "VIEW" | "PROCEDURE" | "FUNCTION" | "SEQUENCE" | "PACKAGE" | "PACKAGE_BODY"; export interface ObjectSource { name: string; @@ -288,6 +288,7 @@ export type TreeNodeType = | "view" | "procedure" | "function" + | "sequence" | "package" | "package-body" | "group-columns" @@ -298,6 +299,7 @@ export type TreeNodeType = | "group-views" | "group-procedures" | "group-functions" + | "group-sequences" | "group-packages" | "group-partitions" | "object-browser" diff --git a/crates/dbx-core/src/db/postgres.rs b/crates/dbx-core/src/db/postgres.rs index 60c4fed05..63d7e5009 100644 --- a/crates/dbx-core/src/db/postgres.rs +++ b/crates/dbx-core/src/db/postgres.rs @@ -1151,6 +1151,7 @@ fn list_objects_sql(include_timestamps: bool) -> &'static str { CASE c.relkind \ WHEN 'v' THEN 'VIEW' \ WHEN 'm' THEN 'VIEW' \ + WHEN 'S' THEN 'SEQUENCE' \ ELSE 'TABLE' \ END AS object_type, \ obj_description(c.oid) AS object_comment, \ @@ -1162,7 +1163,7 @@ fn list_objects_sql(include_timestamps: bool) -> &'static str { ) AS updated_at, \ CASE WHEN pc.relkind = 'p' THEN pn.nspname ELSE NULL END AS parent_schema, \ CASE WHEN pc.relkind = 'p' THEN pc.relname ELSE NULL END AS parent_name, \ - CASE c.relkind WHEN 'v' THEN 1 WHEN 'm' THEN 1 ELSE 0 END AS sort_order \ + CASE c.relkind WHEN 'v' THEN 1 WHEN 'm' THEN 1 WHEN 'S' THEN 4 ELSE 0 END AS sort_order \ FROM pg_catalog.pg_class c \ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \ LEFT JOIN pg_catalog.pg_inherits i ON i.inhrelid = c.oid \ @@ -1171,7 +1172,7 @@ fn list_objects_sql(include_timestamps: bool) -> &'static str { LEFT JOIN LATERAL pg_stat_file( \ CASE WHEN c.relkind IN ('r','m','f','p') THEN pg_relation_filepath(c.oid) END, true \ ) stat ON true \ - WHERE n.nspname = $1 AND c.relkind IN ('r','v','m','f','p') \ + WHERE n.nspname = $1 AND c.relkind IN ('r','v','m','f','p','S') \ UNION ALL \ SELECT p.proname AS object_name, \ CASE p.prokind WHEN 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS object_type, \ @@ -1192,6 +1193,7 @@ fn list_objects_sql(include_timestamps: bool) -> &'static str { CASE c.relkind \ WHEN 'v' THEN 'VIEW' \ WHEN 'm' THEN 'VIEW' \ + WHEN 'S' THEN 'SEQUENCE' \ ELSE 'TABLE' \ END AS object_type, \ obj_description(c.oid) AS object_comment, \ @@ -1199,13 +1201,13 @@ fn list_objects_sql(include_timestamps: bool) -> &'static str { NULL::text AS updated_at, \ CASE WHEN pc.relkind = 'p' THEN pn.nspname ELSE NULL END AS parent_schema, \ CASE WHEN pc.relkind = 'p' THEN pc.relname ELSE NULL END AS parent_name, \ - CASE c.relkind WHEN 'v' THEN 1 WHEN 'm' THEN 1 ELSE 0 END AS sort_order \ + CASE c.relkind WHEN 'v' THEN 1 WHEN 'm' THEN 1 WHEN 'S' THEN 4 ELSE 0 END AS sort_order \ FROM pg_catalog.pg_class c \ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \ LEFT JOIN pg_catalog.pg_inherits i ON i.inhrelid = c.oid \ LEFT JOIN pg_catalog.pg_class pc ON pc.oid = i.inhparent \ LEFT JOIN pg_catalog.pg_namespace pn ON pn.oid = pc.relnamespace \ - WHERE n.nspname = $1 AND c.relkind IN ('r','v','m','f','p') \ + WHERE n.nspname = $1 AND c.relkind IN ('r','v','m','f','p','S') \ UNION ALL \ SELECT p.proname AS object_name, \ CASE p.prokind WHEN 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS object_type, \ @@ -2123,6 +2125,8 @@ mod tests { assert!(sql.contains("pg_xact_commit_timestamp")); assert!(sql.contains("'PROCEDURE'")); assert!(sql.contains("'FUNCTION'")); + assert!(sql.contains("'SEQUENCE'")); + assert!(sql.contains("'S'")); } #[test] diff --git a/crates/dbx-core/src/object_source_sql.rs b/crates/dbx-core/src/object_source_sql.rs index baddb7a21..cf997cad6 100644 --- a/crates/dbx-core/src/object_source_sql.rs +++ b/crates/dbx-core/src/object_source_sql.rs @@ -234,6 +234,7 @@ fn object_type_keyword(object_type: &ObjectSourceKind) -> &'static str { ObjectSourceKind::View => "VIEW", ObjectSourceKind::Procedure => "PROCEDURE", ObjectSourceKind::Function => "FUNCTION", + ObjectSourceKind::Sequence => "SEQUENCE", ObjectSourceKind::Package => "PACKAGE", ObjectSourceKind::PackageBody => "PACKAGE BODY", } @@ -384,6 +385,8 @@ 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("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") { diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs index f7ed6125c..34cbb3373 100644 --- a/crates/dbx-core/src/schema.rs +++ b/crates/dbx-core/src/schema.rs @@ -1534,6 +1534,7 @@ fn sqlite_object_type(kind: &db::ObjectSourceKind) -> &'static str { db::ObjectSourceKind::View => "view", db::ObjectSourceKind::Procedure | db::ObjectSourceKind::Function + | db::ObjectSourceKind::Sequence | db::ObjectSourceKind::Package | db::ObjectSourceKind::PackageBody => "routine", } @@ -1544,7 +1545,7 @@ 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::Package | db::ObjectSourceKind::PackageBody => "''", + db::ObjectSourceKind::Sequence | db::ObjectSourceKind::Package | db::ObjectSourceKind::PackageBody => "''", } } @@ -1586,6 +1587,30 @@ pub fn postgres_object_source_sql(schema: &str, name: &str, kind: &db::ObjectSou prokind ) } + db::ObjectSourceKind::Sequence => { + format!( + "SELECT concat_ws(E'\\n\\n', \ + '-- auto-generated definition' || E'\\n' || \ + 'create sequence ' || quote_ident(c.relname) || E'\\n' || \ + ' as ' || pg_catalog.format_type(s.seqtypid, NULL) || ';', \ + 'alter sequence ' || quote_ident(c.relname) || ' owner to ' || quote_ident(pg_get_userbyid(c.relowner)) || ';', \ + CASE WHEN owned.relname IS NOT NULL AND a.attname IS NOT NULL \ + THEN 'alter sequence ' || quote_ident(c.relname) || ' owned by ' || quote_ident(owned.relname) || '.' || quote_ident(a.attname) || ';' \ + END \ + ) \ + FROM pg_catalog.pg_class c \ + JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \ + JOIN pg_catalog.pg_sequence s ON s.seqrelid = c.oid \ + LEFT JOIN pg_catalog.pg_depend d \ + ON d.classid = 'pg_class'::regclass AND d.objid = c.oid AND d.deptype = 'a' \ + LEFT JOIN pg_catalog.pg_class owned ON owned.oid = d.refobjid \ + LEFT JOIN pg_catalog.pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid \ + WHERE n.nspname = {} AND c.relname = {} AND c.relkind = 'S' \ + ORDER BY c.oid LIMIT 1", + sql_string(schema), + sql_string(name) + ) + } db::ObjectSourceKind::Package | db::ObjectSourceKind::PackageBody => "SELECT NULL WHERE FALSE".to_string(), } } @@ -1595,6 +1620,7 @@ pub fn oracle_object_source_sql(schema: &str, name: &str, kind: &db::ObjectSourc db::ObjectSourceKind::View => "VIEW", db::ObjectSourceKind::Procedure => "PROCEDURE", db::ObjectSourceKind::Function => "FUNCTION", + db::ObjectSourceKind::Sequence => "SEQUENCE", db::ObjectSourceKind::Package => "PACKAGE", db::ObjectSourceKind::PackageBody => "PACKAGE_BODY", }; @@ -1623,7 +1649,9 @@ pub fn mysql_object_source_sql(name: &str, kind: &db::ObjectSourceKind) -> Strin db::ObjectSourceKind::View => format!("SHOW CREATE VIEW {}", mysql_ident(name)), db::ObjectSourceKind::Procedure => format!("SHOW CREATE PROCEDURE {}", mysql_ident(name)), db::ObjectSourceKind::Function => format!("SHOW CREATE FUNCTION {}", mysql_ident(name)), - db::ObjectSourceKind::Package | db::ObjectSourceKind::PackageBody => String::new(), + db::ObjectSourceKind::Sequence | db::ObjectSourceKind::Package | db::ObjectSourceKind::PackageBody => { + String::new() + } } } @@ -1883,6 +1911,25 @@ mod object_source_tests { ); } + #[test] + fn builds_postgres_object_source_sql_for_sequences() { + let sql = postgres_object_source_sql("tenant's schema", "order id seq", &ObjectSourceKind::Sequence); + + assert!(sql.contains("-- auto-generated definition")); + assert!(sql.contains("create sequence")); + assert!(sql.contains("alter sequence")); + assert!(sql.contains("owner to")); + assert!(sql.contains("owned by")); + assert!(sql.contains("pg_catalog.pg_sequence")); + assert!(sql.contains("n.nspname = 'tenant''s schema'")); + assert!(sql.contains("c.relname = 'order id seq'")); + assert!(sql.contains("c.relkind = 'S'")); + assert!(!sql.contains("MINVALUE")); + assert!(!sql.contains("START WITH")); + assert!(!sql.contains("CACHE")); + assert!(!sql.contains("NO CYCLE")); + } + #[test] fn builds_postgres_view_source_sql_without_regclass_cast() { let sql = postgres_object_source_sql("tenant's schema", "active users", &ObjectSourceKind::View); diff --git a/crates/dbx-core/src/transfer.rs b/crates/dbx-core/src/transfer.rs index 1b3d68c45..eb0975378 100644 --- a/crates/dbx-core/src/transfer.rs +++ b/crates/dbx-core/src/transfer.rs @@ -2960,7 +2960,9 @@ where rewrite_postgres_routine_schema(&object.source, &request.target_schema) .unwrap_or_else(|| object.source.clone()) } - db::ObjectSourceKind::Package | db::ObjectSourceKind::PackageBody => object.source.clone(), + db::ObjectSourceKind::Sequence | db::ObjectSourceKind::Package | db::ObjectSourceKind::PackageBody => { + 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 9d71dee2e..b16a5c05d 100644 --- a/crates/dbx-core/src/types.rs +++ b/crates/dbx-core/src/types.rs @@ -32,6 +32,7 @@ pub enum ObjectSourceKind { View, Procedure, Function, + Sequence, Package, PackageBody, } diff --git a/packages/app-tests/databaseCapabilities.test.ts b/packages/app-tests/databaseCapabilities.test.ts index 223fa04c9..fbd10304e 100644 --- a/packages/app-tests/databaseCapabilities.test.ts +++ b/packages/app-tests/databaseCapabilities.test.ts @@ -265,7 +265,7 @@ test("object browser entry follows database tree shape", () => { test("sidebar object capability registry describes object groups by database type", () => { assert.deepEqual(sidebarObjectKindsForDatabase("databend"), ["TABLE", "VIEW"]); - assert.deepEqual(sidebarObjectKindsForDatabase("postgres"), ["TABLE", "VIEW", "PROCEDURE", "FUNCTION"]); + assert.deepEqual(sidebarObjectKindsForDatabase("postgres"), ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE"]); assert.deepEqual(sidebarObjectKindsForDatabase("oracle"), [ "TABLE", "VIEW", diff --git a/packages/app-tests/objectBrowserRows.test.ts b/packages/app-tests/objectBrowserRows.test.ts index fc12150e5..55d003dc2 100644 --- a/packages/app-tests/objectBrowserRows.test.ts +++ b/packages/app-tests/objectBrowserRows.test.ts @@ -45,6 +45,20 @@ test("object browser rows normalize Oracle package body objects", () => { ); }); +test("object browser rows normalize PostgreSQL sequence objects", () => { + const rows = buildObjectBrowserRows({ + objects: [{ name: "order_id_seq", object_type: "SEQUENCE", schema: "public" }], + database: "app", + fallbackSchema: "public", + needsSchema: true, + }); + + assert.deepEqual( + rows.map((row) => ({ id: row.id, type: row.type })), + [{ id: "public:order_id_seq:SEQUENCE:0", type: "SEQUENCE" }], + ); +}); + test("object browser search matches names, types, and comments but not schema names", () => { const rows = buildObjectBrowserRows({ objects: [ diff --git a/packages/app-tests/tableTree.test.ts b/packages/app-tests/tableTree.test.ts index a5b795d14..2a164f63a 100644 --- a/packages/app-tests/tableTree.test.ts +++ b/packages/app-tests/tableTree.test.ts @@ -177,7 +177,7 @@ test("buildObjectGroupPlaceholderNodes creates capability-driven lazy sidebar gr connectionId: "conn", database: "app", schema: "HR", - objectTypes: ["TABLE", "VIEW", "PROCEDURE", "FUNCTION"], + objectTypes: ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE"], }); assert.deepEqual( @@ -187,11 +187,12 @@ test("buildObjectGroupPlaceholderNodes creates capability-driven lazy sidebar gr { label: "tree.views", type: "group-views", count: undefined, children: [] }, { label: "tree.procedures", type: "group-procedures", count: undefined, children: [] }, { label: "tree.functions", type: "group-functions", count: undefined, children: [] }, + { label: "tree.sequences", type: "group-sequences", count: undefined, children: [] }, ], ); }); -test("buildSimpleObjectTreeNodes keeps routines and packages visible in flat sidebar mode", () => { +test("buildSimpleObjectTreeNodes keeps routines, sequences, and packages visible in flat sidebar mode", () => { const nodes = buildSimpleObjectTreeNodes({ nodeId: "conn:app:HR", connectionId: "conn", @@ -202,6 +203,7 @@ test("buildSimpleObjectTreeNodes keeps routines and packages visible in flat sid { name: "ACTIVE_ORDERS", object_type: "VIEW", schema: "HR" }, { name: "REFRESH_STATS", object_type: "PROCEDURE", schema: "HR" }, { name: "TOTAL_DUE", object_type: "FUNCTION", schema: "HR" }, + { name: "ORDER_ID_SEQ", object_type: "SEQUENCE", schema: "HR" }, { name: "PAYROLL", object_type: "PACKAGE", schema: "HR" }, { name: "PAYROLL", object_type: "PACKAGE_BODY", schema: "HR" }, ], @@ -212,6 +214,7 @@ test("buildSimpleObjectTreeNodes keeps routines and packages visible in flat sid [ { label: "ORDERS", type: "table" }, { label: "ACTIVE_ORDERS", type: "view" }, + { label: "ORDER_ID_SEQ", type: "sequence" }, { label: "PAYROLL", type: "package" }, { label: "PAYROLL", type: "package-body" }, { label: "REFRESH_STATS", type: "procedure" }, diff --git a/packages/app-tests/treeNodeClick.test.ts b/packages/app-tests/treeNodeClick.test.ts index 73f8583c2..456db121d 100644 --- a/packages/app-tests/treeNodeClick.test.ts +++ b/packages/app-tests/treeNodeClick.test.ts @@ -54,6 +54,7 @@ test("maps source-capable sidebar nodes to object source kinds", () => { assert.equal(objectSourceKindForTreeNode("view"), "VIEW"); assert.equal(objectSourceKindForTreeNode("procedure"), "PROCEDURE"); assert.equal(objectSourceKindForTreeNode("function"), "FUNCTION"); + assert.equal(objectSourceKindForTreeNode("sequence"), "SEQUENCE"); assert.equal(objectSourceKindForTreeNode("package"), "PACKAGE"); assert.equal(objectSourceKindForTreeNode("package-body"), "PACKAGE_BODY"); assert.equal(objectSourceKindForTreeNode("table"), null);