就是要 Markdown - SQL Schema 匯出 Markdown 表格轉 PDF
| | | 6 | |
最近在整理某系統的 MSSQL 資料表結構,準備對所有欄位做一次清查,了解各欄位的用途建立基本概念。照理來說,正常人應該會找工具軟體將 Schema 匯出成 Word 或 Excel 編輯加上說明。但我這幾年寫 Markdown 成癮,很不想用 Word 寫,便興起了將 SQL Schema 匯出成 Markdown Table 再用 VSCode 編輯的想法。
花了幾分鐘沒找到 MSSQL Schema 轉 Markdown 的現成工具,與其花時間繼續找,不如自己寫一個比較快,還能 100% 符合自己的奇怪要求。反正現在有 AI 幫忙,查詢 MSSQL Column 資料、轉 Markdown Table 這類零散技能沒啥營養,又是可拋式免洗工具,正是絕佳的 Vibe Coding 題材。
連生成帶修改含測試,不到十分鐘做完,好個 AI 時代。
param (
[string]$ConnectionString = "Server=(localdb)\MSSQLLocalDB;Database=Northwind;Integrated Security=True;"
)
$ErrorActionPreference = "Stop"
$dumpScript = @"
SELECT
t.name AS TableName,
c.name AS ColumnName,
ty.name AS DataType,
c.max_length AS MaxLength,
c.precision AS Precision,
c.scale AS Scale,
CASE
WHEN c.is_nullable = 1 THEN 'Y'
ELSE 'N'
END AS IsNullable,
CASE
WHEN ic.column_id IS NOT NULL THEN 'PK'
ELSE ''
END AS IsPK
FROM
sys.tables t
INNER JOIN sys.columns c ON t.object_id = c.object_id
INNER JOIN sys.types ty ON c.user_type_id = ty.user_type_id
LEFT JOIN sys.key_constraints kc ON t.object_id = kc.parent_object_id
AND kc.type = 'PK'
LEFT JOIN sys.index_columns ic ON kc.parent_object_id = ic.object_id
AND kc.unique_index_id = ic.index_id
AND c.column_id = ic.column_id
ORDER BY
t.name, c.column_id;
"@
# SQL 2017+ 支援 STRING_AGG,更早版本需改用 FOR XML PATH 產生 CSV
$indexScript = @"
SELECT
t.name AS TableName,
i.name AS IndexName,
i.type_desc AS IndexType,
i.is_unique AS IsUnique,
i.is_primary_key AS IsPrimaryKey,
STRING_AGG(c.name, ', ') WITHIN GROUP (ORDER BY ic.key_ordinal) AS IndexColumns
FROM
sys.tables t
INNER JOIN sys.indexes i ON t.object_id = i.object_id
INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
WHERE
i.type > 0 -- Exclude heaps
GROUP BY
t.name, i.name, i.type_desc, i.is_unique, i.is_primary_key
ORDER BY
t.name, i.name;
"@
$cn = New-Object System.Data.SqlClient.SqlConnection($ConnectionString)
$cmd = New-Object System.Data.SqlClient.SqlCommand($dumpScript, $cn)
$cn.Open()
$reader = $cmd.ExecuteReader()
$schema = @()
while ($reader.Read()) {
$row = [PSCustomObject]@{
TableName = $reader["TableName"]
ColumnName = $reader["ColumnName"]
DataType = $reader["DataType"]
MaxLength = $reader["MaxLength"]
Precision = $reader["Precision"]
Scale = $reader["Scale"]
IsNullable = $reader["IsNullable"]
IsPK = $reader["IsPK"]
}
if ($row.DataType.EndsWith("char") -or $row.DataType.EndsWith("text")) {
$row.MaxLength = if ($row.MaxLength -eq -1) { "MAX" } else { $row.MaxLength }
$row.DataType = "$($row.DataType)($($row.MaxLength))"
} elseif ($row.Precision -ne 0 -and $row.Scale -ne 0) {
$row.DataType = "$($row.DataType)($($row.Precision),$($row.Scale))"
} elseif ($row.Precision -ne 0) {
$row.DataType = "$($row.DataType)($($row.Precision))"
}
$row.DataType = $row.DataType.ToUpper()
$schema += $row
}
$reader.Close()
# Get Index Information
$cmd = New-Object System.Data.SqlClient.SqlCommand($indexScript, $cn)
$reader = $cmd.ExecuteReader()
$indexes = @()
while ($reader.Read()) {
$indexRow = [PSCustomObject]@{
TableName = $reader["TableName"]
IndexName = $reader["IndexName"]
IndexType = $reader["IndexType"]
IsUnique = $reader["IsUnique"]
IsPrimaryKey = $reader["IsPrimaryKey"]
IndexColumns = $reader["IndexColumns"]
}
$indexes += $indexRow
}
$reader.Close()
$cn.Close()
$markdown = @"
# 資料庫 Schema
"@
$widths = $(2, 24, 24, 16, 4, 32)
$ascEnc = [System.Text.Encoding]::GetEncoding("big5")
function FixWidth($idx, $text)
{
$width = $widths[$idx]
if ($text -eq '-') {
return '-' * $width
}
$len = $ascEnc.GetByteCount($text)
return $text + (' ' * ($width - $len))
}
foreach ($table in $schema | Group-Object TableName) {
$markdown += "## 資料表 $($table.Name)`n`n"
$markdown += "| PK | $(FixWidth 1 '欄位名稱') | $(FixWidth 2 '欄位說明') | $(FixWidth 3 '資料型別') | $(FixWidth 4 '空值') | $(FixWidth 5 '備註') |`n"
$markdown += "|-$(FixWidth 0 '-')-|-$(FixWidth 1 '-')-|-$(FixWidth 2 '-')-|-$(FixWidth 3 '-')-|:$(FixWidth 4 '-'):|-$(FixWidth 5 '-')-|`n"
foreach ($column in $table.Group) {
$markdown += "| $(FixWidth 0 ($column.IsPK)) | $(FixWidth 1 $column.ColumnName) | $(FixWidth 2 ' ') | $(FixWidth 3 $column.DataType) | $(FixWidth 4 $column.IsNullable) | $(FixWidth 5 ' ') |`n"
}
# Add index information for this table
$tableIndexes = $indexes | Where-Object { $_.TableName -eq $table.Name }
if ($tableIndexes) {
$indexInfo = ($tableIndexes | ForEach-Object {
$isUnique = if ($_.IsUnique) { "/UNIQUE" } else { '' }
$isPrimary = if ($_.IsPrimaryKey) { "/PRIMARY_KEY" } else { '' }
"- $($_.IndexType)$isUnique$isPrimary INDEX: $($_.IndexName)($($_.IndexColumns))"
}) -join "`n"
$markdown += "`n" + $indexInfo + "`n"
}
$markdown += "`n"
}
$markdown
輕鬆搞定。

存成 .md 開 VSCode 編輯,年輕同學可能會覺得,在純文字模式打表格是什麼鬼?但這對 PE2、HE5 使用經驗超過十年的老人完全不是問題呀,打著打著還會回想起自己的年少時光。

編輯成果我是用 Markdown PDF 匯出成 PDF 檔,預設的表格樣式呈現起來不太理想:

我準備了一個 markdown-pdf.css,主要是調字型大小、設邊框、順便指定各欄位的寬度:
h1 {
font-size: 14pt;
}
h2 {
font-size: 12pt;
}
h3 {
font-size: 11pt;
}
table {
width: 100%;
border-collapse: collapse;
}
table > thead > tr > th {
border-bottom-color: #aaa;
text-align: center;
}
th, td {
border: 1px solid #aaa;
padding: 3px;
}
thead {
background-color: #f2f2f2;
}
td:nth-child(1) {
width: 2em;
text-align: center;
}
td:nth-child(2) {
width: 6em;
}
td:nth-child(3) {
width: 10em;
}
td:nth-child(4) {
width: 8em;
}
td:nth-child(5) {
width: 2em;
text-align: center;
}
由於包含表格欄寬,這個 .css 是依 .md 量身訂做的,這種情況可以啟用 Markdown PDF 的「Styles Relative Path File」,設定路徑 markdown-pdf.css,此時 Markdown PDF 會取用 .md 同目錄下的 markdown-pdf.css,做到不同 .md 搭配專屬 .css。

除了用 VSCode GUI 設定,更簡單的方法是在 VSCode 專案目錄放個 .vscode/settings.json 完成以上設定:
{
"markdown-pdf.styles": [
"markdown-pdf.css"
],
"markdown-pdf.stylesRelativePathFile": true
}
效果還不錯。

Markdown 版圖擴張成功。
Comments
# by ChrisTorng
看到您 VSCode 中參差不齊的表格線,先前我也研究過如何改善,但就是沒找到。就我的觀察,是一個中文字比兩個英數字母窄一點,因此中文越多的行就會越短,最後都無法對齊。不知有沒有人貢獻可對齊的方法?
# by Nick
給我書中仙。
# by 小黑
謝哥
# by mick
Table 中每一個欄位 有一個 extendedproperty "MS_Description" 可以用來存放 [欄位說明] DECLARE @table SYSNAME = N'streetname'; DECLARE @column SYSNAME = N'zipcode'; DECLARE @schema SYSNAME = N'dbo'; DECLARE @desc NVARCHAR(500) = N'郵遞區號'; IF EXISTS ( SELECT 1 FROM sys.extended_properties ep JOIN sys.columns c ON ep.major_id = c.object_id AND ep.minor_id = c.column_id JOIN sys.tables t ON c.object_id = t.object_id JOIN sys.schemas s ON t.schema_id = s.schema_id WHERE ep.name = 'MS_Description' AND s.name = @schema AND t.name = @table AND c.name = @column ) BEGIN EXEC sys.sp_updateextendedproperty @name = N'MS_Description', @value = @desc, @level0type = N'SCHEMA', @level0name = @schema, @level1type = N'TABLE', @level1name = @table, @level2type = N'COLUMN', @level2name = @column; END ELSE BEGIN EXEC sys.sp_addextendedproperty @name = N'MS_Description', @value = @desc, @level0type = N'SCHEMA', @level0name = @schema, @level1type = N'TABLE', @level1name = @table, @level2type = N'COLUMN', @level2name = @column; END ----- 然後再 SELECT t.name AS TableName, c.name AS ColumnName, ty.name AS DataType, c.max_length AS MaxLength, c.precision AS Precision, c.scale AS Scale, IIF(c.is_nullable = 1, 'Y', 'N') AS IsNullable, IIF(kc.name IS NOT NULL AND ic.column_id IS NOT NULL, 'PK', '') AS IsPK, ep.value AS Description FROM sys.tables t JOIN sys.columns c ON t.object_id = c.object_id JOIN sys.types ty ON c.user_type_id = ty.user_type_id LEFT JOIN sys.key_constraints kc ON kc.parent_object_id = t.object_id AND kc.type = 'PK' LEFT JOIN sys.index_columns ic ON ic.object_id = t.object_id AND ic.index_id = kc.unique_index_id AND ic.column_id = c.column_id LEFT JOIN sys.extended_properties ep ON ep.major_id = c.object_id AND ep.minor_id = c.column_id AND ep.name = 'MS_Description' AND ep.class = 1 ORDER BY t.name, c.column_id;
# by Eric
> 一個中文字比兩個英數字母窄一點,...不知有沒有人貢獻可對齊的方法? 找一個中文字剛好是兩個英文字寬度的字體就可以了。以前BBS常有這種排版需求,有不少字體符合這個設計。
# by Alex Lee
搜尋 等寬字型(英語:Monospaced Font)