[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*)?'(?(?:''|[^'])*)'") 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+)?(?`?[A-Za-z_][A-Za-z0-9_$]*`?)\s*\((?.*?)\)\s*(?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*(?`?[A-Za-z_][A-Za-z0-9_$]*`?)\s+(?[A-Za-z].*?)\s*,?\s*$') if (-not $columnMatch.Success) { continue } $definition = $columnMatch.Groups["definition"].Value $typeMatch = [regex]::Match($definition, '^(?[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