From 2d47c949921084edc9d2cacfbc6b4dc61279c77e Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 14 Aug 2026 05:26:21 +0000 Subject: [PATCH] fix(docs): preserve database column comments Closes #6212 --- crates/dbx-core/src/docs/annotations.rs | 32 ++++++++++++- crates/dbx-core/src/docs/collector.rs | 63 +++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/crates/dbx-core/src/docs/annotations.rs b/crates/dbx-core/src/docs/annotations.rs index ec28dc143e..975b8dd421 100644 --- a/crates/dbx-core/src/docs/annotations.rs +++ b/crates/dbx-core/src/docs/annotations.rs @@ -655,7 +655,7 @@ mod tests { assert!(!error.contains("unknown field"), "should not surface a raw serde error, got: {error}"); } - use crate::docs::{DocTable, NoteSource, ProjectMeta, SchemaSnapshot, TableKind}; + use crate::docs::{ColumnNote, DocTable, NoteSource, ProjectMeta, SchemaSnapshot, TableKind}; use crate::models::connection::DatabaseType; fn snapshot_with(tables: Vec) -> SchemaSnapshot { @@ -738,8 +738,13 @@ mod tests { table.columns.push(crate::types::ColumnInfo { name: "status".to_string(), data_type: "text".to_string(), + comment: Some("Database lifecycle state.".to_string()), ..Default::default() }); + table.column_notes.insert( + "status".to_string(), + ColumnNote { note: "Database lifecycle state.".to_string(), source: NoteSource::Database, shadowed: None }, + ); let mut snapshot = snapshot_with(vec![table]); let annotations: AnnotationFile = serde_json::from_str(SAMPLE).expect("parse"); @@ -748,6 +753,31 @@ mod tests { let note = snapshot.tables[0].column_notes.get("status").expect("column note"); assert_eq!(note.note, "State machine."); assert_eq!(note.source, NoteSource::Local); + assert_eq!(note.shadowed.as_deref(), Some("Database lifecycle state.")); + } + + #[test] + fn database_column_note_survives_without_a_local_override() { + let mut table = table_named("core", "users", None); + table.columns.push(crate::types::ColumnInfo { + name: "email".to_string(), + data_type: "text".to_string(), + comment: Some("Primary contact address.".to_string()), + ..Default::default() + }); + table.column_notes.insert( + "email".to_string(), + ColumnNote { note: "Primary contact address.".to_string(), source: NoteSource::Database, shadowed: None }, + ); + let mut snapshot = snapshot_with(vec![table]); + let annotations: AnnotationFile = serde_json::from_str(SAMPLE).expect("parse"); + + apply_annotations(&mut snapshot, &annotations, DatabaseType::Postgres); + + let note = snapshot.tables[0].column_notes.get("email").expect("database column note"); + assert_eq!(note.note, "Primary contact address."); + assert_eq!(note.source, NoteSource::Database); + assert_eq!(note.shadowed, None); } #[test] diff --git a/crates/dbx-core/src/docs/collector.rs b/crates/dbx-core/src/docs/collector.rs index ea7aae7f9c..ff6e702c75 100644 --- a/crates/dbx-core/src/docs/collector.rs +++ b/crates/dbx-core/src/docs/collector.rs @@ -6,8 +6,8 @@ use futures::stream::{self, StreamExt}; use crate::connection::AppState; use crate::docs::dbml::{enum_type_name, is_inline_enum_spelling}; use crate::docs::{ - build_relationships, DocEnum, DocTable, NoteSource, ProjectMeta, Relationship, SchemaSnapshot, SnapshotWarning, - TableKind, + build_relationships, ColumnNote, DocEnum, DocTable, NoteSource, ProjectMeta, Relationship, SchemaSnapshot, + SnapshotWarning, TableKind, }; use crate::models::connection::ConnectionConfig; use crate::schema; @@ -106,6 +106,19 @@ fn any_comment_collected(tables: &[DocTable]) -> bool { }) } +fn database_column_notes(columns: &[ColumnInfo]) -> BTreeMap { + columns + .iter() + .filter_map(|column| { + let comment = column.comment.as_ref().filter(|value| !value.trim().is_empty())?; + Some(( + column.name.clone(), + ColumnNote { note: comment.clone(), source: NoteSource::Database, shadowed: None }, + )) + }) + .collect() +} + /// True when the `CommentsUnsupported` warning belongs in the snapshot: the /// engine's DDL capability flag says it can't do comments, and collection /// found none to contradict it. @@ -210,6 +223,7 @@ pub async fn collect_snapshot( table: format!("{schema_name}.{}", info.name), reason: error, })?; + let column_notes = database_column_notes(&columns); let mut table_warnings = Vec::new(); @@ -270,7 +284,7 @@ pub async fn collect_snapshot( NoteSource::None }, shadowed_note: None, - column_notes: BTreeMap::new(), + column_notes, estimated_rows: None, view_definition: None, }, @@ -478,6 +492,49 @@ mod tests { } } + #[test] + fn database_column_comments_become_database_notes() { + let columns = vec![ + crate::types::ColumnInfo { + name: "customer_name".to_string(), + comment: Some("Customer-facing name".to_string()), + ..Default::default() + }, + crate::types::ColumnInfo { + name: "status".to_string(), + comment: Some("Lifecycle state".to_string()), + ..Default::default() + }, + ]; + + let notes = database_column_notes(&columns); + + assert_eq!(notes.len(), 2); + let name = notes.get("customer_name").expect("customer name note"); + assert_eq!(name.note, "Customer-facing name"); + assert_eq!(name.source, NoteSource::Database); + assert_eq!(name.shadowed, None); + let status = notes.get("status").expect("status note"); + assert_eq!(status.note, "Lifecycle state"); + assert_eq!(status.source, NoteSource::Database); + assert_eq!(status.shadowed, None); + } + + #[test] + fn database_column_notes_ignore_empty_and_whitespace_comments() { + let columns = vec![ + crate::types::ColumnInfo { name: "empty".to_string(), comment: Some(String::new()), ..Default::default() }, + crate::types::ColumnInfo { + name: "whitespace".to_string(), + comment: Some(" \t\n".to_string()), + ..Default::default() + }, + crate::types::ColumnInfo { name: "missing".to_string(), comment: None, ..Default::default() }, + ]; + + assert!(database_column_notes(&columns).is_empty()); + } + fn sample_relationship() -> Relationship { Relationship { id: "orders.customer_id->customers.id".to_string(),