Files
baiyue-firstleg/scripts/database-comment-audit.ps1
T

231 lines
8.9 KiB
PowerShell
Raw Normal View History

2026-08-11 18:06:26 +08:00
[CmdletBinding()]
param(
[string]$RepositoryRoot,
[string[]]$Modules,
[ValidateSet("Text", "Json")]
[string]$Format = "Text",
[switch]$SummaryOnly,
[switch]$FailOnIssues,
[switch]$SelfTest
)
$ErrorActionPreference = "Stop"
function Get-SqlComment {
param([string]$Text)
$match = [regex]::Match($Text, "(?is)\bCOMMENT\s*(?:=\s*)?'(?<value>(?:''|[^'])*)'")
if (-not $match.Success) {
return $null
}
return $match.Groups["value"].Value.Replace("''", "'").Trim()
}
function Get-CreateTableDefinitions {
param(
[string]$Sql,
[string]$Source
)
$pattern = '(?is)\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?<name>`?[A-Za-z_][A-Za-z0-9_$]*`?)\s*\((?<body>.*?)\)\s*(?<tail>ENGINE\s*=.*?);'
$mysqlColumnTypes = @(
"BIGINT", "INT", "INTEGER", "MEDIUMINT", "SMALLINT", "TINYINT",
"DECIMAL", "NUMERIC", "FIXED", "FLOAT", "DOUBLE", "REAL", "BIT", "BOOL", "BOOLEAN",
"CHAR", "VARCHAR", "NCHAR", "NVARCHAR", "BINARY", "VARBINARY",
"TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT",
"TINYBLOB", "BLOB", "MEDIUMBLOB", "LONGBLOB", "ENUM", "SET",
"DATE", "TIME", "DATETIME", "TIMESTAMP", "YEAR", "JSON",
"GEOMETRY", "POINT", "LINESTRING", "POLYGON", "MULTIPOINT",
"MULTILINESTRING", "MULTIPOLYGON", "GEOMETRYCOLLECTION"
)
$matches = [regex]::Matches($Sql, $pattern)
$definitions = @()
foreach ($match in $matches) {
$columns = @()
foreach ($line in ($match.Groups["body"].Value -split "`r?`n")) {
$columnMatch = [regex]::Match($line, '^\s*(?<name>`?[A-Za-z_][A-Za-z0-9_$]*`?)\s+(?<definition>[A-Za-z].*?)\s*,?\s*$')
if (-not $columnMatch.Success) {
continue
}
$definition = $columnMatch.Groups["definition"].Value
$typeMatch = [regex]::Match($definition, '^(?<type>[A-Za-z]+)\b')
if (-not $typeMatch.Success -or $typeMatch.Groups["type"].Value.ToUpperInvariant() -notin $mysqlColumnTypes) {
continue
}
$columnName = $columnMatch.Groups["name"].Value.Trim('`')
$columns += [pscustomobject]@{
Name = $columnName
Comment = Get-SqlComment -Text $line
}
}
$definitions += [pscustomobject]@{
Source = $Source
Table = $match.Groups["name"].Value.Trim('`')
Comment = Get-SqlComment -Text $match.Groups["tail"].Value
Columns = $columns
}
}
return $definitions
}
function Test-ChineseComment {
param([AllowNull()][string]$Comment)
return -not [string]::IsNullOrWhiteSpace($Comment) -and $Comment -match '[\p{IsCJKUnifiedIdeographs}]'
}
function Get-AuditResult {
param([object[]]$Definitions)
$issues = @()
$tableCount = 0
$commentedTableCount = 0
$columnCount = 0
$commentedColumnCount = 0
foreach ($definition in $Definitions) {
$tableCount++
if (-not [string]::IsNullOrWhiteSpace($definition.Comment)) {
$commentedTableCount++
}
if ([string]::IsNullOrWhiteSpace($definition.Comment)) {
$issues += [pscustomobject]@{ Code = "MISSING_TABLE_COMMENT"; Source = $definition.Source; Table = $definition.Table; Column = $null }
} elseif (-not (Test-ChineseComment -Comment $definition.Comment)) {
$issues += [pscustomobject]@{ Code = "NON_CHINESE_TABLE_COMMENT"; Source = $definition.Source; Table = $definition.Table; Column = $null }
}
foreach ($column in $definition.Columns) {
$columnCount++
if (-not [string]::IsNullOrWhiteSpace($column.Comment)) {
$commentedColumnCount++
}
if ([string]::IsNullOrWhiteSpace($column.Comment)) {
$issues += [pscustomobject]@{ Code = "MISSING_COLUMN_COMMENT"; Source = $definition.Source; Table = $definition.Table; Column = $column.Name }
} elseif (-not (Test-ChineseComment -Comment $column.Comment)) {
$issues += [pscustomobject]@{ Code = "NON_CHINESE_COLUMN_COMMENT"; Source = $definition.Source; Table = $definition.Table; Column = $column.Name }
}
}
}
$tableCoverage = if ($tableCount -eq 0) { 100 } else { [math]::Round(100 * $commentedTableCount / $tableCount, 2) }
$columnCoverage = if ($columnCount -eq 0) { 100 } else { [math]::Round(100 * $commentedColumnCount / $columnCount, 2) }
return [pscustomobject]@{
Summary = [pscustomobject]@{
TableDefinitions = $tableCount
CommentedTables = $commentedTableCount
TableCoveragePercent = $tableCoverage
Columns = $columnCount
CommentedColumns = $commentedColumnCount
ColumnCoveragePercent = $columnCoverage
Issues = $issues.Count
}
Issues = $issues
}
}
function Invoke-SelfTest {
$validSql = @"
CREATE TABLE IF NOT EXISTS sample_valid (
id BIGINT NOT NULL COMMENT '主键ID',
status VARCHAR(16) NOT NULL COMMENT '状态:ACTIVE启用、DISABLED停用',
amount DECIMAL(20,6) NOT NULL COMMENT '含税金额;币种由currency_code指定',
and_flag TINYINT(1) NOT NULL COMMENT '附加条件标记:0否、1是',
CONSTRAINT chk_sample_status CHECK (
status IN ('ACTIVE', 'DISABLED')
AND amount >= 0
OR (status = 'DISABLED' AND amount = 0)
)
) ENGINE=InnoDB COMMENT='自测业务表';
"@
$invalidSql = @"
CREATE TABLE sample_invalid (
id BIGINT NOT NULL,
payload JSON NULL COMMENT 'JSON payload'
) ENGINE=InnoDB COMMENT='Sample table';
"@
$valid = Get-AuditResult -Definitions @(Get-CreateTableDefinitions -Sql $validSql -Source "valid.sql")
if ($valid.Summary.TableDefinitions -ne 1 -or $valid.Summary.Columns -ne 4 -or $valid.Summary.Issues -ne 0) {
throw "Self-test failed: valid SQL was not parsed as expected."
}
$invalid = Get-AuditResult -Definitions @(Get-CreateTableDefinitions -Sql $invalidSql -Source "invalid.sql")
$codes = @($invalid.Issues | ForEach-Object { $_.Code })
foreach ($expected in @("NON_CHINESE_TABLE_COMMENT", "MISSING_COLUMN_COMMENT", "NON_CHINESE_COLUMN_COMMENT")) {
if ($expected -notin $codes) {
throw "Self-test failed: missing expected issue $expected."
}
}
Write-Output "SELF-TEST PASS: parser, coverage and Chinese-comment checks are working."
}
if ($SelfTest) {
Invoke-SelfTest
exit 0
}
if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) {
$RepositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
} else {
$RepositoryRoot = (Resolve-Path $RepositoryRoot).Path
}
$dbDirectories = @(Get-ChildItem -LiteralPath $RepositoryRoot -Directory | ForEach-Object {
$candidate = Join-Path $_.FullName "src/main/resources/db"
if (Test-Path -LiteralPath $candidate -PathType Container) {
[pscustomobject]@{ Module = $_.Name; Path = $candidate }
}
})
if ($Modules -and $Modules.Count -gt 0) {
$requested = @($Modules | ForEach-Object { $_.ToLowerInvariant() })
$dbDirectories = @($dbDirectories | Where-Object { $_.Module.ToLowerInvariant() -in $requested })
$found = @($dbDirectories | ForEach-Object { $_.Module.ToLowerInvariant() })
$missingModules = @($requested | Where-Object { $_ -notin $found })
if ($missingModules.Count -gt 0) {
Write-Error ("Unknown modules or missing db directories: " + ($missingModules -join ", "))
exit 2
}
}
$definitions = @()
foreach ($dbDirectory in ($dbDirectories | Sort-Object Module)) {
foreach ($file in (Get-ChildItem -LiteralPath $dbDirectory.Path -Filter "*.sql" -File | Sort-Object FullName)) {
$relativePath = $file.FullName.Substring($RepositoryRoot.Length + 1).Replace('\', '/')
$sql = Get-Content -Raw -LiteralPath $file.FullName
$definitions += @(Get-CreateTableDefinitions -Sql $sql -Source $relativePath)
}
}
$result = Get-AuditResult -Definitions $definitions
if ($Format -eq "Json") {
$jsonValue = if ($SummaryOnly) { $result.Summary } else { $result }
Write-Output ($jsonValue | ConvertTo-Json -Depth 6)
} else {
Write-Output "Database comment audit"
Write-Output "Repository: $RepositoryRoot"
Write-Output "Database/API access: none"
Write-Output ("Table definitions: {0}, commented: {1}, coverage: {2}%" -f $result.Summary.TableDefinitions, $result.Summary.CommentedTables, $result.Summary.TableCoveragePercent)
Write-Output ("Columns: {0}, commented: {1}, coverage: {2}%" -f $result.Summary.Columns, $result.Summary.CommentedColumns, $result.Summary.ColumnCoveragePercent)
Write-Output ("Issues: {0}" -f $result.Summary.Issues)
if (-not $SummaryOnly) {
foreach ($issue in $result.Issues) {
$target = if ([string]::IsNullOrWhiteSpace($issue.Column)) { $issue.Table } else { "$($issue.Table).$($issue.Column)" }
Write-Output ("[{0}] {1} ({2})" -f $issue.Code, $target, $issue.Source)
}
}
}
if ($FailOnIssues -and $result.Summary.Issues -gt 0) {
exit 1
}
exit 0