diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index e236b5a4f..8506fc349 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -925,13 +925,19 @@ onUnmounted(() => { " @object-schema-change="(schema) => activeTab && queryStore.updateSchema(activeTab.id, schema)" @structure-editor-saved=" - activeTab && - onStructureEditorSaved(onReloadData, toast, { - connectionId: activeTab.connectionId, - database: activeTab.database, - schema: activeTab.schema, - tableName: activeTab.structureTableName || '', - }) + (commentChanged) => + activeTab && + onStructureEditorSaved( + onReloadData, + toast, + { + connectionId: activeTab.connectionId, + database: activeTab.database, + schema: activeTab.schema, + tableName: activeTab.structureTableName || '', + }, + commentChanged, + ) " @structure-editor-close="activeTab && queryStore.closeTab(activeTab.id)" /> diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 069f34d31..193b8c376 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -83,7 +83,7 @@ const emit = defineEmits<{ clickTable: [tableName: string]; openObjectTable: [target: { tableName: string; schema?: string }]; objectSchemaChange: [schema: string | undefined]; - structureEditorSaved: []; + structureEditorSaved: [commentChanged: boolean]; structureEditorClose: []; }>(); @@ -690,7 +690,7 @@ defineExpose({ focusSearch, refreshData }); :database="activeTab.database" :schema="activeTab.schema" :table-name="activeTab.structureTableName || ''" - @saved="emit('structureEditorSaved')" + @saved="(commentChanged) => emit('structureEditorSaved', commentChanged)" @close="emit('structureEditorClose')" /> diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index 16872c5b3..dca792aa3 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -75,7 +75,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - saved: []; + saved: [commentChanged: boolean]; close: []; }>(); @@ -140,6 +140,8 @@ const indexColLabels = computed(() => [ const targetSchema = computed(() => props.schema || props.database || ""); const isCreateMode = computed(() => !props.tableName); const newTableName = ref(""); +const tableComment = ref(""); +const originalTableComment = ref(""); const targetLabel = computed(() => buildStructureTargetLabel( connection.value?.name, @@ -160,6 +162,8 @@ async function refreshSqlPreview() { tableName: isCreateMode.value ? newTableName.value : props.tableName || "", columns: columns.value, indexes: indexes.value, + tableComment: tableComment.value, + originalTableComment: isCreateMode.value ? undefined : originalTableComment.value, }; try { const result = isCreateMode.value @@ -201,6 +205,8 @@ function resetState() { foreignKeys.value = []; triggers.value = []; newTableName.value = ""; + tableComment.value = ""; + originalTableComment.value = ""; } async function loadStructure(silent = false) { @@ -219,6 +225,16 @@ async function loadStructure(silent = false) { indexes.value = createIndexDrafts(nextIndexes); foreignKeys.value = nextForeignKeys; triggers.value = nextTriggers; + try { + const tables = await api.listTables(props.connectionId, props.database, targetSchema.value); + const table = tables.find( + (t) => t.name.toLowerCase() === props.tableName!.toLowerCase() && t.table_type !== "VIEW", + ); + originalTableComment.value = table?.comment || ""; + tableComment.value = table?.comment || ""; + } catch { + /* ignore — table comment is optional */ + } } catch (e: any) { errorMessage.value = e?.message || String(e); } finally { @@ -385,7 +401,7 @@ async function applyChanges() { try { await api.executeBatch(props.connectionId, props.database, pendingStatements.value); toast(t("structureEditor.saved"), 2500); - emit("saved"); + emit("saved", tableComment.value !== originalTableComment.value); if (isCreateMode.value) { emit("close"); } else { @@ -404,7 +420,7 @@ onMounted(() => { }); watch( - [isCreateMode, databaseType, () => props.schema, () => props.tableName, newTableName, columns, indexes], + [isCreateMode, databaseType, () => props.schema, () => props.tableName, newTableName, tableComment, columns, indexes], () => { void refreshSqlPreview(); }, @@ -444,6 +460,15 @@ watch( /> +
+ + +
+
{{ t("common.loading") }} diff --git a/apps/desktop/src/composables/useNavigationTargets.ts b/apps/desktop/src/composables/useNavigationTargets.ts index c21e508ff..d61a475e1 100644 --- a/apps/desktop/src/composables/useNavigationTargets.ts +++ b/apps/desktop/src/composables/useNavigationTargets.ts @@ -123,6 +123,7 @@ export function useNavigationTargets(dialogs: { reloadData: () => Promise, toast: (msg: string, duration?: number) => void, context: { connectionId: string; database: string; schema?: string; tableName: string }, + commentChanged?: boolean, ) { if (!context.tableName) { try { @@ -134,6 +135,15 @@ export function useNavigationTargets(dialogs: { } catch {} return; } + if (commentChanged) { + try { + await connectionStore.refreshObjectListTreeNode( + context.connectionId, + context.database, + context.schema || undefined, + ); + } catch {} + } const activeTab = queryStore.tabs.find((t) => t.id === queryStore.activeTabId); if (activeTab?.mode === "data" && activeTab.tableMeta?.tableName === context.tableName) { try { @@ -148,11 +158,6 @@ export function useNavigationTargets(dialogs: { columns, primaryKeys: editablePrimaryKeys(connectionStore.getConfig(activeTab.connectionId)?.db_type, columns), }); - await connectionStore.refreshObjectListTreeNode( - activeTab.connectionId, - activeTab.database, - activeTab.tableMeta.schema, - ); await reloadData(); } catch (e: any) { toast(e?.message || String(e), 5000); diff --git a/apps/desktop/src/lib/tableStructureEditorSql.ts b/apps/desktop/src/lib/tableStructureEditorSql.ts index a13741e05..eb6882dcb 100644 --- a/apps/desktop/src/lib/tableStructureEditorSql.ts +++ b/apps/desktop/src/lib/tableStructureEditorSql.ts @@ -33,6 +33,8 @@ export interface BuildTableStructureChangeSqlOptions { tableName: string; columns: EditableStructureColumn[]; indexes: EditableStructureIndex[]; + tableComment?: string; + originalTableComment?: string; } export interface TableStructureChangeSql { diff --git a/crates/dbx-core/src/table_structure_sql.rs b/crates/dbx-core/src/table_structure_sql.rs index fe7a14d2b..628a567d9 100644 --- a/crates/dbx-core/src/table_structure_sql.rs +++ b/crates/dbx-core/src/table_structure_sql.rs @@ -93,6 +93,10 @@ pub struct TableStructureSqlOptions { pub columns: Vec, #[serde(default)] pub indexes: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub table_comment: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub original_table_comment: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -291,9 +295,43 @@ pub fn build_table_structure_change_sql(options: TableStructureSqlOptions) -> Ta let mut warnings = validate_draft(&options); let mut statements = build_column_sql(&options, &mut warnings); statements.extend(build_index_sql(&options, &mut warnings)); + statements.extend(build_table_comment_sql(&options, &mut warnings)); TableStructureSqlResult { statements, warnings } } +fn build_table_comment_sql(options: &TableStructureSqlOptions, warnings: &mut Vec) -> Vec { + let capabilities = capabilities_for(options.database_type); + if !capabilities.comment { + return Vec::new(); + } + let new_comment = options.table_comment.as_deref().unwrap_or(""); + let original_comment = options.original_table_comment.as_deref().unwrap_or(""); + if clean(new_comment) == clean(original_comment) { + return Vec::new(); + } + let dialect = capabilities.dialect; + let table = qualified_table(dialect, options.schema.as_deref(), &options.table_name); + let quoted = quote_string(&clean(new_comment)); + match dialect { + StructureDialect::Mysql => { + vec![format!("ALTER TABLE {table} COMMENT = {quoted};")] + } + StructureDialect::Postgres | StructureDialect::Oracle | StructureDialect::H2 => { + vec![format!("COMMENT ON TABLE {table} IS {quoted};")] + } + StructureDialect::ClickHouse => { + vec![format!("ALTER TABLE {table} MODIFY COMMENT {quoted};")] + } + StructureDialect::SqlServer | StructureDialect::Sqlite | StructureDialect::DuckDb | _ => { + if !clean(new_comment).is_empty() { + warnings + .push(format!("Table comments are not supported for {} from this editor.", dialect_label(dialect))); + } + Vec::new() + } + } +} + pub fn build_create_table_sql(options: TableStructureSqlOptions) -> TableStructureSqlResult { let mut warnings = Vec::new(); if clean(&options.table_name).is_empty() { @@ -338,6 +376,21 @@ pub fn build_create_table_sql(options: TableStructureSqlOptions) -> TableStructu statements.push(format!("CREATE TABLE {table} (\n {}\n);", column_definitions.join(",\n "))); + if capabilities.comment { + let table_comment = clean(options.table_comment.as_deref().unwrap_or("")); + if !table_comment.is_empty() { + if dialect == StructureDialect::Mysql { + if let Some(last) = statements.last_mut() { + *last = last.replace(");", &format!(") COMMENT = {};", quote_string(&table_comment))); + } + } else if matches!(dialect, StructureDialect::Postgres | StructureDialect::Oracle | StructureDialect::H2) { + statements.push(format!("COMMENT ON TABLE {table} IS {};", quote_string(&table_comment))); + } else if dialect == StructureDialect::ClickHouse { + statements.push(format!("ALTER TABLE {table} MODIFY COMMENT {};", quote_string(&table_comment))); + } + } + } + if capabilities.comment && matches!(dialect, StructureDialect::Postgres | StructureDialect::Oracle | StructureDialect::H2) {