[Enhance] add comments in SQL Server DDL output (#1017)

Thanks!
This commit is contained in:
miracle 2026-06-10 22:49:42 +08:00 committed by GitHub
parent 215b0cf7d4
commit 871fc57c52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 166 additions and 3 deletions

View File

@ -831,7 +831,7 @@ pub async fn list_indexes(client: &mut SqlServerClient, schema: &str, table: &st
} else {
Some(inc_str.split(',').map(|s| s.to_string()).collect())
},
comment: None,
comment: row.get::<&str, _>(7).filter(|s: &&str| !s.is_empty()).map(|s: &str| s.to_string()),
}
})
.collect())
@ -853,8 +853,10 @@ fn sqlserver_indexes_sql(schema: &str, table: &str) -> String {
WHERE ic3.object_id = i.object_id AND ic3.index_id = i.index_id AND ic3.is_included_column = 1 \
ORDER BY ic3.index_column_id \
FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 1, '') AS included_cols, \
i.filter_definition \
i.filter_definition, \
ep.value AS index_comment \
FROM sys.indexes i \
OUTER APPLY (SELECT CAST(ep.value AS NVARCHAR(MAX)) AS value FROM sys.extended_properties ep WHERE ep.major_id = i.object_id AND ep.minor_id = i.index_id AND ep.name = N'MS_Description' AND ep.class = 7) ep \
WHERE i.object_id = OBJECT_ID('{s}.{t}') AND i.name IS NOT NULL \
ORDER BY i.name",
s = schema.replace('\'', "''"),
@ -893,6 +895,29 @@ pub async fn list_foreign_keys(
.collect())
}
pub async fn get_table_comment(
client: &mut SqlServerClient,
schema: &str,
table: &str,
) -> Result<Option<String>, String> {
let sql = sqlserver_table_comment_sql(schema, table);
let stream = client.query(&*sql, &[]).await.map_err(|e| e.to_string())?;
let rows = stream.into_first_result().await.map_err(|e| e.to_string())?;
Ok(rows.first().and_then(|row| row.get::<&str, _>(0)).filter(|s| !s.is_empty()).map(|s| s.to_string()))
}
fn sqlserver_table_comment_sql(schema: &str, table: &str) -> String {
let s = schema.replace('\'', "''");
let t = table.replace('\'', "''");
format!(
"SELECT CAST(ep.value AS NVARCHAR(MAX)) \
FROM sys.extended_properties ep \
WHERE ep.major_id = OBJECT_ID(QUOTENAME('{s}') + '.' + QUOTENAME('{t}')) \
AND ep.minor_id = 0 \
AND ep.name = N'MS_Description'"
)
}
pub async fn list_triggers(
client: &mut SqlServerClient,
schema: &str,
@ -1084,7 +1109,7 @@ mod tests {
use super::{
build_spatial_safe_sqlserver_query, is_sqlserver_spatial_column, requires_simple_query_batch,
sqlserver_cell_to_json, sqlserver_columns_sql, sqlserver_indexes_sql, sqlserver_list_objects_sql,
SqlServerDescribedColumn, SqlServerResultSet,
sqlserver_table_comment_sql, SqlServerDescribedColumn, SqlServerResultSet,
};
use chrono::NaiveDate;
use std::time::Instant;
@ -1160,6 +1185,26 @@ mod tests {
assert!(sql.contains("OBJECT_ID('dbo.DF_Rule')"));
}
#[test]
fn sqlserver_indexes_sql_includes_index_comment_via_extended_properties() {
let sql = sqlserver_indexes_sql("dbo", "orders");
assert!(sql.contains("sys.extended_properties ep"));
assert!(sql.contains("ep.minor_id = i.index_id"));
assert!(sql.contains("MS_Description"));
}
#[test]
fn sqlserver_table_comment_sql_queries_extended_properties() {
let sql = sqlserver_table_comment_sql("dbo", "users");
assert!(sql.contains("sys.extended_properties ep"));
assert!(sql.contains("ep.minor_id = 0"));
assert!(sql.contains("MS_Description"));
assert!(sql.contains("QUOTENAME('dbo')"));
assert!(sql.contains("QUOTENAME('users')"));
}
#[test]
fn sqlserver_metadata_sql_escapes_literals() {
let columns_sql = sqlserver_columns_sql("d'bo", "t'able");

View File

@ -1452,6 +1452,67 @@ mod ddl_tests {
"SELECT pg_get_tabledef('\"tenant''s schema\".\"active users\"')"
);
}
#[test]
fn sqlserver_comment_ddl_includes_table_comment() {
let ddl = render_sqlserver_comment_ddl("dbo", "users", Some("User accounts table"), &[], &[]);
assert!(ddl.contains("EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'User accounts table'"));
assert!(ddl.contains("@level0type=N'SCHEMA', @level0name=N'dbo'"));
assert!(ddl.contains("@level1type=N'TABLE', @level1name=N'users'"));
}
#[test]
fn sqlserver_comment_ddl_includes_column_comments() {
let mut email_col = column("email", "nvarchar(255)");
email_col.comment = Some("User's email address".to_string());
let mut name_col = column("name", "nvarchar(100)");
name_col.comment = Some("User's display name".to_string());
let columns = vec![email_col, name_col];
let ddl = render_sqlserver_comment_ddl("dbo", "users", None, &columns, &[]);
assert!(ddl.contains("@level2type=N'COLUMN', @level2name=N'email'"));
assert!(ddl.contains("@value=N'User''s email address'"));
assert!(ddl.contains("@level2type=N'COLUMN', @level2name=N'name'"));
assert!(ddl.contains("@value=N'User''s display name'"));
}
#[test]
fn sqlserver_comment_ddl_includes_index_comments() {
let idx = db::IndexInfo {
name: "IX_users_email".to_string(),
columns: vec!["email".to_string()],
is_unique: true,
is_primary: false,
filter: None,
index_type: Some("NONCLUSTERED".to_string()),
included_columns: None,
comment: Some("Unique email index".to_string()),
};
let indexes = vec![idx];
let ddl = render_sqlserver_comment_ddl("dbo", "users", None, &[], &indexes);
assert!(ddl.contains("@level2type=N'INDEX', @level2name=N'IX_users_email'"));
assert!(ddl.contains("@value=N'Unique email index'"));
}
#[test]
fn sqlserver_comment_ddl_skips_empty_comments() {
let ddl = render_sqlserver_comment_ddl("dbo", "users", Some(""), &[], &[]);
assert!(ddl.is_empty());
let ddl = render_sqlserver_comment_ddl("dbo", "users", None, &[], &[]);
assert!(ddl.is_empty());
}
#[test]
fn sqlserver_comment_ddl_escapes_single_quotes() {
let ddl = render_sqlserver_comment_ddl("dbo", "users", Some("User's table for 'admin'"), &[], &[]);
assert!(ddl.contains("@value=N'User''s table for ''admin'''"));
}
}
pub async fn mysql_ddl(pool: &db::mysql::MySqlPool, table: &str) -> Result<String, String> {
@ -1592,6 +1653,7 @@ pub async fn build_sqlserver_ddl(
let columns = db::sqlserver::get_columns(client, schema, table).await?;
let indexes = db::sqlserver::list_indexes(client, schema, table).await?;
let fkeys = db::sqlserver::list_foreign_keys(client, schema, table).await?;
let table_comment = db::sqlserver::get_table_comment(client, schema, table).await?;
let mut ddl = format!("CREATE TABLE [{schema}].[{table}] (\n");
let col_lines: Vec<String> = columns
@ -1643,5 +1705,61 @@ pub async fn build_sqlserver_ddl(
idx.name
));
}
let comment_ddl = render_sqlserver_comment_ddl(schema, table, table_comment.as_deref(), &columns, &indexes);
if !comment_ddl.is_empty() {
ddl.push('\n');
ddl.push_str(comment_ddl.trim_end());
}
Ok(ddl)
}
fn render_sqlserver_comment_ddl(
schema: &str,
table: &str,
table_comment: Option<&str>,
columns: &[db::ColumnInfo],
indexes: &[db::IndexInfo],
) -> String {
let mut ddl = String::new();
let escaped_schema = schema.replace('\'', "''");
let escaped_table = table.replace('\'', "''");
if let Some(comment) = table_comment.filter(|s| !s.is_empty()) {
let escaped_comment = comment.replace('\'', "''");
ddl.push_str(&format!(
"EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'{escaped_comment}', \
@level0type=N'SCHEMA', @level0name=N'{escaped_schema}', \
@level1type=N'TABLE', @level1name=N'{escaped_table}';\n"
));
}
for col in columns {
if let Some(comment) = col.comment.as_deref().filter(|s| !s.is_empty()) {
let escaped_comment = comment.replace('\'', "''");
let escaped_col = col.name.replace('\'', "''");
ddl.push_str(&format!(
"EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'{escaped_comment}', \
@level0type=N'SCHEMA', @level0name=N'{escaped_schema}', \
@level1type=N'TABLE', @level1name=N'{escaped_table}', \
@level2type=N'COLUMN', @level2name=N'{escaped_col}';\n"
));
}
}
for idx in indexes {
if let Some(comment) = idx.comment.as_deref().filter(|s| !s.is_empty()) {
let escaped_comment = comment.replace('\'', "''");
let escaped_idx = idx.name.replace('\'', "''");
ddl.push_str(&format!(
"EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'{escaped_comment}', \
@level0type=N'SCHEMA', @level0name=N'{escaped_schema}', \
@level1type=N'TABLE', @level1name=N'{escaped_table}', \
@level2type=N'INDEX', @level2name=N'{escaped_idx}';\n"
));
}
}
ddl
}