diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue
index 82bf2294ab..3a41e0d946 100644
--- a/apps/desktop/src/components/sidebar/TreeItem.vue
+++ b/apps/desktop/src/components/sidebar/TreeItem.vue
@@ -83,11 +83,14 @@ import {
buildDropDatabaseSql,
buildDropObjectSql,
buildDropSchemaSql,
+ buildGetSchemaCommentSql,
buildDropTableSql,
buildDropTableChildObjectSql,
buildDuplicateTableStructureSql,
buildEmptyTableSql,
+ buildSetSchemaCommentSql,
buildTruncateTableSql,
+ supportsSchemaComment,
type DropTableChildObjectSqlOptions,
type DropObjectSqlOptions,
type TableChildObjectType,
@@ -407,17 +410,18 @@ const connectionInfoTooltip = computed(() => {
return { rows };
});
-const tableInfoTooltip = computed(() => {
+const objectCommentTooltip = computed(() => {
const node = props.node;
- if (node.type !== "table" && node.type !== "view") return null;
+ const comment = node.type === "column" && node.meta && "comment" in node.meta ? (node.meta as ColumnInfo).comment : node.comment;
+ if (!comment || (node.type !== "schema" && node.type !== "table" && node.type !== "view" && node.type !== "column")) return null;
const rows: DetailTooltipRow[] = [
- { label: t("structureEditor.tableName"), value: visibleLabel(node) },
- { label: t("structureEditor.comment"), value: cleanTooltipValue(node.comment), multiline: true },
+ { label: t("connection.name"), value: visibleLabel(node) },
+ { label: t("structureEditor.comment"), value: cleanTooltipValue(comment), multiline: true },
].filter((row) => row.value);
return { rows };
});
-const detailTooltip = computed(() => connectionInfoTooltip.value ?? tableInfoTooltip.value);
+const detailTooltip = computed(() => connectionInfoTooltip.value ?? objectCommentTooltip.value);
function isTooltipDisabled(): boolean {
if (detailTooltip.value?.rows.length) return isRenamingGroup.value;
@@ -1421,6 +1425,9 @@ const showFlushRedisDbConfirm = ref(false);
const showCreateSchemaDialog = ref(false);
const createSchemaName = ref("");
const showDropSchemaConfirm = ref(false);
+const showEditSchemaCommentDialog = ref(false);
+const schemaCommentText = ref("");
+const schemaCommentLoading = ref(false);
// --- Procedure / Function Management ---
const showDropObjectConfirm = ref(false);
@@ -1935,6 +1942,11 @@ const canDropSchema = computed(() => {
return props.node.type === "schema" && !isSqlServerLinkedNode(props.node) && usesTreeSchemaMode(effectiveDatabaseTypeForConnection(config)) && !connectionUsesDatabaseObjectTreeMode(config);
});
+const canEditSchemaComment = computed(() => {
+ const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
+ return props.node.type === "schema" && !!props.node.database && !config?.read_only && supportsSchemaComment(effectiveDatabaseTypeForConnection(config));
+});
+
function tableAdminSqlOptions(): TableAdminSqlOptions {
return {
databaseType: currentDatabaseType(),
@@ -2038,6 +2050,75 @@ async function refreshDropSchemaPreviewSql() {
}).catch(() => "");
}
+const schemaCommentPreviewSql = computed(() => {
+ if (!canEditSchemaComment.value) return "";
+ try {
+ return buildSetSchemaCommentSql({
+ databaseType: currentDatabaseType(),
+ name: props.node.schema || props.node.label,
+ comment: schemaCommentText.value,
+ });
+ } catch {
+ return "";
+ }
+});
+
+function schemaCommentFromResult(result: { columns?: string[]; rows?: unknown[] }): string {
+ const firstRow = result.rows?.[0];
+ if (Array.isArray(firstRow)) {
+ const index = Math.max(0, result.columns?.findIndex((column) => column === "comment") ?? 0);
+ return firstRow[index] == null ? "" : String(firstRow[index]);
+ }
+ if (firstRow && typeof firstRow === "object" && "comment" in firstRow) {
+ const value = (firstRow as { comment?: unknown }).comment;
+ return value == null ? "" : String(value);
+ }
+ return "";
+}
+
+async function openEditSchemaCommentDialog() {
+ const node = props.node;
+ if (!canEditSchemaComment.value || !node.connectionId || !node.database) return;
+ schemaCommentText.value = "";
+ schemaCommentLoading.value = true;
+ showEditSchemaCommentDialog.value = true;
+ try {
+ await connectionStore.ensureConnected(node.connectionId);
+ const sql = buildGetSchemaCommentSql({
+ databaseType: currentDatabaseType(),
+ name: node.schema || node.label,
+ });
+ const result = await api.executeQuery(node.connectionId, node.database, sql, node.schema, undefined, { maxRows: 1 });
+ schemaCommentText.value = schemaCommentFromResult(result);
+ } catch {
+ schemaCommentText.value = "";
+ } finally {
+ schemaCommentLoading.value = false;
+ }
+}
+
+async function confirmEditSchemaComment() {
+ const node = props.node;
+ if (!canEditSchemaComment.value || !node.connectionId || !node.database || schemaCommentLoading.value) return;
+ schemaCommentLoading.value = true;
+ try {
+ await connectionStore.ensureConnected(node.connectionId);
+ const sql = buildSetSchemaCommentSql({
+ databaseType: currentDatabaseType(),
+ name: node.schema || node.label,
+ comment: schemaCommentText.value,
+ });
+ await api.executeQuery(node.connectionId, node.database, sql, node.schema);
+ toast(t("contextMenu.editSchemaCommentSuccess", { name: node.label }), 3000);
+ showEditSchemaCommentDialog.value = false;
+ await connectionStore.loadSchemas(node.connectionId, node.database, { force: true });
+ } catch (e: any) {
+ toast(t("contextMenu.tableOperationFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000);
+ } finally {
+ schemaCommentLoading.value = false;
+ }
+}
+
async function openCreateDatabase() {
if (isDuckDbConnection.value) {
await createDuckDbAttachedDatabaseFile();
@@ -2953,7 +3034,11 @@ const hasTypeMenu = computed(() => {
});
const columnComment = computed(() => (!settingsStore.editorSettings.sidebarHideTableComments && props.node.type === "column" && props.node.meta && "comment" in props.node.meta ? (props.node.meta as any).comment : null));
const tableComment = computed(() =>
- !settingsStore.editorSettings.sidebarHideTableComments && (props.node.type === "table" || props.node.type === "view" || props.node.type === "mongo-collection" || props.node.type === "vector-collection" || props.node.type === "elasticsearch-index") && props.node.comment ? props.node.comment : null,
+ !settingsStore.editorSettings.sidebarHideTableComments &&
+ (props.node.type === "schema" || props.node.type === "table" || props.node.type === "view" || props.node.type === "mongo-collection" || props.node.type === "vector-collection" || props.node.type === "elasticsearch-index") &&
+ props.node.comment
+ ? props.node.comment
+ : null,
);
const paddingLeft = computed(() => treeItemPaddingLeft(props.depth));
const isConnected = computed(() => props.node.type === "connection" && !!props.node.connectionId && connectionStore.connectedIds.has(props.node.connectionId));
@@ -3494,6 +3579,9 @@ function treeItemMenuItems(): ContextMenuItem[] {
if (canCreateSchema.value) {
items.push({ label: t("contextMenu.createSchema"), action: openCreateSchemaDialog, icon: Plus });
}
+ if (canEditSchemaComment.value) {
+ items.push({ label: t("contextMenu.editSchemaComment"), action: openEditSchemaCommentDialog, icon: SquarePen });
+ }
if (canOpenSqlFileExecution.value) {
items.push({ label: t("sqlFile.title"), action: openSqlFileExecution, icon: FileCode });
}
@@ -4224,6 +4312,31 @@ function treeItemMenuItems(): ContextMenuItem[] {
+
+
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 968ae8df8c..291aabe0b9 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -1306,6 +1306,11 @@ export default {
createSchemaSuccess: 'Schema "{name}" created',
dropSchemaSuccess: 'Schema "{name}" dropped',
createSchemaNamePlaceholder: "Schema name",
+ editSchemaComment: "Edit Comment",
+ editSchemaCommentTitle: 'Edit Comment: "{name}"',
+ schemaCommentPlaceholder: "Enter schema comment...",
+ schemaCommentSaving: "Saving...",
+ editSchemaCommentSuccess: 'Schema "{name}" comment updated',
},
visibleDatabases: {
title: "Visible Databases",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index 4214d99950..b1127124b3 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -1118,6 +1118,11 @@ export default {
createSchemaSuccess: 'Esquema "{name}" creado',
dropSchemaSuccess: 'Esquema "{name}" eliminado',
createSchemaNamePlaceholder: "Nombre del esquema",
+ editSchemaComment: "Editar comentario",
+ editSchemaCommentTitle: 'Editar comentario: "{name}"',
+ schemaCommentPlaceholder: "Introduce un comentario del esquema...",
+ schemaCommentSaving: "Guardando...",
+ editSchemaCommentSuccess: 'Comentario del esquema "{name}" actualizado',
},
visibleDatabases: {
title: "Bases de datos visibles",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index 24c01f50f6..29cba3dd83 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -1242,6 +1242,11 @@ export default {
createSchemaSuccess: 'Schema "{name}" creato',
dropSchemaSuccess: 'Schema "{name}" eliminato',
createSchemaNamePlaceholder: "Nome schema",
+ editSchemaComment: "Modifica commento",
+ editSchemaCommentTitle: 'Modifica commento: "{name}"',
+ schemaCommentPlaceholder: "Inserisci commento schema...",
+ schemaCommentSaving: "Salvataggio...",
+ editSchemaCommentSuccess: 'Commento dello schema "{name}" aggiornato',
},
visibleDatabases: {
title: "Database Visibili",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index 5580166496..1d29088f4a 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -1294,6 +1294,11 @@ export default {
createSchemaSuccess: "スキーマ「{name}」を作成しました",
dropSchemaSuccess: "スキーマ「{name}」を削除しました",
createSchemaNamePlaceholder: "スキーマ名",
+ editSchemaComment: "コメントを編集",
+ editSchemaCommentTitle: "コメントを編集: 「{name}」",
+ schemaCommentPlaceholder: "スキーマコメントを入力...",
+ schemaCommentSaving: "保存中...",
+ editSchemaCommentSuccess: "スキーマ「{name}」のコメントを更新しました",
},
visibleDatabases: {
title: "表示するデータベース",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index bcd2206a0e..61703633b2 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -1242,6 +1242,11 @@ export default {
createSchemaSuccess: 'Schema "{name}" criado',
dropSchemaSuccess: 'Schema "{name}" removido',
createSchemaNamePlaceholder: "Nome do schema",
+ editSchemaComment: "Editar comentário",
+ editSchemaCommentTitle: 'Editar comentário: "{name}"',
+ schemaCommentPlaceholder: "Digite o comentário do schema...",
+ schemaCommentSaving: "Salvando...",
+ editSchemaCommentSuccess: 'Comentário do schema "{name}" atualizado',
},
visibleDatabases: {
title: "Bancos de dados visíveis",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index 7359a38111..ba089ef93b 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -1305,6 +1305,11 @@ export default {
createSchemaSuccess: "Schema「{name}」已创建",
dropSchemaSuccess: "Schema「{name}」已删除",
createSchemaNamePlaceholder: "Schema 名称",
+ editSchemaComment: "编辑注释",
+ editSchemaCommentTitle: "编辑注释:「{name}」",
+ schemaCommentPlaceholder: "输入 Schema 注释...",
+ schemaCommentSaving: "正在保存...",
+ editSchemaCommentSuccess: "Schema「{name}」注释已更新",
},
visibleDatabases: {
title: "显示数据库",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index 43cc4a721a..36fa70e5ad 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -1222,6 +1222,11 @@ export default {
createSchemaSuccess: "Schema「{name}」已建立",
dropSchemaSuccess: "Schema「{name}」已刪除",
createSchemaNamePlaceholder: "Schema 名稱",
+ editSchemaComment: "編輯註解",
+ editSchemaCommentTitle: "編輯註解:「{name}」",
+ schemaCommentPlaceholder: "輸入 Schema 註解...",
+ schemaCommentSaving: "正在儲存...",
+ editSchemaCommentSuccess: "Schema「{name}」註解已更新",
},
visibleDatabases: {
title: "顯示資料庫",
diff --git a/apps/desktop/src/lib/api.ts b/apps/desktop/src/lib/api.ts
index 019ccc172a..f3977c0a13 100644
--- a/apps/desktop/src/lib/api.ts
+++ b/apps/desktop/src/lib/api.ts
@@ -113,6 +113,7 @@ export const saveSchemaCache = forward("saveSchemaCache");
export const loadSchemaCache = forward("loadSchemaCache");
export const deleteSchemaCachePrefix = forward("deleteSchemaCachePrefix");
export const listSchemas = forward("listSchemas");
+export const listSchemaInfos = forward("listSchemaInfos");
export const listTables = forward("listTables");
export const getTableComment = forward("getTableComment");
export const listObjects = forward("listObjects");
diff --git a/apps/desktop/src/lib/dbAdminSql.ts b/apps/desktop/src/lib/dbAdminSql.ts
index b6cffda8f9..203d81cbad 100644
--- a/apps/desktop/src/lib/dbAdminSql.ts
+++ b/apps/desktop/src/lib/dbAdminSql.ts
@@ -34,6 +34,10 @@ export interface SchemaNameSqlOptions {
name: string;
}
+export interface SchemaCommentSqlOptions extends SchemaNameSqlOptions {
+ comment: string;
+}
+
export interface DuplicateTableStructureSqlOptions {
databaseType?: DatabaseType;
schema?: string | null;
@@ -73,6 +77,34 @@ export function buildDropSchemaSql(options: SchemaNameSqlOptions): Promise {
return api.buildDuplicateTableStructureSql(options);
}
+
+function quotePostgresIdentifier(value: string): string {
+ return `"${value.replace(/"/g, '""')}"`;
+}
+
+function quoteSqlLiteral(value: string): string {
+ return `'${value.replace(/'/g, "''")}'`;
+}
diff --git a/apps/desktop/src/lib/http.ts b/apps/desktop/src/lib/http.ts
index caa47a7e1a..8aaed4dd93 100644
--- a/apps/desktop/src/lib/http.ts
+++ b/apps/desktop/src/lib/http.ts
@@ -1,6 +1,7 @@
import type {
ConnectionConfig,
DatabaseInfo,
+ SchemaInfo,
LinkedServerInfo,
TableInfo,
ObjectInfo,
@@ -479,6 +480,11 @@ export async function listSchemas(connectionId: string, database: string): Promi
return get(`/api/schema/schemas?${qs({ connection_id: connectionId, database })}`);
}
+export async function listSchemaInfos(connectionId: string, database: string): Promise {
+ const schemas = await listSchemas(connectionId, database);
+ return schemas.map((name) => ({ name, comment: null }));
+}
+
export async function listTables(connectionId: string, database: string, schema: string, filter?: string, limit?: number, offset?: number, objectTypes?: SidebarObjectKind[]): Promise {
return get(`/api/schema/tables?${qs({ connection_id: connectionId, database, schema, filter, limit, offset, object_types: objectTypes?.join(",") })}`);
}
diff --git a/apps/desktop/src/lib/tauri.ts b/apps/desktop/src/lib/tauri.ts
index ffe0f72436..d01ca94635 100644
--- a/apps/desktop/src/lib/tauri.ts
+++ b/apps/desktop/src/lib/tauri.ts
@@ -3,6 +3,7 @@ import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import type {
ConnectionConfig,
DatabaseInfo,
+ SchemaInfo,
LinkedServerInfo,
TableInfo,
ObjectInfo,
@@ -547,6 +548,10 @@ export async function listSchemas(connectionId: string, database: string): Promi
return invoke("list_schemas", { connectionId, database });
}
+export async function listSchemaInfos(connectionId: string, database: string): Promise {
+ return invoke("list_schema_infos", { connectionId, database });
+}
+
export async function getColumns(connectionId: string, database: string, schema: string, table: string): Promise {
return invoke("get_columns", { connectionId, database, schema, table });
}
diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts
index 56df06bbf4..22175943d7 100644
--- a/apps/desktop/src/stores/connectionStore.ts
+++ b/apps/desktop/src/stores/connectionStore.ts
@@ -1,7 +1,7 @@
import { defineStore } from "pinia";
import { uuid } from "@/lib/utils";
import { ref, computed, watch } from "vue";
-import type { ColumnInfo, CompletionAssistantCandidate, CompletionAssistantObjectKind, CompletionAssistantRequest, ConnectionConfig, ForeignKeyInfo, ObjectInfo, SidebarLayout, TableInfo, TreeNode } from "@/types/database";
+import type { ColumnInfo, CompletionAssistantCandidate, CompletionAssistantObjectKind, CompletionAssistantRequest, ConnectionConfig, ForeignKeyInfo, ObjectInfo, SchemaInfo, SidebarLayout, TableInfo, TreeNode } from "@/types/database";
import { applyPinnedTreeNodeState, updatePinnedTreeNodeInPlace } from "@/lib/pinnedItems";
import {
reconcileLayout,
@@ -570,6 +570,16 @@ export const useConnectionStore = defineStore("connection", () => {
return sidebarObjectKindsForDatabase(dbType);
}
+ function sortSidebarSchemaInfos(schemas: readonly SchemaInfo[]): SchemaInfo[] {
+ const byName = new Map();
+ for (const schema of schemas) {
+ const name = schema.name.trim();
+ if (!name) continue;
+ byName.set(name, { name, comment: schema.comment ?? null });
+ }
+ return sortSidebarNames([...byName.keys()]).map((name) => byName.get(name)!);
+ }
+
function objectGroupCacheKey(node: TreeNode): string {
return schemaCacheKey(node.connectionId || "", node.database || "", node.schema || "", node.type, "objects-v3");
}
@@ -1501,7 +1511,7 @@ export const useConnectionStore = defineStore("connection", () => {
try {
await ensureConnected(connectionId);
if (useCachedChildren(node, options)) return;
- const cacheKey = schemaCacheKey(connectionId, database, "schemas");
+ const cacheKey = schemaCacheKey(connectionId, database, "schemas-v2");
if (!options?.force) {
const cached = await loadPersistedTreeChildren(node, cacheKey);
if (cached.hit) {
@@ -1510,17 +1520,21 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
- const schemas = sortSidebarNames(await api.listSchemas(connectionId, database));
- const children = schemas.map((s) => ({
- id: `${connectionId}:${database}:${s}`,
- label: s,
- type: "schema" as const,
- connectionId,
- database,
- schema: s,
- isExpanded: false,
- children: [],
- }));
+ const schemas = sortSidebarSchemaInfos(await api.listSchemaInfos(connectionId, database));
+ const children = schemas.map((schema) => {
+ const s = schema.name;
+ return {
+ id: `${connectionId}:${database}:${s}`,
+ label: s,
+ type: "schema" as const,
+ connectionId,
+ database,
+ schema: s,
+ comment: schema.comment,
+ isExpanded: false,
+ children: [],
+ };
+ });
setChildren(node, children);
await savePersistedTreeChildren(cacheKey, children);
node.isExpanded = true;
diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts
index 62e87a1411..ae4f7b4db0 100644
--- a/apps/desktop/src/types/database.ts
+++ b/apps/desktop/src/types/database.ts
@@ -250,6 +250,11 @@ export interface DatabaseInfo {
name: string;
}
+export interface SchemaInfo {
+ name: string;
+ comment?: string | null;
+}
+
export interface LinkedServerInfo {
name: string;
product?: string | null;
diff --git a/crates/dbx-core/src/db/postgres.rs b/crates/dbx-core/src/db/postgres.rs
index 5dedfe838f..c115a73891 100644
--- a/crates/dbx-core/src/db/postgres.rs
+++ b/crates/dbx-core/src/db/postgres.rs
@@ -25,7 +25,7 @@ use crate::types::{
ColumnInfo, CompletionAssistantCandidate, CompletionAssistantCandidateKind, CompletionAssistantMatchMode,
CompletionAssistantObjectKind, CompletionAssistantRequest, CompletionAssistantResponse, DatabaseInfo,
ForeignKeyInfo, FunctionInfo, IndexInfo, ObjectInfo, ObjectStatistics, OwnerInfo, QueryResult, RuleInfo,
- SequenceInfo, TableInfo, TriggerInfo,
+ SchemaInfo, SequenceInfo, TableInfo, TriggerInfo,
};
fn pg_temporal_to_json_value(row: &Row, idx: usize) -> Option {
@@ -1479,10 +1479,19 @@ pub async fn list_object_statistics(pool: &Pool, schema: &str) -> Result Result, String> {
+ Ok(list_schema_infos(pool).await?.into_iter().map(|schema| schema.name).collect())
+}
+
+pub async fn list_schema_infos(pool: &Pool) -> Result, String> {
let client = pool.get().await.map_err(|e| e.to_string())?;
let stmt = client
.prepare_cached(
- "SELECT n.nspname AS schema_name FROM pg_catalog.pg_namespace n \
+ "SELECT n.nspname AS schema_name, d.description AS schema_comment \
+ FROM pg_catalog.pg_namespace n \
+ LEFT JOIN pg_catalog.pg_description d \
+ ON d.objoid = n.oid \
+ AND d.objsubid = 0 \
+ AND d.classoid = 'pg_namespace'::regclass \
WHERE n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') \
AND n.nspname NOT LIKE 'pg_toast_temp_%' \
AND n.nspname NOT LIKE 'pg_temp_%' \
@@ -1492,7 +1501,13 @@ pub async fn list_schemas(pool: &Pool) -> Result, String> {
.map_err(|e| e.to_string())?;
let rows = client.query(&stmt, &[]).await.map_err(|e| e.to_string())?;
- Ok(rows.iter().map(|row| row.get::<_, String>(0)).collect())
+ Ok(rows
+ .iter()
+ .map(|row| SchemaInfo {
+ name: row.get::<_, String>(0),
+ comment: row.try_get::<_, Option>(1).ok().flatten(),
+ })
+ .collect())
}
const POSTGRES_COLUMNS_SQL: &str = "SELECT a.attname AS column_name, \
diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs
index 0411d821b6..dec35173f2 100644
--- a/crates/dbx-core/src/schema.rs
+++ b/crates/dbx-core/src/schema.rs
@@ -590,6 +590,34 @@ pub async fn list_schemas_core(state: &AppState, connection_id: &str, database:
.await
}
+pub async fn list_schema_infos_core(
+ state: &AppState,
+ connection_id: &str,
+ database: &str,
+) -> Result, String> {
+ retry_metadata_connection(state, connection_id, Some(database), || {
+ list_schema_infos_once(state, connection_id, database)
+ })
+ .await
+}
+
+async fn list_schema_infos_once(
+ state: &AppState,
+ connection_id: &str,
+ database: &str,
+) -> Result, String> {
+ let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
+ {
+ let connections = state.connections.read().await;
+ if let Some(PoolKind::Postgres(pool)) = connections.get(&pool_key) {
+ return db::postgres::list_schema_infos(pool).await;
+ }
+ }
+
+ let schemas = list_schemas_once(state, connection_id, database).await?;
+ Ok(schemas.into_iter().map(|name| db::SchemaInfo { name, comment: None }).collect())
+}
+
async fn list_schemas_once(state: &AppState, connection_id: &str, database: &str) -> Result, String> {
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
let db_config = connection_config(state, connection_id).await;
diff --git a/crates/dbx-core/src/types.rs b/crates/dbx-core/src/types.rs
index 2739cb85eb..030dce0738 100644
--- a/crates/dbx-core/src/types.rs
+++ b/crates/dbx-core/src/types.rs
@@ -5,6 +5,12 @@ pub struct DatabaseInfo {
pub name: String,
}
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SchemaInfo {
+ pub name: String,
+ pub comment: Option,
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LinkedServerInfo {
pub name: String,
diff --git a/packages/app-tests/dbAdminSql.test.ts b/packages/app-tests/dbAdminSql.test.ts
new file mode 100644
index 0000000000..0ea117eaeb
--- /dev/null
+++ b/packages/app-tests/dbAdminSql.test.ts
@@ -0,0 +1,23 @@
+import { strict as assert } from "node:assert";
+import { test } from "vitest";
+
+import { buildGetSchemaCommentSql, buildSetSchemaCommentSql, supportsSchemaComment } from "../../apps/desktop/src/lib/dbAdminSql.ts";
+
+test("postgres schema comment SQL quotes identifiers and literals", () => {
+ assert.equal(supportsSchemaComment("postgres"), true);
+ assert.equal(supportsSchemaComment("mysql"), false);
+ assert.equal(buildSetSchemaCommentSql({ databaseType: "postgres", name: 'tenant"one', comment: "Owner's area" }), `COMMENT ON SCHEMA "tenant""one" IS 'Owner''s area';`);
+});
+
+test("postgres schema comment SQL clears blank comments", () => {
+ assert.equal(buildSetSchemaCommentSql({ databaseType: "postgres", name: "analytics", comment: " " }), `COMMENT ON SCHEMA "analytics" IS NULL;`);
+});
+
+test("postgres schema comment lookup uses pg_namespace description join", () => {
+ const sql = buildGetSchemaCommentSql({ databaseType: "postgres", name: "tenant's schema" });
+
+ assert.match(sql, /FROM pg_catalog\.pg_namespace n/);
+ assert.match(sql, /pg_catalog\.pg_description d/);
+ assert.match(sql, /d\.classoid = 'pg_namespace'::regclass/);
+ assert.match(sql, /WHERE n\.nspname = 'tenant''s schema';/);
+});
diff --git a/src-tauri/src/commands/schema.rs b/src-tauri/src/commands/schema.rs
index d158ae8776..d04222e801 100644
--- a/src-tauri/src/commands/schema.rs
+++ b/src-tauri/src/commands/schema.rs
@@ -72,6 +72,15 @@ pub async fn list_schemas(
dbx_core::schema::list_schemas_core(&state, &connection_id, &database).await
}
+#[tauri::command]
+pub async fn list_schema_infos(
+ state: State<'_, Arc>,
+ connection_id: String,
+ database: String,
+) -> Result, String> {
+ dbx_core::schema::list_schema_infos_core(&state, &connection_id, &database).await
+}
+
#[tauri::command]
pub async fn list_tables(
state: State<'_, Arc>,
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 92cef2265c..a4f9622f8f 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -457,6 +457,7 @@ pub fn run() {
commands::schema::completion_assistant_search,
commands::schema::get_object_source,
commands::schema::list_schemas,
+ commands::schema::list_schema_infos,
commands::schema::get_columns,
commands::schema::list_indexes,
commands::schema::list_foreign_keys,