最近在整理某系統的 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)

Post a comment