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)
{